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)效果
    • Flutter - 危!3.24版本苹果审核被拒!
    • Flutter - 子部件任意位置观察滚动数据
    • Flutter - iOS编译加速
    • Flutter - Xcode16 还原编译速度
  • 移动端
  • Flutter开发
LinXunFeng
2022-04-17
目录

Flutter - 实现列表上下拉切换header

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

# 背景

这是之前需求需要实现的效果:

  1. 一进入页面显示的是品牌广告视图(代号: A),上拉超过一定的距离后,向上翻到相册视图(代号: B)
  2. 下拉超过一定距离后切换为 A,快速下拉回到顶部的情况下不会切换到 A !

这里的 A 和 B 对应的就是下图效果图中的 AAA 和 BBB 视图

# 实现逻辑

定义正在展示的视图类型

enum VerticalFlipShowingType {
  aaa,
  bbb,
}

添加变量 headerFlipShowingType 用来记录当前 header 正在显示的类型,默认为 aaa

/// 当前header翻页视图显示的视图类型
VerticalFlipShowingType headerFlipShowingType = VerticalFlipShowingType.aaa;

主体结构

Scaffold(
  body: CustomScrollView(
      slivers: [
      
        SliverToBoxAdapter(child: _buildHeaderWidget()), // header(AAA + BBB)
        
        SliverList( // 正常的列表 (显示:index -- 下标)
          delegate: SliverChildBuilderDelegate(
            (ctx, index) {
              return ListTile(title: Text("index -- $index"));
            },
            childCount: 40,
          ),
        )
      ],
    ),
  ),
  
  // 右下角的浮动按钮,用来切换 header
  floatingActionButton: FloatingActionButton(
    onPressed: () {
      setState(() {
          headerFlipShowingType =
              headerFlipShowingType == VerticalFlipShowingType.aaa
                  ? VerticalFlipShowingType.bbb
                  : VerticalFlipShowingType.aaa;
      });
    },
    ...
  ),
);

从上到下,依次为 header,正常的列表数据,以及右下角按钮,点击按钮以测试切换效果

接下来是 header 的构建方法内容,为了方便理解,先来看看整体结构

Widget _buildHeaderWidget() {
  return SizedBox( // 限制 Stack 的高度,否则报错
      height: headerFlipShowingType == VerticalFlipShowingType.aaa
          ? headerFlipHeightA
          : headerFlipHeightB
      child: Stack(
        clipBehavior: Clip.none, // 子视图超出 Stack 不裁剪,依旧显示
        children: [
          Positioned(
            left: 0,
            right: 0,
            bottom: 0,
            height: headerFlipHeight, // AAA 与 BBB 加起来的总高度
            child: ListView(
              padding: EdgeInsets.zero, // 消除顶部刘海造成的安全区域偏移
              physics: const NeverScrollableScrollPhysics(), // 不可滚动
              children: [
                SizedBox(
                  height: headerFlipHeightA,
                  child: const HeaderFlipWidget(
                    text: "AAA",
                    color: Color(0xFFFA5252),
                  ),
                ),
                SizedBox(
                  height: headerFlipHeightB,
                  child: const HeaderFlipWidget(
                    text: "BBB",
                    color: Color(0xFF228BE6),
                  ),
                ),
              ],
            ),
          )
        ],
      ),
  );
}
  • 在 header 中,AAA 和 BBB 视图都是存放到 ListView
  • 包裹Stack 的 SizedBox 就是为了控制 header 的高度,且 header 的高度会跟随当前展示的类型而改变,如:当前展示 AAA,则高度为 AAA 视图的高度,展示 BBB 时,则高度为 BBB 视图的高度
  • 使用 Stack + Positioned 这个组合,将 Positioned 的 bottom 设置为 0,即 ListView 的底部紧贴 Stack 的底部,使得 ListView 只能向上超出 Stack
  • Stack 设置了 clipBehavior 为 Clip.none,因此在正在显示 BBB 视图的情况下,下拉列表可以看到 AAA 的视图内容
  • 固定 Positioned 的 高度为 AAA 和 BBB 视图的总高,目的是为了结合后面提及的透明 Container(或者说 AnimatedContainer B), 方便控制 BBB 视图的显示

如果这里还不太理解 AAA 和 BBB 的显示逻辑也不用担心,下面会进行详细说明

我们来看看此时的效果:

为了完整显示 AAA 视图,且不显示 BBB 视图,在 AAA 的前边添加一个透明的 Container,并把高度设置为 BBB 的高度(显示 BBB 视图时,透明的 Container 的高度设置为 0)

ListView(
  padding: EdgeInsets.zero,
  physics: const NeverScrollableScrollPhysics(),
  children: [
    Container( // 添加了透明的 Container
      height: headerFlipShowingType == VerticalFlipShowingType.aaa
          ? headerFlipHeightB
          : 0,
    ),
    SizedBox(
      height: headerFlipHeightA,
      child: const HeaderFlipWidget(
        text: "AAA",
        color: Color(0xFFFA5252),
      ),
    ),
    SizedBox(
      height: headerFlipHeightB,
      child: const HeaderFlipWidget(
        text: "BBB",
        color: Color(0xFF228BE6),
      ),
    ),
  ],
)

我们再来看看此时的效果:

可以,不过变化很僵硬,加点动画就可以了

完整的 header 的构建方法代码如下

改动的地方在 AnimatedContainer A 和 AnimatedContainer B 注释处

