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:
Denis Koroskin 2015-12-22 13:43:18 -08:00 committed by Ahmed El-Helw
parent ff77456f26
commit d23f86e47b
3 changed files with 119 additions and 3 deletions

View File

@ -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) {

View File

@ -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();

View File

@ -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;
}
}