阅读进度悬浮球应用 技术解析文档

一、项目背景概述

本项目是一个基于 Flutter 框架开发的阅读进度悬浮球组件演示应用。悬浮球(Floating Ball)是一种常见的移动端交互模式,它以一个可拖动的圆形悬浮按钮的形式悬浮在界面之上,用户可以自由拖动改变其位置,点击后可以展开更多功能面板。

本应用的核心功能是展示一个带有阅读进度指示的悬浮球组件。悬浮球以圆形按钮的形式呈现在屏幕上,球体内嵌了一个环形进度指示器和百分比文字,直观地显示当前的阅读进度。用户可以通过手指拖动悬浮球到屏幕任意位置,点击悬浮球则会在旁边展开一个详情面板,面板中显示更详细的进度信息,并提供模拟阅读和重置进度的操作按钮。

项目采用 Material Design 3 设计规范,整体界面简洁现代。悬浮球组件设计为高度可定制化,支持通过参数设置初始进度、球体颜色、进度条颜色和球体大小等属性,方便在不同场景中复用。

从技术实现角度来看,这个项目重点展示了 Flutter 中的手势检测(GestureDetector)、绝对定位布局(Stack + Positioned)、圆形进度指示器(CircularProgressIndicator)以及组件参数化设计等技术点。悬浮球的拖动边界限制逻辑也体现了对用户体验细节的关注。

二、架构分析

应用入口

根组件 MaterialApp

首页 Scaffold

AppBar 顶部导航栏

Stack 堆叠布局

中央提示文字区域

阅读进度悬浮球组件

标题文字

提示文字1

提示文字2

状态管理 - StatefulWidget

Stack 悬浮球容器

Positioned 悬浮球主体

Positioned 详情面板

GestureDetector 手势检测

onPanUpdate 拖动处理

onTap 点击处理

圆形球体 Container

圆形背景装饰

阴影效果

Stack 内容层

SizedBox 环形进度条

Text 百分比文字

Container 面板容器

白色背景 + 圆角 + 阴影

Column 垂直布局

标题文字

LinearProgressIndicator 线性进度条

进度数值文字

按钮行

模拟阅读按钮

重置按钮

架构层级说明

整个应用的架构可以分为四个主要层级,从上到下依次为:

应用框架层:由 MaterialApp 根组件构成,负责全局主题配置和路由管理。主题使用深紫色种子色生成的 ColorScheme,启用 Material 3 设计风格。

页面结构层:首页 Scaffold 提供了标准的 Material 页面结构,包括顶部的 AppBar 导航栏和主体内容区域。主体内容使用 Stack 堆叠布局,使得悬浮球可以浮动在其他内容之上。

背景内容层:Stack 的第一层是居中排列的提示文字区域,包含标题和两行操作说明,向用户介绍悬浮球的功能和使用方法。这一层位于最底部,不会遮挡悬浮球。

悬浮球组件层:这是应用的核心功能层,是一个独立的 StatefulWidget 组件。组件内部通过 Stack 和 Positioned 实现悬浮球和详情面板的绝对定位,通过 GestureDetector 处理用户的拖动和点击手势。组件内部维护了进度值、位置坐标、展开状态等多个状态变量。

三、入口组件流程

应用的启动流程遵循标准的 Flutter 应用模式。主入口函数调用 runApp 方法,将根组件挂载到 Flutter 的渲染树上。根组件是一个无状态组件,其 build 方法返回一个 MaterialApp,配置了应用标题、主题数据和首页。

首页是一个有状态组件,其状态类中保留了默认模板的计数器变量和方法,但在实际的 build 方法中并未使用。首页的 Scaffold 包含一个 AppBar 和一个 Stack 类型的 body。

AppBar 使用主题的 inversePrimary 颜色作为背景色,标题文字从 widget 的 title 属性获取。body 的 Stack 布局中包含两个子元素:第一个是居中的 Column,显示三行提示文字;第二个是阅读进度悬浮球组件,它会浮动在所有内容之上。

悬浮球组件在首页中被构造时,传入了四个参数:初始进度设为 0.3(即30%),球体颜色为蓝色,进度条颜色为白色,球体大小为 60 像素。这些参数通过组件的构造函数传递给状态类,在 initState 生命周期中初始化进度值。

