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;
    }
   ...
  }
}
复制代码

动画处理
ps;如果你对Flutter,这有个iOS技术交流
我们在触摸手势结束后开始动画处理,动画分为两个,一个是滑动成功的动画切换到下一个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地址
小结;希望可以对大家有所帮助,谢谢你的阅读,动动小手点个赞吧。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容