资讯动态

鸿蒙 Flutter 骨架屏:shimmer_pro 适配踩坑与性能优化

发布时间:2026/10/14 3:55:30 来源:尧图企业网站定制
做跨端开发最怕的不是 UI 写不出来而是真机上看起来跟设计稿完全两个样。最近我在负责某资讯客户端的 Flutter 页面迁移到鸿蒙平台其他功能都还好骨架屏加载这块最先被测试提了异常——页面切换后要么白屏一闪要么 shimmer 高光像抽筋一样来回抖。这个问题最后定位到 shimmer_pro 这个三方库的适配方式上。这篇文章把从依赖落地、最小组件封装到多层动效实现的完整适配过程整理出来重点讲讲鸿蒙侧 Flutter 工程里跑 shimmer 类效果时容易踩的坑和性能调优思路正在做或准备做鸿蒙侧 Flutter 开发的人可以直接参考。1. 这次适配要解决什么骨架屏与 shimmer_pro 的价值1.1 加载反馈比加载速度更容易被用户感知页面数据加载的耗时很多时候不是后端能立刻优化的尤其资讯流、商品列表这类场景首包接口动不动就要几百毫秒到一两秒。用户看到白屏的第一反应要么是“卡死了”要么是直接退出。骨架屏和菊花 loading 最大的区别在于它会先把内容的结构轮廓画出来让用户知道“这里马上会有数据”产品的整体节奏感也更强。我在这次适配的某资讯客户端里首屏是一个推荐流卡片列表包含封面图、标题行、摘要行和底部操作栏。骨架屏方案就是把这些区域的灰色占位块先渲染出来再叠加 shimmer 扫光效果。之前用普通 CircularProgressIndicator 的时候页面切进来总是先白一下加完骨架屏之后人工回归测试的反馈好了非常多明显不再有那种“页面没响应”的感觉。不过这里有个容易被忽略的点骨架屏不是简单的灰色矩形堆叠而是要尽量还原真实内容的排版比例。标题行宽度、摘要行宽度、头像圆角、卡片边距这些都要和最终数据保持一致否则接口返回的瞬间布局突变反而会造成视觉抖动。骨架屏组件尽量和真实 Item 用同一套布局结构只是把内容替换成占位块。1.2 为什么选 shimmer_pro 而不是自己写动画团队里最开始也有一种声音shimmer 不就是一条高光扫过去吗自己用 AnimationController 加 LinearGradient 写一个不就完了。但实际评估下来shimmer_pro 在动效覆盖面上要省事得多。它内置了多种扫光方向、循环周期控制、颜色渐变配置、嵌套布局支持还针对不同形状的占位块做了优化比从零手写一个能应对产品后续“增加一种高光样式”的需求稳定得多。我列过一个简单的对比方便大家判断选型方案灵活度维护成本鸿蒙适配工作量适合场景手写 AnimationController ShaderMask高高颜色、方向、周期都要自己调同样需要做渲染兼容验证动效非常简单且项目内数量少直接用 shimmer_pro较高低版本迭代快需要做依赖本地化但 Dart 层改动小资讯、商城等需要统一骨架屏体验的项目自研通用骨架屏框架最高极高要投入测试和文档初期投入很大大型团队且多端并行对中小团队来说shimmer_pro 是三套方案里性价比最高的。它对底部颜色的处理、扫光高光位置的计算全部封装好了我们只需要关注 Data 状态切换和真机渲染表现。需要提一句的是鸿蒙生态下三方库不一定能直接flutter pub get拉下来所以“依赖本地化”这件事基本绕不开后面第二章详细展开。1.3 鸿蒙适配的主要挑战在哪里shimmer_pro 本身是纯 Dart 实现核心原理是渐变加遮罩理论上只要 Flutter 引擎能正常渲染它就能跑。但鸿蒙 Flutter 工程和 Android 工程存在几处不一样的地方工程结构不同构建链不同插件注册方式也不同。最直接的影响是依赖解析和原生编译阶段一些 package 在 pub 上虽然可用但它的构建脚本可能仍然尝试去读 Android 目录导致鸿蒙构建报错。另一个挑战在动画调度上。骨架屏动画本质上是高频 UI 刷新如果在鸿蒙容器层没有正确接入 vsync动画会出现掉帧或者忽快忽慢。这个问题在模拟器上不明显真机上稍不注意就是一顿一顿的。后面我用到的办法是把动画尽量收敛在单层 Shimmer 组件内减少跨层同步同时用 RepaintBoundary 隔离重绘区域。还有一个很多人会忽略的坑包管理仓库。团队内部如果使用统一的 pub 仓库shimmer_pro 可能因为版本滞后拉取不到。最好的方式是先把源码放到本地 packages 目录通过 path 方式接入。这样做还能顺手给库打补丁不影响线上版本。2. 鸿蒙适配前需要准备的开发环境和依赖梳理2.1 先搞清楚 Flutter 在鸿蒙侧的运行方式所谓“鸿蒙适配”并不是把所有 Dart 代码重写一遍。Flutter 应用的 UI 逻辑基本都跑在 Flutter Engine 内部鸿蒙的底层只是提供容器、窗口和事件注入。对纯 Dart 的三方库来说适配更多地是“确保它不在构建期出错、不在渲染期踩平台差异”而不是改语言。所以在第一件事不是急着写代码是把工程环境理清楚。我用的是支持 ohos 平台的 Flutter SDK创建工程后首先看一眼根目录结构确认存在鸿蒙宿主工程、Flutter module 目录、以及原生插件注册入口。然后跑一遍flutter doctor -v确认引擎版本、工具链和真机调试通道都正常。确认完环境之后再跑flutter pub get把现有依赖全部拉下来。这里如果出现任何包带有plugin标识都要额外检查它的原生实现是否有鸿蒙目录。shimmer_pro 不依赖原生插件所以它的适配重点不在插件通道而在包引用和渲染兼容。我这里的原则是先跑通一个最小 demo确认 Flutter 能在鸿蒙真机上渲染动画再逐步叠加业务代码。2.2 依赖树梳理和原生插件缺口检查一个大型项目里依赖往往层层嵌套直接改一个包很容易带出隐形问题。我会先用flutter pub deps --stylecompact把依赖树导出来再逐个确认直接依赖和间接依赖有哪些每个包是否有 android/ios 原生目录非 Dart-only 的包是否提供了鸿蒙平台实现运行期间是否通过 Platform Channel 调用原生能力。整理成一张表会更直观依赖包平台实现鸿蒙侧状态处理方案shimmer_pro纯 Dart可直接用需本地化放入 packages 目录path 引用某图片加载库Android/iOS缺鸿蒙实现替换为支持多端的加载方案某缓存工具Dart FFI需验证 so 库框架兼容升级新版本或改为内置缓存逻辑某埋点 SDKAndroid/iOS缺鸿蒙实现走日志回调先做降级埋点一开始我以为 shimmer_pro 会像图片库一样直接出问题结果反而很简单。真正麻烦的是和它配套的列表加载库因为列表库内部依赖了原生长列表组件在鸿蒙侧并没有实现。我当时的处理方案是把列表改成 Flutter 自带的 ListView用懒加载和 viewport 控制性能正好绕开了原生依赖。这里给一个我的建议不要在适配一开始就全量替换依赖而是先跑通一个只包含 shimmer_pro 基础 ListView 的测试页面把这条链路固定下来再一页一页接入业务。否则几十个依赖一起上报错时连“是不是 shimmer_pro 引发的”都分辨不出来。2.3 依赖版本锁定与本地化尽管 shimmer_pro 是纯 Dart我仍然建议不要直接依赖 pub 上的远程版本。原因很简单鸿蒙构建环境不一定能访问默认 pub 仓库而且如果后期需要针对鸿蒙引擎的特殊行为改动动画参数改远程包几乎不可行。我最终的做法是把 shimmer_pro 源码放到项目根目录的packages/shimmer_pro文件夹里然后在pubspec.yaml中这么写dependencies: shimmer_pro: path: ./packages/shimmer_pro这样有几个好处。依赖完全随仓库走CI 构建不会因为网络问题失败本地改动可以快速验证版本号依然保留在包的pubspec.yaml里之后做版本升级有据可查。但本地化有一个要小心的地方如果直接改包内部代码后面从远程更新版本时会冲突。我的习惯是只在包外面包一层自己的 Widget用组合方式扩展能力尽量不动包里原有逻辑。只有当某个 Bug 必须改源码时才做最小改动并且在包里加注释说明改了哪里方便以后合并。3. 脚手架搭建从 pub 依赖到首屏渲染3.1 创建鸿蒙侧 Flutter 工程并接入 shimmer_pro确认好 Flutter SDK 支持 ohos 平台后创建一个新工程flutter create --org com.example.demo --project-name my_app --platforms ohos .如果你的 Flutter SDK 模板里没有 ohos 平台可以先通过鸿蒙侧的工程模板生成宿主工程再接入 Flutter module。这一步完成之后在pubspec.yaml里加入依赖并执行flutter pub get。environment: sdk: 2.17.0 4.0.0 dependencies: flutter: sdk: flutter shimmer_pro: path: ./packages/shimmer_pro第一次跑flutter pub get时有很大概率会遇到两个报错一个是包版本和当前 SDK 约束不匹配另一个是仓库地址访问超时。第一种情况我通常先查看 shimmer_pro 的pubspec.yaml中声明的 SDK range确认当前 Flutter 版本在范围内第二种情况就是上一步说的本地 path 方案。依赖装好后我先不做任何业务逻辑只在首页放一个最朴素的灰色矩形用 shimmer_pro 试跑几秒。这一步看似简单却能把“能不能渲染”“动画是否流畅”“热重载是否正常”这些基础问题提前暴露掉避免后面排查复杂页面时还要怀疑引擎有问题。3.2 骨架屏组件的最小封装工程能跑起来之后我封装了三个基础组件SkeletonBlock、SkeletonCircle、ArticleSkeleton。核心思路是让所有占位区域都用同一套底色和圆角风格方便后续调整主题。import package:flutter/material.dart; import package:shimmer_pro/shimmer_pro.dart; class SkeletonBlock extends StatelessWidget { const SkeletonBlock({ Key? key, this.width double.infinity, this.height 16, this.radius 4, }) : super(key: key); final double width; final double height; final double radius; override Widget build(BuildContext context) { return Container( width: width, height: height, decoration: BoxDecoration( color: const Color(0xFFE8EBF0), borderRadius: BorderRadius.circular(radius), ), ); } } class ArticleSkeleton extends StatelessWidget { const ArticleSkeleton({Key? key}) : super(key: key); override Widget build(BuildContext context) { return Shimmer.fromColors( baseColor: const Color(0xFFE2E6EC), highlightColor: const Color(0xFFF8FAFC), period: const Duration(milliseconds: 1400), direction: ShimmerDirection.ltr, child: Padding( padding: const EdgeInsets.all(16), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SkeletonBlock(width: 48, height: 48, radius: 24), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: const [ SkeletonBlock(width: 160, height: 18), SizedBox(height: 8), SkeletonBlock(width: double.infinity, height: 14), SizedBox(height: 8), SkeletonBlock(width: 100, height: 14), ], ), ), ], ), ), ); } }这段代码里有几个细节要提一下。ShimmerDirection.ltr是常见的水平扫光不同版本的 shimmer_pro 枚举名可能略有差异接入时以包内部为准。周期 1400 毫秒是实际体验比较稳的值太快会显得浮躁太慢又感觉加载停住了。底层颜色和高亮颜色不要选纯白纯白会把灰色块照得反白在低端机上特别容易产生闪烁感。3.3 动画参数怎么调最舒服参数调优不像写业务代码视觉上的“舒服”需要真机反复看。我在这个项目里沉淀了一套初始参数参数推荐值范围注意事项period1200ms ~ 1600ms小于 800ms 会让人焦虑directionltr / rtl左右扫光最通用向上扫光适合照片墙baseColor低饱和灰蓝调纯灰色容易显脏highlightColor比 baseColor 亮 2 档对比太强会产生刺眼的光带borderRadius4 ~ 8过大影响卡片真实感另外要注意高光宽度。很多同学以为扫光是一条很细的白线真正稳定耐看的其实是宽度占容器 30% 左右的“光带”移动时更像是光线自然滑过。如果底部深色内容多还可以把高光透明度从 1.0 降到 0.85观感会柔和很多。调参阶段我建议固定一台中低端真机把动画开着反复看 5 分钟以上。只看一两遍很容易被新鲜感欺骗看久了才能发现光带是不是在某个位置有卡顿亮度是不是让人疲劳。4. 核心动效实现多层叠加打造沉浸式骨架屏4.1 理解 shimmer 的底层原理shimmer_pro 的功能再丰富底层无非是“一个移动的渐变遮罩”。Flutter 里最直接的实现方式是ShaderMask用LinearGradient的 begin/end 位置随着动画值变化从而让高光带扫过一组子组件。下面是一段精简版方便理解原理class SimpleShimmer extends StatefulWidget { const SimpleShimmer({Key? key, required this.child}) : super(key: key); final Widget child; override StateSimpleShimmer createState() _SimpleShimmerState(); } class _SimpleShimmerState extends StateSimpleShimmer with SingleTickerProviderStateMixin { late final AnimationController _controller AnimationController( vsync: this, duration: const Duration(milliseconds: 1400), )..repeat(); override void dispose() { _controller.dispose(); super.dispose(); } override Widget build(BuildContext context) { return AnimatedBuilder( animation: _controller, child: widget.child, builder: (context, child) { final t _controller.value; return ShaderMask( shaderCallback: (Rect bounds) { return LinearGradient( begin: Alignment(-1 2 * t, 0), end: Alignment(1 2 * t, 0), colors: const [ Color(0xFFE2E6EC), Color(0xFFF8FAFC), Color(0xFFE2E6EC), ], stops: const [0.35, 0.5, 0.65], ).createShader(bounds); }, blendMode: BlendMode.srcATop, child: child, ); }, ); } }生产环境不太建议直接用这个简化版因为它没有处理渐变方向枚举、边缘回弹、多颜色排列等细节。但理解这段代码后你就能明白 shimmer 卡顿或者光带位置不对时问题大概率出在动画值和渐变坐标的映射关系上而不是鸿蒙引擎渲染不了。4.2 三层层级叠加呼吸、扫光、局部聚焦标题里的“多维动效沉浸体验”我落地的方案是三层效果叠加底层做呼吸明暗中层做横向扫光关键组件上做局部聚焦高光。第一层呼吸效果用一个半透明黑色遮罩透明度在 0.9 到 0.6 之间循环变化。第二层扫光就是 shimmer_pro 默认方向控制高光从左到右经过。第三层局部聚焦比较微妙它会让头像和标题区域的高光停留时间略长一点模拟“视线首先落在主要内容上”的效果。代码上不推荐真的开三个 AnimationController那对性能负担太重而且相位同步非常难调。我的做法是只用一个 controller然后派生不同的Tweenfinal breathOpacity Tween(begin: 0.6, end: 0.9).transform(_controller.value); final sweepOffset Curves.easeInOut.transform(_controller.value);把三层视觉变化统一由同一个时间轴驱动视觉上丰富但引擎只响应一个动画源性能压力会小很多。如果产品需要不同层级有不同节奏再考虑第二 controller但要保证页面级只有一个页面控制器不要每个组件都 new 一个。这里想提醒一句沉浸感不等于疯狂堆效果。动效加载页毕竟只是过渡态用户真正想看到的是内容本身。扫光和呼吸两层已经能覆盖绝大多数场景局部聚焦只用在首屏第一张卡片上其他卡片保持普通 shimmer这样页面会更有层次感也不会因为全部区域都在闪而导致视觉噪音。4.3 状态机接入加载、成功、错误三态骨架屏不只是“加载时展示”它还需要和接口状态联动。我把页面状态收敛为一个枚举构建逻辑完全由状态驱动enum LoadState { loading, success, error } class ArticleListPage extends StatefulWidget { const ArticleListPage({Key? key}) : super(key: key); override StateArticleListPage createState() _ArticleListPageState(); } class _ArticleListPageState extends StateArticleListPage { LoadState _state LoadState.loading; ListArticle _articles []; Futurevoid _load() async { setState(() _state LoadState.loading); try { final data await Future.wait([ fetchArticles(), Future.delayed(const Duration(milliseconds: 500)), ]); _articles data[0] as ListArticle; if (mounted) setState(() _state LoadState.success); } catch (_) { if (mounted) setState(() _state LoadState.error); } } override Widget build(BuildContext context) { switch (_state) { case LoadState.loading: return const ArticleSkeleton(); case LoadState.success: return ArticleListView(data: _articles); case LoadState.error: return ErrorRetryView(onRetry: _load); } } }这里有两个经验比较值钱。第一请求接口通常在几十毫秒内就返回了如果骨架屏出现后立刻消失会产生“屏幕闪了一下”的印象反而比长寿 loading 更伤体验。所以我用一个Future.wait保证骨架屏最少展示 500ms。第二从骨架屏切到真实列表时我给外层包了一个AnimatedSwitcher让过渡变成 200ms 的淡入淡出避免灰块直接变成内容的硬切换。状态机的好处是职责清晰loading 分支不会和 success 分支混在一起产品后续要加“下拉刷新时保留旧列表”也只用把状态切回 loading 时保留旧数据即可。5. 性能优化与渲染细节梳理5.1 隔离重绘区域RepaintBoundary 的正确姿势shimmer 动画最直接的性能风险是“动画区域内所有组件每帧全部重绘”。如果页面里有几十个骨架卡片每个卡片又包含十多个灰色色块Flutter 的绘制层会被迅速打满。我采取的隔离策略是给每个列表项包一层RepaintBoundaryListView.builder( padding: const EdgeInsets.all(16), itemCount: 8, itemBuilder: (context, index) { return RepaintBoundary( child: const ArticleSkeleton(), ); }, )这样每个卡片的重绘边界是独立的扫描高光经过某一张卡片时只有那一块区域会被重绘其他卡片可以复用上一帧的绘制结果。对于整页只有一个Shimmer组件包住所有内容的情况增加 RepaintBoundary 的效果尤其明显因为默认情况下任何动画变化都可能导致整棵子树重绘。不过也不是边界越多越好。边界本身也有组合成本如果每行内部每个灰色块都包一层 RepaintBoundary反而会增加绘制层的合并开销。我的经验是以“卡片”为单位切分而不是以“灰色块”为单位切分。5.2 减少 rebuild 与动画回调开销很多同学遇到动画掉帧第一反应是怪真机性能差其实大多数时候问题都出在动画帧回调里做了不该做的事。比如AnimatedBuilder的 builder 里直接执行了耗时的数据查询、解析 JSON、或者创建新的对象每帧都在触发性能自然好不了。下面几个点是我这次适配过程中逐条排查后的优化清单动画组件的外层 child 尽量缓存不要每次 build 都重新构造骨架块的子组件全部声明为const避免不必要的 widget 重建不要在_controller.addListener里去setState动画本身会通过 AnimatedBuilder 驱动重绘接口请求、图片解码、列表数据处理全部放到动画链路之外用ListenableBuilder时把不依赖动画值的大树放在 child 参数里。代码层面最直接的一个优化是复用 childAnimatedBuilder( animation: _controller, // 这里的 child 在动画过程中不会重建 child: const ArticleSkeleton(), builder: (context, child) { return ShaderMask( shaderCallback: (bounds) { ... }, child: child, ); }, )这样动画帧期间只重建 ShaderMask 这一层灰色块创建一次就够了。我实测下来页面内卡片数量超过 6 条时这个优化前后帧时间能差出 3ms 到 5ms低端机上感知很明显。5.3 真机帧率观测与降级策略优化不能靠感觉我在鸿蒙真机上开了 Flutter Performance Overlay分两步观测。第一步看 UI 线程帧耗时第二步看 Raster 线程帧耗时。如果 UI 线程高通常是 widget 重建和布局开销大如果 Raster 线程高通常是图层合成和着色器编译压力大。shimmer 高光本身依赖渐变着色器在部分真机上首次渲染会发生着色器编译导致前几十帧卡顿。应对办法是在骨架屏真正出现前先在内存里用一个不可见的简单渐变做预热。这个听起来像玄学但在低端机上效果非常明显首次进入列表页的掉帧能减少大半。如果过完上面所有优化低端机仍然掉帧我会启动降级策略降级档位策略适用场景第一档将双层扫光降为单层扫光中端机视觉仍然流畅时第二档只保留呼吸效果关闭扫光大量卡片同时进入屏幕时第三档仅页面可见的首屏内容保持动画离屏内容使用静态灰块列表滚动很快的页面离屏内容降级可以借助VisibilityDetector或者PageView生命周期事件。其实代码原理很简单检测到组件不可见时调controller.stop()重新可见时调repeat()。注意 stop 和 repeat 时要判断页面是否 mounted否则会在页面销毁后收到状态异常。6. 踩坑实录鸿蒙适配中常见问题与排查6.1 pub get 时依赖找不到症状是执行flutter pub get后直接报“Because shimmer_pro requires SDK version compatible with Flutter xx, my_app doesnt match”。第一次遇到时我差点以为是 shimmer_pro 不支持鸿蒙后来发现只是 Flutter SDK 版本太旧而 shimmer_pro 新版对 SDK 的最低版本要求提高了。另外一种报错是仓库地址访问异常。鸿蒙 Flutter 工程在部分团队网络环境下拉远程依赖会失败最稳的解决办法就是第二章说到的本地 path 引用把包放到packages/目录下。如果确实要用远程仓库优先把编译机的仓库地址换成团队内部可访问的地址不要在个人电脑上手动复制依赖否则 CI 上仍然会挂。这个坑排查起来比较费时间因为报错信息里不一定会显示真实原因。我的经验是先单独跑一个空工程只加 shimmer_pro 这一个依赖如果空工程能过再把业务页面代码挪进来很快就能定位到是不是依赖冲突。6.2 文字占位不显示或黑块骨架屏最容易出问题的不是灰色块而是用Text去模拟文字行的场景。鸿蒙侧一部分低版本引擎在字体 fallback 上不如 Android 生态稳定如果 Text 控件里的字符集在系统字体里找不到对应字形真实设备上就可能渲染成黑块。解决方式很简单所有文字占位区域一律用Container或DecoratedBox不要用Text。如果某个骨架组件里需要展示一个“空白行”但又要保持语义用SizedBox或者ExcludeSemantics包住即可。这样也能避免辅助功能读出无意义的空字符串。还有一个隐蔽问题给灰色块设置圆角后某些设备上圆角边缘会有一条黑边。这是因为容器背景色和页面背景色不一致且动画过程中灰色块被 BlendMode 叠加后产生了颜色边界。处理办法是给圆角块加一层很细的边框颜色和底色完全一致或者在父容器上设置 clipBehavior。6.3 动画偶发停止与内存上涨我遇到过一种情况列表页反复进入退出几次后骨架屏动画不再动了但页面还能正常滑动。控制台里没有报错只是 AnimationController 一直停留在某个值。定位后发现是页面销毁时没有把 controller dispose 掉旧的 controller 仍然被底层 Ticker 引用占用了 vsync 通道。处理方式是要保证所有 AnimationController 都在 State 的dispose里释放override void dispose() { _controller.dispose(); super.dispose(); }另一个相关问题是内存曲线一路上涨。原因通常是页面的 push/pop 生命周期和引擎的回调绑定不够干净后台页面的动画没有暂停。我给页面基类加了一个WidgetsBindingObserver在didChangeAppLifecycleState里暂停动画回到前台再恢复同时确保Unfocus的时候也停掉不可见区域的 Ticker。排查这类问题没有捷径打开 DevTools 的 Memory 和 Frame 页面看曲线配合日志确认 controller 生命周期。我自己的排查记录里最顽固的一次问题竟然是在一个静态变量里缓存了骨架屏组件导致 State 永远不会释放直到页面销毁后动画还在后台跑。6.4 热重载不生效与状态残留鸿蒙真机调试和 Android 的体验存在一点差异热重载有时不会重新触发 build或者动画状态没有归零导致高光停在一个奇怪的位置。这其实不是 shimmer_pro 的问题而是动画控制器在热重载后的状态残留。我有两个习惯可以很大程度规避这个问题。一是把 controller 的初始化放在initState而不是用字段的 late 初始化加静态赋值这样热重载后状态更容易被完整重置。二是如果发现热重载后动画位置异常直接hot restart不要纠结因为动画类组件在热重载时对旧状态的清理本来就不如普通纯函数稳定。另外当页面进入后台再回到前台时我会在StatefulWidget的state里重写生命周期回调主动恢复动画override void didChangeAppLifecycleState(AppLifecycleState state) { if (state AppLifecycleState.resumed) { _controller.repeat(); } else if (state AppLifecycleState.paused) { _controller.stop(); } }这个小改动在真机上效果很明显尤其是锁屏后重新解锁骨架屏动画不会僵住。这次适配下来我最大的体会是鸿蒙侧跑 Flutter 三方库不能默认“pub 上能拉到就能直接用”。shimmer_pro 虽然是纯 Dart 库依赖解析、渲染边界、动画生命周期这些环节依然需要逐个验证。我现在做新增依赖前都会先看一眼它的平台实现纯 Dart 库也先在空工程里跑一个最小 demo再放进业务页面。骨架屏这种看似边缘的功能恰恰是用户对 App 性能的第一印象花点时间把动效和生命周期梳理干净后续页面接入会省下很多排查时间。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