Skip to content

Commit

Permalink
Add hitSlop prop on iOS and Android
Browse files Browse the repository at this point in the history
  • Loading branch information
jesseruder committed Feb 3, 2016
1 parent 5ec1d35 commit 34cec67
Show file tree
Hide file tree
Showing 12 changed files with 114 additions and 5 deletions.
10 changes: 10 additions & 0 deletions Libraries/Components/Touchable/Touchable.js
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,16 @@ var TouchableMixin = {
var pressExpandRight = pressRectOffset.right;
var pressExpandBottom = pressRectOffset.bottom;

var hitSlop = this.touchableGetHitSlop ?
this.touchableGetHitSlop() : null;

if (hitSlop) {
pressExpandLeft += hitSlop.left;
pressExpandTop += hitSlop.top;
pressExpandRight += hitSlop.right;
pressExpandBottom += hitSlop.bottom;
}

var touch = TouchEventUtils.extractSingleTouch(e.nativeEvent);
var pageX = touch && touch.pageX;
var pageY = touch && touch.pageY;
Expand Down
10 changes: 10 additions & 0 deletions Libraries/Components/Touchable/TouchableBounce.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ var TouchableBounce = React.createClass({
* is disabled. Ensure you pass in a constant to reduce memory allocations.
*/
pressRetentionOffset: EdgeInsetsPropType,
/**
* This defines how far your touch can start away from the button. This is
* added to `pressRetentionOffset` when moving off of the button.
*/
hitSlop: EdgeInsetsPropType,
},

getInitialState: function(): State {
Expand Down Expand Up @@ -108,6 +113,10 @@ var TouchableBounce = React.createClass({
return this.props.pressRetentionOffset || PRESS_RETENTION_OFFSET;
},

touchableGetHitSlop: function(): Object {
return this.props.hitSlop;
},

touchableGetHighlightDelayMS: function(): number {
return 0;
},
Expand All @@ -118,6 +127,7 @@ var TouchableBounce = React.createClass({
style={[{transform: [{scale: this.state.scale}]}, this.props.style]}
accessible={true}
testID={this.props.testID}
hitSlop={this.props.hitSlop}
onStartShouldSetResponder={this.touchableHandleStartShouldSetResponder}
onResponderTerminationRequest={this.touchableHandleResponderTerminationRequest}
onResponderGrant={this.touchableHandleResponderGrant}
Expand Down
5 changes: 5 additions & 0 deletions Libraries/Components/Touchable/TouchableHighlight.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,10 @@ var TouchableHighlight = React.createClass({
return this.props.pressRetentionOffset || PRESS_RETENTION_OFFSET;
},

touchableGetHitSlop: function() {
return this.props.hitSlop;
},

touchableGetHighlightDelayMS: function() {
return this.props.delayPressIn;
},
Expand Down Expand Up @@ -220,6 +224,7 @@ var TouchableHighlight = React.createClass({
ref={UNDERLAY_REF}
style={this.state.underlayStyle}
onLayout={this.props.onLayout}
hitSlop={this.props.hitSlop}
onStartShouldSetResponder={this.touchableHandleStartShouldSetResponder}
onResponderTerminationRequest={this.touchableHandleResponderTerminationRequest}
onResponderGrant={this.touchableHandleResponderGrant}
Expand Down
5 changes: 5 additions & 0 deletions Libraries/Components/Touchable/TouchableOpacity.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ var TouchableOpacity = React.createClass({
return this.props.pressRetentionOffset || PRESS_RETENTION_OFFSET;
},

touchableGetHitSlop: function() {
return this.props.hitSlop;
},

touchableGetHighlightDelayMS: function() {
return this.props.delayPressIn || 0;
},
Expand Down Expand Up @@ -159,6 +163,7 @@ var TouchableOpacity = React.createClass({
style={[this.props.style, {opacity: this.state.anim}]}
testID={this.props.testID}
onLayout={this.props.onLayout}
hitSlop={this.props.hitSlop}
onStartShouldSetResponder={this.touchableHandleStartShouldSetResponder}
onResponderTerminationRequest={this.touchableHandleResponderTerminationRequest}
onResponderGrant={this.touchableHandleResponderGrant}
Expand Down
12 changes: 11 additions & 1 deletion Libraries/Components/Touchable/TouchableWithoutFeedback.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ var PRESS_RETENTION_OFFSET = {top: 20, left: 20, right: 20, bottom: 30};
* Do not use unless you have a very good reason. All the elements that
* respond to press should have a visual feedback when touched. This is
* one of the primary reason a "web" app doesn't feel "native".
*
*
* > **NOTE**: TouchableWithoutFeedback supports only one child
* >
* > If you wish to have several child components, wrap them in a View.
Expand Down Expand Up @@ -80,6 +80,11 @@ var TouchableWithoutFeedback = React.createClass({
* is disabled. Ensure you pass in a constant to reduce memory allocations.
*/
pressRetentionOffset: EdgeInsetsPropType,
/**
* This defines how far your touch can start away from the button. This is
* added to `pressRetentionOffset` when moving off of the button.
*/
hitSlop: EdgeInsetsPropType,
},

getInitialState: function() {
Expand Down Expand Up @@ -118,6 +123,10 @@ var TouchableWithoutFeedback = React.createClass({
return this.props.pressRetentionOffset || PRESS_RETENTION_OFFSET;
},

touchableGetHitSlop: function(): Object {
return this.props.hitSlop;
},

touchableGetHighlightDelayMS: function(): number {
return this.props.delayPressIn || 0;
},
Expand All @@ -139,6 +148,7 @@ var TouchableWithoutFeedback = React.createClass({
accessibilityTraits: this.props.accessibilityTraits,
testID: this.props.testID,
onLayout: this.props.onLayout,
hitSlop: this.props.hitSlop,
onStartShouldSetResponder: this.touchableHandleStartShouldSetResponder,
onResponderTerminationRequest: this.touchableHandleResponderTerminationRequest,
onResponderGrant: this.touchableHandleResponderGrant,
Expand Down
8 changes: 7 additions & 1 deletion Libraries/Components/View/View.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
*/
'use strict';

const EdgeInsetsPropType = require('EdgeInsetsPropType');
const NativeMethodsMixin = require('NativeMethodsMixin');
const PropTypes = require('ReactPropTypes');
const React = require('React');
Expand Down Expand Up @@ -53,7 +54,7 @@ const AccessibilityComponentType = [

const forceTouchAvailable = (UIManager.RCTView.Constants &&
UIManager.RCTView.Constants.forceTouchAvailable) || false;

const statics = {
AccessibilityTraits,
AccessibilityComponentType,
Expand Down Expand Up @@ -201,6 +202,11 @@ const View = React.createClass({
onMoveShouldSetResponder: PropTypes.func,
onMoveShouldSetResponderCapture: PropTypes.func,

/**
* This defines how far a touch event can start away from the view.
*/
hitSlop: EdgeInsetsPropType,

/**
* Invoked on mount and layout changes with
*
Expand Down
5 changes: 5 additions & 0 deletions React/Views/RCTView.h
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,9 @@
*/
@property (nonatomic, assign) RCTBorderStyle borderStyle;

/**
* Insets used when hit testing inside this view.
*/
@property (nonatomic, assign) UIEdgeInsets hitTestEdgeInsets;

@end
10 changes: 10 additions & 0 deletions React/Views/RCTView.m
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ - (instancetype)initWithFrame:(CGRect)frame
_borderBottomLeftRadius = -1;
_borderBottomRightRadius = -1;
_borderStyle = RCTBorderStyleSolid;
_hitTestEdgeInsets = UIEdgeInsetsZero;

_backgroundColor = super.backgroundColor;
}
Expand Down Expand Up @@ -180,6 +181,15 @@ - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
}
}

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
if (UIEdgeInsetsEqualToEdgeInsets(self.hitTestEdgeInsets, UIEdgeInsetsZero)) {
return [super pointInside:point withEvent:event];
}
CGRect hitFrame = UIEdgeInsetsInsetRect(self.bounds, self.hitTestEdgeInsets);
return CGRectContainsPoint(hitFrame, point);
}

- (BOOL)accessibilityActivate
{
if (_onAccessibilityTap) {
Expand Down
11 changes: 11 additions & 0 deletions React/Views/RCTViewManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,17 @@ - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(__unused NSDictio
view.borderStyle = json ? [RCTConvert RCTBorderStyle:json] : defaultView.borderStyle;
}
}
RCT_CUSTOM_VIEW_PROPERTY(hitSlop, UIEdgeInsets, RCTView)
{
if ([view respondsToSelector:@selector(setHitTestEdgeInsets:)]) {
if (json) {
UIEdgeInsets hitSlopInsets = [RCTConvert UIEdgeInsets:json];
view.hitTestEdgeInsets = UIEdgeInsetsMake(-hitSlopInsets.top, -hitSlopInsets.left, -hitSlopInsets.bottom, -hitSlopInsets.right);
} else {
view.hitTestEdgeInsets = defaultView.hitTestEdgeInsets;
}
}
}
RCT_EXPORT_VIEW_PROPERTY(onAccessibilityTap, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onMagicTap, RCTDirectEventBlock)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@
import android.view.View;
import android.view.ViewGroup;

import com.facebook.react.R;
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.react.bridge.UiThreadUtil;

import java.util.HashMap;


/**
* Class responsible for identifying which react view should handle a given {@link MotionEvent}.
* It uses the event coordinates to traverse the view hierarchy and return a suitable view.
Expand Down Expand Up @@ -118,7 +122,7 @@ private static View findTouchTargetView(float[] eventCoords, ViewGroup viewGroup
}
}
return viewGroup;
}
}

/**
* Returns whether the touch point is within the child View
Expand All @@ -144,8 +148,19 @@ private static boolean isTransformedTouchPointInView(
localX = localXY[0];
localY = localXY[1];
}
if ((localX >= 0 && localX < (child.getRight() - child.getLeft()))
&& (localY >= 0 && localY < (child.getBottom() - child.getTop()))) {
float hitSlopTop = 0;
float hitSlopBottom = 0;
float hitSlopLeft = 0;
float hitSlopRight = 0;
if (child.getTag(R.id.hitSlopTag) != null) {
HashMap<String, Float> hitSlopTag = (HashMap<String, Float>) child.getTag(R.id.hitSlopTag);
hitSlopTop = hitSlopTag.get("top");
hitSlopBottom = hitSlopTag.get("bottom");
hitSlopLeft = hitSlopTag.get("left");
hitSlopRight = hitSlopTag.get("right");
}
if ((localX >= -hitSlopLeft && localX < (child.getRight() - child.getLeft()) + hitSlopRight)
&& (localY >= -hitSlopTop && localY < (child.getBottom() - child.getTop()) + hitSlopBottom)) {
outLocalPoint.set(localX, localY);
return true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import javax.annotation.Nullable;

import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

Expand All @@ -19,9 +20,12 @@

import com.facebook.csslayout.CSSConstants;
import com.facebook.csslayout.Spacing;
import com.facebook.react.R;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.common.annotations.VisibleForTesting;
import com.facebook.react.uimanager.PixelUtil;
Expand Down Expand Up @@ -61,6 +65,20 @@ public void setBorderStyle(ReactViewGroup view, @Nullable String borderStyle) {
view.setBorderStyle(borderStyle);
}

@ReactProp(name = "hitSlop")
public void setHitSlop(final ReactViewGroup view, @Nullable ReadableMap hitSlop) {
if (hitSlop == null) {
view.setTag(R.id.hitSlopTag, null);
} else {
HashMap<String, Float> hitSlopPixels = new HashMap<>();
hitSlopPixels.put("top", PixelUtil.toPixelFromDIP(hitSlop.getDouble("top")));
hitSlopPixels.put("bottom", PixelUtil.toPixelFromDIP(hitSlop.getDouble("bottom")));
hitSlopPixels.put("left", PixelUtil.toPixelFromDIP(hitSlop.getDouble("left")));
hitSlopPixels.put("right", PixelUtil.toPixelFromDIP(hitSlop.getDouble("right")));
view.setTag(R.id.hitSlopTag, hitSlopPixels);
}
}

@ReactProp(name = "pointerEvents")
public void setPointerEvents(ReactViewGroup view, @Nullable String pointerEventsStr) {
if (pointerEventsStr != null) {
Expand Down
4 changes: 4 additions & 0 deletions ReactAndroid/src/main/res/devsupport/values/ids.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="hitSlopTag" type="id"/>
</resources>

0 comments on commit 34cec67

Please sign in to comment.