背景
由于这篇总结是产品需求驱动的,先简要描述下 Sofanovel 项目的需求:仿照 inkitt 首页,实现个带有 hover 效果的横向列表,我们先直接来看下最后实现效果:
解决思路
这个需求在 iOS 原生的 UIKIt 下 很好解决的,UIScrollView 本来就有个 paging 的属性,来实现这个 “翻页” 效果。而 Flutter 也有个类似的控件 PageView, 我们先来看下 PageView 的实现:
PageView
普通的 PageView 实现是这样的:1
2
3
4
5
6
7
8
9
10
11
12
13return Container(
height: 200,
width: 200,
child: PageView(
children: TestDatas.map((color) {
return Container(
width: 100,
height: 200,
color: color,
);
}).toList(),
),
)
效果是 width 永远不受控制,充满屏幕,如图:
另一种实现:
加上 PageController 的 viewportFraction 修饰:1
2
3
4
5
6
7
8
9
10
11
12
13return Container(
height: 200,
child: PageView(
controller: PageController(initialPage: 0, viewportFraction: 0.8),
children: TestDatas.map((color) {
return Container(
width: 100,
height: 200,
color: color,
);
}).toList(),
),
)
实现效果是这个样子的:
viewportFraction 这个参数只能粗略地表示 选中区域 占屏幕的百分比,而这个区域永远落在中央,不能简单实现偏左或者偏右的自定义化,因此舍弃了 pageView 的实现。
ListView
赋予翻页效果
从横向布局的 ListView 入手开搞,自定义一个带有 pageView 特性的 physics
1 | class PagingScrollPhysics extends ScrollPhysics { |
代码一大堆,我们聚焦入口 createBallisticSimulation ,这是每次滑动手势结束后会触发,最终都是为了调用下面这句,来产生滑动效果:
1 | ScrollSpringSimulation(spring, position.pixels, target, velocity, |
target 这个参数是整个类的主角,其他辅助函数都是为了计算出这个值而已,target 是表示这次滑动的终点,也就是说,我们通过控制这个参数来控制这次触摸结束后,listview 停在哪里。
其次,构造方法里面里面的 parent 参数也是挺重要的,主要用来组合各种 physics 属性,这里留在后面再说。
选中动效
这一步无非就是用 scrollView 监听 scroll offset, 到了指定位置就 setState ,已触发选中效果。
1 | _scrollCtl.addListener(() { |
1 | _buildBookItem(Map data, bool active, {num width}) { |
后话
在自测时发现过这样一个问题:当 listView 里面的 children 过少时, 整个 listView 压根不能滑动, physics 里面的 createBallisticSimulation 实现得再完美,也触发不了其中的回调的。为了避免这种情况,比较粗暴的方法是,在 children 加空白 Container,以充满 listView 固有的宽度或者高度,来让 listView 满足可滑动的前提。
正规军解法
为何 chidren 过少就滑动不了?这里要看下 ScrollPhysics 的源码了,里面有这样一个方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/// Whether the scrollable should let the user adjust the scroll offset, for
/// example by dragging.
///
/// By default, the user can manipulate the scroll offset if, and only if,
/// there is actually content outside the viewport to reveal.
///
/// The given `position` is only valid during this method call. Do not keep a
/// reference to it to use later, as the values may update, may not update, or
/// may update to reflect an entirely unrelated scrollable.
bool shouldAcceptUserOffset(ScrollMetrics position) {
if (parent == null)
return position.pixels != 0.0 || position.minScrollExtent != position.maxScrollExtent;
return parent.shouldAcceptUserOffset(position);
}
源码里面注释得很清楚了,唯有内容超出显示范围时,才可以触发他的滚动,即 position.minScrollExtent != position.maxScrollExtent 的时候。
所以,我们重载一下这个方法就可以了。
1 | @override |
另外,也可以通过构造方法 parent 这个入参去组合多个的已有的 physics 来完成这种特性:1
2
3
4
5_physics = PagingScrollPhysics(
itemDimension: itemWidth,
leadingSpacing: _leadingPortion,
maxSize: itemWidth * (testData.length - 1) - _leadingPortion,
parent: BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()));