四、核心组件逐段解析

4.1 组件参数设计

阅读进度悬浮球组件设计为可高度定制的复用组件,通过构造函数接收四个可选参数:

  • initialProgress:初始进度值,范围为 0 到 1,默认值为 0.0
  • ballColor:悬浮球的背景颜色,默认为蓝色
  • progressColor:进度条和百分比文字的颜色,默认为白色
  • ballSize:悬浮球的直径大小,默认为 60.0 像素

这些参数都被声明为 final 类型,符合 Flutter 组件的不可变设计原则。组件的状态类在 initState 生命周期方法中将 initialProgress 赋值给内部的 _progress 变量,完成状态的初始化。

4.2 悬浮球主体

悬浮球的主体部分通过 Positioned 定位在 Stack 中,位置由 _position 变量控制,初始位置为左上角偏移 (20, 100)。

Positioned 的子元素是一个 GestureDetector,它负责检测用户的手势操作。注册了两个手势回调:onPanUpdate 用于处理拖动事件,onTap 用于处理点击事件。

GestureDetector 的子元素是一个 Container,构成悬浮球的视觉主体。容器的宽高都设置为 ballSize,通过 BoxDecoration 实现圆形外观(borderRadius 设置为球体直径的一半)和阴影效果。阴影使用半透明黑色,模糊半径为 8 像素,垂直偏移 4 像素,营造出悬浮的视觉效果。

球体内部使用 Stack 布局叠加两层内容:环形进度条和百分比文字,两者都居中对齐。

环形进度条使用 SizedBox 包裹的 CircularProgressIndicator,尺寸为球体大小的 80%,留出适当的边距。进度条的 strokeWidth 为 4 像素,值颜色使用 AlwaysStoppedAnimation 包裹的 progressColor,背景色为半透明白色。

百分比文字显示当前进度的整数百分比,字体颜色与进度条一致,字体粗细为粗体,字号为球体大小的 20%,确保文字大小与球体成比例。

4.3 拖动交互逻辑

拖动交互是悬浮球组件的核心功能之一,由 _handlePanUpdate 方法处理。该方法接收 DragUpdateDetails 参数,其中包含了拖动的增量信息(delta)。

当用户拖动手指时,每次位置变化都会触发 onPanUpdate 回调。在回调方法中,通过 setState 更新 _position 的值,将拖动增量累加到当前位置上。

为了防止悬浮球被拖出屏幕边界,代码中使用了 clamp 方法对 x 和 y 坐标进行限制。x 坐标的范围是 0 到屏幕宽度减去球体直径,y 坐标的范围是 0 到屏幕高度减去球体直径。这样可以确保悬浮球始终保持在可视区域内,不会有部分或全部被拖出屏幕。

屏幕尺寸通过 MediaQuery.of(context).size 获取,这是 Flutter 中获取屏幕尺寸的标准方式。需要注意的是,这个方法必须在有上下文的环境中调用,因此放在手势回调中执行是合适的。

4.4 点击展开与详情面板

点击悬浮球会触发 _handleTap 方法,该方法切换 _isExpanded 状态变量的值。当 _isExpanded 为 true 时,详情面板会显示在悬浮球的右侧。

详情面板同样使用 Positioned 定位,水平位置在悬浮球右侧 10 像素处,垂直位置与悬浮球顶部对齐。面板宽度固定为 200 像素,内部有 16 像素的内边距。

面板的视觉设计包括白色背景、12 像素圆角和阴影效果,阴影比悬浮球的阴影更大(模糊半径10像素),增强了面板的层次感。

面板内部使用 Column 垂直布局,包含以下内容:

  • 标题:“阅读进度”,使用 titleMedium 样式并加粗
  • 线性进度条:高度为 8 像素的 LinearProgressIndicator,背景色为浅灰色,进度值颜色与悬浮球颜色一致
  • 进度文字:显示当前进度的百分比
  • 按钮行:包含"模拟阅读"和"重置"两个按钮,使用 Row 水平排列

