FSA全栈行动 FSA全栈行动
首页
  • 移动端文章

    • Android
    • iOS
    • Flutter
  • 学习笔记

    • 《Kotlin快速入门进阶》笔记
    • 《Flutter从入门到实战》笔记
    • 《Flutter复习》笔记
前端
后端
  • 学习笔记

    • 《深入浅出设计模式Java版》笔记
  • 逆向
  • 分类
  • 标签
  • 归档
  • LinXunFeng
  • GitLqr

公众号:FSA全栈行动

记录学习过程中的知识
首页
  • 移动端文章

    • Android
    • iOS
    • Flutter
  • 学习笔记

    • 《Kotlin快速入门进阶》笔记
    • 《Flutter从入门到实战》笔记
    • 《Flutter复习》笔记
前端
后端
  • 学习笔记

    • 《深入浅出设计模式Java版》笔记
  • 逆向
  • 分类
  • 标签
  • 归档
  • LinXunFeng
  • GitLqr
  • AndroidUI

  • Android第三方SDK

  • Android混淆

  • Android仓库

  • Android新闻

  • Android系统开发

  • Android源码

  • Android注解AOP

  • Android脚本

  • AndroidTv开发

  • AndroidNDK

  • Android音视频

  • Android热修复

  • Android性能优化

  • Android云游戏

  • Android插件化

  • iOSUI

  • iOS工具

  • iOS底层原理与应用

  • iOS组件化

  • iOS音视频

  • iOS疑难杂症

  • iOS之Swift

  • iOS之RxSwift

  • iOS开源项目

  • iOS逆向

  • Flutter开发

    • Dart - 抽象类的实例化
    • Flutter - 打印好用的Debug日志
    • Flutter - 混合开发
    • Flutter - 解决混合开发iOS脚本打包遇到的问题
    • Flutter - 低版本在iOS14上遇到的问题与解决方案
    • Flutter - 解决原生弹窗的触摸事件被Flutter响应的问题
    • Flutter - 实现列表上下拉切换header
    • Flutter - 获取ListView当前正在显示的Widget信息
    • Flutter - 列表滚动定位超强辅助库,墙裂推荐!🔥
    • Flutter - 快速实现聊天会话列表的效果,完美💯
    • Flutter - 聊天输入框更新文本时的必备优化点🔖
    • Flutter - 我给官方提PR,解决run命令卡住问题 😃
    • Flutter - 探索run命令到底做了什么 🤔
    • Flutter - 引擎调试(iOS篇)🛠
    • Flutter - 引擎调试bug到提交PR实战 🐞
    • Flutter - 船新升级😱支持观察第三方构建的滚动视图💪
    • Flutter - 瀑布流交替播放视频 🎞
    • Flutter - IM保持消息位置大升级(支持ChatGPT生成式消息) 🤖
    • Flutter - 滚动视图中的表单防遮挡 🗒
    • Flutter - 秒杀1/2曝光统计 📊
    • 一天内加入 Flutter 和 FlutterCandies 两大组织是什么体验 🧐
    • Flutter - 如何快速搓一个微信通讯录列表(azlist) 📓
    • Flutter - 混编项目集成Shorebird热更新🐦(安卓篇)
    • Flutter - 混编项目集成Shorebird热更新🐦(iOS篇)
    • Flutter - 解决返回原生页面时dispose方法未被触发的问题 🐞
    • Flutter - 升级3.19之后页面多次rebuild?🤨
    • Flutter - 热更新 Shorebird 1.0 正式版来了 🐦
    • Flutter - 使用Pigeon实现视频缓存插件 🐌
    • Flutter - 轻松搞定屏幕旋转功能 😎
    • Flutter - 解决Connection closed before full header was received
    • Flutter - 实现聊天键盘与功能面板的丝滑切换 🍻
    • Flutter - 支持观察NestedScrollView,兼容性更强 😈
    • Flutter - 聊天键盘与面板丝滑切换的强势升级 🍻
    • Flutter - 升级到3.24后页面还会多次rebuild吗?🧐
    • Flutter - 轻松实现PageView卡片偏移效果
    • Flutter - 轻松搞定炫酷视差(Parallax)效果
      • 一、概述
      • 二、研发
        • 基础页面搭建
        • 实现解析
        • 观察 PageView
      • 三、最后
    • Flutter - 危!3.24版本苹果审核被拒!
    • Flutter - 子部件任意位置观察滚动数据
    • Flutter - iOS编译加速
    • Flutter - Xcode16 还原编译速度
  • 移动端
  • Flutter开发
LinXunFeng
2024-09-21
目录

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

欢迎关注微信公众号:[FSA全栈行动 👋]

# 一、概述

在上一篇 【Flutter - 轻松实现PageView卡片偏移效果】 中已经详细讲解过观察 PageView 所需要的具体步骤,今天基于此我们继续再来实现一个炫酷的视差效果,如下图所示。

# 二、研发

# 基础页面搭建

初始化 PageController

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

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 = [];

@override
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 进行观察,用法很简单:

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

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

好了,大功告成~

完整代码: https://github.com/fluttercandies/flutter_scrollview_observer/blob/main/example/lib/features/pageview/pageview_demo/pageview_parallax_page.dart (opens new window)

# 三、最后

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

GitHub: https://github.com/fluttercandies/flutter_scrollview_observer (opens new window)

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

#Dart#Flutter
Flutter - 轻松实现PageView卡片偏移效果
Flutter - 危!3.24版本苹果审核被拒!

← Flutter - 轻松实现PageView卡片偏移效果 Flutter - 危!3.24版本苹果审核被拒!→

最近更新
01
Flutter - Xcode16 还原编译速度
04-05
02
AI - 免费的 Cursor 平替方案
03-30
03
Android - 2025年安卓真的闭源了吗
03-28
更多文章>
Theme by Vdoing | Copyright © 2020-2025 FSA全栈行动
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×