资讯动态

Flutter适配OpenHarmony:蜘蛛纸牌牌面渲染与性能优化实践

发布时间:2026/10/1 19:26:21 来源:尧图企业网站定制
前阵子我在做游戏集合App的OpenHarmony适配时第一个单独拎出来重做的模块是蜘蛛纸牌。可能有人觉得蜘蛛纸牌是个几十年前的老玩法但在Flutter跨端跑OpenHarmony这件事上它反而是最考验“牌面显示”基本功的模块——牌面要清晰、堆叠要均匀、翻牌要流畅还要在鸿蒙设备的屏幕尺寸和渲染引擎差异下保持稳定。这篇文章我就围绕Flutter for OpenHarmony这套技术栈把蜘蛛纸牌的牌面显示从数据模型、绘制方案、布局算法到平台适配整个链路拆开讲包括我踩过的坑和最后保留的实测方案。适合正在把Flutter项目往OpenHarmony上迁移、或者打算做棋牌/卡牌类游戏的开发同学参考。1. 游戏集合里的蜘蛛纸牌为什么先做牌面显示整个模块怎么搭1.1 蜘蛛纸牌在游戏集合App里的定位游戏集合App一般会塞不少轻量玩法进去扫雷、俄罗斯方块、蜘蛛纸牌是比较常见的三件套。这类App的开发节奏通常是先搭一个容器页面再逐个填充玩法模块。我选择先把蜘蛛纸牌拿出来做不是因为它简单而是因为它作为“渲染样板”非常合适。蜘蛛纸牌的牌面显示覆盖了卡牌游戏几乎所有的UI需求单张牌面的花色点数绘制、多列牌堆的错位堆叠、牌背和翻牌动画、拖拽移动一组牌时的视觉反馈、牌桌在不同屏幕尺寸下的缩放适配。这些能力一旦跑通后面接斗地主、接接龙、甚至接麻将牌型展示都能直接复用。反过来如果先做扫雷或者俄罗斯方块等做到纸牌时还是会回来补Canvas绘制、手势拖拽、布局缩放这些基础能力等于多走一圈弯路。另外有一点容易被忽略游戏集合App的整体体验会直接影响用户留存而牌面显示是否舒服是最直观的第一印象。牌面太小看不清、牌背纹理粗糙、拖拽时牌组不跟手这种细节问题在评测里会被放大很多倍。把牌面显示作为第一个攻坚点从项目管理的角度也划算。1.2 技术选型Flutter跑OpenHarmony的可行性和代价选Flutter做OpenHarmony游戏集合App这套组合我在实际项目里权衡过不少。Flutter在Android、iOS、Windows、macOS、Linux之外已经有一套OpenHarmony的适配分支社区维护的flutter_flutter仓库可以直接构建出OpenHarmony的hap包Dart代码几乎不需要为平台修改。这正好符合游戏集合App“一套代码多处跑”的需求。和别的跨端框架比Flutter在卡牌这类UI密集型场景有两个明显优势。首先是自绘渲染引擎UI的每个像素都由Flutter自己控制不像WebView套壳方案那样有DOM节点和浏览器排版的开销。其次是Dart语言的AOT编译帧率稳定性比解释型方案好一些处理发牌腾挪这种连续动画时不容易掉帧。代价也是实实在在的。OpenHarmony分支通常落后于Flutter官方主版本不能用最新的Dart SDK和Flutter引擎特性我项目里就遇到过flutter_flutter分支版本与本地OpenHarmony SDK不匹配导致构建报错的情况这点后面单独说。所以技术选型不是“能不能跑”的问题而是“愿不愿意把版本锁死”的问题。对游戏集合App来说答案显然是愿意。1.3 模块目录与边界划分蜘蛛纸牌模块我建议独立成一个目录别和游戏集合的通用层纠缠在一起。我在lib/games/spider/下做了五个子文件lib/games/spider/ ├── model/ │ └── card.dart # 卡牌数据模型 ├── widget/ │ ├── card_view.dart # 单张牌面的绘制组件 │ └── tableau_view.dart # 牌桌整体布局组件 ├── layout/ │ └── tableau_layout.dart # 列宽、牌堆偏移、缩放等几何计算 ├── controller/ │ └── game_controller.dart # 牌局状态、发牌、移动逻辑 └── platform/ └── game_channel.dart # OpenHarmony平台通道封装这个划分的核心思想是牌面显示相关的几何计算和绘制逻辑全部从业务状态里剥离。model/card.dart里只有牌的数据属性widget/里只管怎么画和怎么摆controller/管规则和状态platform/管OpenHarmony原生侧交互。这样后续如果要加多花色难度、重新洗牌或者存档恢复每一层都可以单独改不会出现改一个牌面阴影结果把发牌逻辑改坏了的情况。2. 牌面渲染我没用Widget堆牌而是用CustomPaint一张张画出来2.1 卡牌数据模型业务字段和视图状态分开卡牌数据模型是牌面显示的地基。这里我踩过一个坑一开始把“是否被选中”“当前偏移量”这种视图状态直接塞进了CardModel结果拖动一张牌时同一个CardModel被多处引用改一个字段其他牌也跟着变。正确的做法是业务字段和视图状态彻底分开。enum CardSuit { spade, heart, club, diamond } enum CardRank { ace, two, three, four, five, six, seven, eight, nine, ten, jack, queen, king } class CardModel { final CardSuit suit; final CardRank rank; bool faceUp; CardModel({required this.suit, required this.rank, this.faceUp false}); bool get isRed suit CardSuit.heart || suit CardSuit.diamond; String get rankLabel { switch (rank) { case CardRank.ace: return A; case CardRank.jack: return J; case CardRank.queen: return Q; case CardRank.king: return K; default: return rank.index.toString(); } } }faceUp属于业务状态因为它影响规则判断——只能翻面朝上的牌、只能移动同花色连续降序的明牌这些都是游戏逻辑。而isSelected、dragOffset、dropTarget这类东西纯粹是UI表现不该出现在模型里。我的做法是在列容器层用Map维护临时选中状态或者用独立的SelectionState保存这样控制逻辑和渲染逻辑就不会互相污染。2.2 牌面的绘制流程圆角、渐变、阴影和花色单张牌面的绘制是整个模块最核心的部分。我用CustomPaint而不是Container堆叠核心原因是性能和一致性。牌面绘制流程分四层第一层是阴影。直接在Canvas上画一张牌先用canvas.drawShadow()压出阴影阴影颜色用黑色45%透明度偏移控制在垂直方向2个像素模糊半径不要超过4。很多卡牌游戏会把阴影画得很重实际在移动端小尺寸牌面上反而显得脏。第二层是牌底。牌面主体是一个圆角矩形底色用接近纯白的#FDFDFD圆角半径8宽度1点5的描边。牌底内部加了一层非常淡的垂直渐变从顶部到右下角让白色微微偏暖这样可以避免大面积纯白在低亮度屏幕上晃眼。第三层是内边框。距牌边缘4个像素再画一个更细的圆角矩形作为视觉装饰线。第四层是花色和点数。左上角和右下角各画一个角标包含点数和花色符号牌面中央画一个大号花色符号。角标的右下角需要旋转180度这样牌拿起来时两个角都是正向的。实现方式是用canvas.translate()到右下角然后canvas.rotate(pi)再画一遍。贴一段核心绘制代码方便直接抄class CardPainter extends CustomPainter { final CardModel card; final bool selected; CardPainter({required this.card, this.selected false}); override void paint(Canvas canvas, Size size) { final rrect RRect.fromRectAndRadius( Offset.zero size, const Radius.circular(8)); // 阴影 canvas.drawShadow( Path()..addRRect(rrect), Colors.black45, selected ? 6 : 3, false, ); // 牌底 final baseGradient Paint() ..shader const LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Color(0xFFFEFEFE), Color(0xFFF0F0F0)], ).createShader(Offset.zero size); canvas.drawRRect(rrect, baseGradient); // 描边 canvas.drawRRect( rrect, Paint() ..style PaintingStyle.stroke ..strokeWidth 1.2 ..color const Color(0xFFB0B0B0), ); if (!card.faceUp) { _drawBack(canvas, size); return; } _drawCorners(canvas, size); _drawCenterSuit(canvas, size); } override bool shouldRepaint(covariant CardPainter oldDelegate) { return oldDelegate.card.faceUp ! card.faceUp || oldDelegate.card.suit ! card.suit || oldDelegate.card.rank ! card.rank || oldDelegate.selected ! selected; } }2.3 牌背、角标和选中态的处理牌背比牌面容易被人忽略但凡是玩过实体纸牌的人对牌背的期待都很高。我做了两种牌背方案第一种是深蓝渐变底加菱形中心花纹第二种是蓝白细条纹。实测下来第一种在OpenHarmony上的低端模拟器里表现更稳因为条纹方案在缩放时会产生摩尔纹。牌背的绘制逻辑是这样深蓝渐变底#2C3E50到#1A252F中间画一个菱形或者梭形图案图案颜色用淡金色四角再各加一个小圆点。这样牌背正放反放都看得出方向在堆叠状态里也能快速识别有没有翻错。选中态我用了一整套视觉组合而不是只改一个属性。选中的牌阴影从3像素扩大到6像素同时牌底渐变稍微亮一点再在圆角矩形外侧画一圈2像素的高亮描边。这套组合在阳光下和暗光下都看得清比单纯调透明度靠谱。角标绘制需要特别注意TextPainter的缓存。每张牌的角标内容只有“A到K”加四个花色符号组合是有限的但如果不缓存每次重绘都新建TextPainter50张牌同时出现在画面里就会有明显的GC压力。我的做法是在初始化时预构建所有(rank, suit)组合的角标纹理绘制时直接从Map里取。2.4 自绘方案对比Widget堆叠性能账要算清楚为什么不用Container、Text堆一张牌我用真实的数据算笔账。用Widget堆叠方案画一张牌圆角容器至少2个、文本至少4个、边框和阴影各若干一张普通牌面轻松生成30个左右的Widget对象。蜘蛛纸牌开局是10列54张牌加上容器、行布局、手势层整个牌桌的Widget数量奔着2000个去了。每帧动画重建时diff、布局、渲染的负担全部压在UI线程上。而CustomPaint方案里一张牌就是一个RenderCustomPaint一颗RenderObject牌面内部的花色、文字全都是Canvas绘制指令不产生额外的Widget节点。等于把30个节点的树压成1个节点代价是写绘制代码麻烦一点。但从性能账来看非常划算我实测在OpenHarmony模拟器上同样的牌桌用Widget堆叠方案UI线程单帧耗时8到12毫秒切到自绘后降到4到6毫秒帧率稳定性是两种体验级别。自绘方案也有一个坑文字在不同倍率缩放下容易出现轻微模糊。解决办法是用Paragraph而非直接TextPainter并且设置好textScaleFactor。Flutter的TextScaler.noScaling在牌面上是必须的否则用户系统字号调大后牌面排版会直接崩掉。3. 牌桌布局算法10列54张牌的堆叠、错位与屏幕适配3.1 初始牌堆的数量与显隐偏移计算蜘蛛纸牌的标准开局是使用两副牌共104张初始发牌54张到10列前4列每列6张后6列每列5张剩下的50张作为库存牌堆放在右下角。这个数量关系是固定的实现时可以直接写死。牌堆在垂直方向上的偏移是布局里最讲究的部分。牌面朝下的牌只要露出一条边让人知道下面还有牌就行偏移量用小值牌面朝上的牌要能看清点数和花色偏移量要大一些。我的经验值是牌状态垂直偏移量说明朝下牌堆10像素只露出一条牌边朝上牌堆26到30像素保证点数花色清晰选中牌组比正常多2像素提升跟手感计算列内每张牌的Y坐标可以用一个简单的乘法double yForIndex(int index, bool faceUp) { return index * (faceUp ? visibleOffset : hiddenOffset); }但有个细节很容易忽略当一列堆了很多牌时总高度会超出可视区域。我的策略是牌堆底部对齐所在列的底部前面多出来的部分向上延伸并裁剪。这样做的原因是玩家操作时最关心的永远是这一列最末尾那张可操作的牌底部锚定让最后一张牌永远可见。选牌拖拽时如果目标列顶部牌超出屏幕则触发自动滚动把目标牌带回视野。3.2 卡牌尺寸的动态计算与横竖屏适配蜘蛛纸牌10列牌桌在手机竖屏上最大的挑战是宽度。单张牌不能小于44像素宽10列加9个列间距加左右安全边距需要的总宽度通常超过460像素。大多数手机逻辑宽度只有390到430所以不处理缩放是没法用的。我的做法是先计算理想牌宽再对整体牌桌做等比缩放。核心公式大概是这样final cardWidthBase 60.0; final columnGap 2.0; final tablePadding EdgeInsets.symmetric(horizontal: 6); final idealTableWidth cardWidthBase * 10 columnGap * 9 tablePadding.horizontal; final scale min(1.0, constraints.maxWidth / idealTableWidth);这里有个先决条件卡牌宽高比。我固定用5比7也就是牌宽60时牌高84这个比例在竖屏和横屏下都比较自然。得到scale之后不再单独计算每张牌的尺寸而是用Transform.scale把整个牌桌按比例放缩。这个方案的好处是手势命中区域也一起缩放了不会出现牌画小了但点击热区还是原来大小的问题。横屏时反而要处理另一个问题宽度太宽、卡牌被迫放大。所以scale的下限控制在0.78上限控制在1.15。超过1.15的富余宽度留给左右留白让牌桌居中显示别盲目把牌撑满整屏。3.3 顶部回收区、底部发牌区怎么安排蜘蛛纸牌的操作区分为三块顶部是8个回收槽中间是10列牌桌右下角是库存发牌区。回收槽我用一个横向排列的Row每个槽位大小和单张牌一致空槽位用虚线圆角矩形占位。当一组同花色从K到A的完整序列被移到回收区时槽位会被填满并播放一个化简动画同时整列牌桌里的那一组被移除视觉上是一次“塌缩”不需要额外画特效。发牌区放在右下角用库存剩余数量做角标提示。点击发牌区时从库存牌堆里抽出10张分别飞到10列的最底部。注意这个动画是所有列同时进行的如果此时某列已经有大量朝下的牌发牌也要有两三帧的先后错位视觉上按从左到右的波次飞入而不是全部同帧这样看起来更自然。整个牌桌的布局层级是外层Column里放回收区和牌桌区域牌桌区域用Expanded包住一个Stack10列在Stack里用Positioned定位。发牌区悬浮在Stack的右下角不参与流式布局方便做飞行动画。库存发牌区之上再盖一层透明按钮点击事件在手势层统一处理避免发牌动画进行中误触。4. OpenHarmony平台适配引擎、通道和原生视图的取舍4.1 Flutter跑OpenHarmony的环境组合与构建流程OpenHarmony上跑Flutter不能直接用普通安装包那一套流程。首先要拿到社区维护的OpenHarmony分支Flutter SDK这个仓库实际上是flutter官方仓库的ohos分支也可以叫flutter_flutter的OpenHarmony适配版。版本对应关系必须锁死我用的组合是OpenHarmony SDK 5.0.0系配合flutter_flutter对应的release分支这套组合能稳定构建hap。构建流程大致的路径是Flutter SDK路径配好之后用命令行创建或者打开工程生成ohos/目录在DevEco Studio里打开这个ohos/目录作为OpenHarmony工程从Flutter工程里声明的插件依赖生成har包最后用hvigor构建出hap。注意这里不要再走Android Gradle的apply plugin方式去集成Flutter插件OpenHarmony工程的Flutter插件是作为har依赖被hvigor管理的照搬Android的Gradle用法会在构建时报出You are applying flutters main gradle plugin imperatively using the apply这种错误我第一次折腾这个就花了大半天。具体工程配置要以你本地OpenHarmony SDK版本的官方说明为准但这个整体框架是通用的引擎so库、资源包、插件har三件套缺一个hap包都跑不起来。4.2 Impeller和Skia的选择牌面渲染在两者下的表现Flutter 3.10之后Impeller渲染引擎逐渐成为新默认OpenHarmony分支也在跟进Impeller能力。在蜘蛛纸牌这个场景里Impeller和Skia的差异是真实存在的。Impeller的优势是着色器预编译避免了Skia在首次运行时编译着色器带来的卡顿。但我实测在部分OpenHarmony模拟器和入门级设备上Impeller渲染大量带圆角、渐变、阴影的牌面时偶尔会出现首帧模糊或残影尤其在快速拖拽几十张牌的场景。这通常不是引擎本身的问题而是设备GPU驱动对某些着色器组合支持不完整。我的建议是牌面绘制本身不要依赖复杂的模糊遮罩阴影用简单的drawShadow渐变控制在两到三个色标。这样无论切到Impeller还是回退Skia表现都接近。如果产品最低配置只覆盖模拟器或低端开发板可以在运行时把渲染引擎切回Skia用牌面的基础绘制兜底。这个开关应该在设置页暴露出来方便线上问题反馈后远程切换。4.3 EventChannel和MethodChannel的实际用途传统Flutter的MethodChannel在OpenHarmony上同样可用但EventChannel的使用频率比很多人想的高。玩法本身不需要原生能力但游戏集合App会有一些统一个性化设置比如牌面主题、触感反馈、截图分享。我用EventChannel做了一件比较典型的事把牌局截图保存到系统相册保存完毕后把“成功”或“失败”的结果通过事件流推回Dart层。这个场景用EventChannel比MethodChannel顺手因为保存动作是异步的原生侧可能在文件系统完成后才返回用Stream等待事件更符合直觉。Dart侧封装大概是这样的class GameChannel { static const _method MethodChannel(spider_game/storage); static const _event EventChannel(spider_game/feedback); static Futurevoid saveScreenshot() async { return _method.invokeMethod(saveScreenshot); } static Streambool onSaveResult() { return _event.receiveBroadcastStream() .map((event) event as bool); } }原生侧在OpenHarmony应用里用ArkTS或者Native接口去注册对应的MethodChannel和EventChannel业务逻辑里要记得在页面销毁时取消订阅避免流泄漏。4.4 PlatformView在牌局中的边界能不用就不用游戏集合App难免需要嵌入原生视图比如插屏广告、用户协议WebView、原生分享面板。OpenHarmony上Flutter的PlatformView适配也已经有方案但我自己对“在牌局中嵌入PlatformView”持保守态度。原因很直接每个PlatformView都会在Flutter渲染层之外独立合成一个原生纹理牌桌上有几十张牌、拖拽动画帧率又高再叠加原生纹理合成在入门设备上很容易突破合成层上限出现闪烁或者帧率骤降。我的处理方式是广告、WebView等原生视图只放在开局前、结算页这类静止画面里牌局进行中绝不插入。这样平台通道和PlatformView都保持在可控范围牌面显示区域始终是纯Flutter自绘渲染路径最干净。如果你确实需要在牌面区域附近显示原生控件尽量把它放在牌桌之外的固定层并且用Visibility隐藏而非从树上移除减少PlatformView反复重建的开销。5. 牌面刷新与交互状态翻牌发牌拖拽时视图怎么稳住5.1 翻牌动画的状态换算与动画控制翻牌是蜘蛛纸牌里最高频的动画。从背面翻成正面我建议用一个两阶段动画第一阶段横向缩放从1到0第二阶段从0到1同时切换绘制的牌面朝向。视觉上就是“牌翻过去了”。每个翻牌动作由AnimationController驱动时长控制在180到220毫秒之间。翻牌动画会产生一个容易被忽略的问题动画期间如果用户再点击同一张牌状态机可能混乱。我在GameController里给每列加了动画锁动画进行中该列不可响应新的翻牌和拖拽。锁定不是阻塞整个牌桌而是只锁受影响的那两列这样其他列的移动仍然顺畅。动画驱动过程中还有一个重要细节翻牌完成后要把卡牌状态和列布局都标记为“已稳定”否则翻完半张牌却因为布局计算出错又闪回背面这种bug排查起来特别头疼。我的做法是在动画StatusListener里做一次强制同步把faceUp字段的写入和最终绘制放在同一帧。5.2 选牌和拖拽移动一摞牌的手势实现蜘蛛纸牌的选牌不是单张点击而是可以选择一摞牌——从列顶开始的一串连续同花色降序牌组。所以手势交互要做两个层次。第一层是点击选牌。点击一张朝上的牌时判断从这张牌到列顶是否构成合法可移动牌组合法则整组高亮非法则只更新震动反馈不清空选择。这里视觉上要做一个“整组抬起”的反馈把这组牌整体向上偏移2像素并给整组外圈加亮。第二层是拖拽。我用LongPressDraggable加DragTarget的组合来实现。拖拽的data是一个MovePayload包含来源列序号和起始牌索引feedback组件不是单张牌而是整组牌的快照。快照用RepaintBoundary.toImage生成组牌区的位图拖拽过程中这个位图带透明度和轻微缩放看起来就是整个牌叠被拎起来。到达目标列时DragTarget校验目标列顶牌是否比被拖牌组首牌大1合法则显示一个高亮插入槽位松手后整组牌落到新列。拖拽结束时如果判定不合法或没有落在任何列上整组牌返回原列原列状态通过控制器回滚。这里最关键的是别在拖拽过程中修改牌组数据源Draggable的child快照一旦生成就冻结否则跨列过程中源列发生变化会让整组牌“分身”。5.3 精确刷新Widget树避免整棵牌桌重建牌桌级的setState肯定能跑但对蜘蛛纸牌这种几十张牌同时存在的界面来说会影响动画流畅度。我采用的是控制器加局部监听的方式。GameController继承ChangeNotifier发牌、翻牌、移动都通过控制器方法触发。牌桌组件用一个AnimatedBuilder监听控制器但AnimatedBuilder只包住顶层结构每列的牌堆再各自包一层ValueListenableBuilder订阅控制器里对应列的ValueNotifierListCardModel。这样翻一张牌只重建那一列发牌动画则只重建发牌区和受影响的目标列其他列完全不动。这里顺便说一下Flutter路由切换的坑。游戏集合App里从蜘蛛纸牌切到其他游戏再返回如果页面State没有正确保持牌局数据会被重置。我的做法是把GameController的实例放在页面State里并用PageStorageKey保持关键布局状态切换页面后继续用同一个Controller实例牌局进度自然就还在。不要用GlobalKey去管理牌局数据页面级状态和全局状态在这个场景里必须分开。6. 实测调优与坑位记录帧率、缓存、构建报错的真实经历6.1 主要性能优化项我把性能优化总结成五个项目每一项都是在OpenHarmony模拟器和真机上对比过的第一绘制对象预创建。牌底渐变、阴影画笔、描边画笔全部在初始化时创建不要写在paint方法里每次新建。Paint对象在Dart里开销不小50张牌每帧各自new一遍性能直接崩。第二文字纹理缓存。花色符号和点数的TextPainter/Paragraph全部预构建建立(rank, suit)到纹理的映射。牌面重绘时只做一次drawParagraph不重复layout。第三牌背纹理缓存。牌背图案不变化用一次性生成的ui.Image缓存下来绘制时直接drawImageRect到牌面范围。如果每帧都重新画牌背的菱形图案CPU开销很大。第四RepaintBoundary隔离。每张牌外层包一个RepaintBoundary不对这个粒度太细反而增加Layer开销。正确的做法是按列包RepaintBoundary整列变化时只重绘该列单张牌的隔离完全没必要。第五避免Overdraw。牌堆里被完全遮挡的牌其实不需要绘制。我用布局层做一个可见性裁剪只绘制从列顶往上count个数的牌超出可视区域的底层牌直接跳过绘制。6.2 优化前后实测数据以下数据来自我本地的OpenHarmony模拟器1080P分辨率开启Impeller仅供参考指标Widget堆叠方案自绘未优化自绘优化后UI线程单帧耗时9到12毫秒6到8毫秒4到5毫秒牌桌Widget数量约1800个约140个约140个拖拽10张牌动画掉帧明显卡顿轻微掉帧稳定内存占用约128MB约118MB约96MB优化后帧耗时稳定在4到6毫秒对60Hz屏幕来说每帧有大约10毫秒的余量即使同时播放发牌、翻牌、回收三个动画也不会掉帧。这里特别强调一下不同设备的差异很大这个数据只能当作相对趋势不要当成绝对基准。6.3 高频踩坑版本报错、阴影闪烁、文本模糊先说版本报错。我遇到最多的是The current configured Flutter SDK is not known to be fully supported这条后来排查下来是flutter_flutter分支和本机OpenHarmony SDK版本不匹配。解决办法不是去网上搜新版本强行升级而是锁死官方release note里测试过的组合。第二条是阴影闪烁。牌面阴影用canvas.drawShadow时如果同时给整张牌叠加了一个很大的模糊效果在Impeller下偶尔会出现阴影时有时无的闪烁。定位后发现是阴影的模糊半径超过了设备支持的像素范围把模糊半径压到4像素以内就稳定了。第三条是文本模糊。在牌桌整体缩放后角标的数字容易发虚。根因是TextScaler未禁用系统字体缩放被带进了Canvas绘制。把MediaQuery.textScalerOf(context)强制设为TextScaler.noScaling同时调大TextPainter的fontSize让缩放后的实际像素仍然覆盖足够多的物理像素模糊问题就解决了。6.4 还能往下扩展什么蜘蛛纸牌的牌面显示这套架构跑通之后扩展方向很明确。一个是难度分级单花色、双花色、四花色对应的牌面花色配色和背景主题要能切换现在的自绘方案只要在绘制层加一套Theme配置就能支持。另一个是存档恢复把GameController的牌局数据序列化成JSON或者二进制通过MethodChannel存到OpenHarmony应用沙箱启动时恢复。还有一个是联网排行榜用分数和步数上报这个和牌面显示没有直接关系但牌桌顶部的状态栏需要预流出来。我个人在整套模块做完后最深的体会是牌面显示这件事画得好看是美术问题画得稳定是性能问题而“在不同平台上画得一样稳定”是平台适配问题。Flutter给OpenHarmony带来自绘渲染的统一性但版本碎片化和引擎差异需要开发者自己盯着。这篇文章里所有方案我都按实战踩过坑的顺序写的希望能帮你少走几步弯路。

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

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

免费获取报价 →
↑