"模拟阅读"按钮使用悬浮球主题色作为背景,每次点击会将进度增加 10%。"重置"按钮使用灰色背景,点击后将进度重置为 0。两个按钮都使用了圆角矩形形状和较小的内边距,适应面板有限的空间。

4.5 中央提示区域

Stack 布局的第一层是中央提示区域,使用 Center 组件将内容居中。内部是一个 Column,垂直排列三行文字:

  • 第一行是标题"阅读进度悬浮球演示",使用 headlineMedium 样式
  • 第二行是提示"点击悬浮球查看详情",使用 bodyMedium 样式
  • 第三行是提示"拖动悬浮球可以改变位置",同样使用 bodyMedium 样式

每段文字之间通过 SizedBox 设置 20 像素的间距。这个区域的作用是向用户说明悬浮球的功能和操作方式,起到引导和教学的作用。

五、状态管理

本项目的状态管理全部在悬浮球组件内部完成,使用 Flutter 内置的 setState 机制。组件状态可以分为三类:数据状态、位置状态和 UI 状态。

5.1 数据状态

_progress 变量存储当前的阅读进度,类型为 double,取值范围为 0.0 到 1.0。这个变量是组件的核心数据,驱动了环形进度条、线性进度条和百分比文字的显示。

进度值的更新由三个方法触发:

updateProgress 方法:这是一个通用的进度更新方法,接收一个 double 值并使用 clamp 方法将其限制在 0 到 1 的范围内,确保进度值不会越界。虽然这个方法在当前代码中没有被外部直接调用,但它的存在为组件的外部调用提供了接口。

_simulateReading 方法:模拟阅读进度增加,每次调用将进度增加 0.1(即 10%),同样使用 clamp 限制范围。这个方法由详情面板中的"模拟阅读"按钮调用。

_resetProgress 方法:将进度重置为 0.0,由详情面板中的"重置"按钮调用。

5.2 位置状态

_position 变量存储悬浮球在屏幕上的位置,类型为 Offset,初始值为 (20, 100)。这个变量决定了悬浮球和详情面板在 Stack 中的定位坐标。

位置状态的更新只在 _handlePanUpdate 方法中进行,即用户拖动悬浮球时。每次拖动都会将拖动增量(delta)累加到当前位置上,然后通过 clamp 限制在屏幕范围内。

_isDragging 变量记录当前是否正在拖动中,但在当前代码中并未实际使用。这个变量的存在可能是为了后续扩展功能(如拖动时改变视觉效果)而预留的。

5.3 UI 状态

_isExpanded 变量控制详情面板的显示与隐藏,是一个布尔类型的状态。当值为 true 时,详情面板出现在悬浮球右侧;值为 false 时,面板不渲染。

这个状态的切换由 _handleTap 方法触发,即用户点击悬浮球时。状态切换的逻辑很简单,就是对当前值取反。

六、关键代码详解

6.1 手势检测与拖动实现

悬浮球的拖动功能完全依赖于 GestureDetector 的 onPanUpdate 回调。Pan 手势是 Flutter 中的一种连续型手势,适用于用户在屏幕上拖动的场景。

DragUpdateDetails 对象包含了两个重要信息:delta 表示从上次更新到本次更新的位移增量,primaryDelta 表示主轴方向的位移增量。在本项目中使用的是 delta,因为需要同时跟踪水平和垂直两个方向的移动。

拖动处理的核心代码逻辑如下:首先将位移增量加到当前位置上,然后使用 clamp 方法对坐标进行边界限制。clamp 是 Dart 数值类型的一个方法,用于将值限制在指定范围内,如果低于最小值则返回最小值,高于最大值则返回最大值,否则返回原值。

边界计算中,右边界和下边界需要减去球体的尺寸,这是因为 Positioned 的 left 和 top 属性指定的是组件左上角的位置,减去球体大小可以确保整个球体都在屏幕内。

6.2 进度条的双重展示

组件中同时使用了两种进度指示器:CircularProgressIndicator 和 LinearProgressIndicator,分别用于悬浮球主体和详情面板中。

CircularProgressIndicator 是一个圆形进度条,有两种模式:确定模式(determinate)和不确定模式(indeterminate)。本项目中通过传入 value 参数使用确定模式,进度值精确反映 _progress 变量的状态。valueColor 参数接受一个 Animation 类型的值,这里使用 AlwaysStoppedAnimation 包装一个固定颜色,表示进度条颜色保持不变。

