Flutter for OpenHarmony 实战之基础组件:第十七篇 滚动进阶 ScrollController 与 Scrollbar

Flutter for OpenHarmony 实战之基础组件:第十七篇 滚动进阶 ScrollController 与 Scrollbar

前言

滚动(Scrolling)是移动端应用中最频繁的交互操作之一。一个优秀的应用不仅要能显示长列表,还要能感知用户的滚动意图。

在 OpenHarmony 设备上,用户对滑动的流畅度和反馈感有较高的要求。通过 ScrollController,我们可以精确掌握滚动的每一个像素,从而实现:

  • 动态显示/隐藏 UI 元素(如:向上滑动隐藏底部导航栏)。
  • 加载更多逻辑(结合滚动到底部)。
  • 跨组件同步滚动。
  • 自定义滚动条风格。

本文你将学到

  • ScrollController 的核心属性与生命周期
  • 监听滚动位置 (offset) 并触发动画
  • 如何实现平滑的"回到顶部 (Back to Top)"功能
  • 适配 OpenHarmony 的滚动条 (Scrollbar) 配置
  • 实战:打造一个带搜索框动态缩放效果的列表页

一、ScrollController:滚动的灵魂

1.1 什么是 ScrollController

ScrollController 是一个控制器对象,它可以被关联到任何可滚动组件(如 ListView, GridView, SingleChildScrollView)上。

1.2 基本结构

dart 复制代码
class _MyListPageState extends State<MyListPage> {
  // 1. 定义控制器
  final ScrollController _controller = ScrollController();

  @override
  void initState() {
    super.initState();
    // 2. 绑定监听
    _controller.addListener(() {
      print('当前滚动偏移量: ${_controller.offset}');
    });
  }

  @override
  void dispose() {
    // 3. 💡 别忘了在页面销毁时释放内存
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      controller: _controller, // 4. 关联到 ListView
      itemBuilder: (context, index) => ListTile(title: Text('Item $index')),
      itemCount: 100,
    );
  }
}

二、监听与控制

2.1 监听:感知"回到顶部"的时机

我们通常希望在用户向上滑动超过一定距离(例如 200 像素)后,显示一个悬浮按钮。

dart 复制代码
bool _showBackToTop = false;

void _scrollListener() {
  if (_controller.offset >= 200 && !_showBackToTop) {
    setState(() => _showBackToTop = true);
  } else if (_controller.offset < 200 && _showBackToTop) {
    setState(() => _showBackToTop = false);
  }
}

2.2 控制:一键回顶

ScrollController 提供了两个核心方法来改变位置:

  • jumpTo(double value):直接跳转,没有动画(瞬间移动)。
  • animateTo(double value, ...):带动画的平滑平移。
dart 复制代码
void _backToTop() {
  _controller.animateTo(
    0, 
    duration: const Duration(milliseconds: 500),
    curve: Curves.easeInOut, // 💡 使用缓动曲线让滑动更自然
  );
}

三、Scrollbar:适配鸿蒙视觉风格

在长列表中,滚动条能给用户明确的长度预期。

3.1 基础用法

将可滚动组件包裹在 Scrollbar 中即可。

dart 复制代码
Scrollbar(
  child: ListView(
    controller: _controller, // 💡 注意:如果要显式控制,两者必须关联同一个 controller
    children: [...],
  ),
)

3.2 鸿蒙风格适配

OpenHarmony 的滚动条通常比较细长,且在不滚动时会自动隐藏。我们可以通过属性进行精细化调整:

dart 复制代码
Scrollbar(
  controller: _controller,
  thumbVisibility: false,      // 是否始终显示滚动条轨道
  trackVisibility: false,      // 是否始终显示滚动条滑块
  thickness: 6.0,             // 💡 调整宽度,符合鸿蒙 2.0+ 的精致感
  radius: const Radius.circular(3), // 圆角
  child: ListView.builder(
    controller: _controller,
    itemCount: 100,
    itemBuilder: (context, index) => ListTile(title: Text('数据项 $index')),
  ),
)

