Flutter 实现任意tab切换效果处理和响应触摸效果我们可以用GestureDetector实现这个效果GestureDetector( ///手势触摸移动开始,这里我们可以记录开始的触摸点,用
Flutter 实现任意tab切换效果
处理和响应触摸效果
我们可以用GestureDetector实现这个效果
GestureDetector( ///手势触摸移动开始,这里我们可以记录开始的触摸点,用来判断移动比例和动画的初始点 onHorizontalDragStart: onStart, ///手势触摸移动中,这里生成tab的切换效果,具体效果可以用户自定义,效果代码都在delegate类里. onHorizontalDragUpdate: onUpdate, ///手势触摸结束,这里判断是切换到下一张卡片还是滑动失败,回滚当前tab onHorizontalDragEnd: onEnd, child: child,);
触摸开始
///记录触摸初始点onStart(DragStartDetails details) { dragStart = details.globalPosition; ...}
触摸移动中
onUpdate(DragUpdateDetails details) { if (dragStart != null) { ///滑动方向,向左或向右 SlideDirection slideDirection; ///滑动进度.[0, 1] double slidePercent = 0.0; ///当前触摸的点 final newPosition = details.globalPosition; ///拖动距离,如果大于零是向右拖动,如果小于零是向左拖动. ///当前点的x轴位置减去触摸起始点的x轴位置 final dx = newPosition.dx - dragStart.dx; slidePercent = (dx / FULL_TRANSITION_PX).abs().clamp(0.0, 1.0).toDouble(); if (dx > 0) { slideDirection = SlideDirection.leftToRight; } else if (dx < 0) { slideDirection = SlideDirection.rightToLeft; } else { slideDirection = SlideDirection.none; slidePercent = 0; } ... }}
动画处理
我们在触摸手势结束后开始动画处理,动画分为两个,一个是滑动成功的动画切换到下一个tab,一个是滑动失败(比如滑动距离很小,不需要跳转到下一个页面).这里的value是触摸手势的滑动比例和Animation的value,它们两个的值是相同的,这样可以有连贯的动画效果.
onAnimatedStart({SlideUpdate slideUpdate}) { Duration duration; ///判断是否成功, 滑动的值 是否大于我们设置的滑动成功的比例,我们这里设置的是0.5. _isSlideSuccess = value >= slideSuccessProportion; ///成功 if (_isSlideSuccess) { final slideRemaining = 1.0 - value; ///计算tab切换的时间 duration = Duration( milliseconds: (slideRemaining / PERCENT_PER_MILLISECOND).round()); _animationController.duration = duration; ///动画向前运行到1,动画结束后切换当前tab为下一页tab _animationController.forward(from: value).whenComplete(() => animationCompleted()); } else { ///失败,回退当当前tab duration = Duration(milliseconds: (value / PERCENT_PER_MILLISECOND).round()); _animationController.duration = duration; ///将动画值回退到0. _animationController.reverse(from: value); }}
效果自定义
这里用了AnyTabDelegate抽象类,我们可以继承这个抽象类来实现任意效果.这样做最大的好处就是分离ui和逻辑的处理.
abstract class AnyTabDelegate { ///tab列表 List<Widget> tabs; AnyTabDelegate({@required this.tabs}); int get length => tabs.length; ///逻辑处理后调用的build Widget build( BuildContext context, ///当前tab页 int activeIndex, ///下一页 int nextPageIndex, ///动画值,它的value就是手势触摸的值和动画执行的值. Animation animation, ///触摸的初始点,用于动画的初始点 Offset startingOffset, );}
这里我们来看一下CircularAnyTabDelegate的实现,这里我们用了ClipOval来剪裁下一页要显示的tab,如果传入的percentage是0则完全不显示,是1这完全显示.
class CircularAnyTabDelegate extends AnyTabDelegate { CircularAnyTabDelegate({@required List<Widget> tabs}) : assert(tabs != null && tabs.length > 0), super(tabs: tabs); @override Widget build(BuildContext context, int activeIndex, int nextPageIndex, Animation animation, Offset startingOffset) { return Stack( children: [ tabs[activeIndex], ClipOval( clipper: CircularClipper( percentage: animation.value, offset: startingOffset, ), child: tabs[nextPageIndex], ) ], ); }}
再往下看一下CircularClipper的代码.
class CircularClipper extends CustomClipper<Rect> { ///百分比, 0-> 1,1 => 全部显示 final double percentage; ///初始点 final Offset offset; const CircularClipper({this.percentage = 0, this.offset = Offset.zero}); @override Rect getClip(Size size) { ///计算触摸初始点到边缘四个角的最大距离,也就是我们剪裁圆的半径 double maxValue = maxLength(size, offset) * percentage; return Rect.fromLTRB(-maxValue + offset.dx, -maxValue + offset.dy, maxValue + offset.dx, maxValue + offset.dy); } @override bool shouldReclip(CircularClipper oldClipper) { return percentage != oldClipper.percentage || offset != oldClipper.offset; } /// | /// 1 | 2 /// --------- /// 3 | 4 /// | /// 计算矩形内点到边缘的最大距离,这里我们把矩形分成四块, /// 点在那一块,最大的距离就是这个点到对角矩形最远那个点的距离 double maxLength(Size size, Offset offset) { double centerX = size.width / 2; double centerY = size.height / 2; if (offset.dx < centerX && offset.dy < centerY) { ///1 return getEdge(size.width - offset.dx, size.height - offset.dy); } else if (offset.dx > centerX && offset.dy < centerY) { ///2 return getEdge(offset.dx, size.height - offset.dy); } else if (offset.dx < centerX && offset.dy > centerY) { ///3 return getEdge(size.width - offset.dx, offset.dy); } else { ///4 return getEdge(offset.dx, offset.dy); } } double getEdge(double width, double height) { return sqrt(pow(width, 2) + pow(height, 2)); }}
实现的效果如下:
这里触摸的逻辑处理参考的是阿里的flutter-go tab的实现.
...

- 0