LinearProgressIndicator 是线性进度条,用法与圆形进度条类似。在详情面板中,线性进度条被包裹在一个固定高度(8像素)的 SizedBox 中,以控制其粗细。背景色使用灰色调,前景色与悬浮球主题色一致,保持视觉统一。

两种进度指示器的配合使用是一种常见的设计模式:圆形指示器在紧凑空间中提供概览信息,线性指示器在展开面板中提供更清晰的进度可视化。

6.3 组件的可复用性设计

悬浮球组件的设计充分考虑了可复用性,通过构造函数参数将可变属性外部化,使得同一个组件可以在不同场景下以不同的外观和行为使用。

四个参数分别控制了进度初始值、球体颜色、进度颜色和球体大小,基本涵盖了定制化的主要需求。使用者只需要传入不同的参数组合,就可以得到符合自己产品设计风格的悬浮球。

组件的内部状态完全自包含,不依赖外部的状态管理。进度更新、位置管理、展开收起等逻辑都封装在组件内部,外部只需简单地将组件放入 Stack 中即可使用。

此外,updateProgress 方法的存在暗示了组件可以被外部控制进度,虽然目前它是一个内部方法,但可以很容易地通过 GlobalKey 或回调机制暴露给外部调用者,实现更复杂的交互。

七、技术总结

本项目是一个设计精美的阅读进度悬浮球组件演示应用,虽然功能相对单一,但在交互设计和组件封装方面有很多值得学习的地方。

在交互设计方面,悬浮球支持自由拖动和边界限制,点击展开详情面板的设计符合用户对悬浮按钮的常见认知。拖动时的实时位置更新和边界约束,保证了良好的用户体验。

在组件设计方面,组件采用参数化设计,将颜色、尺寸、初始值等属性通过构造函数暴露给外部,提高了组件的复用性和灵活性。内部状态自包含,外部使用简单,是一个良好封装的范例。

在布局技术方面,项目灵活运用了 Stack 和 Positioned 实现绝对定位,结合 GestureDetector 实现拖动手势,展示了 Flutter 在自定义交互组件方面的强大能力。

在视觉设计方面,悬浮球的圆形设计、阴影效果、渐变背景(纯色模拟)都体现了 Material Design 的设计理念。进度指示器的双重展示(环形和线性)为用户提供了不同维度的进度感知。

可改进的方面包括:增加拖拽时的视觉反馈(如放大效果)、添加吸附边缘的动画效果、支持更多的自定义选项(如阴影颜色、展开方向等)、引入动画效果使展开和收起过程更流畅、增加长按手势等更多交互方式。

总体而言,这个项目是 Flutter 手势处理和自定义组件开发的一个优秀案例,代码结构清晰,交互流畅,对于学习如何开发可拖拽的悬浮组件有很好的参考价值。

请添加图片描述

Flutter for OpenHarmony 实战:阅读进度悬浮球

前言:跨生态开发的新机遇

在移动开发领域,我们总是面临着选择与适配。今天,你的Flutter应用在Android和iOS上跑得正欢,明天可能就需要考虑一个新的平台:HarmonyOS(鸿蒙)。这不是一道选答题,而是很多团队正在面对的现实。

Flutter的优势很明确——写一套代码,就能在两个主要平台上运行,开发体验流畅。而鸿蒙代表的是下一个时代的互联生态,它不仅仅是手机系统,更着眼于未来全场景的体验。将现有的Flutter应用适配到鸿蒙,听起来像是一个“跨界”任务,但它本质上是一次有价值的技术拓展:让产品触达更多用户,也让技术栈覆盖更广。

不过,这条路走起来并不像听起来那么简单。Flutter和鸿蒙,从底层的架构到上层的工具链,都有着各自的设计逻辑。会遇到一些具体的问题:代码如何组织?原有的功能在鸿蒙上如何实现?那些平台特有的能力该怎么调用?更实际的是,从编译打包到上架部署,整个流程都需要重新摸索。
这篇文章想做的,就是把这些我们趟过的路、踩过的坑,清晰地摊开给你看。我们不会只停留在“怎么做”,还会聊到“为什么得这么做”,以及“如果出了问题该往哪想”。这更像是一份实战笔记,源自真实的项目经验,聚焦于那些真正卡住过我们的环节。

