@sdcx/bottom-sheet 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +78 -0
- package/RNBottomSheet.podspec +18 -0
- package/android/build.gradle +35 -0
- package/android/proguard-rules.pro +0 -0
- package/android/src/main/AndroidManifest.xml +4 -0
- package/android/src/main/java/com/reactnative/bottomsheet/BottomSheet.java +599 -0
- package/android/src/main/java/com/reactnative/bottomsheet/BottomSheetManager.java +45 -0
- package/android/src/main/java/com/reactnative/bottomsheet/BottomSheetPackage.java +25 -0
- package/android/src/main/java/com/reactnative/bottomsheet/BottomSheetState.java +5 -0
- package/android/src/main/java/com/reactnative/bottomsheet/OffsetChangedEvent.java +40 -0
- package/android/src/main/java/com/reactnative/bottomsheet/StateChangedEvent.java +31 -0
- package/ios/BottomSheet/RNBottomSheet.h +15 -0
- package/ios/BottomSheet/RNBottomSheet.m +321 -0
- package/ios/BottomSheet/RNBottomSheetManager.h +9 -0
- package/ios/BottomSheet/RNBottomSheetManager.m +18 -0
- package/ios/BottomSheet.xcodeproj/project.pbxproj +283 -0
- package/lib/index.d.ts +21 -0
- package/lib/index.js +31 -0
- package/lib/splitLayoutProps.d.ts +15 -0
- package/lib/splitLayoutProps.js +41 -0
- package/package.json +65 -0
- package/src/index.tsx +74 -0
- package/src/splitLayoutProps.ts +47 -0
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
package com.reactnative.bottomsheet;
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import static com.reactnative.bottomsheet.BottomSheetState.COLLAPSED;
|
|
5
|
+
import static com.reactnative.bottomsheet.BottomSheetState.DRAGGING;
|
|
6
|
+
import static com.reactnative.bottomsheet.BottomSheetState.EXPANDED;
|
|
7
|
+
import static com.reactnative.bottomsheet.BottomSheetState.HIDDEN;
|
|
8
|
+
import static com.reactnative.bottomsheet.BottomSheetState.SETTLING;
|
|
9
|
+
import static java.lang.Math.max;
|
|
10
|
+
|
|
11
|
+
import android.annotation.SuppressLint;
|
|
12
|
+
import android.graphics.Rect;
|
|
13
|
+
import android.view.MotionEvent;
|
|
14
|
+
import android.view.VelocityTracker;
|
|
15
|
+
import android.view.View;
|
|
16
|
+
import android.view.ViewGroup;
|
|
17
|
+
import android.view.ViewTreeObserver;
|
|
18
|
+
|
|
19
|
+
import androidx.annotation.NonNull;
|
|
20
|
+
import androidx.annotation.Nullable;
|
|
21
|
+
import androidx.annotation.VisibleForTesting;
|
|
22
|
+
import androidx.core.math.MathUtils;
|
|
23
|
+
import androidx.core.util.Pools;
|
|
24
|
+
import androidx.core.view.NestedScrollingParent;
|
|
25
|
+
import androidx.core.view.NestedScrollingParentHelper;
|
|
26
|
+
import androidx.core.view.ViewCompat;
|
|
27
|
+
import androidx.customview.widget.ViewDragHelper;
|
|
28
|
+
|
|
29
|
+
import com.facebook.react.bridge.ReactContext;
|
|
30
|
+
import com.facebook.react.uimanager.PointerEvents;
|
|
31
|
+
import com.facebook.react.uimanager.ReactPointerEventsView;
|
|
32
|
+
import com.facebook.react.uimanager.ThemedReactContext;
|
|
33
|
+
import com.facebook.react.uimanager.UIManagerHelper;
|
|
34
|
+
import com.facebook.react.uimanager.events.Event;
|
|
35
|
+
import com.facebook.react.uimanager.events.EventDispatcher;
|
|
36
|
+
import com.facebook.react.uimanager.events.NativeGestureUtil;
|
|
37
|
+
import com.facebook.react.views.view.ReactViewGroup;
|
|
38
|
+
|
|
39
|
+
import java.lang.ref.WeakReference;
|
|
40
|
+
|
|
41
|
+
@SuppressLint("ViewConstructor")
|
|
42
|
+
public class BottomSheet extends ReactViewGroup implements NestedScrollingParent, ReactPointerEventsView {
|
|
43
|
+
|
|
44
|
+
private static final String TAG = "ReactBottomSheet";
|
|
45
|
+
|
|
46
|
+
private final ReactContext reactContext;
|
|
47
|
+
|
|
48
|
+
public BottomSheet(ThemedReactContext reactContext) {
|
|
49
|
+
super(reactContext);
|
|
50
|
+
this.reactContext = reactContext;
|
|
51
|
+
this.nestedScrollingParentHelper = new NestedScrollingParentHelper(this);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private BottomSheetState state = COLLAPSED;
|
|
55
|
+
|
|
56
|
+
private SettleRunnable settleRunnable = null;
|
|
57
|
+
|
|
58
|
+
private int peekHeight;
|
|
59
|
+
|
|
60
|
+
private int expandedOffset;
|
|
61
|
+
|
|
62
|
+
private int collapsedOffset;
|
|
63
|
+
|
|
64
|
+
private View contentView;
|
|
65
|
+
|
|
66
|
+
@Nullable
|
|
67
|
+
private WeakReference<View> nestedScrollingChildRef;
|
|
68
|
+
|
|
69
|
+
private boolean touchingScrollingChild;
|
|
70
|
+
|
|
71
|
+
@Nullable
|
|
72
|
+
private ViewDragHelper viewDragHelper;
|
|
73
|
+
|
|
74
|
+
private final NestedScrollingParentHelper nestedScrollingParentHelper;
|
|
75
|
+
|
|
76
|
+
private boolean ignoreEvents;
|
|
77
|
+
|
|
78
|
+
private int lastNestedScrollDy;
|
|
79
|
+
|
|
80
|
+
private VelocityTracker velocityTracker;
|
|
81
|
+
|
|
82
|
+
private int activePointerId;
|
|
83
|
+
|
|
84
|
+
private int initialY;
|
|
85
|
+
|
|
86
|
+
private int contentHeight = -1;
|
|
87
|
+
|
|
88
|
+
@Override
|
|
89
|
+
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
|
90
|
+
if (viewDragHelper == null) {
|
|
91
|
+
viewDragHelper = ViewDragHelper.create(this, dragCallback);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
layoutChild();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private void layoutChild() {
|
|
98
|
+
int count = getChildCount();
|
|
99
|
+
if (count == 1) {
|
|
100
|
+
View child = getChildAt(0);
|
|
101
|
+
if (contentView == null) {
|
|
102
|
+
contentView = child;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
contentHeight = contentView.getHeight();
|
|
106
|
+
calculateOffset();
|
|
107
|
+
|
|
108
|
+
getViewTreeObserver().removeOnPreDrawListener(preDrawListener);
|
|
109
|
+
int top = contentView.getTop();
|
|
110
|
+
if (state == COLLAPSED) {
|
|
111
|
+
child.offsetTopAndBottom(collapsedOffset - top);
|
|
112
|
+
} else if (state == EXPANDED) {
|
|
113
|
+
child.offsetTopAndBottom(expandedOffset -top);
|
|
114
|
+
} else if (state == HIDDEN) {
|
|
115
|
+
child.offsetTopAndBottom(getHeight() - top);
|
|
116
|
+
}
|
|
117
|
+
getViewTreeObserver().addOnPreDrawListener(preDrawListener);
|
|
118
|
+
|
|
119
|
+
dispatchOnSlide(child.getTop());
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@Override
|
|
124
|
+
protected void onDetachedFromWindow() {
|
|
125
|
+
super.onDetachedFromWindow();
|
|
126
|
+
getViewTreeObserver().removeOnPreDrawListener(preDrawListener);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
ViewTreeObserver.OnPreDrawListener preDrawListener = new ViewTreeObserver.OnPreDrawListener() {
|
|
130
|
+
@Override
|
|
131
|
+
public boolean onPreDraw() {
|
|
132
|
+
if (contentHeight != -1 && contentHeight != contentView.getHeight()) {
|
|
133
|
+
layoutChild();
|
|
134
|
+
}
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
private void calculateOffset() {
|
|
140
|
+
expandedOffset = Math.max(0, getHeight() - contentView.getHeight());
|
|
141
|
+
collapsedOffset = Math.max(getHeight() - peekHeight, expandedOffset);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
public void setPeekHeight(int peekHeight) {
|
|
145
|
+
this.peekHeight = max(peekHeight, 0);
|
|
146
|
+
if (contentView != null) {
|
|
147
|
+
calculateOffset();
|
|
148
|
+
if (state == COLLAPSED) {
|
|
149
|
+
settleToState(contentView, state);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
public void setState(BottomSheetState state) {
|
|
155
|
+
if (state == this.state) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (contentView == null) {
|
|
160
|
+
// The view is not laid out yet; modify mState and let onLayoutChild handle it later
|
|
161
|
+
if (state == COLLAPSED || state == EXPANDED || state == HIDDEN) {
|
|
162
|
+
this.state = state;
|
|
163
|
+
}
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
settleToState(contentView, state);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@Nullable
|
|
170
|
+
@VisibleForTesting
|
|
171
|
+
View findScrollingChild(View view) {
|
|
172
|
+
if (ViewCompat.isNestedScrollingEnabled(view)) {
|
|
173
|
+
if (!view.canScrollHorizontally(1) && !view.canScrollHorizontally(-1)) {
|
|
174
|
+
return view;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (view instanceof ViewGroup) {
|
|
179
|
+
ViewGroup group = (ViewGroup) view;
|
|
180
|
+
for (int i = 0, count = group.getChildCount(); i < count; i++) {
|
|
181
|
+
View child = group.getChildAt(i);
|
|
182
|
+
if (child.getVisibility() == VISIBLE) {
|
|
183
|
+
View scrollingChild = findScrollingChild(child);
|
|
184
|
+
if (scrollingChild != null) {
|
|
185
|
+
return scrollingChild;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
public PointerEvents getPointerEvents() {
|
|
194
|
+
return PointerEvents.BOX_NONE;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
@Override
|
|
198
|
+
public boolean onInterceptTouchEvent(MotionEvent event) {
|
|
199
|
+
if (shouldInterceptTouchEvent(event)) {
|
|
200
|
+
NativeGestureUtil.notifyNativeGestureStarted(this, event);
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private boolean shouldInterceptTouchEvent(MotionEvent event) {
|
|
207
|
+
int action = event.getActionMasked();
|
|
208
|
+
// Record the velocity
|
|
209
|
+
if (action == MotionEvent.ACTION_DOWN) {
|
|
210
|
+
reset();
|
|
211
|
+
}
|
|
212
|
+
if (velocityTracker == null) {
|
|
213
|
+
velocityTracker = VelocityTracker.obtain();
|
|
214
|
+
}
|
|
215
|
+
velocityTracker.addMovement(event);
|
|
216
|
+
switch (action) {
|
|
217
|
+
case MotionEvent.ACTION_UP:
|
|
218
|
+
case MotionEvent.ACTION_CANCEL:
|
|
219
|
+
touchingScrollingChild = false;
|
|
220
|
+
activePointerId = MotionEvent.INVALID_POINTER_ID;
|
|
221
|
+
// Reset the ignore flag
|
|
222
|
+
if (ignoreEvents) {
|
|
223
|
+
ignoreEvents = false;
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
break;
|
|
227
|
+
case MotionEvent.ACTION_DOWN:
|
|
228
|
+
nestedScrollingChildRef = new WeakReference<>(findScrollingChild(contentView));
|
|
229
|
+
int initialX = (int) event.getX();
|
|
230
|
+
initialY = (int) event.getY();
|
|
231
|
+
// Only intercept nested scrolling events here if the view not being moved by the
|
|
232
|
+
// ViewDragHelper.
|
|
233
|
+
if (state != SETTLING) {
|
|
234
|
+
View scroll = nestedScrollingChildRef != null ? nestedScrollingChildRef.get() : null;
|
|
235
|
+
if (scroll != null && isPointInChildBounds(scroll, initialX, initialY)) {
|
|
236
|
+
activePointerId = event.getPointerId(event.getActionIndex());
|
|
237
|
+
touchingScrollingChild = true;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
ignoreEvents = activePointerId == MotionEvent.INVALID_POINTER_ID
|
|
242
|
+
&& !isPointInChildBounds(contentView, initialX, initialY);
|
|
243
|
+
break;
|
|
244
|
+
default: // fall out
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (!ignoreEvents && viewDragHelper != null && viewDragHelper.shouldInterceptTouchEvent(event)) {
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// We have to handle cases that the ViewDragHelper does not capture the bottom sheet because
|
|
252
|
+
// it is not the top most view of its parent. This is not necessary when the touch event is
|
|
253
|
+
// happening over the scrolling content as nested scrolling logic handles that case.
|
|
254
|
+
View scroll = nestedScrollingChildRef != null ? nestedScrollingChildRef.get() : null;
|
|
255
|
+
return action == MotionEvent.ACTION_MOVE
|
|
256
|
+
&& scroll != null
|
|
257
|
+
&& !ignoreEvents
|
|
258
|
+
&& state != DRAGGING
|
|
259
|
+
&& !isPointInChildBounds(scroll, (int) event.getX(), (int) event.getY())
|
|
260
|
+
&& viewDragHelper != null
|
|
261
|
+
&& Math.abs(initialY - event.getY()) > viewDragHelper.getTouchSlop();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
@Override
|
|
265
|
+
public boolean onTouchEvent(MotionEvent event) {
|
|
266
|
+
int action = event.getActionMasked();
|
|
267
|
+
if (state == DRAGGING && action == MotionEvent.ACTION_DOWN) {
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
if (viewDragHelper != null) {
|
|
271
|
+
viewDragHelper.processTouchEvent(event);
|
|
272
|
+
}
|
|
273
|
+
// Record the velocity
|
|
274
|
+
if (action == MotionEvent.ACTION_DOWN) {
|
|
275
|
+
reset();
|
|
276
|
+
}
|
|
277
|
+
if (velocityTracker == null) {
|
|
278
|
+
velocityTracker = VelocityTracker.obtain();
|
|
279
|
+
}
|
|
280
|
+
velocityTracker.addMovement(event);
|
|
281
|
+
if (viewDragHelper != null && action == MotionEvent.ACTION_MOVE && !ignoreEvents) {
|
|
282
|
+
if (Math.abs(initialY - event.getY()) > viewDragHelper.getTouchSlop()) {
|
|
283
|
+
viewDragHelper.captureChildView(contentView, event.getPointerId(event.getActionIndex()));
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return !ignoreEvents;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
@Override
|
|
291
|
+
public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int nestedScrollAxes) {
|
|
292
|
+
return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
@Override
|
|
296
|
+
public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes) {
|
|
297
|
+
lastNestedScrollDy = 0;
|
|
298
|
+
nestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
@Override
|
|
302
|
+
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed) {
|
|
303
|
+
View scrollingChild = nestedScrollingChildRef != null ? nestedScrollingChildRef.get() : null;
|
|
304
|
+
if (target != scrollingChild) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
View child = contentView;
|
|
309
|
+
int currentTop = child.getTop();
|
|
310
|
+
int newTop = currentTop - dy;
|
|
311
|
+
if (dy > 0) { // Upward
|
|
312
|
+
if (newTop < expandedOffset) {
|
|
313
|
+
consumed[1] = currentTop - expandedOffset;
|
|
314
|
+
ViewCompat.offsetTopAndBottom(child, -consumed[1]);
|
|
315
|
+
setStateInternal(EXPANDED);
|
|
316
|
+
} else {
|
|
317
|
+
consumed[1] = dy;
|
|
318
|
+
ViewCompat.offsetTopAndBottom(child, -dy);
|
|
319
|
+
setStateInternal(DRAGGING);
|
|
320
|
+
}
|
|
321
|
+
} else if (dy < 0) { // Downward
|
|
322
|
+
if (!target.canScrollVertically(-1)) {
|
|
323
|
+
if (newTop <= collapsedOffset) {
|
|
324
|
+
consumed[1] = dy;
|
|
325
|
+
ViewCompat.offsetTopAndBottom(child, -dy);
|
|
326
|
+
setStateInternal(DRAGGING);
|
|
327
|
+
} else {
|
|
328
|
+
consumed[1] = currentTop - collapsedOffset;
|
|
329
|
+
ViewCompat.offsetTopAndBottom(child, -consumed[1]);
|
|
330
|
+
setStateInternal(COLLAPSED);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (currentTop != child.getTop()) {
|
|
335
|
+
dispatchOnSlide(child.getTop());
|
|
336
|
+
}
|
|
337
|
+
lastNestedScrollDy = dy;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
@Override
|
|
341
|
+
public int getNestedScrollAxes() {
|
|
342
|
+
return nestedScrollingParentHelper.getNestedScrollAxes();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
@Override
|
|
346
|
+
public void onStopNestedScroll(@NonNull View target) {
|
|
347
|
+
nestedScrollingParentHelper.onStopNestedScroll(target);
|
|
348
|
+
View child = contentView;
|
|
349
|
+
|
|
350
|
+
if (child.getTop() == expandedOffset) {
|
|
351
|
+
setStateInternal(EXPANDED);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (nestedScrollingChildRef == null || target != nestedScrollingChildRef.get()) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
int top;
|
|
360
|
+
BottomSheetState targetState;
|
|
361
|
+
|
|
362
|
+
if (lastNestedScrollDy > 0) {
|
|
363
|
+
top = expandedOffset;
|
|
364
|
+
targetState = EXPANDED;
|
|
365
|
+
} else if (lastNestedScrollDy == 0) {
|
|
366
|
+
int currentTop = child.getTop();
|
|
367
|
+
if (Math.abs(currentTop - collapsedOffset) < Math.abs(currentTop - expandedOffset)) {
|
|
368
|
+
top = collapsedOffset;
|
|
369
|
+
targetState = COLLAPSED;
|
|
370
|
+
} else {
|
|
371
|
+
top = expandedOffset;
|
|
372
|
+
targetState = EXPANDED;
|
|
373
|
+
}
|
|
374
|
+
} else {
|
|
375
|
+
top = collapsedOffset;
|
|
376
|
+
targetState = COLLAPSED;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
startSettlingAnimation(child, targetState, top, false);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
@Override
|
|
383
|
+
public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
|
|
384
|
+
// Overridden to prevent the default consumption of the entire scroll distance.
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
@Override
|
|
388
|
+
public boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY) {
|
|
389
|
+
if (nestedScrollingChildRef != null) {
|
|
390
|
+
return target == nestedScrollingChildRef.get() && (state != EXPANDED);
|
|
391
|
+
} else {
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
@Override
|
|
397
|
+
public boolean onNestedFling(@NonNull View target, float velocityX, float velocityY, boolean consumed) {
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private void reset() {
|
|
402
|
+
activePointerId = ViewDragHelper.INVALID_POINTER;
|
|
403
|
+
if (velocityTracker != null) {
|
|
404
|
+
velocityTracker.recycle();
|
|
405
|
+
velocityTracker = null;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
private static final Pools.Pool<Rect> sRectPool = new Pools.SynchronizedPool<>(12);
|
|
410
|
+
|
|
411
|
+
private static Rect acquireTempRect() {
|
|
412
|
+
Rect rect = sRectPool.acquire();
|
|
413
|
+
if (rect == null) {
|
|
414
|
+
rect = new Rect();
|
|
415
|
+
}
|
|
416
|
+
return rect;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
private static void releaseTempRect(@NonNull Rect rect) {
|
|
420
|
+
rect.setEmpty();
|
|
421
|
+
sRectPool.release(rect);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
public boolean isPointInChildBounds(@NonNull View child, int x, int y) {
|
|
425
|
+
final Rect r = acquireTempRect();
|
|
426
|
+
child.getDrawingRect(r);
|
|
427
|
+
offsetDescendantRectToMyCoords(child, r);
|
|
428
|
+
try {
|
|
429
|
+
return r.contains(x, y);
|
|
430
|
+
} finally {
|
|
431
|
+
releaseTempRect(r);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
private final ViewDragHelper.Callback dragCallback = new ViewDragHelper.Callback() {
|
|
436
|
+
@Override
|
|
437
|
+
public boolean tryCaptureView(@NonNull View child, int pointerId) {
|
|
438
|
+
if (state == DRAGGING) {
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
if (touchingScrollingChild) {
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
if (state == EXPANDED && activePointerId == pointerId) {
|
|
445
|
+
View scroll = nestedScrollingChildRef != null ? nestedScrollingChildRef.get() : null;
|
|
446
|
+
if (scroll != null && scroll.canScrollVertically(-1)) {
|
|
447
|
+
// Let the content scroll up
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return contentView == child;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
@Override
|
|
455
|
+
public void onViewPositionChanged(@NonNull View changedView, int left, int top, int dx, int dy) {
|
|
456
|
+
dispatchOnSlide(top);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
@Override
|
|
460
|
+
public void onViewDragStateChanged(int state) {
|
|
461
|
+
if (state == ViewDragHelper.STATE_DRAGGING) {
|
|
462
|
+
setStateInternal(DRAGGING);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
@Override
|
|
467
|
+
public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
|
|
468
|
+
int top;
|
|
469
|
+
BottomSheetState targetState;
|
|
470
|
+
if (yvel < 0) { // Moving up
|
|
471
|
+
top = expandedOffset;
|
|
472
|
+
targetState = EXPANDED;
|
|
473
|
+
} else if (yvel == 0.f || Math.abs(xvel) > Math.abs(yvel)) {
|
|
474
|
+
// If the Y velocity is 0 or the swipe was mostly horizontal indicated by the X velocity
|
|
475
|
+
// being greater than the Y velocity, settle to the nearest correct height.
|
|
476
|
+
int currentTop = releasedChild.getTop();
|
|
477
|
+
if (Math.abs(currentTop - collapsedOffset) < Math.abs(currentTop - expandedOffset)) {
|
|
478
|
+
top = collapsedOffset;
|
|
479
|
+
targetState = COLLAPSED;
|
|
480
|
+
} else {
|
|
481
|
+
top = expandedOffset;
|
|
482
|
+
targetState = EXPANDED;
|
|
483
|
+
}
|
|
484
|
+
} else { // Moving Down
|
|
485
|
+
top = collapsedOffset;
|
|
486
|
+
targetState = COLLAPSED;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
startSettlingAnimation(releasedChild, targetState, top, true);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
@Override
|
|
493
|
+
public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
|
|
494
|
+
return MathUtils.clamp(top, expandedOffset, collapsedOffset);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
@Override
|
|
498
|
+
public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
|
|
499
|
+
return child.getLeft();
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
@Override
|
|
503
|
+
public int getViewVerticalDragRange(@NonNull View child) {
|
|
504
|
+
return collapsedOffset;
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
void dispatchOnSlide(int top) {
|
|
509
|
+
if (contentView != null) {
|
|
510
|
+
sentEvent(new OffsetChangedEvent(UIManagerHelper.getSurfaceId(reactContext), getId(), top, expandedOffset, collapsedOffset));
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
void settleToState(@NonNull View child, BottomSheetState state) {
|
|
515
|
+
int top;
|
|
516
|
+
if (state == COLLAPSED) {
|
|
517
|
+
top = collapsedOffset;
|
|
518
|
+
} else if (state == EXPANDED) {
|
|
519
|
+
top = expandedOffset;
|
|
520
|
+
} else if (state == HIDDEN) {
|
|
521
|
+
top = getHeight();
|
|
522
|
+
} else {
|
|
523
|
+
throw new IllegalArgumentException("Illegal state argument: " + state);
|
|
524
|
+
}
|
|
525
|
+
startSettlingAnimation(child, state, top, false);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
void startSettlingAnimation(View child, BottomSheetState state, int top, boolean settleFromViewDragHelper) {
|
|
529
|
+
boolean startedSettling =
|
|
530
|
+
viewDragHelper != null
|
|
531
|
+
&& (settleFromViewDragHelper
|
|
532
|
+
? viewDragHelper.settleCapturedViewAt(child.getLeft(), top)
|
|
533
|
+
: viewDragHelper.smoothSlideViewTo(child, child.getLeft(), top));
|
|
534
|
+
if (startedSettling) {
|
|
535
|
+
setStateInternal(SETTLING);
|
|
536
|
+
// STATE_SETTLING won't animate the material shape, so do that here with the target state.
|
|
537
|
+
// updateDrawableForTargetState(state);
|
|
538
|
+
if (settleRunnable == null) {
|
|
539
|
+
// If the singleton SettleRunnable instance has not been instantiated, create it.
|
|
540
|
+
settleRunnable = new SettleRunnable(child, state);
|
|
541
|
+
}
|
|
542
|
+
// If the SettleRunnable has not been posted, post it with the correct state.
|
|
543
|
+
if (!settleRunnable.isPosted) {
|
|
544
|
+
settleRunnable.targetState = state;
|
|
545
|
+
ViewCompat.postOnAnimation(child, settleRunnable);
|
|
546
|
+
settleRunnable.isPosted = true;
|
|
547
|
+
} else {
|
|
548
|
+
// Otherwise, if it has been posted, just update the target state.
|
|
549
|
+
settleRunnable.targetState = state;
|
|
550
|
+
}
|
|
551
|
+
} else {
|
|
552
|
+
setStateInternal(state);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
void setStateInternal(BottomSheetState state) {
|
|
557
|
+
if (this.state == state) {
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
this.state = state;
|
|
561
|
+
|
|
562
|
+
if (state == COLLAPSED || state == EXPANDED || state == HIDDEN) {
|
|
563
|
+
sentEvent(new StateChangedEvent(UIManagerHelper.getSurfaceId(reactContext), getId(), state.name().toLowerCase()));
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
private class SettleRunnable implements Runnable {
|
|
568
|
+
|
|
569
|
+
private final View view;
|
|
570
|
+
|
|
571
|
+
private boolean isPosted;
|
|
572
|
+
|
|
573
|
+
BottomSheetState targetState;
|
|
574
|
+
|
|
575
|
+
SettleRunnable(View view, BottomSheetState targetState) {
|
|
576
|
+
this.view = view;
|
|
577
|
+
this.targetState = targetState;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
@Override
|
|
581
|
+
public void run() {
|
|
582
|
+
if (viewDragHelper != null && viewDragHelper.continueSettling(true)) {
|
|
583
|
+
ViewCompat.postOnAnimation(view, this);
|
|
584
|
+
} else {
|
|
585
|
+
setStateInternal(targetState);
|
|
586
|
+
}
|
|
587
|
+
this.isPosted = false;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
void sentEvent(Event<?> event) {
|
|
592
|
+
int viewId = getId();
|
|
593
|
+
EventDispatcher eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, viewId);
|
|
594
|
+
if (eventDispatcher != null) {
|
|
595
|
+
eventDispatcher.dispatchEvent(event);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
package com.reactnative.bottomsheet;
|
|
2
|
+
|
|
3
|
+
import androidx.annotation.NonNull;
|
|
4
|
+
|
|
5
|
+
import com.facebook.react.common.MapBuilder;
|
|
6
|
+
import com.facebook.react.uimanager.PixelUtil;
|
|
7
|
+
import com.facebook.react.uimanager.ThemedReactContext;
|
|
8
|
+
import com.facebook.react.uimanager.ViewGroupManager;
|
|
9
|
+
import com.facebook.react.uimanager.annotations.ReactProp;
|
|
10
|
+
|
|
11
|
+
import java.util.Map;
|
|
12
|
+
|
|
13
|
+
public class BottomSheetManager extends ViewGroupManager<BottomSheet> {
|
|
14
|
+
|
|
15
|
+
@NonNull
|
|
16
|
+
@Override
|
|
17
|
+
public String getName() {
|
|
18
|
+
return "BottomSheet";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@NonNull
|
|
22
|
+
@Override
|
|
23
|
+
protected BottomSheet createViewInstance(@NonNull ThemedReactContext reactContext) {
|
|
24
|
+
return new BottomSheet(reactContext);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@Override
|
|
28
|
+
public Map<String, Object> getExportedCustomDirectEventTypeConstants() {
|
|
29
|
+
return MapBuilder.<String, Object>builder()
|
|
30
|
+
.put(StateChangedEvent.Name, MapBuilder.of("registrationName", StateChangedEvent.JSEventName))
|
|
31
|
+
.put(OffsetChangedEvent.Name, MapBuilder.of("registrationName", OffsetChangedEvent.JSEventName))
|
|
32
|
+
.build();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@ReactProp(name = "peekHeight", defaultInt = 200)
|
|
36
|
+
public void setPeekHeight(BottomSheet view, int dp) {
|
|
37
|
+
view.setPeekHeight((int) (PixelUtil.toPixelFromDIP(dp) + 0.5));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@ReactProp(name = "state")
|
|
41
|
+
public void setState(BottomSheet view, String state) {
|
|
42
|
+
view.setState(BottomSheetState.valueOf(state.toUpperCase()));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
package com.reactnative.bottomsheet;
|
|
2
|
+
|
|
3
|
+
import androidx.annotation.NonNull;
|
|
4
|
+
|
|
5
|
+
import java.util.Collections;
|
|
6
|
+
import java.util.List;
|
|
7
|
+
|
|
8
|
+
import com.facebook.react.ReactPackage;
|
|
9
|
+
import com.facebook.react.bridge.NativeModule;
|
|
10
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
11
|
+
import com.facebook.react.uimanager.ViewManager;
|
|
12
|
+
|
|
13
|
+
public class BottomSheetPackage implements ReactPackage {
|
|
14
|
+
@NonNull
|
|
15
|
+
@Override
|
|
16
|
+
public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactContext) {
|
|
17
|
+
return Collections.emptyList();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@NonNull
|
|
21
|
+
@Override
|
|
22
|
+
public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactContext) {
|
|
23
|
+
return Collections.singletonList(new BottomSheetManager());
|
|
24
|
+
}
|
|
25
|
+
}
|