侧边栏壁纸
博主头像
落叶人生博主等级

走进秋风,寻找秋天的落叶

  • 累计撰写 130562 篇文章
  • 累计创建 28 个标签
  • 累计收到 9 条评论
标签搜索

目 录CONTENT

文章目录

Flutter 实现任意tab切换效果

2022-06-08 星期三 / 0 评论 / 0 点赞 / 170 阅读 / 7006 字

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));  }}

实现的效果如下:

代码地址

Demo地址

.

这里触摸的逻辑处理参考的是阿里的flutter-go tab的实现.

..
.

广告 广告

评论区