四、OpenHarmony 实战:搜索栏动态缩放

这是一个常见的 UI 效果:当用户向上滑动列表时,顶部的搜索框逐渐缩小并变淡,为内容留出更多空间。

核心实现逻辑

dart 复制代码
import 'package:flutter/material.dart';

class ScrollAdvancedPage extends StatefulWidget {
  const ScrollAdvancedPage({super.key});

  @override
  State<ScrollAdvancedPage> createState() => _ScrollAdvancedPageState();
}

class _ScrollAdvancedPageState extends State<ScrollAdvancedPage> {
  // 1. 定义 ScrollController
  final ScrollController _scrollController = ScrollController();

  bool _showBackToTop = false; // 是否显示"回到顶部"按钮
  double _headerOpacity = 1.0; // 头部透明度
  double _headerHeight = 80.0; // 头部高度

  @override
  void initState() {
    super.initState();
    // 2. 绑定监听
    _scrollController.addListener(_onScroll);
  }

  void _onScroll() {
    double offset = _scrollController.offset;

    // 逻辑 A: 控制"回到顶部"按钮的显示隐藏
    if (offset >= 200 && !_showBackToTop) {
      setState(() => _showBackToTop = true);
    } else if (offset < 200 && _showBackToTop) {
      setState(() => _showBackToTop = false);
    }

    // 逻辑 B: 实现搜索栏动态缩放与淡化效果
    setState(() {
      // 优化:透明度不完全消失(最低 0.4),高度保留更多(最低 56)
      _headerOpacity = (1 - offset / 150).clamp(0.4, 1.0);
      _headerHeight = (80 - offset).clamp(46.0, 80.0);
    });
  }

  // 3. 平滑回到顶部
  void _backToTop() {
    _scrollController.animateTo(
      0,
      duration: const Duration(milliseconds: 500),
      curve: Curves.easeInOut,
    );
  }

  @override
  void dispose() {
    // 4. 重要:释放控制器
    _scrollController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.grey[100],
      appBar: AppBar(
        title: const Text('ScrollController & Scrollbar'),
        backgroundColor: Colors.blue,
        foregroundColor: Colors.white,
        elevation: 0,
      ),
      body: Stack(
        children: [
          Column(
            children: [
              // 动态头部 (搜索框模拟)
              _buildDynamicHeader(),

              // 列表区域
              Expanded(
                child: Scrollbar(
                  controller: _scrollController,
                  thickness: 6.0,
                  radius: const Radius.circular(3),
                  thumbVisibility: true, // 为了演示始终显示滑块
                  child: ListView.builder(
                    controller: _scrollController,
                    padding: const EdgeInsets.fromLTRB(0, 8, 0, 80),
                    itemCount: 50,
                    itemBuilder: (context, index) {
                      return Card(
                        margin: const EdgeInsets.symmetric(
                            horizontal: 16, vertical: 6),
                        child: ListTile(
                          leading: CircleAvatar(
                            backgroundColor: Colors.blue[100],
                            child: Text('${index + 1}',
                                style: const TextStyle(color: Colors.blue)),
                          ),
                          title: Text('鸿蒙新闻资讯条目 $index'),
                          subtitle: const Text('探索 OpenHarmony 滚动控制器的精妙用法...'),
                          trailing: const Icon(Icons.chevron_right,
                              color: Colors.grey),
                          onTap: () {},
                        ),
                      );
                    },
                  ),
                ),
              ),
            ],
          ),

          // 悬浮回到顶部按钮
          if (_showBackToTop)
            Positioned(
              right: 16,
              bottom: 16,
              child: FloatingActionButton(
                onPressed: _backToTop,
                mini: true,
                backgroundColor: Colors.blue,
                child: const Icon(Icons.arrow_upward, color: Colors.white),
              ),
            ),
        ],
      ),
    );
  }

  Widget _buildDynamicHeader() {
    // 计算动态内边距:高度越小(越收缩),两侧间距越大
    double horizontalPadding =
        (16 + (80 - _headerHeight) * 0.5).clamp(16.0, 32.0);

    return Opacity(
      opacity: _headerOpacity,
      child: Container(
        height: _headerHeight,
        width: double.infinity,
        color: Colors.blue,
        padding: EdgeInsets.symmetric(horizontal: horizontalPadding),
        alignment: Alignment.center,
        child: Container(
          height: 36,
          decoration: BoxDecoration(
            color: Colors.white.withOpacity(0.9),
            borderRadius: BorderRadius.circular(18),
            border: Border.all(color: Colors.white.withOpacity(0.3), width: 1),
          ),
          child: const Row(
            children: [
              SizedBox(width: 12),
              Icon(Icons.search, color: Colors.grey, size: 18),
              SizedBox(width: 8),
              Text('搜索内容...',
                  style: TextStyle(color: Colors.grey, fontSize: 13)),
            ],
          ),
        ),
      ),
    );
  }
}

图 1:通过监听滚动位移,实现搜索框随列表滑动而动态缩放的交互动效。


五、注意事项

  1. 共享 Controller 问题 :如果你在一个页面里有多个 ListView,不要共用同一个 ScrollController 实例,否则其中一个滚动时,另一个会同步跳动甚至报错。
  2. Dispose 释放ScrollController 内部包含 ChangeNotifier,如果忘记 dispose(),在复杂应用中会导致严重的内存泄漏。
  3. NotificationListener :如果你只需要监听滚动,不想控制滚动,建议使用 NotificationListener<ScrollNotification>,它性能更好且不需要手动销毁。

六、总结

ScrollController 让我们拥有了操纵时间(滚动位置)的能力。

核心要点:

  1. 监听位置 :通过 offset 获取当前位置。
  2. 控制行为 :通过 animateTo 实现平滑的页面跳转。
  3. 视觉增强 :使用 Scrollbar 提升长列表的交互体验。
  4. 适配建议:在鸿蒙大屏上,利用滚动反馈来动态调整侧边栏或顶栏的显示状态。

下一篇预告

当简单的 ListView 满足不了你,你想在一个页面里混合瀑布流、列表、吸顶头部,并让它们共享一个完美的滚动动效时,该请出 Flutter 布局的"终极杀器"了。
《Flutter for OpenHarmony 实战之基础组件:第十八篇 布局终极者 CustomScrollView 与 Slivers》

准备好进入 Flutter 布局的深水区了吗?


🌐 欢迎加入开源鸿蒙跨平台社区开源鸿蒙跨平台开发者社区

相关推荐
小哥Mark2 小时前
Flutter开发鸿蒙年味 + 实用实战应用|春节祝福:列表选卡 + 贴纸拖动 + 截图分享
flutter·harmonyos·鸿蒙
王码码20352 小时前
Flutter for OpenHarmony 实战之基础组件:第十六篇 约束布局 ConstrainedBox 与 AspectRatio
flutter·harmonyos
2501_921930832 小时前
基础入门 React Native 鸿蒙跨平台开发:Video 全屏播放与画中画 鸿蒙实战
react native·react.js·harmonyos
果粒蹬i2 小时前
【HarmonyOS】鸿蒙React Native 实战:打造流畅的底部导航
react native·华为·harmonyos
2501_921930833 小时前
基础入门 React Native 鸿蒙跨平台开发:react-native-switch 开关适配
react native·react.js·harmonyos
王码码20353 小时前
Flutter for OpenHarmony 实战之基础组件:第十八篇 布局终极者 CustomScrollView 与 Slivers
flutter·harmonyos
ujainu3 小时前
Flutter + OpenHarmony 实战:构建清晰、健壮的三屏状态流转
flutter·游戏·openharmony
铅笔侠_小龙虾3 小时前
Flutter 组件层级关系
前端·flutter·servlet
一起养小猫3 小时前
Flutter for OpenHarmony 实战:打地鼠游戏完整开发指南
flutter·游戏·harmonyos