尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Flutter踩坑 · 转场动画卡顿优化:智能延迟 + 遮罩方案

Flutter踩坑 · 转场动画卡顿优化:智能延迟 + 遮罩方案 问题Flutter 跳转二级页面时,转场动画(~460ms)与 API 请求的同步处理(JSON 解析 setState Widget build)共用 UI 线程。请求响应回来后,同步处理抢占 UI 线程,导致动画丢帧、卡顿。根因:Dart 是单线程,动画渲染、网络数据处理、Widget build 全在同一条线程上排队。每帧只有 16ms 预算,处理逻辑占用超时就会丢帧。作用封装TransitionGuardLoadingOverlay两个组件,解决两个问题:组件解决什么TransitionGuard请求和动画同时跑,响应快就等动画结束再 setState,响应慢就直接处理LoadingOverlay请求慢时遮罩盖住整屏,防止用户看到空白/错乱页面,数据渲染后淡出时间线:请求 200ms 回来 → 动画还在播 → 等 260ms → setState → 不卡帧 请求 800ms 回来 → 动画早结束 → 直接 setState → 不卡帧(动画已结束)使用方式模式一:无遮罩适合:请求快、数据不是 UI 必需、空白一瞬间可接受。classDetailPageextendsStatefulWidget{constDetailPage({super.key});overrideStateDetailPagecreateState()_DetailPageState();}class_DetailPageStateextendsStateDetailPage{String?_data;bool _loadingtrue;overridevoidinitState(){super.initState();_fetch();}void_fetch()async{awaitTransitionGuard.run(future:_request(),onDone:(data){if(!mounted)return;setState((){_datadata;_loadingfalse;});},);}FutureString_request()async{awaitFuture.delayed(constDuration(milliseconds:200));return数据;}overrideWidgetbuild(BuildContextcontext){returnScaffold(appBar:AppBar(title:constText(详情)),body:Center(child:_loading?constText(加载中...):Text(_data!),),);}}模式二:有遮罩适合:请求慢、数据是 UI 必需、不能让用户看到空白页面。classDetailPageextendsStatefulWidget{constDetailPage({super.key});overrideStateDetailPagecreateState()_DetailPageState();}class_DetailPageStateextendsStateDetailPage{String?_data;bool _loadingtrue;overridevoidinitState(){super.initState();_fetch();}void_fetch()async{awaitTransitionGuard.run(future:_request(),onDone:(data){if(!mounted)return;setState((){_datadata;_loadingfalse;});},);}FutureString_request()async{awaitFuture.delayed(constDuration(milliseconds:800));return数据;}overrideWidgetbuild(BuildContextcontext){returnLoadingOverlay(isLoading:_loading,showLoading:true,child:Scaffold(appBar:AppBar(title:constText(详情)),body:Center(child:_loading?constSizedBox.shrink():Text(_data!),),),);}}两种模式的代码差异// 无遮罩returnScaffold(body:_loading?Text(加载中...):Text(_data!),);// 有遮罩returnLoadingOverlay(// ← 多包这一层isLoading:_loading,showLoading:true,// ← 这个参数child:Scaffold(body:_loading?SizedBox.shrink():Text(_data!),),);其余代码完全一样:initState、_fetch、TransitionGuard.run、onDone、setState。组件代码importpackage:flutter/material.dart;/// 转场动画防卡顿工具。////// 请求立即发出,响应回来后根据耗时判断是否需要等待:/// - 请求耗时 动画时长 → 等待剩余时间,让动画播完再处理;/// - 请求耗时 ≥ 动画时长 → 动画已结束,直接处理。classTransitionGuard{TransitionGuard._();staticFuturevoidrunT({requiredFutureTfuture,requiredvoidFunction(Tdata)onDone,voidFunction(Objecterror)?onError,int animationDurationMs460,})async{finalstopwatchStopwatch()..start();try{finalresultawaitfuture;stopwatch.stop();finalremainingMsanimationDurationMs-stopwatch.elapsedMilliseconds;if(remainingMs0){awaitFuturevoid.delayed(Duration(milliseconds:remainingMs));}onDone(result);}catch(error){if(onError!null){onError(error);}else{rethrow;}}}}/// Loading 遮罩 Widget,带淡入淡出效果。////// 包在 Scaffold 外层,isLoading showLoading 为 true 时/// 自动叠加半透明遮罩 转圈,盖住整屏(含导航栏)。classLoadingOverlayextendsStatelessWidget{finalbool isLoading;finalbool showLoading;finalWidgetchild;finalDurationfadeDuration;constLoadingOverlay({super.key,requiredthis.isLoading,this.showLoadingtrue,requiredthis.child,this.fadeDurationconstDuration(milliseconds:300),});overrideWidgetbuild(BuildContextcontext){if(!showLoading)returnchild;returnStack(children:[child,Positioned.fill(child:AnimatedOpacity(opacity:isLoading?1.0:0.0,duration:fadeDuration,curve:Curves.easeOut,child:IgnorePointer(ignoring:!isLoading,child:ColoredBox(color:Colors.black54,child:constCenter(child:CircularProgressIndicator()),),),),),],);}}关键设计决策决策原因用Stopwatch计时比DateTime.now().difference()更精确异常不额外等待错误处理通常很轻量,尽快让用户感知更重要遮罩在Stack内部跟着页面滑进来,不影响转场动画AnimatedOpacity淡出避免遮罩瞬间消失的突兀感IgnorePointer淡出期间透明遮罩不拦截点击showLoading参数同一套组件,一个参数切换两种模式适用边界JSON 大小建议 10KB本方案足够,不卡10-100KB本方案可用,动画结束后的 build 可能感知到轻微延迟 100KB建议用compute()把 JSON 解析丢到 Isolate,本方案治标不治本为什么 UniApp 没这个问题UniApp 的渲染层和逻辑层在不同线程——动画在原生/渲染线程,JSON 解析在 JS 线程,互不影响。Flutter 把所有事情放在同一个 UI 线程,拿到完全自定义能力的同时,需要自己处理重任务不卡线程。
返回列表