背景
由于这篇总结是产品需求驱动的,先简要描述下 Sofanovel 项目的需求:仿照 inkitt 首页,实现个带有 hover 效果的横向列表,我们先直接来看下最后实现效果:
解决思路
这个需求在 iOS 原生的 UIKIt 下 很好解决的,UIScrollView 本来就有个 paging 的属性,来实现这个 “翻页” 效果。而 Flutter 也有个类似的控件 PageView, 我们先来看下 PageView 的实现:
PageView
普通的 PageView 实现是这样的:
return Container(
height: 200,
width: 200,
child: PageView(
children: TestDatas.map((color) {
return Container(
width: 100,
height: 200,
color: color,
);
}).toList(),
),
)
效果是 width 永远不受控制,充满屏幕,如图:
另一种实现: 加上 PageController 的 viewportFraction 修饰:
return 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
class PagingScrollPhysics extends ScrollPhysics {
final double itemDimension; // ListView children item 固定宽度
final double leadingSpacing; // 选中 item 离左边缘留白
final double maxSize; // 最大可滑动区域
PagingScrollPhysics(
{this.maxSize,
this.leadingSpacing,
this.itemDimension,
ScrollPhysics parent})
: super(parent: parent);
@override
PagingScrollPhysics applyTo(ScrollPhysics ancestor) {
return PagingScrollPhysics(
maxSize: maxSize,
itemDimension: itemDimension,
leadingSpacing: leadingSpacing,
parent: buildParent(ancestor));
}
double _getPage(ScrollPosition position, double leading) {
return (position.pixels + leading) / itemDimension;
}
double _getPixels(double page, double leading) {
return (page * itemDimension) - leading;
}
double _getTargetPixels(
ScrollPosition position,
Tolerance tolerance,
double velocity,
double leading,
) {
double page = _getPage(position, leading);
if (position.pixels < 0) {
return 0;
}
if (position.pixels >= maxSize) {
return maxSize;
}
if (position.pixels > 0) {
if (velocity < -tolerance.velocity) {
page -= 0.5;
} else if (velocity > tolerance.velocity) {
page += 0.5;
}
return _getPixels(page.roundToDouble(), leading);
}
}
@override
Simulation createBallisticSimulation(
ScrollMetrics position, double velocity) {
// If we're out of range and not headed back in range, defer to the parent
// ballistics, which should put us back in range at a page boundary.
if ((velocity <= 0.0 && position.pixels <= position.minScrollExtent))
return super.createBallisticSimulation(position, velocity);
final Tolerance tolerance = this.tolerance;
final double target =
_getTargetPixels(position, tolerance, velocity, leadingSpacing);
if (target != position.pixels)
return ScrollSpringSimulation(spring, position.pixels, target, velocity,
tolerance: tolerance);
return null;
}
@override
bool get allowImplicitScrolling => false;
}
代码一大堆,我们聚焦入口 createBallisticSimulation ,这是每次滑动手势结束后会触发,最终都是为了调用下面这句,来产生滑动效果:
ScrollSpringSimulation(spring, position.pixels, target, velocity,
tolerance: tolerance);
target 这个参数是整个类的主角,其他辅助函数都是为了计算出这个值而已,target 是表示这次滑动的终点,也就是说,我们通过控制这个参数来控制这次触摸结束后,listview 停在哪里。
其次,构造方法里面里面的 parent 参数也是挺重要的,主要用来组合各种 physics 属性,这里留在后面再说。
选中动效
这一步无非就是用 scrollView 监听 scroll offset, 到了指定位置就 setState ,已触发选中效果。
_scrollCtl.addListener(() {
double test =
_bookWidth != null ? _scrollCtl.offset / (_bookWidth + margin) : 1;
int next = test.round();
if (next < 0) {
next = 0;
}
if (next >= testData.length) {
next = testData.length - 1;
}
if (_currentPage != next) {
setState(() {
_currentPage = next;
});
}
});
_buildBookItem(Map data, bool active, {num width}) {
width = _bookWidth;
// Animated Properties
final double blur = active ? 5 : 0;
final double offset = active ? 2 : 0;
final double top = active ? 10 : 20;
final double bottom = active ? 10 : 20;
return GestureDetector(
onTap: () {
if (data['index'] == _currentPage) {
_jump();
} else {
scrollToPage(data['index']);
}
},
child: AnimatedContainer(
width: width,
height: 1.38 * width,
child: Center(child: Text(data['index'].toString())),
duration: Duration(milliseconds: 500),
curve: Curves.easeOutQuint,
margin: EdgeInsets.only(top: top, bottom: bottom, right: margin),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: randomColor,
boxShadow: [
BoxShadow(
color: Colors.black87,
blurRadius: blur,
offset: Offset(offset, offset))
]),
),
);
}
后话
在自测时发现过这样一个问题:当 listView 里面的 children 过少时, 整个 listView 压根不能滑动, physics 里面的 createBallisticSimulation 实现得再完美,也触发不了其中的回调的。为了避免这种情况,比较粗暴的方法是,在 children 加空白 Container,以充满 listView 固有的宽度或者高度,来让 listView 满足可滑动的前提。
正规军解法
为何 chidren 过少就滑动不了?这里要看下 ScrollPhysics 的源码了,里面有这样一个方法:
/// 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 的时候。 所以,我们重载一下这个方法就可以了。
@override bool shouldAcceptUserOffset(ScrollMetrics position) => true;
另外,也可以通过构造方法 parent 这个入参去组合多个的已有的 physics 来完成这种特性:
_physics = PagingScrollPhysics(
itemDimension: itemWidth,
leadingSpacing: _leadingPortion,
maxSize: itemWidth * (testData.length - 1) - _leadingPortion,
parent: BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()));
Author:Terrence
今天的文章Flutter 实战系列:个性化 ListView physics分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/18751.html