无论你是在为一个成熟产品寻找新的落地平台,还是从一开始就希望构建能面向多端的应用,这里的思路和解决方案都能提供直接的参考。理解了两套体系之间的异同,掌握了关键的衔接技术,不仅能完成这次迁移,更能积累起应对未来技术变化的能力。

混合工程结构深度解析

项目目录架构

当Flutter项目集成鸿蒙支持后,典型的项目结构会发生显著变化。以下是经过ohos_flutter插件初始化后的项目结构:

my_flutter_harmony_app/
├── lib/                          # Flutter业务代码(基本不变)
│   ├── main.dart                 # 应用入口
│   ├── home_page.dart           # 首页
│   └── utils/
│       └── platform_utils.dart  # 平台工具类
├── pubspec.yaml                  # Flutter依赖配置
├── ohos/                         # 鸿蒙原生层(核心适配区)
│   ├── entry/                    # 主模块
│   │   └── src/main/
│   │       ├── ets/              # ArkTS代码
│   │       │   ├── MainAbility/
│   │       │   │   ├── MainAbility.ts       # 主Ability
│   │       │   │   └── MainAbilityContext.ts
│   │       │   └── pages/
│   │       │       ├── Index.ets           # 主页面
│   │       │       └── Splash.ets          # 启动页
│   │       ├── resources/        # 鸿蒙资源文件
│   │       │   ├── base/
│   │       │   │   ├── element/  # 字符串等
│   │       │   │   ├── media/    # 图片资源
│   │       │   │   └── profile/  # 配置文件
│   │       │   └── en_US/        # 英文资源
│   │       └── config.json       # 应用核心配置
│   ├── ohos_test/               # 测试模块
│   ├── build-profile.json5      # 构建配置
│   └── oh-package.json5         # 鸿蒙依赖管理
└── README.md

展示效果图片

flutter 实时预览 效果展示

运行到鸿蒙虚拟设备中效果展示

目录

功能代码实现

ReadingProgressBall 组件

ReadingProgressBall 是一个可交互的阅读进度悬浮球组件,支持拖动、点击展开详情和进度展示功能。

核心功能实现

1. 组件结构设计
class ReadingProgressBall extends StatefulWidget {
  final double initialProgress; // 初始进度 0-1
  final Color ballColor; // 悬浮球颜色
  final Color progressColor; // 进度条颜色
  final double ballSize; // 悬浮球大小

  const ReadingProgressBall({
    super.key,
    this.initialProgress = 0.0,
    this.ballColor = Colors.blue,
    this.progressColor = Colors.white,
    this.ballSize = 60.0,
  });

  
  State<ReadingProgressBall> createState() => _ReadingProgressBallState();
}

class _ReadingProgressBallState extends State<ReadingProgressBall> {
  double _progress = 0.0;
  Offset _position = const Offset(20, 100); // 初始位置
  bool _isExpanded = false; // 是否展开详情

  
  void initState() {
    super.initState();
    _progress = widget.initialProgress;
  }
  
  // 其他方法...
}
2. 拖动功能实现
// 处理拖动
void _handlePanUpdate(DragUpdateDetails details) {
  setState(() {
    _position += details.delta;
    // 限制在屏幕内
    _position = Offset(
      _position.dx.clamp(0, MediaQuery.of(context).size.width - widget.ballSize),
      _position.dy.clamp(0, MediaQuery.of(context).size.height - widget.ballSize),
    );
  });
}
3. 点击交互实现
// 处理点击
void _handleTap() {
  setState(() {
    _isExpanded = !_isExpanded;
  });
}

// 模拟阅读进度增加
void _simulateReading() {
  setState(() {
    _progress = (_progress + 0.1).clamp(0.0, 1.0);
  });
}

// 重置进度
void _resetProgress() {
  setState(() {
    _progress = 0.0;
  });
}
4. UI 布局实现

