效果图

Flutter自定义widget 纯手撸一个循环滚动的组件(包含手势和动画)

这是看了RenderObject的源码,写的一个总结性的小widget。运用到了Flutter里的自定义RenderObjectWidget 手势 动画等知识点。

该widget完成的功能

  • 左右无限循环翻滚
  • 模仿物理阻尼滑动
  • 滑动结束主动归位
缘起

之前项目中有用到首页Banner的组件(左右无限滑动,主动切换),那会一向奉行拿来主义有轮子能跑就行,不去重复造轮子。所以在网上找了受欢迎的几个组件,比较猎奇都是怎么完成的无限切换。大致分为两类:

  1. 定义一个很大的ListView比方30w个,然后把当时的方位定位到中间第15w个下标,以这样的方法完成左右无限滑动,左右两边都能够滑15w次,从日常运用的视点来讲完成了无限滑动。
  2. 在实践的轮播数量之上额定再加上2个widget,第1项前面参加最终1项widget,最终1项的后边参加第1项widget。比方有6个轮播图,那实践顺序便是F A B C D E F A。默许显现下标1的widgetA,当往右边滑动时就会到下标0显现出F,在完结切换到下标0后重新设置PageView的方位为(n-2),也便是下标6对应的F完成无限滑动, 左滑同理

这两种方法都是运用结构提供的根底组建封装而成的,榜首种方法从程序的视点来讲是伪无限,第二种方法是真无限可是在榜首项和最终一项会停住没法接连翻滚 像这个姿态:

Flutter自定义widget 纯手撸一个循环滚动的组件(包含手势和动画)

我期望的姿态是这样:

Flutter自定义widget 纯手撸一个循环滚动的组件(包含手势和动画)

无限滑动

前面有说到的两种计划比较容易完成,代码量很少运用现有的滑动widget(ListView和PageView)即可。这里开端正式讲不凭借ListView、PageView来完成无限滑动的逻辑。

忘掉上面的两种方法,咱们这里是经过RenderObject完成那么一切的widget都听我号令。只需想通什么是无限循环—-即首位相连,榜首个前面的永远是最终一个,最终一个的前面永远是榜首个。

先看完好的布局进程,实践只会显现中间无阴影的部分。这里是为了便于了解,将布局都绘制出来,用阴影遮挡表明该区域不行见。

为了便利描绘红色的1取名为firstChildfirstChild处于可见区域时布局应该是这个姿态,才能确保右滑的时分左面能显现出正确的widget。

初始状况的姿态

Flutter自定义widget 纯手撸一个循环滚动的组件(包含手势和动画)
firstChild右滑一格
Flutter自定义widget 纯手撸一个循环滚动的组件(包含手势和动画)
firstChild左滑一格
Flutter自定义widget 纯手撸一个循环滚动的组件(包含手势和动画)
firstChild左滑两格
Flutter自定义widget 纯手撸一个循环滚动的组件(包含手势和动画)
为了便利核算没有采用两边widget平均摆放的布局方法。而是左面只放一个,右边 按顺序摆放。附上动态滑动的gif

右滑

Flutter自定义widget 纯手撸一个循环滚动的组件(包含手势和动画)

左滑

Flutter自定义widget 纯手撸一个循环滚动的组件(包含手势和动画)

看到这里其实无限滑动的原理现已十分显着了,依据左右滑动的方向,和间隔来动态的布局就能够了 所谓知易行难,原理是看理解了,那么这个动态布局怎么去完成呢?

布局

要害点在于红色1的wiget下文用firstChild来代替。firstChild是榜首个子widget,后续的其他子widget布局方位都依赖于firstChild。也便是说咱们只需要管理好firstChild的方位就能够了。管理firstChild也便是管理好它在左滑和右滑时的方位改换

前置条件:子widget同宽,可见区域的巨细等于子widget的巨细

左右鸿沟:左面界(-size.width) 右鸿沟((n-1)*size.width)

firstChild左滑的方位改换

Flutter自定义widget 纯手撸一个循环滚动的组件(包含手势和动画)
firstChild超越黄线的方位-size.width时(上图),就把它放到紫6的后边(n-1)*size.width(下图)
Flutter自定义widget 纯手撸一个循环滚动的组件(包含手势和动画)

firstChild右滑的方位改换

Flutter自定义widget 纯手撸一个循环滚动的组件(包含手势和动画)
当firstChild超越黄线方位(n-1)*size.width时(上图),又把它放到最左面的方位-size.width(下图)
Flutter自定义widget 纯手撸一个循环滚动的组件(包含手势和动画)

正常滑动

其他情况下就依据手势滑动的方向和间隔线性加减就行了。仅仅到了这两个鸿沟值时进行方位互换代码如下

