|
|
1
|
+/**
|
|
|
2
|
+ * Copyright (c) 2015-present, Facebook, Inc.
|
|
|
3
|
+ * All rights reserved.
|
|
|
4
|
+ *
|
|
|
5
|
+ * This source code is licensed under the BSD-style license found in the
|
|
|
6
|
+ * LICENSE file in the root directory of this source tree. An additional grant
|
|
|
7
|
+ * of patent rights can be found in the PATENTS file in the same directory.
|
|
|
8
|
+ *
|
|
|
9
|
+ * @providesModule ScrollResponder
|
|
|
10
|
+ * @flow
|
|
|
11
|
+ */
|
|
|
12
|
+'use strict';
|
|
|
13
|
+
|
|
|
14
|
+const Dimensions = require('Dimensions');
|
|
|
15
|
+// modify by yoho 2018/6/5
|
|
|
16
|
+// const FrameRateLogger = require('FrameRateLogger');
|
|
|
17
|
+const Keyboard = require('Keyboard');
|
|
|
18
|
+const ReactNative = require('ReactNative');
|
|
|
19
|
+const Platform = require('Platform');
|
|
|
20
|
+// const ReactNative = require('react/lib/ReactNative');
|
|
|
21
|
+// <eof> modify by yoho
|
|
|
22
|
+const Subscribable = require('Subscribable');
|
|
|
23
|
+const TextInputState = require('TextInputState');
|
|
|
24
|
+const UIManager = require('UIManager');
|
|
|
25
|
+
|
|
|
26
|
+const invariant = require('fbjs/lib/invariant');
|
|
|
27
|
+const nullthrows = require('fbjs/lib/nullthrows');
|
|
|
28
|
+const performanceNow = require('fbjs/lib/performanceNow');
|
|
|
29
|
+const warning = require('fbjs/lib/warning');
|
|
|
30
|
+
|
|
|
31
|
+const { ScrollViewManager } = require('NativeModules');
|
|
|
32
|
+const { getInstanceFromNode } = require('ReactNativeComponentTree');
|
|
|
33
|
+
|
|
|
34
|
+/**
|
|
|
35
|
+ * Mixin that can be integrated in order to handle scrolling that plays well
|
|
|
36
|
+ * with `ResponderEventPlugin`. Integrate with your platform specific scroll
|
|
|
37
|
+ * views, or even your custom built (every-frame animating) scroll views so that
|
|
|
38
|
+ * all of these systems play well with the `ResponderEventPlugin`.
|
|
|
39
|
+ *
|
|
|
40
|
+ * iOS scroll event timing nuances:
|
|
|
41
|
+ * ===============================
|
|
|
42
|
+ *
|
|
|
43
|
+ *
|
|
|
44
|
+ * Scrolling without bouncing, if you touch down:
|
|
|
45
|
+ * -------------------------------
|
|
|
46
|
+ *
|
|
|
47
|
+ * 1. `onMomentumScrollBegin` (when animation begins after letting up)
|
|
|
48
|
+ * ... physical touch starts ...
|
|
|
49
|
+ * 2. `onTouchStartCapture` (when you press down to stop the scroll)
|
|
|
50
|
+ * 3. `onTouchStart` (same, but bubble phase)
|
|
|
51
|
+ * 4. `onResponderRelease` (when lifting up - you could pause forever before * lifting)
|
|
|
52
|
+ * 5. `onMomentumScrollEnd`
|
|
|
53
|
+ *
|
|
|
54
|
+ *
|
|
|
55
|
+ * Scrolling with bouncing, if you touch down:
|
|
|
56
|
+ * -------------------------------
|
|
|
57
|
+ *
|
|
|
58
|
+ * 1. `onMomentumScrollBegin` (when animation begins after letting up)
|
|
|
59
|
+ * ... bounce begins ...
|
|
|
60
|
+ * ... some time elapses ...
|
|
|
61
|
+ * ... physical touch during bounce ...
|
|
|
62
|
+ * 2. `onMomentumScrollEnd` (Makes no sense why this occurs first during bounce)
|
|
|
63
|
+ * 3. `onTouchStartCapture` (immediately after `onMomentumScrollEnd`)
|
|
|
64
|
+ * 4. `onTouchStart` (same, but bubble phase)
|
|
|
65
|
+ * 5. `onTouchEnd` (You could hold the touch start for a long time)
|
|
|
66
|
+ * 6. `onMomentumScrollBegin` (When releasing the view starts bouncing back)
|
|
|
67
|
+ *
|
|
|
68
|
+ * So when we receive an `onTouchStart`, how can we tell if we are touching
|
|
|
69
|
+ * *during* an animation (which then causes the animation to stop)? The only way
|
|
|
70
|
+ * to tell is if the `touchStart` occurred immediately after the
|
|
|
71
|
+ * `onMomentumScrollEnd`.
|
|
|
72
|
+ *
|
|
|
73
|
+ * This is abstracted out for you, so you can just call this.scrollResponderIsAnimating() if
|
|
|
74
|
+ * necessary
|
|
|
75
|
+ *
|
|
|
76
|
+ * `ScrollResponder` also includes logic for blurring a currently focused input
|
|
|
77
|
+ * if one is focused while scrolling. The `ScrollResponder` is a natural place
|
|
|
78
|
+ * to put this logic since it can support not dismissing the keyboard while
|
|
|
79
|
+ * scrolling, unless a recognized "tap"-like gesture has occurred.
|
|
|
80
|
+ *
|
|
|
81
|
+ * The public lifecycle API includes events for keyboard interaction, responder
|
|
|
82
|
+ * interaction, and scrolling (among others). The keyboard callbacks
|
|
|
83
|
+ * `onKeyboardWill/Did/*` are *global* events, but are invoked on scroll
|
|
|
84
|
+ * responder's props so that you can guarantee that the scroll responder's
|
|
|
85
|
+ * internal state has been updated accordingly (and deterministically) by
|
|
|
86
|
+ * the time the props callbacks are invoke. Otherwise, you would always wonder
|
|
|
87
|
+ * if the scroll responder is currently in a state where it recognizes new
|
|
|
88
|
+ * keyboard positions etc. If coordinating scrolling with keyboard movement,
|
|
|
89
|
+ * *always* use these hooks instead of listening to your own global keyboard
|
|
|
90
|
+ * events.
|
|
|
91
|
+ *
|
|
|
92
|
+ * Public keyboard lifecycle API: (props callbacks)
|
|
|
93
|
+ *
|
|
|
94
|
+ * Standard Keyboard Appearance Sequence:
|
|
|
95
|
+ *
|
|
|
96
|
+ * this.props.onKeyboardWillShow
|
|
|
97
|
+ * this.props.onKeyboardDidShow
|
|
|
98
|
+ *
|
|
|
99
|
+ * `onScrollResponderKeyboardDismissed` will be invoked if an appropriate
|
|
|
100
|
+ * tap inside the scroll responder's scrollable region was responsible
|
|
|
101
|
+ * for the dismissal of the keyboard. There are other reasons why the
|
|
|
102
|
+ * keyboard could be dismissed.
|
|
|
103
|
+ *
|
|
|
104
|
+ * this.props.onScrollResponderKeyboardDismissed
|
|
|
105
|
+ *
|
|
|
106
|
+ * Standard Keyboard Hide Sequence:
|
|
|
107
|
+ *
|
|
|
108
|
+ * this.props.onKeyboardWillHide
|
|
|
109
|
+ * this.props.onKeyboardDidHide
|
|
|
110
|
+ */
|
|
|
111
|
+
|
|
|
112
|
+const IS_ANIMATING_TOUCH_START_THRESHOLD_MS = 16;
|
|
|
113
|
+
|
|
|
114
|
+type State = {
|
|
|
115
|
+ isTouching: boolean,
|
|
|
116
|
+ lastMomentumScrollBeginTime: number,
|
|
|
117
|
+ lastMomentumScrollEndTime: number,
|
|
|
118
|
+ observedScrollSinceBecomingResponder: boolean,
|
|
|
119
|
+ becameResponderWhileAnimating: boolean,
|
|
|
120
|
+};
|
|
|
121
|
+type Event = Object;
|
|
|
122
|
+
|
|
|
123
|
+// modify by yoho 2018/6/5
|
|
|
124
|
+// function isTagInstanceOfTextInput(tag) {
|
|
|
125
|
+// const instance = getInstanceFromNode(tag);
|
|
|
126
|
+// return instance && instance.viewConfig && (
|
|
|
127
|
+// instance.viewConfig.uiViewClassName === 'AndroidTextInput' ||
|
|
|
128
|
+// instance.viewConfig.uiViewClassName === 'RCTMultilineTextInputView' ||
|
|
|
129
|
+// instance.viewConfig.uiViewClassName === 'RCTSinglelineTextInputView'
|
|
|
130
|
+// );
|
|
|
131
|
+// }
|
|
|
132
|
+// <eof> modify by yoho
|
|
|
133
|
+
|
|
|
134
|
+const ScrollResponderMixin = {
|
|
|
135
|
+ mixins: [Subscribable.Mixin],
|
|
|
136
|
+ scrollResponderMixinGetInitialState: function(): State {
|
|
|
137
|
+ return {
|
|
|
138
|
+ isTouching: false,
|
|
|
139
|
+ lastMomentumScrollBeginTime: 0,
|
|
|
140
|
+ lastMomentumScrollEndTime: 0,
|
|
|
141
|
+
|
|
|
142
|
+ // Reset to false every time becomes responder. This is used to:
|
|
|
143
|
+ // - Determine if the scroll view has been scrolled and therefore should
|
|
|
144
|
+ // refuse to give up its responder lock.
|
|
|
145
|
+ // - Determine if releasing should dismiss the keyboard when we are in
|
|
|
146
|
+ // tap-to-dismiss mode (this.props.keyboardShouldPersistTaps !== 'always').
|
|
|
147
|
+ observedScrollSinceBecomingResponder: false,
|
|
|
148
|
+ becameResponderWhileAnimating: false,
|
|
|
149
|
+ };
|
|
|
150
|
+ },
|
|
|
151
|
+
|
|
|
152
|
+ /**
|
|
|
153
|
+ * Invoke this from an `onScroll` event.
|
|
|
154
|
+ */
|
|
|
155
|
+ scrollResponderHandleScrollShouldSetResponder: function(): boolean {
|
|
|
156
|
+ return this.state.isTouching;
|
|
|
157
|
+ },
|
|
|
158
|
+
|
|
|
159
|
+ /**
|
|
|
160
|
+ * Merely touch starting is not sufficient for a scroll view to become the
|
|
|
161
|
+ * responder. Being the "responder" means that the very next touch move/end
|
|
|
162
|
+ * event will result in an action/movement.
|
|
|
163
|
+ *
|
|
|
164
|
+ * Invoke this from an `onStartShouldSetResponder` event.
|
|
|
165
|
+ *
|
|
|
166
|
+ * `onStartShouldSetResponder` is used when the next move/end will trigger
|
|
|
167
|
+ * some UI movement/action, but when you want to yield priority to views
|
|
|
168
|
+ * nested inside of the view.
|
|
|
169
|
+ *
|
|
|
170
|
+ * There may be some cases where scroll views actually should return `true`
|
|
|
171
|
+ * from `onStartShouldSetResponder`: Any time we are detecting a standard tap
|
|
|
172
|
+ * that gives priority to nested views.
|
|
|
173
|
+ *
|
|
|
174
|
+ * - If a single tap on the scroll view triggers an action such as
|
|
|
175
|
+ * recentering a map style view yet wants to give priority to interaction
|
|
|
176
|
+ * views inside (such as dropped pins or labels), then we would return true
|
|
|
177
|
+ * from this method when there is a single touch.
|
|
|
178
|
+ *
|
|
|
179
|
+ * - Similar to the previous case, if a two finger "tap" should trigger a
|
|
|
180
|
+ * zoom, we would check the `touches` count, and if `>= 2`, we would return
|
|
|
181
|
+ * true.
|
|
|
182
|
+ *
|
|
|
183
|
+ */
|
|
|
184
|
+ scrollResponderHandleStartShouldSetResponder: function(): boolean {
|
|
|
185
|
+ // modify by yoho 2018/6/5
|
|
|
186
|
+ // const currentlyFocusedTextInput = TextInputState.currentlyFocusedField();
|
|
|
187
|
+ //
|
|
|
188
|
+ // if (this.props.keyboardShouldPersistTaps === 'handled' &&
|
|
|
189
|
+ // currentlyFocusedTextInput != null &&
|
|
|
190
|
+ // e.target !== currentlyFocusedTextInput) {
|
|
|
191
|
+ // return true;
|
|
|
192
|
+ // }
|
|
|
193
|
+ // <eof> modify by yoho
|
|
|
194
|
+ return false;
|
|
|
195
|
+ },
|
|
|
196
|
+
|
|
|
197
|
+ /**
|
|
|
198
|
+ * There are times when the scroll view wants to become the responder
|
|
|
199
|
+ * (meaning respond to the next immediate `touchStart/touchEnd`), in a way
|
|
|
200
|
+ * that *doesn't* give priority to nested views (hence the capture phase):
|
|
|
201
|
+ *
|
|
|
202
|
+ * - Currently animating.
|
|
|
203
|
+ * - Tapping anywhere that is not a text input, while the keyboard is
|
|
|
204
|
+ * up (which should dismiss the keyboard).
|
|
|
205
|
+ *
|
|
|
206
|
+ * Invoke this from an `onStartShouldSetResponderCapture` event.
|
|
|
207
|
+ */
|
|
|
208
|
+ scrollResponderHandleStartShouldSetResponderCapture: function(e: Event): boolean {
|
|
|
209
|
+ // First see if we want to eat taps while the keyboard is up
|
|
|
210
|
+ const currentlyFocusedTextInput = TextInputState.currentlyFocusedField();
|
|
|
211
|
+ // modify by yoho 2018/6/5
|
|
|
212
|
+ // const {keyboardShouldPersistTaps} = this.props;
|
|
|
213
|
+ // const keyboardNeverPersistTaps = !keyboardShouldPersistTaps ||
|
|
|
214
|
+ // keyboardShouldPersistTaps === 'never';
|
|
|
215
|
+ // if (keyboardNeverPersistTaps &&
|
|
|
216
|
+ // currentlyFocusedTextInput != null &&
|
|
|
217
|
+ // !isTagInstanceOfTextInput(e.target)) {
|
|
|
218
|
+ // return true;
|
|
|
219
|
+ // }
|
|
|
220
|
+ if (!this.props.keyboardShouldPersistTaps &&
|
|
|
221
|
+ currentlyFocusedTextInput != null &&
|
|
|
222
|
+ e.target !== currentlyFocusedTextInput) {
|
|
|
223
|
+ return true;
|
|
|
224
|
+ }
|
|
|
225
|
+ // <eof> modify by yoho
|
|
|
226
|
+ return this.scrollResponderIsAnimating();
|
|
|
227
|
+ },
|
|
|
228
|
+
|
|
|
229
|
+ /**
|
|
|
230
|
+ * Invoke this from an `onResponderReject` event.
|
|
|
231
|
+ *
|
|
|
232
|
+ * Some other element is not yielding its role as responder. Normally, we'd
|
|
|
233
|
+ * just disable the `UIScrollView`, but a touch has already began on it, the
|
|
|
234
|
+ * `UIScrollView` will not accept being disabled after that. The easiest
|
|
|
235
|
+ * solution for now is to accept the limitation of disallowing this
|
|
|
236
|
+ * altogether. To improve this, find a way to disable the `UIScrollView` after
|
|
|
237
|
+ * a touch has already started.
|
|
|
238
|
+ */
|
|
|
239
|
+ scrollResponderHandleResponderReject: function() {
|
|
|
240
|
+ },
|
|
|
241
|
+
|
|
|
242
|
+ /**
|
|
|
243
|
+ * We will allow the scroll view to give up its lock iff it acquired the lock
|
|
|
244
|
+ * during an animation. This is a very useful default that happens to satisfy
|
|
|
245
|
+ * many common user experiences.
|
|
|
246
|
+ *
|
|
|
247
|
+ * - Stop a scroll on the left edge, then turn that into an outer view's
|
|
|
248
|
+ * backswipe.
|
|
|
249
|
+ * - Stop a scroll mid-bounce at the top, continue pulling to have the outer
|
|
|
250
|
+ * view dismiss.
|
|
|
251
|
+ * - However, without catching the scroll view mid-bounce (while it is
|
|
|
252
|
+ * motionless), if you drag far enough for the scroll view to become
|
|
|
253
|
+ * responder (and therefore drag the scroll view a bit), any backswipe
|
|
|
254
|
+ * navigation of a swipe gesture higher in the view hierarchy, should be
|
|
|
255
|
+ * rejected.
|
|
|
256
|
+ */
|
|
|
257
|
+ scrollResponderHandleTerminationRequest: function(): boolean {
|
|
|
258
|
+ return !this.state.observedScrollSinceBecomingResponder;
|
|
|
259
|
+ },
|
|
|
260
|
+
|
|
|
261
|
+ /**
|
|
|
262
|
+ * Invoke this from an `onTouchEnd` event.
|
|
|
263
|
+ *
|
|
|
264
|
+ * @param {SyntheticEvent} e Event.
|
|
|
265
|
+ */
|
|
|
266
|
+ scrollResponderHandleTouchEnd: function(e: Event) {
|
|
|
267
|
+ const nativeEvent = e.nativeEvent;
|
|
|
268
|
+ this.state.isTouching = nativeEvent.touches.length !== 0;
|
|
|
269
|
+ this.props.onTouchEnd && this.props.onTouchEnd(e);
|
|
|
270
|
+ },
|
|
|
271
|
+
|
|
|
272
|
+ /**
|
|
|
273
|
+ * Invoke this from an `onTouchCancel` event.
|
|
|
274
|
+ *
|
|
|
275
|
+ * @param {SyntheticEvent} e Event.
|
|
|
276
|
+ */
|
|
|
277
|
+ scrollResponderHandleTouchCancel: function(e: Event) {
|
|
|
278
|
+ this.state.isTouching = false;
|
|
|
279
|
+ this.props.onTouchCancel && this.props.onTouchCancel(e);
|
|
|
280
|
+ },
|
|
|
281
|
+
|
|
|
282
|
+ /**
|
|
|
283
|
+ * Invoke this from an `onResponderRelease` event.
|
|
|
284
|
+ */
|
|
|
285
|
+ scrollResponderHandleResponderRelease: function(e: Event) {
|
|
|
286
|
+ this.props.onResponderRelease && this.props.onResponderRelease(e);
|
|
|
287
|
+
|
|
|
288
|
+ // By default scroll views will unfocus a textField
|
|
|
289
|
+ // if another touch occurs outside of it
|
|
|
290
|
+ const currentlyFocusedTextInput = TextInputState.currentlyFocusedField();
|
|
|
291
|
+ // modify by yoho 2018/6/5
|
|
|
292
|
+ // if (this.props.keyboardShouldPersistTaps !== true &&
|
|
|
293
|
+ // this.props.keyboardShouldPersistTaps !== 'always' &&
|
|
|
294
|
+ // currentlyFocusedTextInput != null &&
|
|
|
295
|
+ // e.target !== currentlyFocusedTextInput &&
|
|
|
296
|
+ // !this.state.observedScrollSinceBecomingResponder &&
|
|
|
297
|
+ // !this.state.becameResponderWhileAnimating) {
|
|
|
298
|
+ // this.props.onScrollResponderKeyboardDismissed &&
|
|
|
299
|
+ // this.props.onScrollResponderKeyboardDismissed(e);
|
|
|
300
|
+ // TextInputState.blurTextInput(currentlyFocusedTextInput);
|
|
|
301
|
+ // }
|
|
|
302
|
+ if (!this.props.keyboardShouldPersistTaps &&
|
|
|
303
|
+ currentlyFocusedTextInput != null &&
|
|
|
304
|
+ e.target !== currentlyFocusedTextInput &&
|
|
|
305
|
+ !this.state.observedScrollSinceBecomingResponder &&
|
|
|
306
|
+ !this.state.becameResponderWhileAnimating) {
|
|
|
307
|
+ this.props.onScrollResponderKeyboardDismissed &&
|
|
|
308
|
+ this.props.onScrollResponderKeyboardDismissed(e);
|
|
|
309
|
+ TextInputState.blurTextInput(currentlyFocusedTextInput);
|
|
|
310
|
+ }
|
|
|
311
|
+ // <eof> modify by yoho
|
|
|
312
|
+ },
|
|
|
313
|
+
|
|
|
314
|
+ scrollResponderHandleScroll: function(e: Event) {
|
|
|
315
|
+ this.state.observedScrollSinceBecomingResponder = true;
|
|
|
316
|
+ this.props.onScroll && this.props.onScroll(e);
|
|
|
317
|
+ },
|
|
|
318
|
+
|
|
|
319
|
+ /**
|
|
|
320
|
+ * Invoke this from an `onResponderGrant` event.
|
|
|
321
|
+ */
|
|
|
322
|
+ scrollResponderHandleResponderGrant: function(e: Event) {
|
|
|
323
|
+ this.state.observedScrollSinceBecomingResponder = false;
|
|
|
324
|
+ this.props.onResponderGrant && this.props.onResponderGrant(e);
|
|
|
325
|
+ this.state.becameResponderWhileAnimating = this.scrollResponderIsAnimating();
|
|
|
326
|
+ },
|
|
|
327
|
+
|
|
|
328
|
+ /**
|
|
|
329
|
+ * Unfortunately, `onScrollBeginDrag` also fires when *stopping* the scroll
|
|
|
330
|
+ * animation, and there's not an easy way to distinguish a drag vs. stopping
|
|
|
331
|
+ * momentum.
|
|
|
332
|
+ *
|
|
|
333
|
+ * Invoke this from an `onScrollBeginDrag` event.
|
|
|
334
|
+ */
|
|
|
335
|
+ scrollResponderHandleScrollBeginDrag: function(e: Event) {
|
|
|
336
|
+ // FrameRateLogger.beginScroll(); // TODO: track all scrolls after implementing onScrollEndAnimation // modify by yoho 2018/6/5
|
|
|
337
|
+ this.props.onScrollBeginDrag && this.props.onScrollBeginDrag(e);
|
|
|
338
|
+ },
|
|
|
339
|
+
|
|
|
340
|
+ /**
|
|
|
341
|
+ * Invoke this from an `onScrollEndDrag` event.
|
|
|
342
|
+ */
|
|
|
343
|
+ scrollResponderHandleScrollEndDrag: function(e: Event) {
|
|
|
344
|
+ // modify by yoho 2018/6/5
|
|
|
345
|
+ // const {velocity} = e.nativeEvent;
|
|
|
346
|
+ // // - If we are animating, then this is a "drag" that is stopping the scrollview and momentum end
|
|
|
347
|
+ // // will fire.
|
|
|
348
|
+ // // - If velocity is non-zero, then the interaction will stop when momentum scroll ends or
|
|
|
349
|
+ // // another drag starts and ends.
|
|
|
350
|
+ // // - If we don't get velocity, better to stop the interaction twice than not stop it.
|
|
|
351
|
+ // if (!this.scrollResponderIsAnimating() &&
|
|
|
352
|
+ // (!velocity || velocity.x === 0 && velocity.y === 0)) {
|
|
|
353
|
+ // FrameRateLogger.endScroll();
|
|
|
354
|
+ // }
|
|
|
355
|
+ // <eof> modify by yoho
|
|
|
356
|
+ this.props.onScrollEndDrag && this.props.onScrollEndDrag(e);
|
|
|
357
|
+ },
|
|
|
358
|
+
|
|
|
359
|
+ /**
|
|
|
360
|
+ * Invoke this from an `onMomentumScrollBegin` event.
|
|
|
361
|
+ */
|
|
|
362
|
+ scrollResponderHandleMomentumScrollBegin: function(e: Event) {
|
|
|
363
|
+ this.state.lastMomentumScrollBeginTime = performanceNow();
|
|
|
364
|
+ this.props.onMomentumScrollBegin && this.props.onMomentumScrollBegin(e);
|
|
|
365
|
+ },
|
|
|
366
|
+
|
|
|
367
|
+ /**
|
|
|
368
|
+ * Invoke this from an `onMomentumScrollEnd` event.
|
|
|
369
|
+ */
|
|
|
370
|
+ scrollResponderHandleMomentumScrollEnd: function(e: Event) {
|
|
|
371
|
+ // FrameRateLogger.endScroll(); // modify by yoho 2018/6/5
|
|
|
372
|
+ this.state.lastMomentumScrollEndTime = performanceNow();
|
|
|
373
|
+ this.props.onMomentumScrollEnd && this.props.onMomentumScrollEnd(e);
|
|
|
374
|
+ },
|
|
|
375
|
+
|
|
|
376
|
+ /**
|
|
|
377
|
+ * Invoke this from an `onTouchStart` event.
|
|
|
378
|
+ *
|
|
|
379
|
+ * Since we know that the `SimpleEventPlugin` occurs later in the plugin
|
|
|
380
|
+ * order, after `ResponderEventPlugin`, we can detect that we were *not*
|
|
|
381
|
+ * permitted to be the responder (presumably because a contained view became
|
|
|
382
|
+ * responder). The `onResponderReject` won't fire in that case - it only
|
|
|
383
|
+ * fires when a *current* responder rejects our request.
|
|
|
384
|
+ *
|
|
|
385
|
+ * @param {SyntheticEvent} e Touch Start event.
|
|
|
386
|
+ */
|
|
|
387
|
+ scrollResponderHandleTouchStart: function(e: Event) {
|
|
|
388
|
+ this.state.isTouching = true;
|
|
|
389
|
+ this.props.onTouchStart && this.props.onTouchStart(e);
|
|
|
390
|
+ },
|
|
|
391
|
+
|
|
|
392
|
+ /**
|
|
|
393
|
+ * Invoke this from an `onTouchMove` event.
|
|
|
394
|
+ *
|
|
|
395
|
+ * Since we know that the `SimpleEventPlugin` occurs later in the plugin
|
|
|
396
|
+ * order, after `ResponderEventPlugin`, we can detect that we were *not*
|
|
|
397
|
+ * permitted to be the responder (presumably because a contained view became
|
|
|
398
|
+ * responder). The `onResponderReject` won't fire in that case - it only
|
|
|
399
|
+ * fires when a *current* responder rejects our request.
|
|
|
400
|
+ *
|
|
|
401
|
+ * @param {SyntheticEvent} e Touch Start event.
|
|
|
402
|
+ */
|
|
|
403
|
+ scrollResponderHandleTouchMove: function(e: Event) {
|
|
|
404
|
+ this.props.onTouchMove && this.props.onTouchMove(e);
|
|
|
405
|
+ },
|
|
|
406
|
+
|
|
|
407
|
+ /**
|
|
|
408
|
+ * A helper function for this class that lets us quickly determine if the
|
|
|
409
|
+ * view is currently animating. This is particularly useful to know when
|
|
|
410
|
+ * a touch has just started or ended.
|
|
|
411
|
+ */
|
|
|
412
|
+ scrollResponderIsAnimating: function(): boolean {
|
|
|
413
|
+ const now = performanceNow();
|
|
|
414
|
+ const timeSinceLastMomentumScrollEnd = now - this.state.lastMomentumScrollEndTime;
|
|
|
415
|
+ const isAnimating = timeSinceLastMomentumScrollEnd < IS_ANIMATING_TOUCH_START_THRESHOLD_MS ||
|
|
|
416
|
+ this.state.lastMomentumScrollEndTime < this.state.lastMomentumScrollBeginTime;
|
|
|
417
|
+ return isAnimating;
|
|
|
418
|
+ },
|
|
|
419
|
+
|
|
|
420
|
+ /**
|
|
|
421
|
+ * Returns the node that represents native view that can be scrolled.
|
|
|
422
|
+ * Components can pass what node to use by defining a `getScrollableNode`
|
|
|
423
|
+ * function otherwise `this` is used.
|
|
|
424
|
+ */
|
|
|
425
|
+ scrollResponderGetScrollableNode: function(): any {
|
|
|
426
|
+ return this.getScrollableNode ?
|
|
|
427
|
+ this.getScrollableNode() :
|
|
|
428
|
+ ReactNative.findNodeHandle(this);
|
|
|
429
|
+ },
|
|
|
430
|
+
|
|
|
431
|
+ /**
|
|
|
432
|
+ * A helper function to scroll to a specific point in the ScrollView.
|
|
|
433
|
+ * This is currently used to help focus child TextViews, but can also
|
|
|
434
|
+ * be used to quickly scroll to any element we want to focus. Syntax:
|
|
|
435
|
+ *
|
|
|
436
|
+ * `scrollResponderScrollTo(options: {x: number = 0; y: number = 0; animated: boolean = true})`
|
|
|
437
|
+ *
|
|
|
438
|
+ * Note: The weird argument signature is due to the fact that, for historical reasons,
|
|
|
439
|
+ * the function also accepts separate arguments as as alternative to the options object.
|
|
|
440
|
+ * This is deprecated due to ambiguity (y before x), and SHOULD NOT BE USED.
|
|
|
441
|
+ */
|
|
|
442
|
+ scrollResponderScrollTo: function(
|
|
|
443
|
+ x?: number | { x?: number, y?: number, animated?: boolean },
|
|
|
444
|
+ y?: number,
|
|
|
445
|
+ animated?: boolean
|
|
|
446
|
+ ) {
|
|
|
447
|
+ if (typeof x === 'number') {
|
|
|
448
|
+ console.warn('`scrollResponderScrollTo(x, y, animated)` is deprecated. Use `scrollResponderScrollTo({x: 5, y: 5, animated: true})` instead.');
|
|
|
449
|
+ } else {
|
|
|
450
|
+ ({x, y, animated} = x || {});
|
|
|
451
|
+ }
|
|
|
452
|
+ UIManager.dispatchViewManagerCommand(
|
|
|
453
|
+ nullthrows(this.scrollResponderGetScrollableNode()),
|
|
|
454
|
+ UIManager.RCTScrollView.Commands.scrollTo,
|
|
|
455
|
+ [x || 0, y || 0, animated !== false],
|
|
|
456
|
+ );
|
|
|
457
|
+ },
|
|
|
458
|
+
|
|
|
459
|
+ // add by yoho 2018/6/5
|
|
|
460
|
+ /****
|
|
|
461
|
+ *设置刷新事件
|
|
|
462
|
+ *
|
|
|
463
|
+ *
|
|
|
464
|
+ **/
|
|
|
465
|
+ scrollResponderStartPullToRefresh: function() {
|
|
|
466
|
+ UIManager.dispatchViewManagerCommand(
|
|
|
467
|
+ this.scrollResponderGetScrollableNode(),
|
|
|
468
|
+ UIManager.RCTScrollView.Commands.startPullToRefresh,
|
|
|
469
|
+ [],
|
|
|
470
|
+ );
|
|
|
471
|
+ },
|
|
|
472
|
+
|
|
|
473
|
+ /****
|
|
|
474
|
+ *停止刷新事件
|
|
|
475
|
+ *
|
|
|
476
|
+ *
|
|
|
477
|
+ **/
|
|
|
478
|
+ scrollResponderStopPullToRefresh: function() {
|
|
|
479
|
+ UIManager.dispatchViewManagerCommand(
|
|
|
480
|
+ this.scrollResponderGetScrollableNode(),
|
|
|
481
|
+ UIManager.RCTScrollView.Commands.stopPullToRefresh,
|
|
|
482
|
+ [],
|
|
|
483
|
+ );
|
|
|
484
|
+ },
|
|
|
485
|
+ // <eof> add by yoho
|
|
|
486
|
+
|
|
|
487
|
+ /**
|
|
|
488
|
+ * Scrolls to the end of the ScrollView, either immediately or with a smooth
|
|
|
489
|
+ * animation.
|
|
|
490
|
+ *
|
|
|
491
|
+ * Example:
|
|
|
492
|
+ *
|
|
|
493
|
+ * `scrollResponderScrollToEnd({animated: true})`
|
|
|
494
|
+ */
|
|
|
495
|
+ scrollResponderScrollToEnd: function(
|
|
|
496
|
+ options?: { animated?: boolean },
|
|
|
497
|
+ ) {
|
|
|
498
|
+ // Default to true
|
|
|
499
|
+ const animated = (options && options.animated) !== false;
|
|
|
500
|
+ UIManager.dispatchViewManagerCommand(
|
|
|
501
|
+ this.scrollResponderGetScrollableNode(),
|
|
|
502
|
+ UIManager.RCTScrollView.Commands.scrollToEnd,
|
|
|
503
|
+ [animated],
|
|
|
504
|
+ );
|
|
|
505
|
+ },
|
|
|
506
|
+
|
|
|
507
|
+ /**
|
|
|
508
|
+ * Deprecated, do not use.
|
|
|
509
|
+ */
|
|
|
510
|
+ scrollResponderScrollWithoutAnimationTo: function(offsetX: number, offsetY: number) {
|
|
|
511
|
+ console.warn('`scrollResponderScrollWithoutAnimationTo` is deprecated. Use `scrollResponderScrollTo` instead');
|
|
|
512
|
+ this.scrollResponderScrollTo({x: offsetX, y: offsetY, animated: false});
|
|
|
513
|
+ },
|
|
|
514
|
+
|
|
|
515
|
+ /**
|
|
|
516
|
+ * A helper function to zoom to a specific rect in the scrollview. The argument has the shape
|
|
|
517
|
+ * {x: number; y: number; width: number; height: number; animated: boolean = true}
|
|
|
518
|
+ *
|
|
|
519
|
+ * @platform ios
|
|
|
520
|
+ */
|
|
|
521
|
+ scrollResponderZoomTo: function(
|
|
|
522
|
+ rect: {| x: number, y: number, width: number, height: number, animated?: boolean |},
|
|
|
523
|
+ animated?: boolean // deprecated, put this inside the rect argument instead
|
|
|
524
|
+ ) {
|
|
|
525
|
+ // modify by yoho 2018/6/5
|
|
|
526
|
+ // invariant(ScrollViewManager && ScrollViewManager.zoomToRect, 'zoomToRect is not implemented');
|
|
|
527
|
+ // if ('animated' in rect) {
|
|
|
528
|
+ // animated = rect.animated;
|
|
|
529
|
+ // delete rect.animated;
|
|
|
530
|
+ // } else if (typeof animated !== 'undefined') {
|
|
|
531
|
+ // console.warn('`scrollResponderZoomTo` `animated` argument is deprecated. Use `options.animated` instead');
|
|
|
532
|
+ // }
|
|
|
533
|
+ // ScrollViewManager.zoomToRect(this.scrollResponderGetScrollableNode(), rect, animated !== false);
|
|
|
534
|
+ if (Platform.OS === 'android') {
|
|
|
535
|
+ invariant('zoomToRect is not implemented');
|
|
|
536
|
+ } else {
|
|
|
537
|
+ if ('animated' in rect) {
|
|
|
538
|
+ var { animated, ...rect } = rect;
|
|
|
539
|
+ } else if (typeof animated !== 'undefined') {
|
|
|
540
|
+ console.warn('`scrollResponderZoomTo` `animated` argument is deprecated. Use `options.animated` instead');
|
|
|
541
|
+ }
|
|
|
542
|
+ ScrollViewManager.zoomToRect(this.scrollResponderGetScrollableNode(), rect, animated !== false);
|
|
|
543
|
+ }
|
|
|
544
|
+ // <eof> modify by yoho
|
|
|
545
|
+ },
|
|
|
546
|
+
|
|
|
547
|
+ /**
|
|
|
548
|
+ * Displays the scroll indicators momentarily.
|
|
|
549
|
+ */
|
|
|
550
|
+ scrollResponderFlashScrollIndicators: function() {
|
|
|
551
|
+ UIManager.dispatchViewManagerCommand(
|
|
|
552
|
+ this.scrollResponderGetScrollableNode(),
|
|
|
553
|
+ UIManager.RCTScrollView.Commands.flashScrollIndicators,
|
|
|
554
|
+ []
|
|
|
555
|
+ );
|
|
|
556
|
+ },
|
|
|
557
|
+
|
|
|
558
|
+ /**
|
|
|
559
|
+ * This method should be used as the callback to onFocus in a TextInputs'
|
|
|
560
|
+ * parent view. Note that any module using this mixin needs to return
|
|
|
561
|
+ * the parent view's ref in getScrollViewRef() in order to use this method.
|
|
|
562
|
+ * @param {any} nodeHandle The TextInput node handle
|
|
|
563
|
+ * @param {number} additionalOffset The scroll view's bottom "contentInset".
|
|
|
564
|
+ * Default is 0.
|
|
|
565
|
+ * @param {bool} preventNegativeScrolling Whether to allow pulling the content
|
|
|
566
|
+ * down to make it meet the keyboard's top. Default is false.
|
|
|
567
|
+ */
|
|
|
568
|
+ scrollResponderScrollNativeHandleToKeyboard: function(nodeHandle: any, additionalOffset?: number, preventNegativeScrollOffset?: bool) {
|
|
|
569
|
+ this.additionalScrollOffset = additionalOffset || 0;
|
|
|
570
|
+ this.preventNegativeScrollOffset = !!preventNegativeScrollOffset;
|
|
|
571
|
+ UIManager.measureLayout(
|
|
|
572
|
+ nodeHandle,
|
|
|
573
|
+ ReactNative.findNodeHandle(this.getInnerViewNode()),
|
|
|
574
|
+ this.scrollResponderTextInputFocusError,
|
|
|
575
|
+ this.scrollResponderInputMeasureAndScrollToKeyboard
|
|
|
576
|
+ );
|
|
|
577
|
+ },
|
|
|
578
|
+
|
|
|
579
|
+ /**
|
|
|
580
|
+ * The calculations performed here assume the scroll view takes up the entire
|
|
|
581
|
+ * screen - even if has some content inset. We then measure the offsets of the
|
|
|
582
|
+ * keyboard, and compensate both for the scroll view's "contentInset".
|
|
|
583
|
+ *
|
|
|
584
|
+ * @param {number} left Position of input w.r.t. table view.
|
|
|
585
|
+ * @param {number} top Position of input w.r.t. table view.
|
|
|
586
|
+ * @param {number} width Width of the text input.
|
|
|
587
|
+ * @param {number} height Height of the text input.
|
|
|
588
|
+ */
|
|
|
589
|
+ scrollResponderInputMeasureAndScrollToKeyboard: function(left: number, top: number, width: number, height: number) {
|
|
|
590
|
+ let keyboardScreenY = Dimensions.get('window').height;
|
|
|
591
|
+ if (this.keyboardWillOpenTo) {
|
|
|
592
|
+ keyboardScreenY = this.keyboardWillOpenTo.endCoordinates.screenY;
|
|
|
593
|
+ }
|
|
|
594
|
+ let scrollOffsetY = top - keyboardScreenY + height + this.additionalScrollOffset;
|
|
|
595
|
+
|
|
|
596
|
+ // By default, this can scroll with negative offset, pulling the content
|
|
|
597
|
+ // down so that the target component's bottom meets the keyboard's top.
|
|
|
598
|
+ // If requested otherwise, cap the offset at 0 minimum to avoid content
|
|
|
599
|
+ // shifting down.
|
|
|
600
|
+ if (this.preventNegativeScrollOffset) {
|
|
|
601
|
+ scrollOffsetY = Math.max(0, scrollOffsetY);
|
|
|
602
|
+ }
|
|
|
603
|
+ this.scrollResponderScrollTo({x: 0, y: scrollOffsetY, animated: true});
|
|
|
604
|
+
|
|
|
605
|
+ this.additionalOffset = 0;
|
|
|
606
|
+ this.preventNegativeScrollOffset = false;
|
|
|
607
|
+ },
|
|
|
608
|
+
|
|
|
609
|
+ scrollResponderTextInputFocusError: function(e: Event) {
|
|
|
610
|
+ console.error('Error measuring text field: ', e);
|
|
|
611
|
+ },
|
|
|
612
|
+
|
|
|
613
|
+ /**
|
|
|
614
|
+ * `componentWillMount` is the closest thing to a standard "constructor" for
|
|
|
615
|
+ * React components.
|
|
|
616
|
+ *
|
|
|
617
|
+ * The `keyboardWillShow` is called before input focus.
|
|
|
618
|
+ */
|
|
|
619
|
+ UNSAFE_componentWillMount: function() {
|
|
|
620
|
+ // modify by yoho 2018/6/5
|
|
|
621
|
+ // const {keyboardShouldPersistTaps} = this.props;
|
|
|
622
|
+ // warning(
|
|
|
623
|
+ // typeof keyboardShouldPersistTaps !== 'boolean',
|
|
|
624
|
+ // `'keyboardShouldPersistTaps={${keyboardShouldPersistTaps}}' is deprecated. `
|
|
|
625
|
+ // + `Use 'keyboardShouldPersistTaps="${keyboardShouldPersistTaps ? 'always' : 'never'}"' instead`
|
|
|
626
|
+ // );
|
|
|
627
|
+// <eof> modify by yoho
|
|
|
628
|
+
|
|
|
629
|
+ this.keyboardWillOpenTo = null;
|
|
|
630
|
+ this.additionalScrollOffset = 0;
|
|
|
631
|
+ this.addListenerOn(Keyboard, 'keyboardWillShow', this.scrollResponderKeyboardWillShow);
|
|
|
632
|
+ this.addListenerOn(Keyboard, 'keyboardWillHide', this.scrollResponderKeyboardWillHide);
|
|
|
633
|
+ this.addListenerOn(Keyboard, 'keyboardDidShow', this.scrollResponderKeyboardDidShow);
|
|
|
634
|
+ this.addListenerOn(Keyboard, 'keyboardDidHide', this.scrollResponderKeyboardDidHide);
|
|
|
635
|
+ },
|
|
|
636
|
+
|
|
|
637
|
+ /**
|
|
|
638
|
+ * Warning, this may be called several times for a single keyboard opening.
|
|
|
639
|
+ * It's best to store the information in this method and then take any action
|
|
|
640
|
+ * at a later point (either in `keyboardDidShow` or other).
|
|
|
641
|
+ *
|
|
|
642
|
+ * Here's the order that events occur in:
|
|
|
643
|
+ * - focus
|
|
|
644
|
+ * - willShow {startCoordinates, endCoordinates} several times
|
|
|
645
|
+ * - didShow several times
|
|
|
646
|
+ * - blur
|
|
|
647
|
+ * - willHide {startCoordinates, endCoordinates} several times
|
|
|
648
|
+ * - didHide several times
|
|
|
649
|
+ *
|
|
|
650
|
+ * The `ScrollResponder` providesModule callbacks for each of these events.
|
|
|
651
|
+ * Even though any user could have easily listened to keyboard events
|
|
|
652
|
+ * themselves, using these `props` callbacks ensures that ordering of events
|
|
|
653
|
+ * is consistent - and not dependent on the order that the keyboard events are
|
|
|
654
|
+ * subscribed to. This matters when telling the scroll view to scroll to where
|
|
|
655
|
+ * the keyboard is headed - the scroll responder better have been notified of
|
|
|
656
|
+ * the keyboard destination before being instructed to scroll to where the
|
|
|
657
|
+ * keyboard will be. Stick to the `ScrollResponder` callbacks, and everything
|
|
|
658
|
+ * will work.
|
|
|
659
|
+ *
|
|
|
660
|
+ * WARNING: These callbacks will fire even if a keyboard is displayed in a
|
|
|
661
|
+ * different navigation pane. Filter out the events to determine if they are
|
|
|
662
|
+ * relevant to you. (For example, only if you receive these callbacks after
|
|
|
663
|
+ * you had explicitly focused a node etc).
|
|
|
664
|
+ */
|
|
|
665
|
+ scrollResponderKeyboardWillShow: function(e: Event) {
|
|
|
666
|
+ this.keyboardWillOpenTo = e;
|
|
|
667
|
+ this.props.onKeyboardWillShow && this.props.onKeyboardWillShow(e);
|
|
|
668
|
+ },
|
|
|
669
|
+
|
|
|
670
|
+ scrollResponderKeyboardWillHide: function(e: Event) {
|
|
|
671
|
+ this.keyboardWillOpenTo = null;
|
|
|
672
|
+ this.props.onKeyboardWillHide && this.props.onKeyboardWillHide(e);
|
|
|
673
|
+ },
|
|
|
674
|
+
|
|
|
675
|
+ scrollResponderKeyboardDidShow: function(e: Event) {
|
|
|
676
|
+ // TODO(7693961): The event for DidShow is not available on iOS yet.
|
|
|
677
|
+ // Use the one from WillShow and do not assign.
|
|
|
678
|
+ if (e) {
|
|
|
679
|
+ this.keyboardWillOpenTo = e;
|
|
|
680
|
+ }
|
|
|
681
|
+ this.props.onKeyboardDidShow && this.props.onKeyboardDidShow(e);
|
|
|
682
|
+ },
|
|
|
683
|
+
|
|
|
684
|
+ scrollResponderKeyboardDidHide: function(e: Event) {
|
|
|
685
|
+ this.keyboardWillOpenTo = null;
|
|
|
686
|
+ this.props.onKeyboardDidHide && this.props.onKeyboardDidHide(e);
|
|
|
687
|
+ }
|
|
|
688
|
+
|
|
|
689
|
+};
|
|
|
690
|
+
|
|
|
691
|
+const ScrollResponder = {
|
|
|
692
|
+ Mixin: ScrollResponderMixin,
|
|
|
693
|
+};
|
|
|
694
|
+
|
|
|
695
|
+module.exports = ScrollResponder; |