Widget build(BuildContext context) {
  return Stack(
    children: [
      // 悬浮球
      Positioned(
        left: _position.dx,
        top: _position.dy,
        child: GestureDetector(
          onPanUpdate: _handlePanUpdate,
          onTap: _handleTap,
          child: Container(
            width: widget.ballSize,
            height: widget.ballSize,
            decoration: BoxDecoration(
              color: widget.ballColor,
              borderRadius: BorderRadius.circular(widget.ballSize / 2),
              boxShadow: [
                BoxShadow(
                  color: Colors.black.withOpacity(0.3),
                  blurRadius: 8,
                  offset: const Offset(0, 4),
                ),
              ],
            ),
            child: Stack(
              alignment: Alignment.center,
              children: [
                // 进度环
                SizedBox(
                  width: widget.ballSize * 0.8,
                  height: widget.ballSize * 0.8,
                  child: CircularProgressIndicator(
                    value: _progress,
                    strokeWidth: 4,
                    valueColor: AlwaysStoppedAnimation<Color>(widget.progressColor),
                    backgroundColor: Colors.white.withOpacity(0.3),
                  ),
                ),
                // 进度百分比
                Text(
                  '${(_progress * 100).toInt()}%',
                  style: TextStyle(
                    color: widget.progressColor,
                    fontWeight: FontWeight.bold,
                    fontSize: widget.ballSize * 0.2,
                  ),
                ),
              ],
            ),
          ),
        ),
      ),

      // 展开的详情面板
      if (_isExpanded)
        Positioned(
          left: _position.dx + widget.ballSize + 10,
          top: _position.dy,
          child: Container(
            width: 200,
            padding: const EdgeInsets.all(16),
            decoration: BoxDecoration(
              color: Colors.white,
              borderRadius: BorderRadius.circular(12),
              boxShadow: [
                BoxShadow(
                  color: Colors.black.withOpacity(0.2),
                  blurRadius: 10,
                  offset: const Offset(0, 4),
                ),
              ],
            ),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  '阅读进度',
                  style: Theme.of(context).textTheme.titleMedium?.copyWith(
                        fontWeight: FontWeight.bold,
                      ),
                ),
                const SizedBox(height: 12),
                Container(
                  height: 8,
                  child: LinearProgressIndicator(
                    value: _progress,
                    backgroundColor: Colors.grey[200],
                    valueColor: AlwaysStoppedAnimation<Color>(widget.ballColor),
                  ),
                ),
                const SizedBox(height: 8),
                Text(
                  '当前进度: ${(_progress * 100).toInt()}%',
                  style: Theme.of(context).textTheme.bodySmall,
                ),
                const SizedBox(height: 16),
                Row(
                  children: [
                    ElevatedButton(
                      onPressed: _simulateReading,
                      style: ElevatedButton.styleFrom(
                        backgroundColor: widget.ballColor,
                        foregroundColor: Colors.white,
                        padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
                        shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.circular(20),
                        ),
                      ),
                      child: const Text('模拟阅读'),
                    ),
                    const SizedBox(width: 8),
                    ElevatedButton(
                      onPressed: _resetProgress,
                      style: ElevatedButton.styleFrom(
                        backgroundColor: Colors.grey,
                        foregroundColor: Colors.white,
                        padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
                        shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.circular(20),
                        ),
                      ),
                      child: const Text('重置'),
                    ),
                  ],
                ),
              ],
            ),
          ),
        ),
    ],
  );
}

开发注意事项

  1. 状态管理:使用 setState() 管理组件状态,确保数据变化时 UI 能够及时更新。

  2. 手势处理:使用 GestureDetector 处理点击和拖动事件,实现交互功能。

  3. 布局定位:使用 Positioned 组件控制悬浮球的位置,结合 Offset 实现拖动效果。

  4. 屏幕边界限制:在拖动时使用 clamp() 方法限制悬浮球在屏幕范围内。

  5. 进度条实现:使用 CircularProgressIndicator 实现圆形进度条,使用 Container 包裹 LinearProgressIndicator 控制高度。

  6. 视觉效果:通过 BoxShadow 添加阴影效果,提升 UI 的层次感。

组件使用方法

在首页直接使用

