美文网首页
利用Flutter 实现任意tab切换效果

利用Flutter 实现任意tab切换效果

作者: iOS三年 | 来源:发表于2020-12-15 16:11 被阅读0次

    处理和响应触摸效果

    我们可以用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));
      }
    }
    
    

    原文链接:https://juejin.cn/post/6900062734994538509

    相关文章

      网友评论

          本文标题:利用Flutter 实现任意tab切换效果

          本文链接:https://www.haomeiwen.com/subject/xpewgktx.html