|
| 1 | +/** |
| 2 | + * Copyright (c) Meta Platforms, Inc. and 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 |
| 8 | + * @format |
| 9 | + */ |
| 10 | + |
| 11 | +import * as React from 'react'; |
| 12 | + |
| 13 | +import type {ColorValue} from '../../StyleSheet/StyleSheet'; |
| 14 | +import type {EdgeInsetsProp} from '../../StyleSheet/EdgeInsetsPropType'; |
| 15 | +import type {PressEvent} from '../../Types/CoreEventTypes'; |
| 16 | + |
| 17 | +/** |
| 18 | + * `Touchable`: Taps done right. |
| 19 | + * |
| 20 | + * You hook your `ResponderEventPlugin` events into `Touchable`. `Touchable` |
| 21 | + * will measure time/geometry and tells you when to give feedback to the user. |
| 22 | + * |
| 23 | + * ====================== Touchable Tutorial =============================== |
| 24 | + * The `Touchable` mixin helps you handle the "press" interaction. It analyzes |
| 25 | + * the geometry of elements, and observes when another responder (scroll view |
| 26 | + * etc) has stolen the touch lock. It notifies your component when it should |
| 27 | + * give feedback to the user. (bouncing/highlighting/unhighlighting). |
| 28 | + * |
| 29 | + * - When a touch was activated (typically you highlight) |
| 30 | + * - When a touch was deactivated (typically you unhighlight) |
| 31 | + * - When a touch was "pressed" - a touch ended while still within the geometry |
| 32 | + * of the element, and no other element (like scroller) has "stolen" touch |
| 33 | + * lock ("responder") (Typically you bounce the element). |
| 34 | + * |
| 35 | + * A good tap interaction isn't as simple as you might think. There should be a |
| 36 | + * slight delay before showing a highlight when starting a touch. If a |
| 37 | + * subsequent touch move exceeds the boundary of the element, it should |
| 38 | + * unhighlight, but if that same touch is brought back within the boundary, it |
| 39 | + * should rehighlight again. A touch can move in and out of that boundary |
| 40 | + * several times, each time toggling highlighting, but a "press" is only |
| 41 | + * triggered if that touch ends while within the element's boundary and no |
| 42 | + * scroller (or anything else) has stolen the lock on touches. |
| 43 | + * |
| 44 | + * To create a new type of component that handles interaction using the |
| 45 | + * `Touchable` mixin, do the following: |
| 46 | + * |
| 47 | + * - Initialize the `Touchable` state. |
| 48 | + * |
| 49 | + * getInitialState: function() { |
| 50 | + * return merge(this.touchableGetInitialState(), yourComponentState); |
| 51 | + * } |
| 52 | + * |
| 53 | + * - Choose the rendered component who's touches should start the interactive |
| 54 | + * sequence. On that rendered node, forward all `Touchable` responder |
| 55 | + * handlers. You can choose any rendered node you like. Choose a node whose |
| 56 | + * hit target you'd like to instigate the interaction sequence: |
| 57 | + * |
| 58 | + * // In render function: |
| 59 | + * return ( |
| 60 | + * <View |
| 61 | + * onStartShouldSetResponder={this.touchableHandleStartShouldSetResponder} |
| 62 | + * onResponderTerminationRequest={this.touchableHandleResponderTerminationRequest} |
| 63 | + * onResponderGrant={this.touchableHandleResponderGrant} |
| 64 | + * onResponderMove={this.touchableHandleResponderMove} |
| 65 | + * onResponderRelease={this.touchableHandleResponderRelease} |
| 66 | + * onResponderTerminate={this.touchableHandleResponderTerminate}> |
| 67 | + * <View> |
| 68 | + * Even though the hit detection/interactions are triggered by the |
| 69 | + * wrapping (typically larger) node, we usually end up implementing |
| 70 | + * custom logic that highlights this inner one. |
| 71 | + * </View> |
| 72 | + * </View> |
| 73 | + * ); |
| 74 | + * |
| 75 | + * - You may set up your own handlers for each of these events, so long as you |
| 76 | + * also invoke the `touchable*` handlers inside of your custom handler. |
| 77 | + * |
| 78 | + * - Implement the handlers on your component class in order to provide |
| 79 | + * feedback to the user. See documentation for each of these class methods |
| 80 | + * that you should implement. |
| 81 | + * |
| 82 | + * touchableHandlePress: function() { |
| 83 | + * this.performBounceAnimation(); // or whatever you want to do. |
| 84 | + * }, |
| 85 | + * touchableHandleActivePressIn: function() { |
| 86 | + * this.beginHighlighting(...); // Whatever you like to convey activation |
| 87 | + * }, |
| 88 | + * touchableHandleActivePressOut: function() { |
| 89 | + * this.endHighlighting(...); // Whatever you like to convey deactivation |
| 90 | + * }, |
| 91 | + * |
| 92 | + * - There are more advanced methods you can implement (see documentation below): |
| 93 | + * touchableGetHighlightDelayMS: function() { |
| 94 | + * return 20; |
| 95 | + * } |
| 96 | + * // In practice, *always* use a predeclared constant (conserve memory). |
| 97 | + * touchableGetPressRectOffset: function() { |
| 98 | + * return {top: 20, left: 20, right: 20, bottom: 100}; |
| 99 | + * } |
| 100 | + */ |
| 101 | + |
| 102 | +// Default amount "active" region protrudes beyond box |
| 103 | + |
| 104 | +/** |
| 105 | + * By convention, methods prefixed with underscores are meant to be @private, |
| 106 | + * and not @protected. Mixers shouldn't access them - not even to provide them |
| 107 | + * as callback handlers. |
| 108 | + * |
| 109 | + * |
| 110 | + * ========== Geometry ========= |
| 111 | + * `Touchable` only assumes that there exists a `HitRect` node. The `PressRect` |
| 112 | + * is an abstract box that is extended beyond the `HitRect`. |
| 113 | + * |
| 114 | + * +--------------------------+ |
| 115 | + * | | - "Start" events in `HitRect` cause `HitRect` |
| 116 | + * | +--------------------+ | to become the responder. |
| 117 | + * | | +--------------+ | | - `HitRect` is typically expanded around |
| 118 | + * | | | | | | the `VisualRect`, but shifted downward. |
| 119 | + * | | | VisualRect | | | - After pressing down, after some delay, |
| 120 | + * | | | | | | and before letting up, the Visual React |
| 121 | + * | | +--------------+ | | will become "active". This makes it eligible |
| 122 | + * | | HitRect | | for being highlighted (so long as the |
| 123 | + * | +--------------------+ | press remains in the `PressRect`). |
| 124 | + * | PressRect o | |
| 125 | + * +----------------------|---+ |
| 126 | + * Out Region | |
| 127 | + * +-----+ This gap between the `HitRect` and |
| 128 | + * `PressRect` allows a touch to move far away |
| 129 | + * from the original hit rect, and remain |
| 130 | + * highlighted, and eligible for a "Press". |
| 131 | + * Customize this via |
| 132 | + * `touchableGetPressRectOffset()`. |
| 133 | + * |
| 134 | + * |
| 135 | + * |
| 136 | + * ======= State Machine ======= |
| 137 | + * |
| 138 | + * +-------------+ <---+ RESPONDER_RELEASE |
| 139 | + * |NOT_RESPONDER| |
| 140 | + * +-------------+ <---+ RESPONDER_TERMINATED |
| 141 | + * + |
| 142 | + * | RESPONDER_GRANT (HitRect) |
| 143 | + * v |
| 144 | + * +---------------------------+ DELAY +-------------------------+ T + DELAY +------------------------------+ |
| 145 | + * |RESPONDER_INACTIVE_PRESS_IN|+-------->|RESPONDER_ACTIVE_PRESS_IN| +------------> |RESPONDER_ACTIVE_LONG_PRESS_IN| |
| 146 | + * +---------------------------+ +-------------------------+ +------------------------------+ |
| 147 | + * + ^ + ^ + ^ |
| 148 | + * |LEAVE_ |ENTER_ |LEAVE_ |ENTER_ |LEAVE_ |ENTER_ |
| 149 | + * |PRESS_RECT |PRESS_RECT |PRESS_RECT |PRESS_RECT |PRESS_RECT |PRESS_RECT |
| 150 | + * | | | | | | |
| 151 | + * v + v + v + |
| 152 | + * +----------------------------+ DELAY +--------------------------+ +-------------------------------+ |
| 153 | + * |RESPONDER_INACTIVE_PRESS_OUT|+------->|RESPONDER_ACTIVE_PRESS_OUT| |RESPONDER_ACTIVE_LONG_PRESS_OUT| |
| 154 | + * +----------------------------+ +--------------------------+ +-------------------------------+ |
| 155 | + * |
| 156 | + * T + DELAY => LONG_PRESS_DELAY_MS + DELAY |
| 157 | + * |
| 158 | + * Not drawn are the side effects of each transition. The most important side |
| 159 | + * effect is the `touchableHandlePress` abstract method invocation that occurs |
| 160 | + * when a responder is released while in either of the "Press" states. |
| 161 | + * |
| 162 | + * The other important side effects are the highlight abstract method |
| 163 | + * invocations (internal callbacks) to be implemented by the mixer. |
| 164 | + * |
| 165 | + * |
| 166 | + * @lends Touchable.prototype |
| 167 | + */ |
| 168 | +interface TouchableMixinType { |
| 169 | + /** |
| 170 | + * Invoked when the item receives focus. Mixers might override this to |
| 171 | + * visually distinguish the `VisualRect` so that the user knows that it |
| 172 | + * currently has the focus. Most platforms only support a single element being |
| 173 | + * focused at a time, in which case there may have been a previously focused |
| 174 | + * element that was blurred just prior to this. This can be overridden when |
| 175 | + * using `Touchable.Mixin.withoutDefaultFocusAndBlur`. |
| 176 | + */ |
| 177 | + touchableHandleFocus: (e: Event) => void; |
| 178 | + |
| 179 | + /** |
| 180 | + * Invoked when the item loses focus. Mixers might override this to |
| 181 | + * visually distinguish the `VisualRect` so that the user knows that it |
| 182 | + * no longer has focus. Most platforms only support a single element being |
| 183 | + * focused at a time, in which case the focus may have moved to another. |
| 184 | + * This can be overridden when using |
| 185 | + * `Touchable.Mixin.withoutDefaultFocusAndBlur`. |
| 186 | + */ |
| 187 | + touchableHandleBlur: (e: Event) => void; |
| 188 | + |
| 189 | + componentDidMount: () => void; |
| 190 | + |
| 191 | + /** |
| 192 | + * Clear all timeouts on unmount |
| 193 | + */ |
| 194 | + componentWillUnmount: () => void; |
| 195 | + |
| 196 | + /** |
| 197 | + * It's prefer that mixins determine state in this way, having the class |
| 198 | + * explicitly mix the state in the one and only `getInitialState` method. |
| 199 | + * |
| 200 | + * @return {object} State object to be placed inside of |
| 201 | + * `this.state.touchable`. |
| 202 | + */ |
| 203 | + touchableGetInitialState: () => $TEMPORARY$object<{| |
| 204 | + touchable: $TEMPORARY$object<{|responderID: null, touchState: void|}>, |
| 205 | + |}>; |
| 206 | + |
| 207 | + // ==== Hooks to Gesture Responder system ==== |
| 208 | + /** |
| 209 | + * Must return true if embedded in a native platform scroll view. |
| 210 | + */ |
| 211 | + touchableHandleResponderTerminationRequest: () => any; |
| 212 | + |
| 213 | + /** |
| 214 | + * Must return true to start the process of `Touchable`. |
| 215 | + */ |
| 216 | + touchableHandleStartShouldSetResponder: () => any; |
| 217 | + |
| 218 | + /** |
| 219 | + * Return true to cancel press on long press. |
| 220 | + */ |
| 221 | + touchableLongPressCancelsPress: () => boolean; |
| 222 | + |
| 223 | + /** |
| 224 | + * Place as callback for a DOM element's `onResponderGrant` event. |
| 225 | + * @param {SyntheticEvent} e Synthetic event from event system. |
| 226 | + * |
| 227 | + */ |
| 228 | + touchableHandleResponderGrant: (e: PressEvent) => void; |
| 229 | + |
| 230 | + /** |
| 231 | + * Place as callback for a DOM element's `onResponderRelease` event. |
| 232 | + */ |
| 233 | + touchableHandleResponderRelease: (e: PressEvent) => void; |
| 234 | + |
| 235 | + /** |
| 236 | + * Place as callback for a DOM element's `onResponderTerminate` event. |
| 237 | + */ |
| 238 | + touchableHandleResponderTerminate: (e: PressEvent) => void; |
| 239 | + |
| 240 | + /** |
| 241 | + * Place as callback for a DOM element's `onResponderMove` event. |
| 242 | + */ |
| 243 | + touchableHandleResponderMove: (e: PressEvent) => void; |
| 244 | + |
| 245 | + withoutDefaultFocusAndBlur: {...}; |
| 246 | +} |
| 247 | + |
| 248 | +export type TouchableType = { |
| 249 | + Mixin: TouchableMixinType, |
| 250 | + /** |
| 251 | + * Renders a debugging overlay to visualize touch target with hitSlop (might not work on Android). |
| 252 | + */ |
| 253 | + renderDebugView: ({ |
| 254 | + color: ColorValue, |
| 255 | + hitSlop: EdgeInsetsProp, |
| 256 | + ... |
| 257 | + }) => null | React.Node, |
| 258 | +}; |
0 commit comments