Flutter - 轻松搞定炫酷视差(Parallax)效果
# 一、概述
在上一篇 【Flutter - 轻松实现PageView卡片偏移效果】 中已经详细讲解过观察 PageView
所需要的具体步骤,今天基于此我们继续再来实现一个炫酷的视差效果,如下图所示。
# 二、研发
# 基础页面搭建
初始化 PageController
late PageController pageController;
/// 图片数据
List<String> pageItemBgPicList = [
'xxxxxx',
...
];
int get pageItemCount => pageItemBgPicList.length;
void initState() {
super.initState();
pageController = PageController(
// 初始化下标: 4
initialPage: 4,
// 视口占比: 0.9
viewportFraction: 0.9,
);
}
构建 PageView
Widget _buildPageView() {
Widget resultWidget = PageView.builder(
controller: pageController,
itemBuilder: (context, index) {
return _buildPageItem(index);
},
itemCount: pageItemCount,
);
...
return resultWidget;
}
构建 item
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,
);
}
# 实现解析
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]
,那怎么在滑动的过程算出这个值呢?
这里又使用到的了我写的滚动视图观察库: https://github.com/fluttercandies/flutter_scrollview_observer (opens new window)
# 观察 PageView
我们先来对 item
进行改造
// 图片数据
List<String> pageItemBgPicList = [
'xxxxxx',
...
];
int get pageItemCount => pageItemBgPicList.length;
/// 存放了各个 item 的 alignment.x
List<ValueNotifier<double>> pageItemBgPicAlignmentXList = [];
void initState() {
super.initState();
...
// 根据 item 的数量创建对应数量的 ValueNotifier<double>
pageItemBgPicAlignmentXList = List.generate(
pageItemCount,
(index) {
return ValueNotifier<double>(0);
},
);
...
}
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
的相关数据(下标、可视区域占比等)进行计算
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
符号的计算
在有了 https://github.com/fluttercandies/flutter_scrollview_observer (opens new window) 之后,这一切又变得很简单,这里就不得不提到观察结果中两个非常有用的属性
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
符号的计算如下:
if (itemModel.leadingMarginToViewport > 0) {
itemAlignmentX = -itemAlignmentX;
}
好了,大功告成~
# 三、最后
通过上述示例的讲解,相信你对 scrollview_observer
的使用又更加清楚,开源不易,如果你也觉得这个库好用,请不吝给个 Star
👍
GitHub: https://github.com/fluttercandies/flutter_scrollview_observer (opens new window)
本篇到此结束,感谢大家的支持,我们下次再见! 👋
- 01
- Flutter - 轻松实现PageView卡片偏移效果09-08
- 02
- Flutter - 升级到3.24后页面还会多次rebuild吗?🧐08-11
- 03
- Flutter - 聊天键盘与面板丝滑切换的强势升级 🍻08-04