import 'package:flutter/material.dart';
import 'components/reading_progress_ball.dart';

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: Stack(
        children: [
          Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Text(
                  '阅读进度悬浮球演示',
                  style: Theme.of(context).textTheme.headlineMedium,
                ),
                const SizedBox(height: 20),
                Text(
                  '点击悬浮球查看详情',
                  style: Theme.of(context).textTheme.bodyMedium,
                ),
                const SizedBox(height: 20),
                Text(
                  '拖动悬浮球可以改变位置',
                  style: Theme.of(context).textTheme.bodyMedium,
                ),
              ],
            ),
          ),
          // 阅读进度悬浮球
          const ReadingProgressBall(
            initialProgress: 0.3,
            ballColor: Colors.blue,
            progressColor: Colors.white,
            ballSize: 60.0,
          ),
        ],
      ),
    );
  }
}

使用步骤

  1. 导入组件:在需要使用的文件中导入 reading_progress_ball.dart

  2. 添加到布局:使用 Stack 布局在页面中添加 ReadingProgressBall 组件。

  3. 自定义配置:根据需要设置初始进度、颜色和大小等参数。

  4. 交互操作

    • 点击悬浮球展开详情面板
    • 拖动悬浮球改变位置
    • 在详情面板中点击按钮模拟阅读或重置进度

本次开发中容易遇到的问题

1. 进度条高度设置问题

问题描述:在设置 LinearProgressIndicator 的高度时,直接使用 height 参数会导致编译错误。

解决方案:使用 Container 包裹 LinearProgressIndicator,通过 Containerheight 属性控制进度条高度。

Container(
  height: 8,
  child: LinearProgressIndicator(
    value: _progress,
    backgroundColor: Colors.grey[200],
    valueColor: AlwaysStoppedAnimation<Color>(widget.ballColor),
  ),
),

2. 拖动边界限制问题

问题描述:拖动悬浮球时,可能会拖出屏幕边界,导致部分内容不可见。

解决方案:在拖动更新位置时,使用 clamp() 方法限制坐标在屏幕范围内。

_position = Offset(
  _position.dx.clamp(0, MediaQuery.of(context).size.width - widget.ballSize),
  _position.dy.clamp(0, MediaQuery.of(context).size.height - widget.ballSize),
);

3. 状态管理问题

问题描述:点击按钮或拖动后,UI 可能不会及时更新,或者状态管理混乱导致应用崩溃。

解决方案:所有状态变化都通过 setState() 方法进行管理,确保状态更新和 UI 重绘的一致性。

4. 详情面板位置问题

问题描述:当悬浮球位于屏幕右侧时,展开的详情面板可能会超出屏幕边界。

解决方案:可以根据悬浮球的位置动态调整详情面板的显示位置,确保其始终在屏幕内。

5. 性能优化问题

问题描述:频繁拖动悬浮球时,可能会导致应用性能下降,出现卡顿现象。

解决方案

  • 合理使用 setState(),避免不必要的重绘
  • 考虑使用 AnimatedBuilderValueNotifier 等优化动画性能
  • 对于复杂的 UI,可以考虑使用 RepaintBoundary 减少重绘区域

总结本次开发中用到的技术点

1. Flutter 基础组件

  • StatefulWidget:用于管理有状态的组件,如悬浮球的位置和展开状态。
  • StatelessWidget:用于构建无状态的 UI 组件,如详情面板中的文本和按钮。
  • Container:用于布局和样式控制,提供 padding、margin、decoration 等属性。
  • Stack:用于层叠布局,实现悬浮球和详情面板的叠加显示。
  • Positioned:用于在 Stack 中精确定位子组件,控制悬浮球的位置。
  • GestureDetector:用于实现点击、拖动等手势交互。
  • Text:用于显示文本信息,支持不同的样式和主题。
  • ElevatedButton:用于添加交互按钮,支持自定义样式和点击事件。
  • CircularProgressIndicator:用于显示圆形进度条。
  • LinearProgressIndicator:用于显示线性进度条。

2. 状态管理

  • setState():用于更新组件状态,触发 UI 重新构建。
  • 状态变量:用于存储组件的状态信息,如进度值、位置和展开状态。
  • initState():用于初始化组件状态,设置初始进度值。

3. 手势处理

  • onPanUpdate:用于处理拖动事件,实现悬浮球的位置移动。
  • onTap:用于处理点击事件,实现详情面板的展开和收起。

4. 布局和定位

  • Offset:用于表示二维坐标,存储悬浮球的位置。
  • MediaQuery:用于获取屏幕尺寸,计算边界限制。
  • clamp():用于限制值在指定范围内,确保悬浮球不超出屏幕边界。

5. 视觉效果

  • BoxDecoration:用于设置容器的背景、边框、阴影等样式。
  • BorderRadius:用于设置圆角,实现圆形悬浮球效果。
  • BoxShadow:用于添加阴影效果,增强 UI 的层次感。
  • Theme:用于获取应用的主题样式,确保组件在不同主题下的一致性。

6. 数据处理

  • clamp():用于限制进度值在 0-1 范围内。
  • toInt():用于将浮点数转换为整数,显示整数百分比。
  • AlwaysStoppedAnimation:用于创建固定颜色的动画,设置进度条颜色。

7. 组件化开发

  • 参数化设计:通过构造函数参数实现组件的可定制性,如颜色、大小和初始进度。
  • 方法抽离:将逻辑代码抽离为单独的方法,提高代码可读性和可维护性。
  • 组件封装:将相关功能封装为独立组件,便于复用和测试。

8. 跨平台适配

  • Flutter 跨平台特性:利用 Flutter 的跨平台能力,确保组件在不同平台上的一致性。
  • OpenHarmony 适配:遵循 OpenHarmony 平台的开发规范,确保应用能够正常运行。

请添加图片描述

flutter_openHarmony(简称 Flutter‑OH)

注意:不是Google官方产物,是OpenHarmony社区TPC组织维护的Flutter引擎移植版本。把Flutter的Dart/Skia引擎做底层改造,让Flutter应用可以直接编译输出 HAP包,跑在OpenHarmony/纯血鸿蒙设备上,不需要依赖Android兼容层。

简单讲:一套Dart/Flutter业务代码,可以同时编译 Android、iOS、OpenHarmony(HAP)

核心原理

对Flutter Engine做Embedder嵌入适配,对接OpenHarmony Rosen图形管线、UIAbility生命周期,通过MethodChannel实现 Dart ↔ ArkTS双向通信,Flutter自绘UI渲染到鸿蒙Surface,复用方舟编译器、系统权限、分布式能力。

  • Dart业务代码几乎不变
  • 底层引擎适配鸿蒙图形、线程、生命周期
  • 输出产物是标准HAP应用包,可上架鸿蒙应用市场

主要优势

  1. 存量Flutter项目低成本接入鸿蒙生态
    纯Dart业务、纯Widget界面几乎不用改代码即可编译出鸿蒙HAP;只有带Android/iOS原生桥接的插件,才需要做鸿蒙适配替换。已经有成熟Flutter App,想快速覆盖鸿蒙设备,不用全部重写ArkTS。

  2. 多端UI高度一致性
    Flutter自绘渲染,不受各平台控件差异影响,手机、平板、车机界面表现统一;滚动、动画、首页各类动效(轮播、吸顶、骨架屏、入场动画)跨平台表现一致,和你前面问的App首页各种效果可以一套代码全部实现。

  3. 继承Flutter完整开发体验
    保留热重载、DevTools调试、完整Widget组件库;pub.dev海量纯Dart三方库直接复用,是鸿蒙跨端方案里三方库最丰富的方案。提供定制CLI,一条命令完成编译、真机调试、打包HAP。

  4. 可调用OpenHarmony原生系统能力
    支持调用分布式软总线、分布式数据KV、原子化服务、鸿蒙权限体系、硬件能力;Flutter页面和ArkTS原生页面可以混合开发、互相跳转,复杂原生逻辑继续写ArkTS,UI业务交给Flutter实现。

  5. 全场景设备覆盖
    支持OpenHarmony手机、平板、智慧屏、车机等设备,适合需要多终端统一UI的业务。引擎做了懒加载,跟随UIAbility生命周期启停,控制内存占用,减少后台资源消耗。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