/** * Copyright (c) 2015-present, Facebook, Inc. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @providesModule TouchableNativeFeedback */ 'use strict'; var Platform = require('Platform'); var React = require('React'); var PropTypes = require('prop-types'); var ReactNative = require('ReactNative'); var Touchable = require('Touchable'); var TouchableWithoutFeedback = require('TouchableWithoutFeedback'); var UIManager = require('UIManager'); var createReactClass = require('create-react-class'); var ensurePositiveDelayProps = require('ensurePositiveDelayProps'); var processColor = require('processColor'); var rippleBackgroundPropType = PropTypes.shape({ type: PropTypes.oneOf(['RippleAndroid']), color: PropTypes.number, borderless: PropTypes.bool, }); var themeAttributeBackgroundPropType = PropTypes.shape({ type: PropTypes.oneOf(['ThemeAttrAndroid']), attribute: PropTypes.string.isRequired, }); var backgroundPropType = PropTypes.oneOfType([ rippleBackgroundPropType, themeAttributeBackgroundPropType, ]); type Event = Object; var PRESS_RETENTION_OFFSET = {top: 20, left: 20, right: 20, bottom: 30}; /** * A wrapper for making views respond properly to touches (Android only). * On Android this component uses native state drawable to display touch * feedback. * * At the moment it only supports having a single View instance as a child * node, as it's implemented by replacing that View with another instance of * RCTView node with some additional properties set. * * Background drawable of native feedback touchable can be customized with * `background` property. * * Example: * * ``` * renderButton: function() { * return ( * * * Button * * * ); * }, * ``` */ var TouchableNativeFeedback = createReactClass({ displayName: 'TouchableNativeFeedback', propTypes: { ...TouchableWithoutFeedback.propTypes, /** * Determines the type of background drawable that's going to be used to * display feedback. It takes an object with `type` property and extra data * depending on the `type`. It's recommended to use one of the static * methods to generate that dictionary. */ background: backgroundPropType, /** * Set to true to add the ripple effect to the foreground of the view, instead of the * background. This is useful if one of your child views has a background of its own, or you're * e.g. displaying images, and you don't want the ripple to be covered by them. * * Check TouchableNativeFeedback.canUseNativeForeground() first, as this is only available on * Android 6.0 and above. If you try to use this on older versions you will get a warning and * fallback to background. */ useForeground: PropTypes.bool, }, statics: { /** * Creates an object that represents android theme's default background for * selectable elements (?android:attr/selectableItemBackground). */ SelectableBackground: function() { return {type: 'ThemeAttrAndroid', attribute: 'selectableItemBackground'}; }, /** * Creates an object that represent android theme's default background for borderless * selectable elements (?android:attr/selectableItemBackgroundBorderless). * Available on android API level 21+. */ SelectableBackgroundBorderless: function() { return {type: 'ThemeAttrAndroid', attribute: 'selectableItemBackgroundBorderless'}; }, /** * Creates an object that represents ripple drawable with specified color (as a * string). If property `borderless` evaluates to true the ripple will * render outside of the view bounds (see native actionbar buttons as an * example of that behavior). This background type is available on Android * API level 21+. * * @param color The ripple color * @param borderless If the ripple can render outside it's bounds */ Ripple: function(color: string, borderless: boolean) { return {type: 'RippleAndroid', color: processColor(color), borderless: borderless}; }, canUseNativeForeground: function() { return Platform.OS === 'android' && Platform.Version >= 23; } }, mixins: [Touchable.Mixin], getDefaultProps: function() { return { background: this.SelectableBackground(), }; }, getInitialState: function() { return this.touchableGetInitialState(); }, componentDidMount: function() { ensurePositiveDelayProps(this.props); }, UNSAFE_componentWillReceiveProps: function(nextProps) { ensurePositiveDelayProps(nextProps); }, /** * `Touchable.Mixin` self callbacks. The mixin will invoke these if they are * defined on your component. */ touchableHandleActivePressIn: function(e: Event) { this.props.onPressIn && this.props.onPressIn(e); this._dispatchPressedStateChange(true); this._dispatchHotspotUpdate(this.pressInLocation.locationX, this.pressInLocation.locationY); }, touchableHandleActivePressOut: function(e: Event) { this.props.onPressOut && this.props.onPressOut(e); this._dispatchPressedStateChange(false); }, touchableHandlePress: function(e: Event) { this.props.onPress && this.props.onPress(e); }, touchableHandleLongPress: function(e: Event) { this.props.onLongPress && this.props.onLongPress(e); }, touchableGetPressRectOffset: function() { // Always make sure to predeclare a constant! return this.props.pressRetentionOffset || PRESS_RETENTION_OFFSET; }, touchableGetHitSlop: function() { return this.props.hitSlop; }, touchableGetHighlightDelayMS: function() { return this.props.delayPressIn; }, touchableGetLongPressDelayMS: function() { return this.props.delayLongPress; }, touchableGetPressOutDelayMS: function() { return this.props.delayPressOut; }, _handleResponderMove: function(e) { this.touchableHandleResponderMove(e); this._dispatchHotspotUpdate(e.nativeEvent.locationX, e.nativeEvent.locationY); }, _dispatchHotspotUpdate: function(destX, destY) { UIManager.dispatchViewManagerCommand( ReactNative.findNodeHandle(this), UIManager.RCTView.Commands.hotspotUpdate, [destX || 0, destY || 0] ); }, _dispatchPressedStateChange: function(pressed) { UIManager.dispatchViewManagerCommand( ReactNative.findNodeHandle(this), UIManager.RCTView.Commands.setPressed, [pressed] ); }, render: function() { const child = React.Children.only(this.props.children); let children = child.props.children; if (Touchable.TOUCH_TARGET_DEBUG && child.type.displayName === 'View') { if (!Array.isArray(children)) { children = [children]; } children.push(Touchable.renderDebugView({color: 'brown', hitSlop: this.props.hitSlop})); } if (this.props.useForeground && !TouchableNativeFeedback.canUseNativeForeground()) { console.warn( 'Requested foreground ripple, but it is not available on this version of Android. ' + 'Consider calling TouchableNativeFeedback.canUseNativeForeground() and using a different ' + 'Touchable if the result is false.'); } const drawableProp = this.props.useForeground && TouchableNativeFeedback.canUseNativeForeground() ? 'nativeForegroundAndroid' : 'nativeBackgroundAndroid'; var childProps = { ...child.props, [drawableProp]: this.props.background, accessible: this.props.accessible !== false, accessibilityLabel: this.props.accessibilityLabel, accessibilityComponentType: this.props.accessibilityComponentType, accessibilityTraits: this.props.accessibilityTraits, children, testID: this.props.testID, onLayout: this.props.onLayout, hitSlop: this.props.hitSlop, onStartShouldSetResponder: this.touchableHandleStartShouldSetResponder, onResponderTerminationRequest: this.touchableHandleResponderTerminationRequest, onResponderGrant: this.touchableHandleResponderGrant, onResponderMove: this._handleResponderMove, onResponderRelease: this.touchableHandleResponderRelease, onResponderTerminate: this.touchableHandleResponderTerminate, }; // We need to clone the actual element so that the ripple background drawable // can be applied directly to the background of this element rather than to // a wrapper view as done in other Touchable* return React.cloneElement( child, childProps ); } }); module.exports = TouchableNativeFeedback;