From 39fdce259dd46de8fd715efbfd12ede4bc24c8c2 Mon Sep 17 00:00:00 2001 From: Dave Miller Date: Tue, 15 Mar 2016 07:28:55 -0700 Subject: [PATCH] Refactor JS touch dispatch from ReactRootView Reviewed By: andreicoman11 Differential Revision: D3035589 fb-gh-sync-id: a4af7b0be89e5ea13367481697ff4b3dda8536b1 shipit-source-id: a4af7b0be89e5ea13367481697ff4b3dda8536b1 --- .../com/facebook/react/ReactRootView.java | 191 +++--------------- .../react/uimanager/JSTouchDispatcher.java | 173 ++++++++++++++++ 2 files changed, 206 insertions(+), 158 deletions(-) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/JSTouchDispatcher.java diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java index bf4b8b4bc..ac0401caf 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java @@ -27,28 +27,26 @@ import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.UiThreadUtil; import com.facebook.react.bridge.WritableMap; import com.facebook.react.common.ReactConstants; -import com.facebook.react.common.SystemClock; import com.facebook.react.common.annotations.VisibleForTesting; import com.facebook.react.modules.core.DeviceEventManagerModule; import com.facebook.react.uimanager.DisplayMetricsHolder; import com.facebook.react.uimanager.PixelUtil; import com.facebook.react.uimanager.RootView; import com.facebook.react.uimanager.SizeMonitoringFrameLayout; -import com.facebook.react.uimanager.TouchTargetHelper; +import com.facebook.react.uimanager.JSTouchDispatcher; import com.facebook.react.uimanager.UIManagerModule; import com.facebook.react.uimanager.events.EventDispatcher; -import com.facebook.react.uimanager.events.TouchEvent; -import com.facebook.react.uimanager.events.TouchEventType; /** * Default root view for catalyst apps. Provides the ability to listen for size changes so that a UI * manager can re-layout its elements. - * It is also responsible for handling touch events passed to any of it's child view's and sending - * those events to JS via RCTEventEmitter module. This view is overriding - * {@link ViewGroup#onInterceptTouchEvent} method in order to be notified about the events for all - * of it's children and it's also overriding {@link ViewGroup#requestDisallowInterceptTouchEvent} - * to make sure that {@link ViewGroup#onInterceptTouchEvent} will get events even when some child - * view start intercepting it. In case when no child view is interested in handling some particular + * It delegates handling touch events for itself and child views and sending those events to JS by + * using JSTouchDispatcher. + * This view is overriding {@link ViewGroup#onInterceptTouchEvent} method in order to be notified + * about the events for all of it's children and it's also overriding + * {@link ViewGroup#requestDisallowInterceptTouchEvent} to make sure that + * {@link ViewGroup#onInterceptTouchEvent} will get events even when some child view start + * intercepting it. In case when no child view is interested in handling some particular * touch event this view's {@link View#onTouchEvent} will still return true in order to be notified * about all subsequent touch events related to that gesture (in case when JS code want to handle * that gesture). @@ -59,13 +57,11 @@ public class ReactRootView extends SizeMonitoringFrameLayout implements RootView private @Nullable String mJSModuleName; private @Nullable Bundle mLaunchOptions; private @Nullable KeyboardListener mKeyboardListener; - private int mTargetTag = -1; - private final float[] mTargetCoordinates = new float[2]; - private boolean mChildIsHandlingNativeGesture = false; private boolean mWasMeasured = false; private boolean mAttachScheduled = false; private boolean mIsAttachedToWindow = false; private boolean mIsAttachedToInstance = false; + private final JSTouchDispatcher mJSTouchDispatcher = new JSTouchDispatcher(this); public ReactRootView(Context context) { super(context); @@ -112,171 +108,50 @@ public class ReactRootView extends SizeMonitoringFrameLayout implements RootView } } - /** - * Main catalyst view is responsible for collecting and sending touch events to JS. This method - * reacts for an incoming android native touch events ({@link MotionEvent}) and calls into - * {@link com.facebook.react.uimanager.events.EventDispatcher} when appropriate. - * It uses {@link com.facebook.react.uimanager.TouchTargetManagerHelper#findTouchTargetView} - * helper method for figuring out a react view ID in the case of ACTION_DOWN - * event (when the gesture starts). - */ - private void handleTouchEvent(MotionEvent ev) { - if (mReactInstanceManager == null || !mIsAttachedToInstance || - mReactInstanceManager.getCurrentReactContext() == null) { - FLog.w( - ReactConstants.TAG, - "Unable to handle touch in JS as the catalyst instance has not been attached"); - return; - } - int action = ev.getAction() & MotionEvent.ACTION_MASK; - ReactContext reactContext = mReactInstanceManager.getCurrentReactContext(); - EventDispatcher eventDispatcher = reactContext.getNativeModule(UIManagerModule.class) - .getEventDispatcher(); - if (action == MotionEvent.ACTION_DOWN) { - if (mTargetTag != -1) { - FLog.e( - ReactConstants.TAG, - "Got DOWN touch before receiving UP or CANCEL from last gesture"); - } - - // First event for this gesture. We expect tag to be set to -1, and we use helper method - // {@link #findTargetTagForTouch} to find react view ID that will be responsible for handling - // this gesture - mChildIsHandlingNativeGesture = false; - mTargetTag = TouchTargetHelper.findTargetTagAndCoordinatesForTouch( - ev.getX(), - ev.getY(), - this, - mTargetCoordinates); - eventDispatcher.dispatchEvent( - TouchEvent.obtain( - mTargetTag, - SystemClock.nanoTime(), - TouchEventType.START, - ev, - mTargetCoordinates[0], - mTargetCoordinates[1])); - } else if (mChildIsHandlingNativeGesture) { - // If the touch was intercepted by a child, we've already sent a cancel event to JS for this - // gesture, so we shouldn't send any more touches related to it. - return; - } else if (mTargetTag == -1) { - // All the subsequent action types are expected to be called after ACTION_DOWN thus target - // is supposed to be set for them. - FLog.e( - ReactConstants.TAG, - "Unexpected state: received touch event but didn't get starting ACTION_DOWN for this " + - "gesture before"); - } else if (action == MotionEvent.ACTION_UP) { - // End of the gesture. We reset target tag to -1 and expect no further event associated with - // this gesture. - eventDispatcher.dispatchEvent( - TouchEvent.obtain( - mTargetTag, - SystemClock.nanoTime(), - TouchEventType.END, - ev, - mTargetCoordinates[0], - mTargetCoordinates[1])); - mTargetTag = -1; - } else if (action == MotionEvent.ACTION_MOVE) { - // Update pointer position for current gesture - eventDispatcher.dispatchEvent( - TouchEvent.obtain( - mTargetTag, - SystemClock.nanoTime(), - TouchEventType.MOVE, - ev, - mTargetCoordinates[0], - mTargetCoordinates[1])); - } else if (action == MotionEvent.ACTION_POINTER_DOWN) { - // New pointer goes down, this can only happen after ACTION_DOWN is sent for the first pointer - eventDispatcher.dispatchEvent( - TouchEvent.obtain( - mTargetTag, - SystemClock.nanoTime(), - TouchEventType.START, - ev, - mTargetCoordinates[0], - mTargetCoordinates[1])); - } else if (action == MotionEvent.ACTION_POINTER_UP) { - // Exactly onw of the pointers goes up - eventDispatcher.dispatchEvent( - TouchEvent.obtain( - mTargetTag, - SystemClock.nanoTime(), - TouchEventType.END, - ev, - mTargetCoordinates[0], - mTargetCoordinates[1])); - } else if (action == MotionEvent.ACTION_CANCEL) { - dispatchCancelEvent(ev); - mTargetTag = -1; - } else { - FLog.w( - ReactConstants.TAG, - "Warning : touch event was ignored. Action=" + action + " Target=" + mTargetTag); - } - } - @Override public void onChildStartedNativeGesture(MotionEvent androidEvent) { - if (mChildIsHandlingNativeGesture) { - // This means we previously had another child start handling this native gesture and now a - // different native parent of that child has decided to intercept the touch stream and handle - // the gesture itself. Example where this can happen: HorizontalScrollView in a ScrollView. - return; - } - - dispatchCancelEvent(androidEvent); - mChildIsHandlingNativeGesture = true; - mTargetTag = -1; - } - - private void dispatchCancelEvent(MotionEvent androidEvent) { - // This means the gesture has already ended, via some other CANCEL or UP event. This is not - // expected to happen very often as it would mean some child View has decided to intercept the - // touch stream and start a native gesture only upon receiving the UP/CANCEL event. - if (mTargetTag == -1) { + if (mReactInstanceManager == null || !mIsAttachedToInstance || + mReactInstanceManager.getCurrentReactContext() == null) { FLog.w( - ReactConstants.TAG, - "Can't cancel already finished gesture. Is a child View trying to start a gesture from " + - "an UP/CANCEL event?"); + ReactConstants.TAG, + "Unable to dispatch touch to JS as the catalyst instance has not been attached"); return; } - - EventDispatcher eventDispatcher = mReactInstanceManager.getCurrentReactContext() - .getNativeModule(UIManagerModule.class) - .getEventDispatcher(); - - Assertions.assertCondition( - !mChildIsHandlingNativeGesture, - "Expected to not have already sent a cancel for this gesture"); - Assertions.assertNotNull(eventDispatcher).dispatchEvent( - TouchEvent.obtain( - mTargetTag, - SystemClock.nanoTime(), - TouchEventType.CANCEL, - androidEvent, - mTargetCoordinates[0], - mTargetCoordinates[1])); + ReactContext reactContext = mReactInstanceManager.getCurrentReactContext(); + EventDispatcher eventDispatcher = reactContext.getNativeModule(UIManagerModule.class) + .getEventDispatcher(); + mJSTouchDispatcher.onChildStartedNativeGesture(androidEvent, eventDispatcher); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { - handleTouchEvent(ev); + dispatchJSTouchEvent(ev); return super.onInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent ev) { - handleTouchEvent(ev); + dispatchJSTouchEvent(ev); super.onTouchEvent(ev); // In case when there is no children interested in handling touch event, we return true from // the root view in order to receive subsequent events related to that gesture return true; } + private void dispatchJSTouchEvent(MotionEvent event) { + if (mReactInstanceManager == null || !mIsAttachedToInstance || + mReactInstanceManager.getCurrentReactContext() == null) { + FLog.w( + ReactConstants.TAG, + "Unable to dispatch touch to JS as the catalyst instance has not been attached"); + return; + } + ReactContext reactContext = mReactInstanceManager.getCurrentReactContext(); + EventDispatcher eventDispatcher = reactContext.getNativeModule(UIManagerModule.class) + .getEventDispatcher(); + mJSTouchDispatcher.handleTouchEvent(event, eventDispatcher); + } + @Override public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { // No-op - override in order to still receive events to onInterceptTouchEvent diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/JSTouchDispatcher.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/JSTouchDispatcher.java new file mode 100644 index 000000000..fefab066f --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/JSTouchDispatcher.java @@ -0,0 +1,173 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +import android.view.MotionEvent; +import android.view.ViewGroup; + +import com.facebook.common.logging.FLog; +import com.facebook.infer.annotation.Assertions; +import com.facebook.react.common.ReactConstants; +import com.facebook.react.common.SystemClock; +import com.facebook.react.uimanager.events.EventDispatcher; +import com.facebook.react.uimanager.events.TouchEvent; +import com.facebook.react.uimanager.events.TouchEventType; + +/** + * JSTouchDispatcher handles dispatching touches to JS from RootViews. If you implement RootView + * you need to call handleTouchEvent from onTouchEvent and onInterceptTouchEvent. It will correctly + * find the right view to handle the touch and also dispatch the appropriate event to JS + */ +public class JSTouchDispatcher { + + private int mTargetTag = -1; + private final float[] mTargetCoordinates = new float[2]; + private boolean mChildIsHandlingNativeGesture = false; + private final ViewGroup mRootViewGroup; + + public JSTouchDispatcher(ViewGroup viewGroup) { + mRootViewGroup = viewGroup; + } + + public void onChildStartedNativeGesture(MotionEvent androidEvent, EventDispatcher eventDispatcher) { + if (mChildIsHandlingNativeGesture) { + // This means we previously had another child start handling this native gesture and now a + // different native parent of that child has decided to intercept the touch stream and handle + // the gesture itself. Example where this can happen: HorizontalScrollView in a ScrollView. + return; + } + + dispatchCancelEvent(androidEvent, eventDispatcher); + mChildIsHandlingNativeGesture = true; + mTargetTag = -1; + } + + /** + * Main catalyst view is responsible for collecting and sending touch events to JS. This method + * reacts for an incoming android native touch events ({@link MotionEvent}) and calls into + * {@link com.facebook.react.uimanager.events.EventDispatcher} when appropriate. + * It uses {@link com.facebook.react.uimanager.TouchTargetHelper#findTouchTargetView} + * helper method for figuring out a react view ID in the case of ACTION_DOWN + * event (when the gesture starts). + */ + public void handleTouchEvent(MotionEvent ev, EventDispatcher eventDispatcher) { + int action = ev.getAction() & MotionEvent.ACTION_MASK; + if (action == MotionEvent.ACTION_DOWN) { + if (mTargetTag != -1) { + FLog.e( + ReactConstants.TAG, + "Got DOWN touch before receiving UP or CANCEL from last gesture"); + } + + // First event for this gesture. We expect tag to be set to -1, and we use helper method + // {@link #findTargetTagForTouch} to find react view ID that will be responsible for handling + // this gesture + mChildIsHandlingNativeGesture = false; + mTargetTag = TouchTargetHelper.findTargetTagAndCoordinatesForTouch( + ev.getX(), + ev.getY(), + mRootViewGroup, + mTargetCoordinates); + eventDispatcher.dispatchEvent( + TouchEvent.obtain( + mTargetTag, + SystemClock.nanoTime(), + TouchEventType.START, + ev, + mTargetCoordinates[0], + mTargetCoordinates[1])); + } else if (mChildIsHandlingNativeGesture) { + // If the touch was intercepted by a child, we've already sent a cancel event to JS for this + // gesture, so we shouldn't send any more touches related to it. + return; + } else if (mTargetTag == -1) { + // All the subsequent action types are expected to be called after ACTION_DOWN thus target + // is supposed to be set for them. + FLog.e( + ReactConstants.TAG, + "Unexpected state: received touch event but didn't get starting ACTION_DOWN for this " + + "gesture before"); + } else if (action == MotionEvent.ACTION_UP) { + // End of the gesture. We reset target tag to -1 and expect no further event associated with + // this gesture. + eventDispatcher.dispatchEvent( + TouchEvent.obtain( + mTargetTag, + SystemClock.nanoTime(), + TouchEventType.END, + ev, + mTargetCoordinates[0], + mTargetCoordinates[1])); + mTargetTag = -1; + } else if (action == MotionEvent.ACTION_MOVE) { + // Update pointer position for current gesture + eventDispatcher.dispatchEvent( + TouchEvent.obtain( + mTargetTag, + SystemClock.nanoTime(), + TouchEventType.MOVE, + ev, + mTargetCoordinates[0], + mTargetCoordinates[1])); + } else if (action == MotionEvent.ACTION_POINTER_DOWN) { + // New pointer goes down, this can only happen after ACTION_DOWN is sent for the first pointer + eventDispatcher.dispatchEvent( + TouchEvent.obtain( + mTargetTag, + SystemClock.nanoTime(), + TouchEventType.START, + ev, + mTargetCoordinates[0], + mTargetCoordinates[1])); + } else if (action == MotionEvent.ACTION_POINTER_UP) { + // Exactly onw of the pointers goes up + eventDispatcher.dispatchEvent( + TouchEvent.obtain( + mTargetTag, + SystemClock.nanoTime(), + TouchEventType.END, + ev, + mTargetCoordinates[0], + mTargetCoordinates[1])); + } else if (action == MotionEvent.ACTION_CANCEL) { + dispatchCancelEvent(ev, eventDispatcher); + mTargetTag = -1; + } else { + FLog.w( + ReactConstants.TAG, + "Warning : touch event was ignored. Action=" + action + " Target=" + mTargetTag); + } + } + + private void dispatchCancelEvent(MotionEvent androidEvent, EventDispatcher eventDispatcher) { + // This means the gesture has already ended, via some other CANCEL or UP event. This is not + // expected to happen very often as it would mean some child View has decided to intercept the + // touch stream and start a native gesture only upon receiving the UP/CANCEL event. + if (mTargetTag == -1) { + FLog.w( + ReactConstants.TAG, + "Can't cancel already finished gesture. Is a child View trying to start a gesture from " + + "an UP/CANCEL event?"); + return; + } + + Assertions.assertCondition( + !mChildIsHandlingNativeGesture, + "Expected to not have already sent a cancel for this gesture"); + Assertions.assertNotNull(eventDispatcher).dispatchEvent( + TouchEvent.obtain( + mTargetTag, + SystemClock.nanoTime(), + TouchEventType.CANCEL, + androidEvent, + mTargetCoordinates[0], + mTargetCoordinates[1])); + } +}