mirror of
https://github.com/status-im/react-native.git
synced 2025-01-17 21:11:45 +00:00
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.content.Context;
|
||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.ViewParent;
|
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.ReactCompoundView;
|
||||||
|
import com.facebook.react.uimanager.ReactPointerEventsView;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A view that FlatShadowNode hierarchy maps to. Performs drawing by iterating over
|
* A view that FlatShadowNode hierarchy maps to. Performs drawing by iterating over
|
||||||
* array of DrawCommands, executing them one by one.
|
* 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.
|
* 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 mIsLayoutRequested = false;
|
||||||
private boolean mNeedsOffscreenAlphaCompositing = false;
|
private boolean mNeedsOffscreenAlphaCompositing = false;
|
||||||
private Drawable mHotspot;
|
private Drawable mHotspot;
|
||||||
|
private PointerEvents mPointerEvents = PointerEvents.AUTO;
|
||||||
|
private @Nullable OnInterceptTouchEventListener mOnInterceptTouchEventListener;
|
||||||
|
|
||||||
/* package */ FlatViewGroup(Context context) {
|
/* package */ FlatViewGroup(Context context) {
|
||||||
super(context);
|
super(context);
|
||||||
@ -82,12 +91,30 @@ import com.facebook.react.uimanager.ReactCompoundView;
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int reactTagForTouch(float touchX, float touchY) {
|
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);
|
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
|
// no children found
|
||||||
return getId();
|
return getId();
|
||||||
}
|
}
|
||||||
@ -195,6 +222,57 @@ import com.facebook.react.uimanager.ReactCompoundView;
|
|||||||
return mNeedsOffscreenAlphaCompositing;
|
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.
|
* See the documentation of needsOffscreenAlphaCompositing in View.js.
|
||||||
*/
|
*/
|
||||||
@ -294,6 +372,16 @@ import com.facebook.react.uimanager.ReactCompoundView;
|
|||||||
LAYOUT_REQUESTS.clear();
|
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) {
|
private View ensureViewHasNoParent(View view) {
|
||||||
ViewParent oldParent = view.getParent();
|
ViewParent oldParent = view.getParent();
|
||||||
if (oldParent != null) {
|
if (oldParent != null) {
|
||||||
|
@ -99,6 +99,11 @@ import com.facebook.react.uimanager.ViewProps;
|
|||||||
getMutableBorder().setBorderStyle(borderStyle);
|
getMutableBorder().setBorderStyle(borderStyle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ReactProp(name = "pointerEvents")
|
||||||
|
public void setPointerEvents(@Nullable String pointerEventsStr) {
|
||||||
|
forceMountToView();
|
||||||
|
}
|
||||||
|
|
||||||
private DrawBorder getMutableBorder() {
|
private DrawBorder getMutableBorder() {
|
||||||
if (mDrawBorder == null) {
|
if (mDrawBorder == null) {
|
||||||
mDrawBorder = new DrawBorder();
|
mDrawBorder = new DrawBorder();
|
||||||
|
@ -20,6 +20,7 @@ import com.facebook.react.bridge.ReadableArray;
|
|||||||
import com.facebook.react.bridge.ReadableMap;
|
import com.facebook.react.bridge.ReadableMap;
|
||||||
import com.facebook.react.common.MapBuilder;
|
import com.facebook.react.common.MapBuilder;
|
||||||
import com.facebook.react.uimanager.PixelUtil;
|
import com.facebook.react.uimanager.PixelUtil;
|
||||||
|
import com.facebook.react.uimanager.PointerEvents;
|
||||||
import com.facebook.react.uimanager.ReactProp;
|
import com.facebook.react.uimanager.ReactProp;
|
||||||
import com.facebook.react.uimanager.ViewProps;
|
import com.facebook.react.uimanager.ViewProps;
|
||||||
import com.facebook.react.views.view.ReactDrawableHelper;
|
import com.facebook.react.views.view.ReactDrawableHelper;
|
||||||
@ -95,4 +96,26 @@ import com.facebook.react.views.view.ReactDrawableHelper;
|
|||||||
boolean needsOffscreenAlphaCompositing) {
|
boolean needsOffscreenAlphaCompositing) {
|
||||||
view.setNeedsOffscreenAlphaCompositing(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…
x
Reference in New Issue
Block a user