Flutter - 轻松实现PageView卡片偏移效果
# 一、概述
在前不久的一个需求里,需要在点击地图页上的大头针时,页面底下显示对应的物件卡片,还可以左右切换,并且在切换的过程中需要突出展示当前的物件卡片,如下图所示。
接下来就来讲讲具体实现
# 二、研发
# 基础页面搭建
/// pageView 控制器
late PageController pageController;
/// item 数量
int pageItemCount = 10;
void initState() {
super.initState();
pageController = PageController(
// 初始化页的下标
initialPage: 4,
// 每一页占据的视口比例
viewportFraction: 0.9,
);
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("PageView"),
),
body: Stack(
children: [
// 底部地图
_buildMap(),
// PageView,用于显示地图大头针对应的一些物件信息
Positioned(
left: 0,
right: 0,
bottom: 0,
child: _buildPageView(),
),
],
),
);
}
底部的卡片视图是使用 PageView
来实现,具体代码如下所示:
Widget _buildPageView() {
Widget resultWidget = PageView.builder(
controller: pageController,
itemBuilder: (context, index) {
// 构建 item
Widget resultWidget = Container(
decoration: BoxDecoration(
color: Colors.blue[100],
borderRadius: BorderRadius.circular(4),
),
alignment: Alignment.center,
child: Text("Page $index"),
);
// 设置 item 的左右边距
resultWidget = Container(
margin: const EdgeInsets.symmetric(horizontal: 8),
child: resultWidget,
);
return resultWidget;
},
itemCount: pageItemCount,
);
// 固定 300 的高度
resultWidget = SizedBox(
height: 300,
child: resultWidget,
);
return resultWidget;
}
以上的视图布局对大家来说都很简单,大致看看就可以了,接下来就是如何实现这种偏移突出的效果。
# 观察 PageView
这里使用到的是我写的一个滚动视图观察库 https://github.com/fluttercandies/flutter_scrollview_observer (opens new window) 。
用法很简单:
- 使用
ListViewObserver
将PageView
包裹起来 - 设置
triggerOnObserveType
为.directly
不做显示item
变化对比,直接将获取到的观察数据返回 - 在观察结果回调
onObserve
中,取出item
的相关数据(下标、可视区域占比等)进行计算
final observerController = ListObserverController();
Widget _buildPageView() {
Widget resultWidget = PageView.builder(
...
);
// 使用 ListViewObserver 包裹 PageView
resultWidget = ListViewObserver(
controller: observerController,
child: resultWidget,
// 直接将观察结果返回出来
triggerOnObserveType: ObserverTriggerOnObserveType.directly,
// 处理观察结果
onObserve: (resultModel) {
final displayingChildModelList = resultModel.displayingChildModelList;
for (var itemModel in displayingChildModelList) {
// 取出 item 的下标
final itemIndex = itemModel.index;
// 取出 item 的可视区域占比
final itemDisplayPercentage = itemModel.displayPercentage;
// 计算偏移
final offsetY = (1 - itemDisplayPercentage) * offsetYDelta;
pageItemOffsetYList[itemIndex].value = offsetY;
}
},
// 指定需要观察的 RenderSliver 对象
customTargetRenderSliverType: (renderObj) {
return renderObj is RenderSliverFillViewport;
},
);
...
return resultWidget;
}
triggerOnObserveType
和customTargetRenderSliverType
在之前的【Flutter - 船新升级😱支持观察第三方构建的滚动视图💪】一文中有详细的说明,这里就不再赘述。
这里重要说明一下,ListViewObserver
默认是处理 ListView
和 SliverList
的,但并不表示它只能处理它们,只要内部的树结构一致就可以处理,如果结构一致但 RenderSliver
与 SliverList
的不同,则可以通过 customTargetRenderSliverType
指定相应的 RenderSliver
类型即可,就比如这里的 PageView
,其 RenderSliver
的类型为 RenderSliverFillViewport
。
在这里附上 PageView
和 ListView
的树结构截图,看最右侧的 Widget Details Tree
,看完应该就能感受到什么叫结构一致了。
PageView
结构
ListView
结构
# item
的偏移实现
// item 的最大偏移量
double offsetYDelta = 50;
// 记录 item 的偏移量
List<ValueNotifier<double>> pageItemOffsetYList = [];
void initState() {
super.initState();
...
// 初始化
pageItemOffsetYList = List.generate(
pageItemCount,
(index) {
return ValueNotifier<double>(0);
},
);
// 延迟 100ms,触发一次观察
Future.delayed(const Duration(milliseconds: 100)).then((_) {
observerController.dispatchOnceObserve();
});
}
Widget _buildPageView() {
Widget resultWidget = PageView.builder(
controller: pageController,
itemBuilder: (context, index) {
Widget itemWidget = Container(
...
);
// item 局部刷新
Widget resultWidget = ValueListenableBuilder(
valueListenable: pageItemOffsetYList[index],
builder: (BuildContext context, double offsetY, Widget? child) {
// 偏移量发生变化
return Transform.translate(
offset: Offset(0, offsetY),
child: itemWidget,
);
},
);
resultWidget = Container(
...
);
return resultWidget;
},
itemCount: pageItemCount,
);
resultWidget = ListViewObserver(
...
onObserve: (resultModel) {
final displayingChildModelList = resultModel.displayingChildModelList;
for (var itemModel in displayingChildModelList) {
// 取出 item 的下标
final itemIndex = itemModel.index;
// 取出 item 的可视区域占比
final itemDisplayPercentage = itemModel.displayPercentage;
// 计算偏移
// item 的【可视占比区间】与【偏移区间】两者间的区间关系如下
// 可视占比区间: [0, 1]
// 偏移区间: [50, 0]
final offsetY = (1 - itemDisplayPercentage) * offsetYDelta;
// 更新当前 item 的偏移量
pageItemOffsetYList[itemIndex].value = offsetY;
}
},
...
);
resultWidget = SizedBox(
height: 300,
child: resultWidget,
);
return resultWidget;
}
关键变量说明
offsetYDelta
:item
的最大偏移量,这里是50
pageItemOffsetYList
: 记录各个item
的当前偏移量
更新 item
偏移说明
- 结合
pageItemOffsetYList
+ValueListenableBuilder
来局部刷新item
- 在
onObserve
中计算item
的偏移量,并更新pageItemOffsetYList
中对应下标的值 item
的【可视占比区间 ([0, 1]
)】与【偏移区间 ([50, 0]
)】成反比,所以用1
减去itemDisplayPercentage
成正向比例,再去乘以offsetYDelta
算出偏移
# 三、最后
通过上述示例的讲解,相信你对 scrollview_observer
的使用又更加清楚,开源不易,如果你也觉得这个库好用,请不吝给个 Star
👍
GitHub: https://github.com/fluttercandies/flutter_scrollview_observer (opens new window)
本篇到此结束,感谢大家的支持,我们下次再见! 👋
- 01
- Flutter - 轻松搞定炫酷视差(Parallax)效果09-21
- 02
- Flutter - 升级到3.24后页面还会多次rebuild吗?🧐08-11
- 03
- Flutter - 聊天键盘与面板丝滑切换的强势升级 🍻08-04