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 等文章, 可能有你想要了解的技能知识点哦~

相关推荐
excel6 分钟前
webpack 核心编译器 十四 节
前端
excel13 分钟前
webpack 核心编译器 十三 节
前端
腾讯TNTWeb前端团队7 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰11 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪11 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪11 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy11 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom12 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom12 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom12 小时前
React与Next.js:基础知识及应用场景
前端·面试·github