Add hitSlop prop on iOS and Android

Summary:New prop `hitSlop` allows extending the touch area of Touchable components. This makes it easier to touch small buttons without needing to change your styles.

It takes `top`, `bottom`, `left`, and `right` same as the `pressRetentionOffset` prop. When a touch is moved, `hitSlop` is combined with `pressRetentionOffset` to determine how far the touch can move off the button before deactivating the button.

On Android I had to add a new file `ids.xml` to generate a unique ID to use for the tag where I store the `hitSlop` state. The iOS side is more straightforward.

terribleben worked on the iOS and JS parts of this diff.

Fixes #110
Closes https://github.com/facebook/react-native/pull/5720

Differential Revision: D2941671

Pulled By: androidtrunkagent

fb-gh-sync-id: 07e3eb8b6a36eebf76968fdaac3c6ac335603194
shipit-source-id: 07e3eb8b6a36eebf76968fdaac3c6ac335603194
This commit is contained in:
Jesse Ruder
2016-02-16 16:50:35 -08:00
committed by facebook-github-bot-7
parent ecf6981093
commit 0176ac488e
15 changed files with 225 additions and 8 deletions

View File

@@ -0,0 +1,28 @@
/**
* 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.touch;
import android.graphics.Rect;
import javax.annotation.Nullable;
/**
* This interface should be implemented by all {@link View} subclasses that want to use the
* hitSlop prop to extend their touch areas.
*/
public interface ReactHitSlopView {
/**
* Called when determining the touch area of a view.
* @return A {@link Rect} representing how far to extend the touch area in each direction.
*/
public @Nullable Rect getHitSlopRect();
}

View File

@@ -13,12 +13,14 @@ import javax.annotation.Nullable;
import android.graphics.Matrix;
import android.graphics.PointF;
import android.graphics.Rect;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.touch.ReactHitSlopView;
/**
* Class responsible for identifying which react view should handle a given {@link MotionEvent}.
@@ -118,7 +120,7 @@ public class TouchTargetHelper {
}
}
return viewGroup;
}
}
/**
* Returns whether the touch point is within the child View
@@ -144,12 +146,24 @@ public class TouchTargetHelper {
localX = localXY[0];
localY = localXY[1];
}
if ((localX >= 0 && localX < (child.getRight() - child.getLeft()))
&& (localY >= 0 && localY < (child.getBottom() - child.getTop()))) {
outLocalPoint.set(localX, localY);
return true;
if (child instanceof ReactHitSlopView && ((ReactHitSlopView) child).getHitSlopRect() != null) {
Rect hitSlopRect = ((ReactHitSlopView) child).getHitSlopRect();
if ((localX >= -hitSlopRect.left && localX < (child.getRight() - child.getLeft()) + hitSlopRect.right)
&& (localY >= -hitSlopRect.top && localY < (child.getBottom() - child.getTop()) + hitSlopRect.bottom)) {
outLocalPoint.set(localX, localY);
return true;
}
return false;
} else {
if ((localX >= 0 && localX < (child.getRight() - child.getLeft()))
&& (localY >= 0 && localY < (child.getBottom() - child.getTop()))) {
outLocalPoint.set(localX, localY);
return true;
}
return false;
}
return false;
}

View File

@@ -23,6 +23,7 @@ import android.view.ViewGroup;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.common.annotations.VisibleForTesting;
import com.facebook.react.touch.ReactHitSlopView;
import com.facebook.react.touch.ReactInterceptingViewGroup;
import com.facebook.react.touch.OnInterceptTouchEventListener;
import com.facebook.react.uimanager.MeasureSpecAssertions;
@@ -34,7 +35,7 @@ import com.facebook.react.uimanager.ReactPointerEventsView;
* initializes most of the storage needed for them.
*/
public class ReactViewGroup extends ViewGroup implements
ReactInterceptingViewGroup, ReactClippingViewGroup, ReactPointerEventsView {
ReactInterceptingViewGroup, ReactClippingViewGroup, ReactPointerEventsView, ReactHitSlopView {
private static final int ARRAY_CAPACITY_INCREMENT = 12;
private static final int DEFAULT_BACKGROUND_COLOR = Color.TRANSPARENT;
@@ -87,6 +88,7 @@ public class ReactViewGroup extends ViewGroup implements
private @Nullable View[] mAllChildren = null;
private int mAllChildrenCount;
private @Nullable Rect mClippingRect;
private @Nullable Rect mHitSlopRect;
private PointerEvents mPointerEvents = PointerEvents.AUTO;
private @Nullable ChildrenLayoutChangeListener mChildrenLayoutChangeListener;
private @Nullable ReactViewBackgroundDrawable mReactBackgroundDrawable;
@@ -513,4 +515,13 @@ public class ReactViewGroup extends ViewGroup implements
return mReactBackgroundDrawable;
}
@Override
public @Nullable Rect getHitSlopRect() {
return mHitSlopRect;
}
public void setHitSlopRect(@Nullable Rect rect) {
mHitSlopRect = rect;
}
}

View File

@@ -14,6 +14,7 @@ import javax.annotation.Nullable;
import java.util.Locale;
import java.util.Map;
import android.graphics.Rect;
import android.os.Build;
import android.view.View;
@@ -75,6 +76,20 @@ public class ReactViewManager extends ViewGroupManager<ReactViewGroup> {
view.setBorderStyle(borderStyle);
}
@ReactProp(name = "hitSlop")
public void setHitSlop(final ReactViewGroup view, @Nullable ReadableMap hitSlop) {
if (hitSlop == null) {
view.setHitSlopRect(null);
} else {
view.setHitSlopRect(new Rect(
(int) PixelUtil.toPixelFromDIP(hitSlop.getDouble("left")),
(int) PixelUtil.toPixelFromDIP(hitSlop.getDouble("top")),
(int) PixelUtil.toPixelFromDIP(hitSlop.getDouble("right")),
(int) PixelUtil.toPixelFromDIP(hitSlop.getDouble("bottom"))
));
}
}
@ReactProp(name = "pointerEvents")
public void setPointerEvents(ReactViewGroup view, @Nullable String pointerEventsStr) {
if (pointerEventsStr != null) {