Skip to content

Commit 3d82f7e

Browse files
Luna Weifacebook-github-bot
authored andcommitted
Touchable
Summary: Changelog: [Internal] Add a flow type for Touchable export Reviewed By: NickGerleman Differential Revision: D38921769 fbshipit-source-id: ebf47250b7b73a185ce63dfef6bfdea75fcd4d93
1 parent 1d3e513 commit 3d82f7e

File tree

2 files changed

+261
-2
lines changed

2 files changed

+261
-2
lines changed
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
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+
};

Libraries/Components/Touchable/Touchable.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import Platform from '../../Utilities/Platform';
1414
import Position from './Position';
1515
import UIManager from '../../ReactNative/UIManager';
1616
import SoundManager from '../Sound/SoundManager';
17+
import type {TouchableType} from './Touchable.flow';
1718

1819
import {PressabilityDebugView} from '../../Pressability/PressabilityDebug';
1920

@@ -927,7 +928,7 @@ const TouchableMixin = {
927928
}
928929
},
929930

930-
withoutDefaultFocusAndBlur: ({}: $TEMPORARY$object<{||}>),
931+
withoutDefaultFocusAndBlur: ({}: {...}),
931932
};
932933

933934
/**
@@ -944,7 +945,7 @@ const {
944945
TouchableMixin.withoutDefaultFocusAndBlur =
945946
TouchableMixinWithoutDefaultFocusAndBlur;
946947

947-
const Touchable = {
948+
const Touchable: TouchableType = {
948949
Mixin: TouchableMixin,
949950
/**
950951
* Renders a debugging overlay to visualize touch target with hitSlop (might not work on Android).

0 commit comments

Comments
 (0)