Implement touch intercepting in RCTView
Summary: @public React allows excluding certain elements from touch handling by assigning `PointerEvents` filter to them, such as BOX_NONE - this element will not receive touch but its children will, BOX_ONLY - only this element will receive pointer event and not children, NONE - neither this element nor its children will receive pointer events, and AUTO - pointer events are allowed for both this element and its children. This diff adds PointerEvents support to flat RCTView. Most of the implementation is copied from ReactViewManager/ReactViewGroup. One small change is made to TouchTargetHelper to ensure that it works correctly with virtual nodes when their parent has PointerEvents set to PointerEvents.BOX_NONE. Reviewed By: ahmedre Differential Revision: D2784208
This commit is contained in:
parent
ff77456f26
commit
d23f86e47b
|
@ -17,17 +17,24 @@ import javax.annotation.Nullable;
|
|||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewParent;
|
||||
|
||||
import com.facebook.react.bridge.SoftAssertions;
|
||||
import com.facebook.react.touch.CatalystInterceptingViewGroup;
|
||||
import com.facebook.react.touch.OnInterceptTouchEventListener;
|
||||
import com.facebook.react.uimanager.PointerEvents;
|
||||
import com.facebook.react.uimanager.ReactCompoundView;
|
||||
import com.facebook.react.uimanager.ReactPointerEventsView;
|
||||
|
||||
/**
|
||||
* A view that FlatShadowNode hierarchy maps to. Performs drawing by iterating over
|
||||
* array of DrawCommands, executing them one by one.
|
||||
*/
|
||||
/* package */ final class FlatViewGroup extends ViewGroup implements ReactCompoundView {
|
||||
/* package */ final class FlatViewGroup extends ViewGroup
|
||||
implements CatalystInterceptingViewGroup, ReactCompoundView, ReactPointerEventsView {
|
||||
/**
|
||||
* Helper class that allows AttachDetachListener to invalidate the hosting View.
|
||||
*/
|
||||
|
@ -59,6 +66,8 @@ import com.facebook.react.uimanager.ReactCompoundView;
|
|||
private boolean mIsLayoutRequested = false;
|
||||
private boolean mNeedsOffscreenAlphaCompositing = false;
|
||||
private Drawable mHotspot;
|
||||
private PointerEvents mPointerEvents = PointerEvents.AUTO;
|
||||
private @Nullable OnInterceptTouchEventListener mOnInterceptTouchEventListener;
|
||||
|
||||
/* package */ FlatViewGroup(Context context) {
|
||||
super(context);
|
||||
|
@ -82,12 +91,30 @@ import com.facebook.react.uimanager.ReactCompoundView;
|
|||
|
||||
@Override
|
||||
public int reactTagForTouch(float touchX, float touchY) {
|
||||
for (NodeRegion nodeRegion : mNodeRegions) {
|
||||
if (nodeRegion.withinBounds(touchX, touchY)) {
|
||||
/**
|
||||
* Make sure we don't find any children if the pointer events are set to BOX_ONLY.
|
||||
* There is no need to special-case any other modes, because if PointerEvents are set to:
|
||||
* a) PointerEvents.AUTO - all children are included, nothing to exclude
|
||||
* b) PointerEvents.NONE - this method will NOT be executed, because the View will be filtered
|
||||
* out by TouchTargetHelper.
|
||||
* c) PointerEvents.BOX_NONE - TouchTargetHelper will make sure that {@link #reactTagForTouch()}
|
||||
* doesn't return getId().
|
||||
*/
|
||||
SoftAssertions.assertCondition(
|
||||
mPointerEvents != PointerEvents.NONE,
|
||||
"TouchTargetHelper should not allow calling this method when pointer events are NONE");
|
||||
|
||||
if (mPointerEvents != PointerEvents.BOX_ONLY) {
|
||||
NodeRegion nodeRegion = nodeRegionWithinBounds(touchX, touchY);
|
||||
if (nodeRegion != null) {
|
||||
return nodeRegion.getReactTag(touchX, touchY);
|
||||
}
|
||||
}
|
||||
|
||||
SoftAssertions.assertCondition(
|
||||
mPointerEvents != PointerEvents.BOX_NONE,
|
||||
"TouchTargetHelper should not allow returning getId() when pointer events are BOX_NONE");
|
||||
|
||||
// no children found
|
||||
return getId();
|
||||
}
|
||||
|
@ -195,6 +222,57 @@ import com.facebook.react.uimanager.ReactCompoundView;
|
|||
return mNeedsOffscreenAlphaCompositing;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnInterceptTouchEventListener(OnInterceptTouchEventListener listener) {
|
||||
mOnInterceptTouchEventListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onInterceptTouchEvent(MotionEvent ev) {
|
||||
if (mOnInterceptTouchEventListener != null &&
|
||||
mOnInterceptTouchEventListener.onInterceptTouchEvent(this, ev)) {
|
||||
return true;
|
||||
}
|
||||
// We intercept the touch event if the children are not supposed to receive it.
|
||||
if (mPointerEvents == PointerEvents.NONE || mPointerEvents == PointerEvents.BOX_ONLY) {
|
||||
return true;
|
||||
}
|
||||
return super.onInterceptTouchEvent(ev);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent ev) {
|
||||
// We do not accept the touch event if this view is not supposed to receive it.
|
||||
if (mPointerEvents == PointerEvents.NONE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mPointerEvents == PointerEvents.BOX_NONE) {
|
||||
// We cannot always return false here because some child nodes could be flatten into this View
|
||||
NodeRegion nodeRegion = nodeRegionWithinBounds(ev.getX(), ev.getY());
|
||||
if (nodeRegion == null) {
|
||||
// no child to handle this touch event, bailing out.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// The root view always assumes any view that was tapped wants the touch
|
||||
// and sends the event to JS as such.
|
||||
// We don't need to do bubbling in native (it's already happening in JS).
|
||||
// For an explanation of bubbling and capturing, see
|
||||
// http://javascript.info/tutorial/bubbling-and-capturing#capturing
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PointerEvents getPointerEvents() {
|
||||
return mPointerEvents;
|
||||
}
|
||||
|
||||
/*package*/ void setPointerEvents(PointerEvents pointerEvents) {
|
||||
mPointerEvents = pointerEvents;
|
||||
}
|
||||
|
||||
/**
|
||||
* See the documentation of needsOffscreenAlphaCompositing in View.js.
|
||||
*/
|
||||
|
@ -294,6 +372,16 @@ import com.facebook.react.uimanager.ReactCompoundView;
|
|||
LAYOUT_REQUESTS.clear();
|
||||
}
|
||||
|
||||
private NodeRegion nodeRegionWithinBounds(float touchX, float touchY) {
|
||||
for (NodeRegion nodeRegion : mNodeRegions) {
|
||||
if (nodeRegion.withinBounds(touchX, touchY)) {
|
||||
return nodeRegion;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private View ensureViewHasNoParent(View view) {
|
||||
ViewParent oldParent = view.getParent();
|
||||
if (oldParent != null) {
|
||||
|
|
|
@ -99,6 +99,11 @@ import com.facebook.react.uimanager.ViewProps;
|
|||
getMutableBorder().setBorderStyle(borderStyle);
|
||||
}
|
||||
|
||||
@ReactProp(name = "pointerEvents")
|
||||
public void setPointerEvents(@Nullable String pointerEventsStr) {
|
||||
forceMountToView();
|
||||
}
|
||||
|
||||
private DrawBorder getMutableBorder() {
|
||||
if (mDrawBorder == null) {
|
||||
mDrawBorder = new DrawBorder();
|
||||
|
|
|
@ -20,6 +20,7 @@ import com.facebook.react.bridge.ReadableArray;
|
|||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.common.MapBuilder;
|
||||
import com.facebook.react.uimanager.PixelUtil;
|
||||
import com.facebook.react.uimanager.PointerEvents;
|
||||
import com.facebook.react.uimanager.ReactProp;
|
||||
import com.facebook.react.uimanager.ViewProps;
|
||||
import com.facebook.react.views.view.ReactDrawableHelper;
|
||||
|
@ -95,4 +96,26 @@ import com.facebook.react.views.view.ReactDrawableHelper;
|
|||
boolean needsOffscreenAlphaCompositing) {
|
||||
view.setNeedsOffscreenAlphaCompositing(needsOffscreenAlphaCompositing);
|
||||
}
|
||||
|
||||
@ReactProp(name = "pointerEvents")
|
||||
public void setPointerEvents(FlatViewGroup view, @Nullable String pointerEventsStr) {
|
||||
view.setPointerEvents(parsePointerEvents(pointerEventsStr));
|
||||
}
|
||||
|
||||
private static PointerEvents parsePointerEvents(@Nullable String pointerEventsStr) {
|
||||
if (pointerEventsStr != null) {
|
||||
switch (pointerEventsStr) {
|
||||
case "none":
|
||||
return PointerEvents.NONE;
|
||||
case "auto":
|
||||
return PointerEvents.AUTO;
|
||||
case "box-none":
|
||||
return PointerEvents.BOX_NONE;
|
||||
case "box-only":
|
||||
return PointerEvents.BOX_ONLY;
|
||||
}
|
||||
}
|
||||
// default or invalid
|
||||
return PointerEvents.AUTO;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue