欢迎关注微信公众号:FSA全栈行动 👋
一、概述
在上一篇 【Flutter - 轻松实现PageView卡片偏移效果】 中已经详细讲解过观察 PageView
所需要的具体步骤,今天基于此我们继续再来实现一个炫酷的视差效果,如下图所示。
二、研发
基础页面搭建
初始化 PageController
dart
late PageController pageController;
/// 图片数据
List<String> pageItemBgPicList = [
'xxxxxx',
...
];
int get pageItemCount => pageItemBgPicList.length;
@override
void initState() {
super.initState();
pageController = PageController(
// 初始化下标: 4
initialPage: 4,
// 视口占比: 0.9
viewportFraction: 0.9,
);
}
构建 PageView
dart
Widget _buildPageView() {
Widget resultWidget = PageView.builder(
controller: pageController,
itemBuilder: (context, index) {
return _buildPageItem(index);
},
itemCount: pageItemCount,
);
...
return resultWidget;
}
构建 item
dart
Widget _buildPageItem(int index) {
Widget resultWidget = Stack(
alignment: AlignmentDirectional.center,
children: [
Positioned(
left: 0,
right: 0,
top: 0,
bottom: 0,
child: _buildPageItemBgPicView(index),
),
const SizedBox.expand(),
_buildNum(index),
],
);
resultWidget = Container(
margin: const EdgeInsets.symmetric(horizontal: 8),
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: Colors.blue[50],
borderRadius: BorderRadius.circular(10),
),
child: resultWidget,
);
return resultWidget;
}
// 背景图片
Widget _buildPageItemBgPicView(int index) {
return Image.network(
pageItemBgPicList[index],
fit: BoxFit.cover,
);
}
实现解析
dart
Widget _buildPageItemBgPicView(int index) {
return Image.network(
pageItemBgPicList[index],
fit: BoxFit.cover,
// x 与 y均居中对齐
alignment: Alignment(0, 0),
// x 左对齐,y 居中对齐
// alignment: Alignment(-1, 0),
// x 右对齐,y 居中对齐
// alignment: Alignment(1, 0),
);
);
上述的 _buildPageItemBgPicView
只是简单展示了图片,使用的 fit
为 BoxFit.cover
,其作用是对图片进行等比例放大,然后填满整个容器。
这里附上各种填充模式的对比
接下来我们来讲讲 alignment
,因为它是实现视差效果的关键。
alignment
用于定义边界内的对齐方式,构造函数为 Alignment(this.x, this.y)
,其 x
和 y
的取值范围都为 [-1, 1]
。
例如:
- 当值为
(-1.0, -1.0)
时,图像将与其布局边界的左上角对齐 - 当值为
(1.0, 1.0)
时,图像将与其布局边界的右下角对齐。
不过本篇只需要用到 x
,以下图为例
left_center | center | right_center |
---|---|---|
Alignment(-1, 0) |
Alignment(0, 0) |
Alignment(1, 0) |
图片 居左 对齐 |
图片 居中 对齐 |
图片 居右 对齐 |
此处以中间的截图(center
)为例进行说明:
- 中间白色边框的视图为
item
- 两侧的毛玻璃是图片在
item
上的不可见区域
这里再次附上效果图,方便理解
- 一开始,
Page4
在最右侧,此时alignment
为Alignment(-1, 0)
,图片展示最左侧的内容(left_center
) - 在慢慢移入到中间完全展示出来的过程中,
alignment.x
一直在增加,直至为0
,即Alignment(0, 0)
,图片展示中间的内容(center
) - 然后再继续慢慢向左侧移出,其
alignment.x
继续增加,直至为1
,即Alignment(1, 0)
,图片展示最右侧的内容(right_center
)
所以视差的实现很简单,就是对 alignment.x
进行不断的调整,范围为 [-1, 1]
,那怎么在滑动的过程算出这个值呢?
这里又使用到的了我写的滚动视图观察库: github.com/fluttercand...
观察 PageView
我们先来对 item
进行改造
dart
// 图片数据
List<String> pageItemBgPicList = [
'xxxxxx',
...
];
int get pageItemCount => pageItemBgPicList.length;
/// 存放了各个 item 的 alignment.x
List<ValueNotifier<double>> pageItemBgPicAlignmentXList = [];
@override
void initState() {
super.initState();
...
// 根据 item 的数量创建对应数量的 ValueNotifier<double>
pageItemBgPicAlignmentXList = List.generate(
pageItemCount,
(index) {
return ValueNotifier<double>(0);
},
);
...
}
dart
Widget _buildPageItemBgPicView(int index) {
// 使用 ValueListenableBuilder 对对应下标的 alignment.x 进行监听与视图刷新
return ValueListenableBuilder(
valueListenable: pageItemBgPicAlignmentXList[index],
builder: (BuildContext context, double alignmentX, Widget? child) {
return Image.network(
pageItemBgPicList[index],
fit: BoxFit.cover,
alignment: Alignment(alignmentX, 0),
);
},
);
}
接下来就是对 PageView
进行观察,用法很简单:
- 使用
ListViewObserver
将PageView
包裹起来 - 设置
triggerOnObserveType
为.directly
不做显示item
变化对比,直接将获取到的观察数据返回 - 在观察结果回调
onObserve
中,取出item
的相关数据(下标、可视区域占比等)进行计算
dart
final observerController = ListObserverController();
Widget _buildPageView() {
Widget resultWidget = PageView.builder(
...
);
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;
// 计算无符号的 alignment.x
double itemAlignmentX = 1 - itemDisplayPercentage;
// 计算有符号的 alignment.x
if (itemModel.leadingMarginToViewport > 0) {
itemAlignmentX = -itemAlignmentX;
}
// 取值范围判断
if (itemAlignmentX > 1) {
itemAlignmentX = 1;
} else if (itemAlignmentX < -1) {
itemAlignmentX = -1;
}
// 赋值
pageItemBgPicAlignmentXList[itemIndex].value = itemAlignmentX;
}
},
customTargetRenderSliverType: (renderObj) {
return renderObj is RenderSliverFillViewport;
},
);
...
return resultWidget;
}
关于无符号的 alignment.x
的计算
- 无论是从左侧滑入到中间,还是从右侧滑入到中间,
item
的displayPercentage
都是0 -> 1
- 对应的
alignment.x
是1 -> 0
或-1 -> 0
,去除符号就是1 -> 0
所以这里用 1
减去 itemDisplayPercentage
即可得到无符号的 alignment.x
。
那接下来的问题就是,当 alignment.x == 1
时,怎么知道此时的 item
是从左侧滑出还是右侧滑出呢?
关于 alignment.x
符号的计算
在有了 github.com/fluttercand... 之后,这一切又变得很简单,这里就不得不提到观察结果中两个非常有用的属性
leadingMarginToViewport
:item
距离视口顶部的距离trailingMarginToViewport
:item
距离视口尾部的距离
这里我们用其中一个就够了,就比如使用 leadingMarginToViewport
,当 item
的左侧顶部触碰到 PageView
的视口时,其值为 0
,继续往左侧移出,其时该值就会成为负数,如果往右侧移出,该值则为正数,如下图所示
左侧滑出 | 完全展示 | 右侧滑出 |
---|---|---|
Alignment(1, 0) |
Alignment(0, 0) |
Alignment(-1, 0) |
leadingMarginToViewport: -300 |
leadingMarginToViewport: 0 |
leadingMarginToViewport: 375 |
很显然,当 leadingMarginToViewport > 0
时,alignment.x < 0
,即 alignment.x
符号的计算如下:
dart
if (itemModel.leadingMarginToViewport > 0) {
itemAlignmentX = -itemAlignmentX;
}
好了,大功告成~
三、最后
通过上述示例的讲解,相信你对 scrollview_observer
的使用又更加清楚,开源不易,如果你也觉得这个库好用,请不吝给个 Star
👍 ,并多多支持!
GitHub: github.com/fluttercand...
本篇到此结束,感谢大家的支持,我们下次再见! 👋
系列文章
- Flutter - 获取ListView当前正在显示的Widget信息
- Flutter - 列表滚动定位超强辅助库,墙裂推荐!🔥
- Flutter - 快速实现聊天会话列表的效果,完美💯
- Flutter - 船新升级😱支持观察第三方构建的滚动视图💪
- Flutter - 瀑布流交替播放视频 🎞
- Flutter - IM保持消息位置大升级(支持ChatGPT生成式消息) 🤖
- Flutter - 滚动视图中的表单防遮挡 🗒
- Flutter - 秒杀1/2曝光统计 📊
- Flutter - 如何快速搓一个微信通讯录列表(azlist) 📓
- Flutter - 支持观察NestedScrollView,兼容性更强 😈
- Flutter - 轻松实现PageView卡片偏移效果
如果文章对您有所帮助, 请不吝点击关注一下我的微信公众号:FSA全栈行动, 这将是对我最大的激励. 公众号不仅有
iOS
技术,还有Android
,Flutter
,Python
等文章, 可能有你想要了解的技能知识点哦~