FlutterUnit 智能电梯绘制作品解析CustomPainter 与显式动画的完整实践【免费下载链接】FlutterUnitAll Platform Flutter Experience App项目地址: https://gitcode.com/GitHub_Trending/fl/FlutterUnitFlutterUnit 绘制集录painting_system中收录了一款名为 智能电梯Elevator Art的互动式绘制作品它位于独立的elevator_artPackage 内使用CustomPainter绘制楼层、井道、轿厢、电梯门与运行状态并以显式动画Explicit Animation依次协调关门、移动与开门三个过程。本文以 elevator_art/README.md 为核心结合该模块源码与测试讲解其公开入口、状态机设计、动画编排、绘制实现以及它在 FlutterUnit 画廊中的接入方式读者可以据此掌握CustomPainter 静态绘制 AnimationController 显式驱动这一经典组合的完整落地套路。一、模块定位与公开入口elevator_art是 FlutterUnit 中modules/painting_system下的一个独立 Flutter Package仓库内与它同级的还有draw_system绘制集录主包见 modules/painting_system。从目录结构可以看出该作品采用模型—视图—绘制分层modules/painting_system/elevator_art/ ├── lib/ │ ├── elevator_art.dart # 库入口导出公开 API │ └── src/ │ ├── model/elevator_phase.dart # 电梯运行阶段枚举 │ └── view/ │ ├── elevator_artwork.dart # 可交互的电梯 Widget含动画编排 │ └── elevator_painter.dart # CustomPainter 绘制实现 ├── test/elevator_artwork_test.dart # Widget 测试 ├── README.md # 模块说明 └── pubspec.yaml # 包配置其公开入口在 lib/elevator_art.dart 中通过export只暴露了ElevatorArtworklibrary elevator_art; export src/view/elevator_artwork.dart;README 中给出的使用方式极为简洁仅需两行import package:elevator_art/elevator_art.dart; const ElevatorArtwork();ElevatorArtwork是构造为const的 StatefulWidget默认提供 8 层楼floorCount 8也可以像测试里那样自定义楼层数例如ElevatorArtwork(floorCount: 4)。包声明在 pubspec.yaml 中要求 Dart SDK3.5.0 4.0.0、Flutter3.24.0并采用resolution: workspace工作区模式属于 FlutterUnit monorepo 体系内不对外发布的内部包publish_to: none。二、运行阶段状态机ElevatorPhase电梯的一切行为都围绕当前处于哪个阶段展开这一语义被建模为 lib/src/model/elevator_phase.dart 中的枚举/// 电梯当前运行阶段。 enum ElevatorPhase { idle, // 待命门开着等待指令 closing, // 关门门扇从打开合拢 moving, // 运行轿厢沿楼层轴移动 opening, // 开门门扇重新展开 }这个四阶段模型是整个作品的状态核心状态机严格按idle → closing → moving → opening → idle单向流转任一时刻只有一个阶段生效。在 elevator_artwork.dart 中阶段被同时用于三处锁定交互bool get _isBusy _phase ! ElevatorPhase.idle;非待命阶段禁用所有楼层按钮的点击回调onTap: _isBusy ? null : ...从根源上避免重复指令打断动画序列驱动文案_phaseLabel把阶段翻译为中文状态 待命 / 关门 / 上行 / 下行 / 开门其中moving阶段还会根据目标楼层与当前楼层的相对大小区分上下行驱动绘制ElevatorPainter收到phase后在画布头部渲染对应的英文指示READY / CLOSING / MOVING / OPENING。这样模型状态 → 交互开关 → 文案 → 画面全部由同一个枚举驱动职责单一、便于测试与扩展。三、显式动画编排关门 → 移动 → 开门作品最核心的工程亮点在于用显式动画把一次乘梯过程编排成严格有序的三段。_ElevatorArtworkState混入了TickerProviderStateMixin并维护两个职责完全分离的AnimationController/// 轿厢楼层移动动画。 late final AnimationController _travelController; /// 电梯门开合动画0 为关闭1 为打开。 late final AnimationController _doorController;_doorController在initState中预设了 420ms 的时长并以value: 1起始表示初始状态门是打开的_travelController时长不固定由运行距离动态决定。完整的运行动画编排在_requestFloor(int floor)异步方法中其调用链是理解全作品的关键Futurevoid _requestFloor(int floor) async { if (_isBusy || floor _currentFloor) { return; // 忙碌或原地请求直接忽略 } try { // 1. 登记目标楼层进入关门阶段 setState(() { _targetFloor floor; _phase ElevatorPhase.closing; }); await _doorController.reverse().orCancel; // 门从打开(1)合拢到关闭(0) // 2. 根据距离计算运行时长移动轿厢 final int distance (floor - _currentFloor).abs(); _travelController.duration Duration(milliseconds: 420 distance * 260); _travelController.reset(); _floorAnimation Tweendouble( begin: _currentFloor.toDouble(), end: floor.toDouble(), ).animate(CurvedAnimation( parent: _travelController, curve: Curves.easeInOutCubic)); setState(() _phase ElevatorPhase.moving); await _travelController.forward(from: 0).orCancel; // 3. 抵达后锁定新楼层开门 _currentFloor floor; _floorAnimation AlwaysStoppedAnimationdouble(floor.toDouble()); setState(() _phase ElevatorPhase.opening); await _doorController.forward(from: 0).orCancel; if (mounted) { setState(() _phase ElevatorPhase.idle); // 回到待命 } } on TickerCanceled { // 页面销毁时动画取消无需继续更新状态。 } }这段编排里值得注意的工程细节用await ... .orCancel把动画时序变成顺序代码关门完成才移动、移动完成才开门把三个动画的先后关系写成了直观的线性流程可读性远高于嵌套回调或StatusListener距离感知的运行时长Duration(milliseconds: 420 distance * 260)让轿厢每跨一层约增加 260ms跨层越多、运行越久节奏接近真实电梯动画取消的安全处理全程捕获TickerCanceled页面销毁如切走 Tab时静默退出且每次await之后都检查mounted避免在已卸载的 State 上调用setState双控制器合并重绘绘制区域用Listenable.merge同时监听两个控制器任一动画 tick 都会触发AnimatedBuilder重建CustomPaint而楼层按钮、状态文案等无关区域不会连带重绘。楼层位置的连续化处理轿厢在楼层轴上的位置不是离散的整数楼层而是一条连续动画曲线_floorAnimationAnimationdouble。待命时它退化为AlwaysStoppedAnimation恒值动画运行时则是Tween(current → target)配Curves.easeInOutCubic的缓动曲线这样绘制层拿到的floorPosition是一个平滑变化的 double轿厢即可在楼层之间滑行而非瞬移。测试之所以用pumpAndSettle(const Duration(seconds: 5))等待正是因为一次含关门、移动、开门的完整旅程需要跨越数秒。四、CustomPainter 绘制井道、楼层、轿厢与门扇画面渲染全部集中在 lib/src/view/elevator_painter.dart。ElevatorPainter是一个纯函数式CustomPainter不持有任何状态五个构造参数即全部输入const ElevatorPainter({ required this.floorCount, // 楼层总数 required this.floorPosition, // 轿厢连续楼层位置 required this.targetFloor, // 当前目标楼层 required this.doorProgress, // 门打开进度 0~1 required this.phase, // 当前运行阶段 });paint方法按从底到顶的顺序分五步绘制每一层都只依赖传入的参数背景与建筑体_drawBuilding深色圆角矩形建筑外墙0xff18212c加描边中央是更暗的井道0xff0b1017并绘制两根电梯钢丝缆绳楼层分隔线与编号_drawFloors按shaft.height / floorCount等分出水平分隔线并在左侧逐层标注数字楼层编号自下而上递增目标楼层指示_drawTarget在建筑右侧绘制一枚青色0xff49f2c2三角指示器其垂直位置由targetFloor决定轿厢与门扇_drawCabin这是最精彩的部分——轿厢外框先画一层带MaskFilter.blur(BlurStyle.normal, 10)的青色辉光再画实体轿厢门扇的开合通过openOffset halfWidth * 0.84 * doorProgress计算两扇门的中缝位移doorProgress 0时两扇门贴合 1时向两侧展开且只有当leftDoor.width 0才绘制避免负宽矩形报错头部状态栏_drawHeader用等宽字体渲染ELEVATOR // READY/CLOSING/MOVING/OPENING运行指示。shouldRepaint对五个输入参数逐一比较任一变化才触发重绘保证闲置时零无效绘制override bool shouldRepaint(covariant ElevatorPainter oldDelegate) { return oldDelegate.floorPosition ! floorPosition || oldDelegate.targetFloor ! targetFloor || oldDelegate.doorProgress ! doorProgress || oldDelegate.phase ! phase || oldDelegate.floorCount ! floorCount; }整幅画面只用Canvas原语drawRect、drawRRect、drawLine、drawPath、drawText完成没有任何图片资源这也正是 FlutterUnit 绘制集录一贯的纯代码作画风格。五、响应式布局宽屏与紧凑模式自适应ElevatorArtwork的build用LayoutBuilder感知可用宽度在 460px 处切换两套布局final bool compact constraints.maxWidth 460; return compact ? _buildCompactLayout() : _buildWideLayout();宽屏布局Row左侧是占满剩余空间的绘制区右侧是 104px 宽的纵向控制面板状态显示在上、2 列楼层按钮网格在下紧凑布局Column绘制区在上底部 92px 高的横向控制面板其中左侧 92px 为状态区、右侧为 4 列楼层按钮按钮网格的crossAxisCount随horizontal标志在 4/2 之间切换。控制面板的楼层按钮由GridView.builder生成注意楼层映射是倒序的final int floor widget.floorCount - index;即列表第一项是最高层。当前目标楼层以蓝色0xff1677ff高亮闲置时可点击忙碌时禁用。这套同一组件、两种布局的做法让它既能在手机竖屏窄幅下可用也能在桌面宽窗下展示完整控制台。六、测试与画廊接入作品如何进入 FlutterUnitWidget 测试验证动画旅程test/elevator_artwork_test.dart 用一个端到端的 Widget 测试验证了选层 → 完整动画 → 抵达全流程testWidgets(电梯作品可以选择楼层并完成动画, _testElevatorTravel); Futurevoid _testElevatorTravel(WidgetTester tester) async { await tester.pumpWidget( const MaterialApp(home: Scaffold(body: ElevatorArtwork(floorCount: 4))), ); expect(find.text(01), findsOneWidget); // 初始停靠 1 层 await tester.tap(find.text(4)); // 点击 4 层按钮 await tester.pumpAndSettle(const Duration(seconds: 5)); expect(find.text(04), findsOneWidget); // 抵达后目标楼层变为 04 expect(tester.takeException(), isNull); // 全程无异常 }它先断言初始状态显示01点击4后让动画跑完5 秒等待覆盖关门 移动 开门全程再断言状态显示变为04且没有抛出任何异常。这个测试同时覆盖了状态机流转、异步编排与绘制重绘的稳定性是作品可被自动化验证的直接证据。在绘制集录画廊中的注册方式elevator_art被上层draw_system包依赖依赖声明位于 modules/painting_system/draw_system/pubspec.yamldependencies: flutter: sdk: flutter elevator_art: path: ../elevator_art而 FlutterUnit 根项目在 pubspec.yaml 的工作区列表中收录了该模块。画廊入口 gallery_factory.dart 以FrameShower卡片的形式把ElevatorArtwork注册进 fun趣味分区并附带了说明文案与源码跳转路径case GalleryType.fun: return [ const FrameShower( title: 智能电梯, author: 张风捷特烈, srcUrl: ../../elevator_art/lib/src/view, info: 可选楼层的交互式电梯绘制包含轿厢运行、门扇开合与楼层灯光动画。, content: ElevatorArtwork()), // ... ];这条注册链说明了一件事作品本身与展示框架完全解耦——elevator_art只负责电梯怎么画、怎么动draw_system的FrameShower负责怎么摆进画廊、怎么跳源码任何新绘制作品都可以照此模式独立成包再接入。七、小结与扩展思路从Elevator Art这一作品可以提炼出一套可复用的绘制作品工程范式模型先行用枚举把运行阶段建模为状态机ElevatorPhase交互、文案、绘制全部以此为唯一事实来源绘制与状态分离CustomPainter保持纯函数式输入参数 shouldRepaint状态与动画全部上移到State显式动画序列化用两个职责分离的AnimationController配合await ... .orCancel把多段动画编排成可读、可取消的顺序流程距离驱动时长动画时长与工作量跨层数挂钩视觉节奏更真实布局自适应LayoutBuilder在 460px 断点切换宽屏/紧凑两套控制面板兼顾手机与桌面测试兜底Widget 测试覆盖选层—动画—抵达全流程保证状态机与异步编排的稳定性。如需进一步扩展可参考同类模式为轿厢增加乘客形态、引入上下行呼梯按方向排队逻辑、把ElevatorPhase扩展为带进度的复杂状态或在_drawCabin中叠加更多Canvas特效如内部灯光渐变。所有相关源码均可从 elevator_art 模块目录 继续深入阅读。【免费下载链接】FlutterUnitAll Platform Flutter Experience App项目地址: https://gitcode.com/GitHub_Trending/fl/FlutterUnit创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
