-
Notifications
You must be signed in to change notification settings - Fork 19.8k
fix(roam): proper touch pinch support for zoom & pan #21655
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -106,12 +106,12 @@ export type RoamEventDefinition = { | |
| type RoamPointerChecker = (e: ZRElementEvent, x: number, y: number) => boolean; | ||
|
|
||
| /** | ||
| * An manager of zoom and pan(darg) hehavior. | ||
| * A manager of zoom and pan(drag) behavior. | ||
| * But it is not responsible for updating the view, since view updates vary and can | ||
| * not be handled in a uniform way. | ||
| * | ||
| * Note: regarding view updates: | ||
| * - Transformabe views typically use `coord/View` (e.g., geo and series.graph roaming). | ||
| * - Transformable views typically use `coord/View` (e.g., geo and series.graph roaming). | ||
| * Some commonly used view update logic has been organized into `roamHelper.ts`. | ||
| * - Non-transformable views handle updates themselves, possibly involving re-layout, | ||
| * (e.g., treemap). | ||
|
|
@@ -132,6 +132,14 @@ class RoamController extends Eventful<RoamEventDefinition> { | |
|
|
||
| private _y: number; | ||
|
|
||
| private _pinchX: number; | ||
|
|
||
| private _pinchY: number; | ||
|
|
||
| private _moveEnabled: boolean; | ||
|
|
||
| private _zoomEnabled: boolean; | ||
|
|
||
| private _controlType: RoamOptionMixin['roam']; | ||
|
|
||
| private _enabled: boolean; | ||
|
|
@@ -187,6 +195,8 @@ class RoamController extends Eventful<RoamEventDefinition> { | |
| if (controlType == null) { | ||
| controlType = true; | ||
| } | ||
| const moveEnabled = controlType === true || (controlType === 'move' || controlType === 'pan'); | ||
| const zoomEnabled = controlType === true || (controlType === 'scale' || controlType === 'zoom'); | ||
|
|
||
| // A quick optimization for repeatedly calling `enable` during roaming. | ||
| // Assert `disable` is only affected by `controlType`. | ||
|
|
@@ -195,16 +205,20 @@ class RoamController extends Eventful<RoamEventDefinition> { | |
| this.disable(); | ||
|
|
||
| this._enabled = true; | ||
| if (controlType === true || (controlType === 'move' || controlType === 'pan')) { | ||
| if (moveEnabled) { | ||
| addRoamZrListener(zr, 'mousedown', mousedownHandler, zInfoParsed); | ||
| addRoamZrListener(zr, 'mousemove', mousemoveHandler, zInfoParsed); | ||
| addRoamZrListener(zr, 'mouseup', mouseupHandler, zInfoParsed); | ||
| } | ||
| if (controlType === true || (controlType === 'scale' || controlType === 'zoom')) { | ||
| if (zoomEnabled) { | ||
| addRoamZrListener(zr, 'mousewheel', mousewheelHandler, zInfoParsed); | ||
| } | ||
| if (moveEnabled || zoomEnabled) { | ||
| addRoamZrListener(zr, 'pinch', pinchHandler, zInfoParsed); | ||
| } | ||
| } | ||
| this._moveEnabled = moveEnabled; | ||
| this._zoomEnabled = zoomEnabled; | ||
| }; | ||
|
|
||
| this.disable = function () { | ||
|
|
@@ -215,6 +229,8 @@ class RoamController extends Eventful<RoamEventDefinition> { | |
| removeRoamZrListener(zr, 'mouseup', mouseupHandler); | ||
| removeRoamZrListener(zr, 'mousewheel', mousewheelHandler); | ||
| removeRoamZrListener(zr, 'pinch', pinchHandler); | ||
| this._moveEnabled = false; | ||
| this._zoomEnabled = false; | ||
| } | ||
| }; | ||
| } | ||
|
|
@@ -355,6 +371,7 @@ class RoamController extends Eventful<RoamEventDefinition> { | |
| const zr = this._zr; | ||
| if (!eventTool.isMiddleOrRightButtonOnMouseUpDown(e)) { | ||
| this._dragging = false; | ||
| this._pinching = false; | ||
|
|
||
| const cursorStyle = this._decideCursorStyle(e, e.offsetX, e.offsetY, true); | ||
| if (cursorStyle) { | ||
|
|
@@ -418,10 +435,52 @@ class RoamController extends Eventful<RoamEventDefinition> { | |
| ) { | ||
| return; | ||
| } | ||
| const scale = e.pinchScale > 1 ? 1.1 : 1 / 1.1; | ||
| this._checkTriggerMoveZoom(this, 'zoom', null, e, { | ||
| scale: scale, originX: e.pinchX, originY: e.pinchY, isAvailableBehavior: null | ||
| }); | ||
| const originX = e.pinchX; | ||
| const originY = e.pinchY; | ||
|
|
||
| // Gate only the beginning of a pinch capture. | ||
| // Once captured, keep handling movement outside the roam area. | ||
| // Requiring a native touchstart for a new capture also | ||
| // prevents another RoamController from picking up an already-moving pinch. | ||
| const isTouchStart = e.event.type === 'touchstart'; | ||
| const isPinchStart = !this._pinching || isTouchStart; | ||
| if (isPinchStart) { | ||
| if (!isTouchStart || !this._checkPointer(e, originX, originY)) { | ||
| return; | ||
| } | ||
| } | ||
|
Comment on lines
+441
to
+451
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ZRender does emit a pinch during touchstart once the event contains two touches. GestureMgr falls back to using the current sample as pinchPre, producing pinchScale === 1, and Handler.processGesture dispatches that event while the native event type remains touchstart. I verified this against ZRender 6.1.0 and the current ZRender source. Requiring touchstart here is intentional: it ensures pinch ownership is decided only when the gesture begins. Allowing an uncaptured controller to start on touchmove would let a pinch be acquired after entering another roam area, potentially causing unexpected mid-gesture zoom or transferring ownership between overlapping controllers.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And empirically this works well too - you may take a look at the attached videos |
||
|
|
||
| eventTool.stop(e.event); | ||
| (e as RoamControllerZREventExtend).__ecRoamConsumed = true; | ||
|
|
||
| this._pinching = true; | ||
|
|
||
| const oldX = this._pinchX; | ||
| const oldY = this._pinchY; | ||
| this._pinchX = originX; | ||
| this._pinchY = originY; | ||
|
|
||
| if (!isPinchStart | ||
| && this._moveEnabled | ||
| && (originX !== oldX || originY !== oldY) | ||
| ) { | ||
| trigger(this, 'pan', null, e, { | ||
| dx: originX - oldX, | ||
| dy: originY - oldY, | ||
| oldX: oldX, | ||
| oldY: oldY, | ||
| newX: originX, | ||
| newY: originY, | ||
| isAvailableBehavior: null | ||
| }); | ||
| } | ||
|
|
||
| const scale = e.pinchScale; | ||
| if (this._zoomEnabled && scale !== 1) { | ||
| trigger(this, 'zoom', null, e, { | ||
| scale: scale, originX: originX, originY: originY, isAvailableBehavior: null | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| private _checkTriggerMoveZoom<T extends 'scrollMove' | 'zoom'>( | ||
|
|
@@ -586,4 +645,4 @@ function isAvailableBehavior( | |
| ); | ||
| } | ||
|
|
||
| export default RoamController; | ||
| export default RoamController; | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, you’re correct that _controlType is never assigned, so the existing idempotency check always removes and re-adds the listeners, but this behavior predates this PR.
Assigning _controlType alone may not be safe, however. Listener registration also captures component, zlevel, z, and z2, which determine precedence between overlapping controllers. These values are recalculated by every enable() call; skipping registration solely because controlType is unchanged could leave stale ordering metadata when a component’s z-order changes.
A complete fix should invalidate or update the registration when either controlType or that z-order metadata changes. Since this is an existing issue rather than a regression from the pinch changes, I suggest handling it separately.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@plainheart / @100pah - do you want me to implement this as well here? It's a bit out of scope and will make the PR quite larger