TouchableOpacity.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. /**
  2. * Copyright (c) Facebook, Inc. and its affiliates.
  3. *
  4. * This source code is licensed under the MIT license found in the
  5. * LICENSE file in the root directory of this source tree.
  6. *
  7. * @flow strict-local
  8. * @format
  9. */
  10. 'use strict';
  11. import Pressability, {
  12. type PressabilityConfig,
  13. } from '../../Pressability/Pressability';
  14. import {PressabilityDebugView} from '../../Pressability/PressabilityDebug';
  15. import TVTouchable from './TVTouchable';
  16. import typeof TouchableWithoutFeedback from './TouchableWithoutFeedback';
  17. import Animated from 'react-native/Libraries/Animated/src/Animated';
  18. import Easing from 'react-native/Libraries/Animated/src/Easing';
  19. import type {ViewStyleProp} from 'react-native/Libraries/StyleSheet/StyleSheet';
  20. import flattenStyle from 'react-native/Libraries/StyleSheet/flattenStyle';
  21. import Platform from '../../Utilities/Platform';
  22. import * as React from 'react';
  23. type TVProps = $ReadOnly<{|
  24. hasTVPreferredFocus?: ?boolean,
  25. nextFocusDown?: ?number,
  26. nextFocusForward?: ?number,
  27. nextFocusLeft?: ?number,
  28. nextFocusRight?: ?number,
  29. nextFocusUp?: ?number,
  30. |}>;
  31. type Props = $ReadOnly<{|
  32. ...React.ElementConfig<TouchableWithoutFeedback>,
  33. ...TVProps,
  34. activeOpacity?: ?number,
  35. style?: ?ViewStyleProp,
  36. hostRef: React.Ref<typeof Animated.View>,
  37. |}>;
  38. type State = $ReadOnly<{|
  39. anim: Animated.Value,
  40. pressability: Pressability,
  41. |}>;
  42. /**
  43. * A wrapper for making views respond properly to touches.
  44. * On press down, the opacity of the wrapped view is decreased, dimming it.
  45. *
  46. * Opacity is controlled by wrapping the children in an Animated.View, which is
  47. * added to the view hierarchy. Be aware that this can affect layout.
  48. *
  49. * Example:
  50. *
  51. * ```
  52. * renderButton: function() {
  53. * return (
  54. * <TouchableOpacity onPress={this._onPressButton}>
  55. * <Image
  56. * style={styles.button}
  57. * source={require('./myButton.png')}
  58. * />
  59. * </TouchableOpacity>
  60. * );
  61. * },
  62. * ```
  63. * ### Example
  64. *
  65. * ```ReactNativeWebPlayer
  66. * import React, { Component } from 'react'
  67. * import {
  68. * AppRegistry,
  69. * StyleSheet,
  70. * TouchableOpacity,
  71. * Text,
  72. * View,
  73. * } from 'react-native'
  74. *
  75. * class App extends Component {
  76. * state = { count: 0 }
  77. *
  78. * onPress = () => {
  79. * this.setState(state => ({
  80. * count: state.count + 1
  81. * }));
  82. * };
  83. *
  84. * render() {
  85. * return (
  86. * <View style={styles.container}>
  87. * <TouchableOpacity
  88. * style={styles.button}
  89. * onPress={this.onPress}>
  90. * <Text> Touch Here </Text>
  91. * </TouchableOpacity>
  92. * <View style={[styles.countContainer]}>
  93. * <Text style={[styles.countText]}>
  94. * { this.state.count !== 0 ? this.state.count: null}
  95. * </Text>
  96. * </View>
  97. * </View>
  98. * )
  99. * }
  100. * }
  101. *
  102. * const styles = StyleSheet.create({
  103. * container: {
  104. * flex: 1,
  105. * justifyContent: 'center',
  106. * paddingHorizontal: 10
  107. * },
  108. * button: {
  109. * alignItems: 'center',
  110. * backgroundColor: '#DDDDDD',
  111. * padding: 10
  112. * },
  113. * countContainer: {
  114. * alignItems: 'center',
  115. * padding: 10
  116. * },
  117. * countText: {
  118. * color: '#FF00FF'
  119. * }
  120. * })
  121. *
  122. * AppRegistry.registerComponent('App', () => App)
  123. * ```
  124. *
  125. */
  126. class TouchableOpacity extends React.Component<Props, State> {
  127. _tvTouchable: ?TVTouchable;
  128. state: State = {
  129. anim: new Animated.Value(this._getChildStyleOpacityWithDefault()),
  130. pressability: new Pressability(this._createPressabilityConfig()),
  131. };
  132. _createPressabilityConfig(): PressabilityConfig {
  133. return {
  134. cancelable: !this.props.rejectResponderTermination,
  135. disabled: this.props.disabled,
  136. hitSlop: this.props.hitSlop,
  137. delayLongPress: this.props.delayLongPress,
  138. delayPressIn: this.props.delayPressIn,
  139. delayPressOut: this.props.delayPressOut,
  140. minPressDuration: 0,
  141. pressRectOffset: this.props.pressRetentionOffset,
  142. onBlur: event => {
  143. if (Platform.isTV) {
  144. this._opacityInactive(250);
  145. }
  146. if (this.props.onBlur != null) {
  147. this.props.onBlur(event);
  148. }
  149. },
  150. onFocus: event => {
  151. if (Platform.isTV) {
  152. this._opacityActive(150);
  153. }
  154. if (this.props.onFocus != null) {
  155. this.props.onFocus(event);
  156. }
  157. },
  158. onLongPress: this.props.onLongPress,
  159. onPress: this.props.onPress,
  160. onPressIn: event => {
  161. this._opacityActive(
  162. event.dispatchConfig.registrationName === 'onResponderGrant'
  163. ? 0
  164. : 150,
  165. );
  166. if (this.props.onPressIn != null) {
  167. this.props.onPressIn(event);
  168. }
  169. },
  170. onPressOut: event => {
  171. this._opacityInactive(250);
  172. if (this.props.onPressOut != null) {
  173. this.props.onPressOut(event);
  174. }
  175. },
  176. };
  177. }
  178. /**
  179. * Animate the touchable to a new opacity.
  180. */
  181. _setOpacityTo(toValue: number, duration: number): void {
  182. Animated.timing(this.state.anim, {
  183. toValue,
  184. duration,
  185. easing: Easing.inOut(Easing.quad),
  186. useNativeDriver: true,
  187. }).start();
  188. }
  189. _opacityActive(duration: number): void {
  190. this._setOpacityTo(this.props.activeOpacity ?? 0.2, duration);
  191. }
  192. _opacityInactive(duration: number): void {
  193. this._setOpacityTo(this._getChildStyleOpacityWithDefault(), duration);
  194. }
  195. _getChildStyleOpacityWithDefault(): number {
  196. const opacity = flattenStyle(this.props.style)?.opacity;
  197. return typeof opacity === 'number' ? opacity : 1;
  198. }
  199. render(): React.Node {
  200. // BACKWARD-COMPATIBILITY: Focus and blur events were never supported before
  201. // adopting `Pressability`, so preserve that behavior.
  202. const {
  203. onBlur,
  204. onFocus,
  205. ...eventHandlersWithoutBlurAndFocus
  206. } = this.state.pressability.getEventHandlers();
  207. return (
  208. <Animated.View
  209. accessible={this.props.accessible !== false}
  210. accessibilityLabel={this.props.accessibilityLabel}
  211. accessibilityHint={this.props.accessibilityHint}
  212. accessibilityRole={this.props.accessibilityRole}
  213. accessibilityState={this.props.accessibilityState}
  214. accessibilityActions={this.props.accessibilityActions}
  215. onAccessibilityAction={this.props.onAccessibilityAction}
  216. accessibilityValue={this.props.accessibilityValue}
  217. importantForAccessibility={this.props.importantForAccessibility}
  218. accessibilityLiveRegion={this.props.accessibilityLiveRegion}
  219. accessibilityViewIsModal={this.props.accessibilityViewIsModal}
  220. accessibilityElementsHidden={this.props.accessibilityElementsHidden}
  221. style={[this.props.style, {opacity: this.state.anim}]}
  222. nativeID={this.props.nativeID}
  223. testID={this.props.testID}
  224. onLayout={this.props.onLayout}
  225. nextFocusDown={this.props.nextFocusDown}
  226. nextFocusForward={this.props.nextFocusForward}
  227. nextFocusLeft={this.props.nextFocusLeft}
  228. nextFocusRight={this.props.nextFocusRight}
  229. nextFocusUp={this.props.nextFocusUp}
  230. hasTVPreferredFocus={this.props.hasTVPreferredFocus}
  231. hitSlop={this.props.hitSlop}
  232. focusable={
  233. this.props.focusable !== false && this.props.onPress !== undefined
  234. }
  235. ref={this.props.hostRef}
  236. {...eventHandlersWithoutBlurAndFocus}>
  237. {this.props.children}
  238. {__DEV__ ? (
  239. <PressabilityDebugView color="cyan" hitSlop={this.props.hitSlop} />
  240. ) : null}
  241. </Animated.View>
  242. );
  243. }
  244. componentDidMount(): void {
  245. if (Platform.isTV) {
  246. this._tvTouchable = new TVTouchable(this, {
  247. getDisabled: () => this.props.disabled === true,
  248. onBlur: event => {
  249. if (this.props.onBlur != null) {
  250. this.props.onBlur(event);
  251. }
  252. },
  253. onFocus: event => {
  254. if (this.props.onFocus != null) {
  255. this.props.onFocus(event);
  256. }
  257. },
  258. onPress: event => {
  259. if (this.props.onPress != null) {
  260. this.props.onPress(event);
  261. }
  262. },
  263. });
  264. }
  265. }
  266. componentDidUpdate(prevProps: Props, prevState: State) {
  267. this.state.pressability.configure(this._createPressabilityConfig());
  268. if (this.props.disabled !== prevProps.disabled) {
  269. this._opacityInactive(250);
  270. }
  271. }
  272. componentWillUnmount(): void {
  273. if (Platform.isTV) {
  274. if (this._tvTouchable != null) {
  275. this._tvTouchable.destroy();
  276. }
  277. }
  278. this.state.pressability.reset();
  279. }
  280. }
  281. module.exports = (React.forwardRef((props, hostRef) => (
  282. <TouchableOpacity {...props} hostRef={hostRef} />
  283. )): React.ComponentType<$ReadOnly<$Diff<Props, {|hostRef: mixed|}>>>);