Widget _buildHeaderWidget() {
  var duration = const Duration(milliseconds: 200);
  // AnimatedContainer A: 控制整个header以动画的方式改变高度
  return AnimatedContainer(
    duration: duration,
    height: headerFlipShowingType == VerticalFlipShowingType.aaa
        ? headerFlipHeightA
        : headerFlipHeightB,
    child: Stack(
      clipBehavior: Clip.none,
      children: [
        Positioned(
          left: 0,
          right: 0,
          bottom: 0,
          height: headerFlipHeight,
          child: ListView(
            padding: EdgeInsets.zero,
            physics: const NeverScrollableScrollPhysics(),
            children: [
              // AnimatedContainer B:控制 AAA 和 BBB 切换时的互推效果
              AnimatedContainer(
                duration: duration,
                height: headerFlipShowingType == VerticalFlipShowingType.aaa
                    ? headerFlipHeightB
                    : 0,
              ),
              SizedBox(
                height: headerFlipHeightA,
                child: const HeaderFlipWidget(
                  text: "AAA",
                  color: Color(0xFFFA5252),
                ),
              ),
              SizedBox(
                height: headerFlipHeightB,
                child: const HeaderFlipWidget(
                  text: "BBB",
                  color: Color(0xFF228BE6),
                ),
              ),
            ],
          ),
        )
      ],
    ),
  );
}
  • AnimatedContainer A: 控制整个 header 以动画的方式改变高度
  • AnimatedContainer B: 控制 AAA 和 BBB 切换时的互推效果(切到 AAA 时,将 BBB 往下推,切到 BBB 时,将 AAA 往上推)

# 加深理解

为了更易于理解 AAA 和 BBB 的显示逻辑,我做了如下一张图

  • 从左往右看:便是上拉时,从 AAA 切换到 BBB 的过程
  • 从右往左看:便是下拉时,从 BBB 切换到 AAA 的过程

整个过程最主要的就是 AnimatedContainer A 和 AnimatedContainer B 的高度变化

  • 前置条件:Positioned 的 高度(即 ListView 的高度)为 AAA 和 BBB 视图的总高
  • 当显示 AAA 时,即图一,Stack 的高度为 AAA 的高度,AnimatedContainer B 的高度为 BBB的高度,此时的 BBB 视图超出了 ListView 的视窗,所以其不显示,注意看图一半透明的 BBB
  • 在切换显示 BBB 的过程中,即图二到图三,AnimatedContainer B 的高度不断减小至 0,使得 BBB 逐渐进入 ListView 的视窗内,直到完全展示,Stack 的高度也从 AAA 的高度减小至 BBB 的高度,使得 AAA 超出 ListView 的视窗,这样便只会展示 BBB 了

# 完善逻辑

上述的 header 视图切换是靠右下角按钮的点击,实际使用时,是要靠列表滚动时来判断是否进行切换的

/// 偏移量阈值
final double headerFlipThreshold = 50;

/// 可监听滚动,控制偏移量
final ScrollController scrollController = ScrollController();

在 CustomScrollView 中传入 scrollController,并用 NotificationListener 包裹起来,监听其滚动

Scaffold(
  body: NotificationListener<ScrollNotification>( // 监听滚动
    onNotification: (ScrollNotification notification) {
      _handleListScroll(notification);
      return false; // return true 会导致进度条将失效
    },
    child: CustomScrollView(
      controller: scrollController, // 传入 scrollController
      ...
    ),
  ),
  floatingActionButton: ...
);

接下来就是处理滚动

void _handleListScroll(ScrollNotification notification) {
  var offset = notification.metrics.pixels;
  if (headerFlipShowingType == VerticalFlipShowingType.aaa) {
    if (offset > headerFlipThreshold) {
      setState(() {
        headerFlipShowingType = VerticalFlipShowingType.bbb;
      });
      scrollController.jumpTo(0);
    }
  } else {
    if (offset < -headerFlipThreshold) {
      setState(() {
        headerFlipShowingType = VerticalFlipShowingType.aaa;
      });
      scrollController.jumpTo(0);
    }
  }
}
  • 根据偏移量是否超过阈值,来控制是否切换 header
  • 当切换 header 后,我们需要使用 scrollController 来设置列表的偏移量为 0

到这里也就差不多了,不过,现在还有个细节需要处理,请看图

如图操作所示,快速下拉滚动会直接从 BBB 切换到 AAA,这并不是我们想要的。

我们想要的是:当前正在显示 BBB,且从最顶部开始下拉才能切到 AAA

代码改动:

/// 是否是从最顶部开始滚动的
bool isStartScrollAtTop = false;
void _handleListScroll(ScrollNotification notification) {
  var offset = notification.metrics.pixels;
  if (notification is ScrollStartNotification) { // 开始滚动
    isStartScrollAtTop = offset == 0; // 记录是否是从最顶部开始滚动
  } else {
    if (headerFlipShowingType == VerticalFlipShowingType.aaa) {
      if (offset > headerFlipThreshold) {
        setState(() {
          headerFlipShowingType = VerticalFlipShowingType.bbb;
        });
        scrollController.jumpTo(0);
      }
    } else {
      if (!isStartScrollAtTop) { // 不是从最顶部开始滚动,则不切换到 AAA
        return;
      }
      if (offset < -headerFlipThreshold) {
        setState(() {
          headerFlipShowingType = VerticalFlipShowingType.aaa;
        });
        scrollController.jumpTo(0);
      }
    }
  }
}

大功告成,最后附上 Demo 链接:flutter_demo/vertical_flip_page.dart (github.com) (opens new window)

#Dart#Flutter
Flutter - 解决原生弹窗的触摸事件被Flutter响应的问题
Flutter - 获取ListView当前正在显示的Widget信息

← Flutter - 解决原生弹窗的触摸事件被Flutter响应的问题 Flutter - 获取ListView当前正在显示的Widget信息→

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