Flutter自定义widget 纯手撸一个循环滚动的组件(包含手势和动画)
效果图
这是看了RenderObject的源码,写的一个总结性的小widget。使用到了Flutter里的自定义RenderObjectWidget
手势
动画
等知识点。
该widget实现的功能
- 左右无限循环滚动
- 模拟物理阻尼滑动
- 滑动结束自动归位
缘起
之前项目中有用到首页Banner的组件(左右无限滑动,自动切换),那会一直奉行拿来主义有轮子能跑就行,不去重复造轮子。所以在网上找了受欢迎的几个组件,比较好奇都是怎么实现的无限切换。大致分为两类:
- 定义一个
很大的ListView比如30w个
,然后把当前的位置定位到中间第15w个下标
,以这样的方式实现左右无限滑动,左右两边都可以滑15w次
,从日常使用的角度来讲实现了无限滑动。 - 在实际的轮播数量之上额外再加上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
实现无限滑动, 左滑同理
这两种方式都是使用框架提供的基础组建封装而成的,第一种方式从程序的角度来讲是伪无限,第二种方式是真无限但是在第一项和最后一项会停住没法连续滚动 像这个样子:
我希望的样子是这样:
无限滑动
前面有提到的两种方案比较容易实现,代码量很少使用现有的滑动widget(ListView和PageView)即可。这里开始正式讲不借助ListView、PageView来实现无限滑动的逻辑。
忘掉上面的两种方式,我们这里是通过RenderObject实现那么一切的widget都听我号令。只要想通什么是无限循环----即首位相连
,第一个前面的永远是最后一个,最后一个的前面永远是第一个。
先看完整的布局过程,实际只会显示中间无阴影的部分。这里是为了便于理解,将布局都绘制出来,用阴影遮挡表示该区域不可见。
为了方便描述红色的1取名为firstChild
当firstChild
处于可见区域时布局应该是这个样子,才能保证右滑的时候左边能显示出正确的widget。
初始状态的样子
firstChild右滑一格
firstChild左滑一格
firstChild左滑两格
为了方便计算没有采用两边widget平均摆放的布局方式。而是左边只放一个,右边
按顺序摆放。附上动态滑动的gif
右滑
左滑
看到这里其实无限滑动的原理已经非常明显了,根据左右滑动的方向,和距离来动态的布局就可以了
所谓知易行难,原理是看明白了,那么这个动态布局怎么去实现呢?
布局
关键点在于红色1的wiget
下文用firstChild
来代替。firstChild
是第一个子widget,后续的其他子widget布局位置都依赖于firstChild
。也就是说我们只需要管理好firstChild
的位置就可以了。管理firstChild
也就是管理好它在左滑和右滑时的位置变换
前置条件:子widget同宽,可见区域的大小等于子widget的大小
左右边界:左边界(-size.width) 右边界((n-1)*size.width)
firstChild左滑的位置变换
当
firstChild
超过黄线的位置-size.width
时(上图),就把它放到紫6的后面(n-1)*size.width
(下图)
firstChild右滑的位置变换
正常滑动
其他情况下就根据手势滑动的方向和距离线性加减就行了。只是到了这两个边界值时进行位置调换代码如下
//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多行,可读性还是很强的,后续链接贴在评论区供有需要的同学浏览。欢迎小伙伴在评论区留言讨论
转载自:https://juejin.cn/post/7129030461770170375