Flutter - 轻松搞定炫酷视差(Parallax)效果

欢迎关注微信公众号: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 只是简单展示了图片,使用的 fitBoxFit.cover,其作用是对图片进行等比例放大,然后填满整个容器。

这里附上各种填充模式的对比

接下来我们来讲讲 alignment,因为它是实现视差效果的关键。

alignment 用于定义边界内的对齐方式,构造函数为 Alignment(this.x, this.y),其 xy 的取值范围都为 [-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 在最右侧,此时 alignmentAlignment(-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 进行观察,用法很简单:

  1. 使用 ListViewObserverPageView 包裹起来
  2. 设置 triggerOnObserveType.directly 不做显示 item 变化对比,直接将获取到的观察数据返回
  3. 在观察结果回调 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 的计算

  1. 无论是从左侧滑入到中间,还是从右侧滑入到中间, itemdisplayPercentage 都是 0 -> 1
  2. 对应的 alignment.x1 -> 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;
}

好了,大功告成~

完整代码: github.com/fluttercand...

三、最后

通过上述示例的讲解,相信你对 scrollview_observer 的使用又更加清楚,开源不易,如果你也觉得这个库好用,请不吝给个 Star 👍 ,并多多支持!

GitHub: github.com/fluttercand...

本篇到此结束,感谢大家的支持,我们下次再见! 👋


系列文章

  1. Flutter - 获取ListView当前正在显示的Widget信息
  2. Flutter - 列表滚动定位超强辅助库,墙裂推荐!🔥
  3. Flutter - 快速实现聊天会话列表的效果,完美💯
  4. Flutter - 船新升级😱支持观察第三方构建的滚动视图💪
  5. Flutter - 瀑布流交替播放视频 🎞
  6. Flutter - IM保持消息位置大升级(支持ChatGPT生成式消息) 🤖
  7. Flutter - 滚动视图中的表单防遮挡 🗒
  8. Flutter - 秒杀1/2曝光统计 📊
  9. Flutter - 如何快速搓一个微信通讯录列表(azlist) 📓
  10. Flutter - 支持观察NestedScrollView,兼容性更强 😈
  11. Flutter - 轻松实现PageView卡片偏移效果

如果文章对您有所帮助, 请不吝点击关注一下我的微信公众号:FSA全栈行动, 这将是对我最大的激励. 公众号不仅有 iOS 技术,还有 AndroidFlutterPython 等文章, 可能有你想要了解的技能知识点哦~

相关推荐
m0_748247551 小时前
Web 应用项目开发全流程解析与实战经验分享
开发语言·前端·php
m0_748255022 小时前
前端常用算法集合
前端·算法
真的很上进2 小时前
如何借助 Babel+TS+ESLint 构建现代 JS 工程环境?
java·前端·javascript·css·react.js·vue·html
web130933203982 小时前
vue elementUI form组件动态添加el-form-item并且动态添加rules必填项校验方法
前端·vue.js·elementui
NiNg_1_2343 小时前
Echarts连接数据库,实时绘制图表详解
前端·数据库·echarts
如若1233 小时前
对文件内的文件名生成目录,方便查阅
java·前端·python
滚雪球~4 小时前
npm error code ETIMEDOUT
前端·npm·node.js
沙漏无语4 小时前
npm : 无法加载文件 D:\Nodejs\node_global\npm.ps1,因为在此系统上禁止运行脚本
前端·npm·node.js
supermapsupport4 小时前
iClient3D for Cesium在Vue中快速实现场景卷帘
前端·vue.js·3d·cesium·supermap
brrdg_sefg4 小时前
WEB 漏洞 - 文件包含漏洞深度解析
前端·网络·安全