ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Flame 缩放事件(Scale Events)完全指南:捏合缩放与双指旋转手势开发

Flame 缩放事件(Scale Events)完全指南:捏合缩放与双指旋转手势开发 Flame 缩放事件Scale Events完全指南捏合缩放与双指旋转手势开发【免费下载链接】flameA Flutter based game engine.项目地址: https://gitcode.com/GitHub_Trending/fl/flame本指南以 Flame 游戏引擎的ScaleCallbacks混入为核心系统讲解缩放pinch、旋转rotate等双指手势事件的触发机制、回调生命周期与事件数据模型并结合仓库源码scale_callbacks.dart、multi_drag_scale_dispatcher.dart 等剖析其底层分发原理。读完本文你将能独立为组件接入缩放回调、调整手势灵敏度阈值并实现单指拖动 双指缩放/旋转的相机控制等经典交互。什么是缩放事件缩放事件Scale Events在用户以两根手指做捏合pinch in或张开pinch out动作时触发。这类手势是构建双指缩放地图双指旋转物体相机捏合变焦等交互的基础。需要注意的一个关键约束是同一时刻只能发生一个缩放手势Only one single scale gesture can occur at the same time。与支持多点同时拖动的DragCallbacks不同Flame 的缩放手势天然是单实例的——底层MultiDragScaleGestureRecognizer在任一时刻只跟踪一个捏合焦点这与源码中handleScaleStart/handleScaleUpdate/handleScaleEnd均以固定pointerId 0构造事件的事实相互印证见 multi_drag_scale_dispatcher.dart。接入缩放事件ScaleCallbacks 混入想让某个组件响应缩放事件只需为它混入ScaleCallbacks该混入为组件增加三个可覆写的方法onScaleStart、onScaleUpdate、onScaleEnd。默认情况下这三个方法什么都不做需要你在子类中覆写以真正实现功能除此之外组件还必须实现containsLocalPoint()方法PositionComponent已基于其size属性提供了该实现因此大多数情况下无需额外处理。Flame 依靠它来判断事件是否发生在组件范围内从而决定是否将事件派发给该组件。最小示例class MyComponent extends PositionComponent with ScaleCallbacks { MyComponent() : super(size: Vector2(180, 120)); override void onScaleStart(ScaleStartEvent event) { // 响应缩放事件例如记录初始角度或初始缩放值 } }源码视角ScaleCallbacks 到底做了什么查看 scale_callbacks.dart 可以确认混入的真实行为onScaleStart与onScaleEnd都带有mustCallSuper注解默认实现会把内部标志_isScaling置为true/false覆写时务必先调用super否则isScaling状态会失效onScaleUpdate没有mustCallSuper默认实现为空可直接自由覆写onMount()时组件通过MultiDragScaleDispatcher.addDispatcher(this, hasDrag: false, hasScale: true)把自己注册为缩放事件接收者onRemove()时自动反注册。也就是说只要组件混入了ScaleCallbacks并挂载到游戏树中Flame 就会自动完成事件分发器的装配无需手动注册。mixin ScaleCallbacks on Component implements PointerInputCallbacks { bool _isScaling false; bool get isScaling _isScaling; mustCallSuper void onScaleStart(ScaleStartEvent event) { _isScaling true; } void onScaleUpdate(ScaleUpdateEvent event) {} mustCallSuper void onScaleEnd(ScaleEndEvent event) { _isScaling false; } // ... }缩放手势解剖三个阶段的生命周期一个完整的缩放手势由三个阶段组成对应三个回调。理解每个阶段的事件语义是写出顺滑交互的前提。onScaleStart这是缩放手势序列中的第一个事件。通常情况下事件会被派发给焦点focal point处最顶层的、混入了ScaleCallbacks的组件。焦点指两根手指连线的中点。如果希望事件继续向下层组件传播可以将event.continuePropagation置为true。ScaleStartEvent对象携带第一个被缩放手势识别器认可的焦点坐标。这个点以多种坐标系同时提供devicePosition整个设备手机屏幕 / 浏览器窗口 / 桌面应用的坐标系canvasPosition游戏组件Game Widget画布的坐标系即 Flame 层的全局坐标localPosition当前组件的局部坐标系坐标。任何收到onScaleStart的组件后续也一定会收到onScaleUpdate与onScaleEnd。这一点由分发器内部使用_scaleRecords集合按pointerId记录开始者集合来保证见 multi_drag_scale_dispatcher.dart。从源码看ScaleStartEvent继承自PositionEventScaleStartDetails其devicePosition直接取自 Flutter 手势细节的details.focalPoint并额外暴露了pointerId本次缩放手势的唯一标识与deviceKind发起手势的设备类型如触摸屏/鼠标/触控笔见 scale_start_event.dart。onScaleUpdate该事件在用户持续移动手指时连续触发如果用户手指保持静止不动则不会触发。默认实现会把该事件派发给所有收到过上一次onScaleStart的组件。分发逻辑分为两路见 multi_drag_scale_dispatcher.dart先通过event.deliverAtPointdeliverToAll: true派发给当前位于焦点处的组件再遍历_scaleRecords补发那些开始过缩放但此刻焦点已不在其上的组件确保onScaleUpdate对每个开始者都会送达。这里有几个重要的坐标语义如果触点仍在组件内event.localPosition给出该点在组件局部坐标系中的位置如果用户的手指移出了组件范围event.localPosition会返回一组 NaN 坐标同时event.renderingTrace为空但此时canvasPosition与devicePosition依然有效可用于继续跟踪手势全局位置。此外ScaleUpdateEvent还携带以下关键数据属性含义说明focalPointDelta焦点自上次onScaleUpdate或本次缩放开始时的onScaleStart以来移动的量Vector2类型可用于实现双指拖动平移timestamp自缩放开始以来经过的时间可用于计算手势移动速度rotation旋转角度弧度指两根手指起始连线与本次事件时连线之间的夹角用于旋转物体scale缩放比例两根手指起始连线长度与本次事件连线长度之比同时源码还提供horizontalScale水平方向比例与verticalScale垂直方向比例以及pointerCount当前参与手势的手指数量对照 scale_update_event.dart 可见ScaleUpdateEvent继承自DisplacementEventScaleUpdateDetails构造时一次性从 Flutter 细节中解包出scale、horizontalScale、verticalScale、rotation、pointerCount、focalPointDelta、timestamp等字段并据此同时推导出deviceStartPosition/deviceEndPosition因此也继承了位移事件家族的全部坐标能力。onScaleEnd当用户抬起手指、缩放手势结束时触发。该事件不关联任何位置信息因此你无法从ScaleEndEvent中读取坐标。不过源码 scale_end_event.dart 显示它额外提供了velocity手势结束时手指的移动速度Vector2类型与pointerId可用于实现惯性缩放、甩动缩放等高级效果。一个值得注意的边界情况分发器在onScaleUpdate阶段发现某个开始者组件已被卸载isMounted false或isRemoving true时会立即为该组件合成一个ScaleEndEvent并调用onScaleEnd然后将其移出_scaleRecords保证手势状态不会被悬挂见 multi_drag_scale_dispatcher.dart。坐标系详解devicePosition / canvasPosition / localPosition三种坐标是 Flame 事件系统的通用设计ScaleStartEvent与ScaleUpdateEvent均继承自PositionEvent见 position_event.dartdevicePosition设备级全局坐标。若游戏全屏运行则与canvasPosition相等否则为 Flutter 层级的全局位置canvasPosition游戏画布内的坐标由devicePosition经game.convertGlobalToLocalCoordinate换算而来可视为 Flame 层全局坐标localPosition组件局部坐标仅当事件通过deliverAtPoint派发给组件时才可访问其实现是renderingTrace.last因此当组件不再位于渲染追踪链中即手指移出组件时读到 NaN 是符合预期的行为。在实现缩放某个组件时一般用localPosition作为缩放中心实现缩放整个相机时则用event.scale/event.rotation全局比例即可。混入细节isScaling 与 scaleThresholdisScalingScaleCallbacks提供一个isScalinggetter在组件正被缩放期间返回true其余时间返回false。其内部状态在onScaleStart开始置为true在onScaleEnd复位为false注意覆写这两个方法时必须调用super才能维持该状态。典型用途在缩放期间改变组件的视觉外观例如放大时显示高亮描边、缩小碰撞体、禁用其他交互等。scaleThreshold手势灵敏度阈值缩放事件并非两根手指一触屏就立刻触发。必须先跨越一个微小的移动阈值默认情况下手指至少要张开或捏合 5%即缩放因子达到 1.05才会触发onScaleStart。这一设计用于避免用户只是把两根手指放在屏幕上、并无意缩放时误触手势。阈值可通过从游戏侧访问MultiDragScaleDispatcher并在任何ScaleCallbacks组件挂载之前设置scaleThreshold来修改class MyGame extends FlameGame { override Futurevoid onLoad() async { final dispatcher MultiDragScaleDispatcher()..scaleThreshold 1.02; registerKey(const MultiDragScaleDispatcherKey(), dispatcher); add(dispatcher); } }更小的值让识别器更灵敏对更小幅度的捏合动作即作出反应更大的值要求更刻意的手势才会触发缩放事件。源码 multi_drag_scale_dispatcher.dart 确认了默认值1.05与约束必须大于 1.0并注明该值在挂载时传入底层MultiDragScaleGestureRecognizer(scaleThreshold: scaleThreshold)因此必须在首个缩放组件挂载前完成设置才会生效参见该文件的onMount实现。MultiDragScaleDispatcher 的分发机制MultiDragScaleDispatcher是缩放与拖动事件的中央分发器它通过MultiDragScaleDispatcherKey注册到FlameGame上并在任一ScaleCallbacks/DragCallbacks组件首次挂载时由addDispatcher静态方法自动创建并添加到游戏multi_drag_scale_dispatcher.dart。其内部用引用计数_dragCount/_scaleCount而非布尔值来管理enableDrag/enableScale目的是保证在onMount之前调用这些方法也是安全的——挂载时再根据计数初始化底层识别器的hasDrag/hasScale标志。事件到达后分发器把 Flutter 层的ScaleStartDetails/ScaleUpdateDetails/ScaleEndDetails包装为 Flame 事件对象再经deliverAtPoint按组件层级与containsLocalPoint结果逐层派发multi_drag_scale_dispatcher.dart。与 DragCallbacks 组合单指拖动 双指缩放/旋转ScaleCallbacks与DragCallbacks可以同时使用。两者由同一个识别器驱动因此天然兼容单指手势产生拖动事件双指手势同时产生缩放事件与拖动事件。这非常适合实现单指拖动、双指捏合缩放或旋转的组件。下面是一个既可用单指拖动、又可用双指旋转的矩形组件class InteractiveRectangle extends RectangleComponent with ScaleCallbacks, DragCallbacks { double _initialAngle 0; override void onDragUpdate(DragUpdateEvent event) { position event.localDelta; } override void onScaleStart(ScaleStartEvent event) { super.onScaleStart(event); _initialAngle angle; } override void onScaleUpdate(ScaleUpdateEvent event) { angle _initialAngle event.rotation; } }这里的关键是onScaleStart时记录初始角度onScaleUpdate时把event.rotation当前与起始连线的夹角差叠加到初始角度上即可实现流畅的双指旋转。由于ScaleUpdateEvent.rotation是相对手势起点的累计差值这种记录初始值 累计差值的写法能避免抖动。游戏级应用捏合缩放相机把这一对混入加到FlameGame上就得到了经典的单指平移pan、双指捏合缩放zoom相机控制。由于双指捏合同样会产生拖动事件拖动处理必须判断当前是否正在缩放否则相机会在缩放的同时被平移造成边放大边漂移class MyGame extends FlameGame with DragCallbacks, ScaleCallbacks { late double startZoom; void clampZoom() { camera.viewfinder.zoom camera.viewfinder.zoom.clamp(0.05, 3.0); } override void onScaleStart(ScaleStartEvent event) { super.onScaleStart(event); startZoom camera.viewfinder.zoom; } override void onScaleUpdate(ScaleUpdateEvent event) { camera.viewfinder.zoom startZoom * event.scale; clampZoom(); } override void onDragUpdate(DragUpdateEvent event) { // 双指捏合会同时产生拖动与缩放事件缩放期间跳过平移 if (isScaling) { return; } final zoom camera.viewfinder.zoom; camera.moveBy((event.localDelta..negate()) / zoom); } }实现要点onScaleStart记录手势开始时的相机缩放值startZoomonScaleUpdate用startZoom * event.scale计算新缩放并通过clampZoom()把缩放限制在0.05 ~ 3.0之间防止缩放过猛onDragUpdate首先用isScaling拦截缩放期间的拖动再用当前缩放值反除位移使平移速度与缩放比例匹配缩放越大相同像素位移对应的世界位移越小。仓库中的官方示例 zoom_example.dart 实现了几乎相同的逻辑另混入了ScrollCallbacks以支持 Web 端滚轮缩放可在 Dashbook 故事的 Camera Viewport → Zoom 中直接体验注册入口见 camera_and_viewport.dart。其onScaleUpdate使用event.verticalScale而非event.scale在垂直方向主导的捏合场景下表现更稳定可按需选用。实战建议与常见误区覆写onScaleStart/onScaleEnd务必调用superisScaling标志依赖默认实现维护漏掉super会导致isScaling永远为false进而破坏缩放期间跳过拖动之类的保护逻辑组件尺寸即命中区域派生自PositionComponent的组件其containsLocalPoint基于size判断务必正确设置size裸Component则必须手动实现containsLocalPoint否则永远收不到缩放事件层级祖先也要命中若组件处于更大的组件层级中只有其所有祖先都正确实现了containsLocalPoint该组件才会收到缩放事件区分scale与verticalScale/horizontalScalescale是整体缩放比例适合相机变焦verticalScale/horizontalScale可分别驱动不同轴适合非等比 UI 元素localPosition可能为 NaN手指移出组件后localPosition与renderingTrace失效此时应回退到canvasPosition/devicePosition或使用focalPointDelta做全局位移阈值设置要趁早scaleThreshold必须在首个ScaleCallbacks组件挂载前通过MultiDragScaleDispatcher设置否则默认的 1.05 已经生效。进一步阅读拖动手势的完整语义与DisplacementEvent细节见 drag_events.md键盘、指针、点击等其他输入方式的编排见 inputs.md事件分发器与手势识别器的完整源码multi_drag_scale_dispatcher.dart、scale_callbacks.dart事件数据模型源码scale_start_event.dart、scale_update_event.dart、scale_end_event.dart、position_event.dart。【免费下载链接】flameA Flutter based game engine.项目地址: https://gitcode.com/GitHub_Trending/fl/flame创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表