//firstChild到达右鸿沟
if (_offset.dx >= (count - 1) * size.width) {
  double d = _offset.dx - (count - 1) * size.width;
  _offset = Offset(-size.width + d, 0);
  data.offset = _offset;
} 
//first到达左面界
else if (_offset.dx < -size.width) {
  double d = _offset.dx + size.width;
  _offset = Offset((count - 1) * size.width + d, 0);
}
其他子widget的方位处理

前面提过只需要处理好firstChild的方位就行了,为什么呢?由于咱们的布局是一个线性摆放的布局而且子widget宽高相等,只需知道开始方位当时是第几个就能够算出来精确的方位。比方第n个child在x轴上的偏移量便是double currentDx = n * wdith + firstChild.dx 乘以n个宽度+firstChild在x轴的偏移量。然后处理下超越右鸿沟的情况即可:用左面界的方位+超出右鸿沟的间隔

//核算i的方位:乘以i个宽度+firstChild在x轴的偏移量
Offset _next = Offset(i * size.width + _offset.dx, 0);
//超越右鸿沟,
if (_next.dx >= (count - 1) * size.width) {
  //核算溢出右鸿沟的间隔
  double overflowOffset = _next.dx - (count - 1) * size.width;
  //左面界的方位+超出的间隔
  _next = Offset(overflowOffset - size.width, 0);
}

到这里一个无限滑动的widget现已完结百分之99了,剩余的便是加上水平滑动手势。把滑动的数据间隔传给firstChild即可

void _dragOnUpdate(DragUpdateDetails details) {
  _offset = Offset(details.delta.dx + _offset.dx, 0);
  markNeedsLayout();}
阻尼滑动

想要在松开手指后,使widget继续坚持滑动就需要在手势识别器的onEnd方法上做文章。在onEnd回调中会传一个手指脱离时滑动的速度primaryVelocity有了初始速度咱们就能够模仿出列表滑动的整个衰减进程。 依据物理公式v = v0+at。能够假定一个加速度a=300,先核算出动画执行的时刻t。

t = (v-v0)/a 最终的速度肯定为0,所以直接用v0/a的绝对值便是t。也便是primaryVelocity/300。由于快速滑动时permiaryVelocity很大,简单点便是把t约束在1-3秒以内。

依据位移公式:s=v0t+at核算滑动的间隔s

double a = 300;
double t = (math.max(math.min((v / a).abs(), 3), 1));
double s = 0;
//  位移公式:s=v0t+at
s = v.abs() * t + a * t * t / 2;
//s缩小十倍,恢复运动的方向
s = s / 10 * (v > 0 ? 1 : -1);

s缩小十倍是由于核算出来的间隔太大了,导致动画播映的时分特别快。 有了动画时刻t和手指脱离后需要滑动的间隔就能够写动画了,在手指脱离后播映动画。要害代码如下:

//用于估值当时动画值所滑动的间隔
var tween = Tween<double>(begin: 0.0, end: s)
    .chain(CurveTween(curve: Curves.easeOutExpo));
animation?.dispose();
animation = null;
//上次翻滚的间隔
double lastS = 0;
animation ??= AnimationController(
    vsync: ticker, duration: Duration(seconds: t.abs().ceil()))
  ..addListener(() {
    double currentS = -tween.evaluate(animation!);
    if (currentS != 0) {
      //增量核算滑动的间隔(_offset是firstChild的坐标)
      _offset = Offset(_offset.dx + (lastS - currentS), 0);
    }
    lastS = currentS;
    markNeedsLayout();
  });
animation!.forward(from: 0);

到这里手指脱离屏暗地,模仿阻尼滑动的动画也现已完结了,仅仅s的随机性不能确保widget和可视区域对齐。接下来便是最终一步,主动对齐

主动归位

前面说到在手指脱离后的阻尼滑动结束时无法对齐,是由于s是依据primaryVelocity核算的,每一次速度不同就会导致停在不同的方位。那么想要他主动对齐也很简单,便是让firstChild_offset+s的值是可见区域width的倍数就行了。 代码如下:

//中止的方位
double endPos = _offset.dx + s;
//取余数
double remPos = endPos % size.width;
//补整
double complement =
    remPos < (size.width / 2) ? -remPos : size.width - remPos;
s = s + complement;

4行代码搞定主动归位。

写在最终

这两天一向在构思这个循环滑动的widge怎么完成,在笔记本上整整图画了两页手稿,最终决定以这种方法完成,算是比较偏高级一点的自定义widget,包容的内容也比较丰富(手势、动画、RenderObject),可是整个代码量才100多行,可读性还是很强的,后续链接贴在谈论区供有需要的同学浏览。欢迎小伙伴在谈论区留言评论