@sdcx/bottom-sheet 0.8.0 → 0.10.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/README.md +9 -2
- package/ios/BottomSheet/RNBottomSheet.h +8 -3
- package/ios/BottomSheet/RNBottomSheet.m +125 -79
- package/ios/BottomSheet/RNBottomSheetManager.m +8 -3
- package/ios/BottomSheet/RNBottomSheetOffsetChangedEvent.h +11 -0
- package/ios/BottomSheet/RNBottomSheetOffsetChangedEvent.m +58 -0
- package/ios/BottomSheet/RNBottomSheetState.h +17 -0
- package/ios/BottomSheet/RNBottomSheetState.m +38 -0
- package/ios/BottomSheet/RNBottomSheetStateChangedEvent.h +13 -0
- package/ios/BottomSheet/RNBottomSheetStateChangedEvent.m +50 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +7 -16
- package/package.json +1 -1
- package/src/index.tsx +24 -29
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# BottomSheet
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[BottomSheet](https://github.com/sdcxtech/react-native-troika/blob/master/packages/bottom-sheet/README.md) 是一个类似于 Android 原生的 [BottomSheetBehavior](https://developer.android.com/reference/com/google/android/material/bottomsheet/BottomSheetBehavior) 组件,我们在 API 设计上也尽量和 Android 原生保持一致。
|
|
4
4
|
|
|
5
5
|
它位于屏幕底部,可拖拽,支持嵌套滚动,可以和可滚动视图(`FlatList`, `FlashList`, `WebView` 等等)一起使用。
|
|
6
6
|
|
|
@@ -43,7 +43,12 @@ const App = () => {
|
|
|
43
43
|
}
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
> :
|
|
46
|
+
> :exclamation: :exclamation: :exclamation:
|
|
47
|
+
> Android 是基于 [NestedScrolling API](https://developer.android.com/reference/androidx/core/view/NestedScrollingChild) 实现的。
|
|
48
|
+
>
|
|
49
|
+
> <h3>请记得为你的列表开启 `nestedScrollEnabled` 属性。</h3>
|
|
50
|
+
>
|
|
51
|
+
> :exclamation: :exclamation: :exclamation:
|
|
47
52
|
|
|
48
53
|
## 基本概念和 API
|
|
49
54
|
|
|
@@ -67,6 +72,8 @@ const App = () => {
|
|
|
67
72
|
|
|
68
73
|
- `fitToContents`,是指 BottomSheet 在展开时,是否适应内容的高度,默认是 `false`。当和可滚动列表,譬如 ScrollView 一起使用时,请保持默认值。
|
|
69
74
|
|
|
75
|
+
- `contentContainerStyle`,用来设置内层视图的样式。
|
|
76
|
+
|
|
70
77
|
### 回调
|
|
71
78
|
|
|
72
79
|
- `onStateChanged`, 是指 BottomSheet 状态变化时的回调,它和 `state` 属性是一对,用来实现受控模式。
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
#import <UIKit/UIKit.h>
|
|
2
|
-
#import <React/
|
|
2
|
+
#import <React/RCTView.h>
|
|
3
|
+
#import <React/RCTEventDispatcher.h>
|
|
4
|
+
|
|
5
|
+
#import "RNBottomSheetState.h"
|
|
3
6
|
|
|
4
7
|
NS_ASSUME_NONNULL_BEGIN
|
|
5
8
|
|
|
6
|
-
@interface RNBottomSheet :
|
|
9
|
+
@interface RNBottomSheet : RCTView
|
|
7
10
|
|
|
8
11
|
@property(nonatomic, copy) RCTDirectEventBlock onSlide;
|
|
9
12
|
@property(nonatomic, copy) RCTDirectEventBlock onStateChanged;
|
|
10
13
|
@property(nonatomic, assign) CGFloat peekHeight;
|
|
11
|
-
@property(nonatomic,
|
|
14
|
+
@property(nonatomic, assign) RNBottomSheetState state;
|
|
15
|
+
|
|
16
|
+
- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher;
|
|
12
17
|
|
|
13
18
|
@end
|
|
14
19
|
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
#import "RNBottomSheet.h"
|
|
2
|
+
#import "RNBottomSheetStateChangedEvent.h"
|
|
3
|
+
#import "RNBottomSheetOffsetChangedEvent.h"
|
|
2
4
|
|
|
3
5
|
#import <React/UIView+React.h>
|
|
6
|
+
#import <React/RCTRootContentView.h>
|
|
7
|
+
#import <React/RCTTouchHandler.h>
|
|
4
8
|
#import <React/RCTLog.h>
|
|
5
9
|
|
|
6
10
|
@interface RNBottomSheet () <UIGestureRecognizerDelegate>
|
|
7
11
|
|
|
12
|
+
@property(nonatomic, strong) UIView *contentView;
|
|
13
|
+
|
|
8
14
|
@property(nonatomic, strong) UIScrollView *target;
|
|
9
15
|
@property(nonatomic, strong) UIPanGestureRecognizer *panGestureRecognizer;
|
|
10
16
|
|
|
@@ -12,24 +18,41 @@
|
|
|
12
18
|
@property(nonatomic, assign) CGFloat maxY;
|
|
13
19
|
|
|
14
20
|
@property(nonatomic, assign) BOOL nextReturn;
|
|
15
|
-
@property(nonatomic, assign) CGFloat lastDragDistance;
|
|
16
21
|
|
|
17
22
|
@property(nonatomic, strong) CADisplayLink *displayLink;
|
|
23
|
+
@property(nonatomic, strong) RCTEventDispatcher *eventDispatcher;
|
|
18
24
|
|
|
19
25
|
@end
|
|
20
26
|
|
|
21
|
-
@implementation RNBottomSheet
|
|
27
|
+
@implementation RNBottomSheet {
|
|
28
|
+
__weak RCTRootContentView *_rootView;
|
|
29
|
+
}
|
|
22
30
|
|
|
23
|
-
- (instancetype)
|
|
31
|
+
- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher {
|
|
24
32
|
if (self = [super init]) {
|
|
25
33
|
_panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)];
|
|
26
34
|
_panGestureRecognizer.delegate = self;
|
|
27
|
-
_state =
|
|
28
|
-
|
|
35
|
+
_state = RNBottomSheetStateCollapsed;
|
|
36
|
+
_eventDispatcher = eventDispatcher;
|
|
29
37
|
}
|
|
30
38
|
return self;
|
|
31
39
|
}
|
|
32
40
|
|
|
41
|
+
- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex {
|
|
42
|
+
[super insertReactSubview:subview atIndex:atIndex];
|
|
43
|
+
if (atIndex == 0) {
|
|
44
|
+
self.contentView = subview;
|
|
45
|
+
[subview addGestureRecognizer:_panGestureRecognizer];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
- (void)removeReactSubview:(UIView *)subview {
|
|
50
|
+
[super removeReactSubview:subview];
|
|
51
|
+
if (self.contentView == subview) {
|
|
52
|
+
[subview removeGestureRecognizer:_panGestureRecognizer];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
33
56
|
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
|
|
34
57
|
if (gestureRecognizer == self.panGestureRecognizer && otherGestureRecognizer == self.target.panGestureRecognizer) {
|
|
35
58
|
return YES;
|
|
@@ -37,6 +60,17 @@
|
|
|
37
60
|
return NO;
|
|
38
61
|
}
|
|
39
62
|
|
|
63
|
+
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
|
|
64
|
+
if (gestureRecognizer == self.panGestureRecognizer) {
|
|
65
|
+
if ([super gestureRecognizerShouldBegin:gestureRecognizer]) {
|
|
66
|
+
[self cancelRootViewTouches];
|
|
67
|
+
return YES;
|
|
68
|
+
}
|
|
69
|
+
return NO;
|
|
70
|
+
}
|
|
71
|
+
return [super gestureRecognizerShouldBegin:gestureRecognizer];
|
|
72
|
+
}
|
|
73
|
+
|
|
40
74
|
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
|
|
41
75
|
if (gestureRecognizer != self.panGestureRecognizer) {
|
|
42
76
|
return YES;
|
|
@@ -82,6 +116,26 @@
|
|
|
82
116
|
}
|
|
83
117
|
}
|
|
84
118
|
|
|
119
|
+
- (void)didMoveToWindow {
|
|
120
|
+
[super didMoveToWindow];
|
|
121
|
+
if (self.window) {
|
|
122
|
+
[self cacheRootView];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
- (void)cacheRootView {
|
|
127
|
+
UIView *rootView = self;
|
|
128
|
+
while (rootView.superview && ![rootView isReactRootView]) {
|
|
129
|
+
rootView = rootView.superview;
|
|
130
|
+
}
|
|
131
|
+
_rootView = rootView;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
- (void)cancelRootViewTouches {
|
|
135
|
+
RCTRootContentView *rootView = (RCTRootContentView *)_rootView;
|
|
136
|
+
[rootView.touchHandler cancel];
|
|
137
|
+
}
|
|
138
|
+
|
|
85
139
|
- (BOOL)isHorizontal:(UIScrollView *)scrollView {
|
|
86
140
|
return scrollView.contentSize.width > self.frame.size.width;
|
|
87
141
|
}
|
|
@@ -89,39 +143,42 @@
|
|
|
89
143
|
|
|
90
144
|
- (void)reactSetFrame:(CGRect)frame {
|
|
91
145
|
[super reactSetFrame:frame];
|
|
92
|
-
|
|
93
|
-
[self calculateOffset];
|
|
94
|
-
if ([self.state isEqualToString:@"collapsed"]) {
|
|
95
|
-
self.frame = CGRectOffset(self.frame, 0, self.frame.size.height - self.peekHeight);
|
|
96
|
-
[self dispatchOnSlide:self.frame.origin.y];
|
|
97
|
-
} else if ([self.state isEqualToString:@"hidden"]) {
|
|
98
|
-
self.frame = CGRectOffset(self.frame, 0, self.frame.size.height);
|
|
99
|
-
[self dispatchOnSlide:self.frame.origin.y];
|
|
100
|
-
}
|
|
101
|
-
}
|
|
146
|
+
|
|
102
147
|
}
|
|
103
148
|
|
|
104
149
|
- (void)layoutSubviews {
|
|
105
150
|
[super layoutSubviews];
|
|
106
|
-
|
|
107
|
-
|
|
151
|
+
if (!self.contentView) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!CGRectEqualToRect(self.contentView.frame, CGRectZero)) {
|
|
156
|
+
[self calculateOffset];
|
|
157
|
+
if (self.state == RNBottomSheetStateCollapsed) {
|
|
158
|
+
self.contentView.frame = CGRectOffset(self.contentView.frame, 0, self.maxY - self.contentView.frame.origin.y);
|
|
159
|
+
} else if (self.state == RNBottomSheetStateExpanded) {
|
|
160
|
+
self.contentView.frame = CGRectOffset(self.contentView.frame, 0, self.minY - self.contentView.frame.origin.y);
|
|
161
|
+
} else if (self.state == RNBottomSheetStateHidden) {
|
|
162
|
+
self.contentView.frame = CGRectOffset(self.contentView.frame, 0, self.frame.size.height - self.contentView.frame.origin.y);
|
|
163
|
+
}
|
|
164
|
+
[self dispatchOnSlide:self.contentView.frame.origin.y];
|
|
165
|
+
}
|
|
108
166
|
}
|
|
109
167
|
|
|
110
168
|
- (void)calculateOffset {
|
|
111
|
-
CGFloat parentHeight = self.
|
|
112
|
-
self.minY = fmax(0, parentHeight - self.frame.size.height);
|
|
169
|
+
CGFloat parentHeight = self.frame.size.height;
|
|
170
|
+
self.minY = fmax(0, parentHeight - self.contentView.frame.size.height);
|
|
113
171
|
self.maxY = fmax(self.minY, parentHeight - self.peekHeight);
|
|
114
172
|
}
|
|
115
173
|
|
|
116
174
|
- (void)handlePan:(UIPanGestureRecognizer *)pan {
|
|
175
|
+
CGFloat translationY = [pan translationInView:self.contentView].y;
|
|
176
|
+
[pan setTranslation:CGPointZero inView:self.contentView];
|
|
117
177
|
|
|
118
|
-
CGFloat
|
|
119
|
-
[pan setTranslation:CGPointZero inView:self];
|
|
120
|
-
|
|
121
|
-
CGFloat top = self.frame.origin.y;
|
|
178
|
+
CGFloat top = self.contentView.frame.origin.y;
|
|
122
179
|
|
|
123
180
|
if (pan.state == UIGestureRecognizerStateChanged) {
|
|
124
|
-
[self setStateInternal
|
|
181
|
+
[self setStateInternal:RNBottomSheetStateDragging];
|
|
125
182
|
}
|
|
126
183
|
|
|
127
184
|
// 如果有嵌套滚动
|
|
@@ -129,15 +186,15 @@
|
|
|
129
186
|
if(translationY > 0 && top < self.maxY && self.target.contentOffset.y <= 0) {
|
|
130
187
|
//向下拖
|
|
131
188
|
CGFloat y = fmin(top + translationY, self.maxY);
|
|
132
|
-
self.frame = CGRectOffset(self.frame, 0, y - top);
|
|
133
|
-
[self dispatchOnSlide:self.frame.origin.y];
|
|
189
|
+
self.contentView.frame = CGRectOffset(self.contentView.frame, 0, y - top);
|
|
190
|
+
[self dispatchOnSlide:self.contentView.frame.origin.y];
|
|
134
191
|
}
|
|
135
192
|
|
|
136
193
|
if (translationY < 0 && top > self.minY) {
|
|
137
194
|
//向上拖
|
|
138
195
|
CGFloat y = fmax(top + translationY, self.minY);
|
|
139
|
-
self.frame = CGRectOffset(self.frame, 0, y - top);
|
|
140
|
-
[self dispatchOnSlide:self.frame.origin.y];
|
|
196
|
+
self.contentView.frame = CGRectOffset(self.contentView.frame, 0, y - top);
|
|
197
|
+
[self dispatchOnSlide:self.contentView.frame.origin.y];
|
|
141
198
|
}
|
|
142
199
|
}
|
|
143
200
|
|
|
@@ -146,43 +203,43 @@
|
|
|
146
203
|
if(translationY > 0 && top < self.maxY) {
|
|
147
204
|
//向下拖
|
|
148
205
|
CGFloat y = fmin(top + translationY, self.maxY);
|
|
149
|
-
self.frame = CGRectOffset(self.frame, 0, y - top);
|
|
150
|
-
[self dispatchOnSlide:self.frame.origin.y];
|
|
206
|
+
self.contentView.frame = CGRectOffset(self.contentView.frame, 0, y - top);
|
|
207
|
+
[self dispatchOnSlide:self.contentView.frame.origin.y];
|
|
151
208
|
}
|
|
152
209
|
|
|
153
210
|
if (translationY < 0 && top > self.minY) {
|
|
154
211
|
//向上拖
|
|
155
212
|
CGFloat y = fmax(top + translationY, self.minY);
|
|
156
|
-
self.frame = CGRectOffset(self.frame, 0, y - top);
|
|
157
|
-
[self dispatchOnSlide:self.frame.origin.y];
|
|
213
|
+
self.contentView.frame = CGRectOffset(self.contentView.frame, 0, y - top);
|
|
214
|
+
[self dispatchOnSlide:self.contentView.frame.origin.y];
|
|
158
215
|
}
|
|
159
216
|
}
|
|
160
217
|
|
|
161
218
|
if (pan.state == UIGestureRecognizerStateEnded || pan.state == UIGestureRecognizerStateCancelled) {
|
|
162
|
-
|
|
219
|
+
// RCTLogInfo(@"velocity:%f", [pan velocityInView:self.contentView].y);
|
|
220
|
+
CGFloat velocity = [pan velocityInView:self.contentView].y;
|
|
221
|
+
if (velocity > 400) {
|
|
163
222
|
if (self.target && self.target.contentOffset.y <= 0) {
|
|
164
223
|
//如果是类似轻扫的那种
|
|
165
|
-
[self settleToState
|
|
224
|
+
[self settleToState:RNBottomSheetStateCollapsed];
|
|
166
225
|
}
|
|
167
226
|
|
|
168
227
|
if (!self.target) {
|
|
169
228
|
//如果是类似轻扫的那种
|
|
170
|
-
[self settleToState
|
|
229
|
+
[self settleToState:RNBottomSheetStateCollapsed];
|
|
171
230
|
}
|
|
172
|
-
} else if (
|
|
231
|
+
} else if (velocity < -400) {
|
|
173
232
|
//如果是类似轻扫的那种
|
|
174
|
-
[self settleToState
|
|
233
|
+
[self settleToState:RNBottomSheetStateExpanded];
|
|
175
234
|
} else {
|
|
176
235
|
//如果是普通拖拽
|
|
177
|
-
if(fabs(self.frame.origin.y - self.minY) > fabs(self.frame.origin.y - self.maxY)) {
|
|
178
|
-
[self settleToState
|
|
236
|
+
if(fabs(self.contentView.frame.origin.y - self.minY) > fabs(self.contentView.frame.origin.y - self.maxY)) {
|
|
237
|
+
[self settleToState:RNBottomSheetStateCollapsed];
|
|
179
238
|
} else {
|
|
180
|
-
[self settleToState
|
|
239
|
+
[self settleToState:RNBottomSheetStateExpanded];
|
|
181
240
|
}
|
|
182
241
|
}
|
|
183
242
|
}
|
|
184
|
-
|
|
185
|
-
self.lastDragDistance = translationY;
|
|
186
243
|
}
|
|
187
244
|
|
|
188
245
|
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(UIScrollView *)target change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
|
|
@@ -214,7 +271,7 @@
|
|
|
214
271
|
|
|
215
272
|
if (dy < 0) {
|
|
216
273
|
//向上
|
|
217
|
-
if (self.frame.origin.y > self.minY) {
|
|
274
|
+
if (self.contentView.frame.origin.y > self.minY) {
|
|
218
275
|
_nextReturn = true;
|
|
219
276
|
target.contentOffset = CGPointMake(0, old);
|
|
220
277
|
}
|
|
@@ -223,20 +280,20 @@
|
|
|
223
280
|
|
|
224
281
|
- (void)setPeekHeight:(CGFloat)peekHeight {
|
|
225
282
|
_peekHeight = peekHeight;
|
|
226
|
-
if (!CGRectEqualToRect(self.frame, CGRectZero)) {
|
|
283
|
+
if (!CGRectEqualToRect(self.contentView.frame, CGRectZero)) {
|
|
227
284
|
[self calculateOffset];
|
|
228
|
-
if (
|
|
229
|
-
[self settleToState
|
|
285
|
+
if (self.state == RNBottomSheetStateCollapsed) {
|
|
286
|
+
[self settleToState:RNBottomSheetStateCollapsed];
|
|
230
287
|
}
|
|
231
288
|
}
|
|
232
289
|
}
|
|
233
290
|
|
|
234
|
-
- (void)setState:(
|
|
235
|
-
if (
|
|
291
|
+
- (void)setState:(RNBottomSheetState)state {
|
|
292
|
+
if (_state == state) {
|
|
236
293
|
return;
|
|
237
294
|
}
|
|
238
295
|
|
|
239
|
-
if (CGRectEqualToRect(self.frame, CGRectZero)) {
|
|
296
|
+
if (CGRectEqualToRect(self.contentView.frame, CGRectZero)) {
|
|
240
297
|
[self setStateInternal:state];
|
|
241
298
|
return;
|
|
242
299
|
}
|
|
@@ -244,24 +301,24 @@
|
|
|
244
301
|
[self settleToState:state];
|
|
245
302
|
}
|
|
246
303
|
|
|
247
|
-
- (void)settleToState:(
|
|
248
|
-
if (
|
|
304
|
+
- (void)settleToState:(RNBottomSheetState)state {
|
|
305
|
+
if (state == RNBottomSheetStateCollapsed) {
|
|
249
306
|
[self startSettlingToState:state top:self.maxY];
|
|
250
|
-
} else if (
|
|
307
|
+
} else if (state == RNBottomSheetStateExpanded) {
|
|
251
308
|
[self startSettlingToState:state top:self.minY];
|
|
252
|
-
} else if (
|
|
253
|
-
[self startSettlingToState
|
|
309
|
+
} else if (state == RNBottomSheetStateHidden) {
|
|
310
|
+
[self startSettlingToState:state top:self.frame.size.height];
|
|
254
311
|
}
|
|
255
312
|
}
|
|
256
313
|
|
|
257
|
-
- (void)startSettlingToState:(
|
|
314
|
+
- (void)startSettlingToState:(RNBottomSheetState)state top:(CGFloat)top {
|
|
258
315
|
self.target.pagingEnabled = YES;
|
|
259
|
-
[self setStateInternal
|
|
316
|
+
[self setStateInternal:RNBottomSheetStateSettling];
|
|
260
317
|
[self startWatchBottomSheetTransition];
|
|
261
318
|
[self.layer removeAllAnimations];
|
|
262
|
-
CGFloat duration = fmin(fabs(self.frame.origin.y - top) / (self.maxY - self.minY) * 0.3, 0.3);
|
|
263
|
-
[UIView animateWithDuration:
|
|
264
|
-
self.frame = CGRectOffset(self.frame, 0, top - self.frame.origin.y);
|
|
319
|
+
// CGFloat duration = fmin(fabs(self.contentView.frame.origin.y - top) / (self.maxY - self.minY) * 0.3, 0.3);
|
|
320
|
+
[UIView animateWithDuration:0.25 delay:0 usingSpringWithDamping:1 initialSpringVelocity:0.5 options:NULL animations:^{
|
|
321
|
+
self.contentView.frame = CGRectOffset(self.contentView.frame, 0, top - self.contentView.frame.origin.y);
|
|
265
322
|
} completion:^(BOOL finished) {
|
|
266
323
|
self.target.pagingEnabled = NO;
|
|
267
324
|
[self stopWatchBottomSheetTransition];
|
|
@@ -269,18 +326,14 @@
|
|
|
269
326
|
}];
|
|
270
327
|
}
|
|
271
328
|
|
|
272
|
-
- (void)setStateInternal:(
|
|
273
|
-
if (
|
|
329
|
+
- (void)setStateInternal:(RNBottomSheetState)state {
|
|
330
|
+
if (_state == state) {
|
|
274
331
|
return;
|
|
275
332
|
}
|
|
276
333
|
_state = state;
|
|
277
334
|
|
|
278
|
-
if (
|
|
279
|
-
|
|
280
|
-
self.onStateChanged(@{
|
|
281
|
-
@"state": state,
|
|
282
|
-
});
|
|
283
|
-
}
|
|
335
|
+
if (state == RNBottomSheetStateCollapsed || state == RNBottomSheetStateExpanded || state == RNBottomSheetStateHidden) {
|
|
336
|
+
[self.eventDispatcher sendEvent:[[RNBottomSheetStateChangedEvent alloc] initWithViewTag:self.reactTag state:state]];
|
|
284
337
|
}
|
|
285
338
|
}
|
|
286
339
|
|
|
@@ -288,15 +341,8 @@
|
|
|
288
341
|
if (top < 0 || self.maxY == 0) {
|
|
289
342
|
return;
|
|
290
343
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
self.onSlide(@{
|
|
294
|
-
@"progress": @(progress),
|
|
295
|
-
@"offset": @(top),
|
|
296
|
-
@"collapsedOffset": @(self.maxY),
|
|
297
|
-
@"expandedOffset": @(self.minY)
|
|
298
|
-
});
|
|
299
|
-
}
|
|
344
|
+
CGFloat progress = fmin((top - self.minY) * 1.0f / (self.maxY - self.minY), 1);
|
|
345
|
+
[self.eventDispatcher sendEvent:[[RNBottomSheetOffsetChangedEvent alloc] initWithViewTag:self.reactTag progress:progress offset:top minY:self.minY maxY:self.maxY]];
|
|
300
346
|
}
|
|
301
347
|
|
|
302
348
|
- (void)startWatchBottomSheetTransition {
|
|
@@ -307,9 +353,9 @@
|
|
|
307
353
|
}
|
|
308
354
|
|
|
309
355
|
- (void)stopWatchBottomSheetTransition {
|
|
310
|
-
if (
|
|
356
|
+
if (self.state == RNBottomSheetStateCollapsed) {
|
|
311
357
|
[self dispatchOnSlide:self.maxY];
|
|
312
|
-
} else if (
|
|
358
|
+
} else if (self.state == RNBottomSheetStateExpanded) {
|
|
313
359
|
[self dispatchOnSlide:self.minY];
|
|
314
360
|
}
|
|
315
361
|
if(_displayLink){
|
|
@@ -319,7 +365,7 @@
|
|
|
319
365
|
}
|
|
320
366
|
|
|
321
367
|
- (void)watchBottomSheetTransition {
|
|
322
|
-
CGFloat top = [self.layer presentationLayer].frame.origin.y;
|
|
368
|
+
CGFloat top = [self.contentView.layer presentationLayer].frame.origin.y;
|
|
323
369
|
[self dispatchOnSlide:top];
|
|
324
370
|
}
|
|
325
371
|
|
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
#import "RNBottomSheetManager.h"
|
|
2
2
|
#import "RNBottomSheet.h"
|
|
3
|
+
#import "RNBottomSheetState.h"
|
|
3
4
|
|
|
4
5
|
@implementation RNBottomSheetManager
|
|
5
6
|
|
|
6
7
|
RCT_EXPORT_MODULE(BottomSheet)
|
|
7
8
|
|
|
8
9
|
- (UIView *)view {
|
|
9
|
-
|
|
10
|
+
RNBottomSheet *bottomSheet = [[RNBottomSheet alloc] initWithEventDispatcher:self.bridge.eventDispatcher];
|
|
11
|
+
bottomSheet.pointerEvents = RCTPointerEventsBoxNone;
|
|
12
|
+
return bottomSheet;
|
|
10
13
|
}
|
|
11
14
|
|
|
12
15
|
RCT_EXPORT_VIEW_PROPERTY(onSlide, RCTDirectEventBlock)
|
|
13
16
|
RCT_EXPORT_VIEW_PROPERTY(onStateChanged, RCTDirectEventBlock)
|
|
14
|
-
|
|
15
17
|
RCT_EXPORT_VIEW_PROPERTY(peekHeight, CGFloat)
|
|
16
|
-
|
|
18
|
+
|
|
19
|
+
RCT_CUSTOM_VIEW_PROPERTY(state, NSString, RNBottomSheet) {
|
|
20
|
+
view.state = RNBottomSheetStateFromString([RCTConvert NSString:json]);
|
|
21
|
+
}
|
|
17
22
|
|
|
18
23
|
@end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#import <React/RCTEventDispatcherProtocol.h>
|
|
2
|
+
|
|
3
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
4
|
+
|
|
5
|
+
@interface RNBottomSheetOffsetChangedEvent : NSObject <RCTEvent>
|
|
6
|
+
|
|
7
|
+
- (instancetype)initWithViewTag:(NSNumber *)viewTag progress:(CGFloat)progress offset:(CGFloat)offset minY:(CGFloat)minY maxY:(CGFloat)maxY;
|
|
8
|
+
|
|
9
|
+
@end
|
|
10
|
+
|
|
11
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#import "RNBottomSheetOffsetChangedEvent.h"
|
|
2
|
+
|
|
3
|
+
@interface RNBottomSheetOffsetChangedEvent ()
|
|
4
|
+
|
|
5
|
+
@property (nonatomic, assign) CGFloat progress;
|
|
6
|
+
@property (nonatomic, assign) CGFloat offest;
|
|
7
|
+
@property (nonatomic, assign) CGFloat minY;
|
|
8
|
+
@property (nonatomic, assign) CGFloat maxY;
|
|
9
|
+
|
|
10
|
+
@end
|
|
11
|
+
|
|
12
|
+
@implementation RNBottomSheetOffsetChangedEvent
|
|
13
|
+
|
|
14
|
+
@synthesize viewTag = _viewTag;
|
|
15
|
+
|
|
16
|
+
+ (NSString *)moduleDotMethod {
|
|
17
|
+
return @"RCTEventEmitter.receiveEvent";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
RCT_NOT_IMPLEMENTED(- (instancetype)init)
|
|
21
|
+
- (instancetype)initWithViewTag:(NSNumber *)viewTag progress:(CGFloat)progress offset:(CGFloat)offset minY:(CGFloat)minY maxY:(CGFloat)maxY {
|
|
22
|
+
if (self = [super init]) {
|
|
23
|
+
_viewTag = viewTag;
|
|
24
|
+
_progress = progress;
|
|
25
|
+
_offest = offset;
|
|
26
|
+
_minY = minY;
|
|
27
|
+
_maxY = maxY;
|
|
28
|
+
}
|
|
29
|
+
return self;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
- (NSString *)eventName {
|
|
33
|
+
return @"onSlide";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
- (BOOL)canCoalesce {
|
|
37
|
+
return YES;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
- (uint16_t)coalescingKey {
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
- (NSArray *)arguments {
|
|
45
|
+
return @[self.viewTag, RCTNormalizeInputEventName(self.eventName), @{
|
|
46
|
+
@"progress": @(self.progress),
|
|
47
|
+
@"offset": @(self.offest),
|
|
48
|
+
@"expandedOffset": @(self.minY),
|
|
49
|
+
@"collapsedOffset": @(self.maxY)
|
|
50
|
+
}];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
- (id<RCTEvent>)coalesceWithEvent:(id<RCTEvent>)newEvent {
|
|
54
|
+
return newEvent;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#import <React/RCTUtils.h>
|
|
2
|
+
|
|
3
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
4
|
+
|
|
5
|
+
typedef NS_ENUM(NSUInteger, RNBottomSheetState) {
|
|
6
|
+
RNBottomSheetStateCollapsed = 0,
|
|
7
|
+
RNBottomSheetStateExpanded,
|
|
8
|
+
RNBottomSheetStateHidden,
|
|
9
|
+
RNBottomSheetStateDragging,
|
|
10
|
+
RNBottomSheetStateSettling,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
RCT_EXTERN RNBottomSheetState RNBottomSheetStateFromString(NSString *state);
|
|
14
|
+
|
|
15
|
+
RCT_EXTERN NSString* RNBottomSheetStateToString(RNBottomSheetState state);
|
|
16
|
+
|
|
17
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#import "RNBottomSheetState.h"
|
|
2
|
+
|
|
3
|
+
RNBottomSheetState RNBottomSheetStateFromString(NSString *state) {
|
|
4
|
+
if ([state isEqualToString:@"collapsed"]) {
|
|
5
|
+
return RNBottomSheetStateCollapsed;
|
|
6
|
+
}
|
|
7
|
+
if ([state isEqualToString:@"expanded"]) {
|
|
8
|
+
return RNBottomSheetStateExpanded;
|
|
9
|
+
}
|
|
10
|
+
if ([state isEqualToString:@"settling"]) {
|
|
11
|
+
return RNBottomSheetStateSettling;
|
|
12
|
+
}
|
|
13
|
+
if ([state isEqualToString:@"dragging"]) {
|
|
14
|
+
return RNBottomSheetStateDragging;
|
|
15
|
+
}
|
|
16
|
+
return RNBottomSheetStateHidden;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
NSString* RNBottomSheetStateToString(RNBottomSheetState state) {
|
|
21
|
+
if (state == RNBottomSheetStateCollapsed) {
|
|
22
|
+
return @"collapsed";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (state == RNBottomSheetStateExpanded) {
|
|
26
|
+
return @"expanded";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (state == RNBottomSheetStateSettling) {
|
|
30
|
+
return @"settling";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (state == RNBottomSheetStateDragging) {
|
|
34
|
+
return @"dragging";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return @"hidden";
|
|
38
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#import <React/RCTEventDispatcherProtocol.h>
|
|
2
|
+
|
|
3
|
+
#import "RNBottomSheetState.h"
|
|
4
|
+
|
|
5
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
6
|
+
|
|
7
|
+
@interface RNBottomSheetStateChangedEvent : NSObject <RCTEvent>
|
|
8
|
+
|
|
9
|
+
- (instancetype)initWithViewTag:(NSNumber *)viewTag state:(RNBottomSheetState)state;
|
|
10
|
+
|
|
11
|
+
@end
|
|
12
|
+
|
|
13
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#import "RNBottomSheetStateChangedEvent.h"
|
|
2
|
+
|
|
3
|
+
static uint16_t _coalescingKey = 0;
|
|
4
|
+
|
|
5
|
+
@interface RNBottomSheetStateChangedEvent ()
|
|
6
|
+
|
|
7
|
+
@property (nonatomic, assign) RNBottomSheetState state;
|
|
8
|
+
|
|
9
|
+
@end
|
|
10
|
+
|
|
11
|
+
@implementation RNBottomSheetStateChangedEvent
|
|
12
|
+
|
|
13
|
+
@synthesize viewTag = _viewTag;
|
|
14
|
+
|
|
15
|
+
+ (NSString *)moduleDotMethod {
|
|
16
|
+
return @"RCTEventEmitter.receiveEvent";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
RCT_NOT_IMPLEMENTED(- (instancetype)init)
|
|
20
|
+
- (instancetype)initWithViewTag:(NSNumber *)viewTag state:(RNBottomSheetState)state {
|
|
21
|
+
if (self = [super init]) {
|
|
22
|
+
_viewTag = viewTag;
|
|
23
|
+
_state = state;
|
|
24
|
+
}
|
|
25
|
+
return self;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
- (NSString *)eventName {
|
|
29
|
+
return @"onStateChanged";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
- (BOOL)canCoalesce {
|
|
33
|
+
return NO;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
- (uint16_t)coalescingKey {
|
|
37
|
+
return _coalescingKey++;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
- (NSArray *)arguments {
|
|
41
|
+
return @[self.viewTag, RCTNormalizeInputEventName(self.eventName), @{
|
|
42
|
+
@"state": RNBottomSheetStateToString(self.state)
|
|
43
|
+
}];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
- (id<RCTEvent>)coalesceWithEvent:(id<RCTEvent>)newEvent {
|
|
47
|
+
return newEvent;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@end
|
package/lib/index.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ interface NativeBottomSheetProps extends ViewProps {
|
|
|
15
15
|
onStateChanged?: (event: NativeSyntheticEvent<StateChangedEventData>) => void;
|
|
16
16
|
peekHeight?: number;
|
|
17
17
|
state?: BottomSheetState;
|
|
18
|
+
contentContainerStyle?: ViewProps['style'];
|
|
18
19
|
}
|
|
19
20
|
declare const BottomSheet: React.ForwardRefExoticComponent<NativeBottomSheetProps & {
|
|
20
21
|
fitToContents?: boolean | undefined;
|
package/lib/index.js
CHANGED
|
@@ -1,24 +1,15 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { requireNativeComponent, StyleSheet, View } from 'react-native';
|
|
3
3
|
import splitLayoutProps from './splitLayoutProps';
|
|
4
4
|
const NativeBottomSheet = requireNativeComponent('BottomSheet');
|
|
5
5
|
const BottomSheet = React.forwardRef((props, ref) => {
|
|
6
|
-
const { style, children, peekHeight = 200, state = 'collapsed', fitToContents, ...rest } = props;
|
|
6
|
+
const { style, contentContainerStyle, children, peekHeight = 200, state = 'collapsed', fitToContents, ...rest } = props;
|
|
7
7
|
const { outer, inner } = splitLayoutProps(StyleSheet.flatten(style));
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
</NativeBottomSheet>);
|
|
14
|
-
}
|
|
15
|
-
else {
|
|
16
|
-
return (<View style={[StyleSheet.absoluteFill, outer]} pointerEvents="box-none">
|
|
17
|
-
<NativeBottomSheet style={[fitToContents ? styles.fitToContents : StyleSheet.absoluteFill, inner]} peekHeight={peekHeight} state={state} {...rest} ref={ref}>
|
|
18
|
-
{children}
|
|
19
|
-
</NativeBottomSheet>
|
|
20
|
-
</View>);
|
|
21
|
-
}
|
|
8
|
+
return (<NativeBottomSheet style={[StyleSheet.absoluteFill, outer]} peekHeight={peekHeight} state={state} {...rest} ref={ref}>
|
|
9
|
+
<View style={[fitToContents ? styles.fitToContents : StyleSheet.absoluteFill, inner, contentContainerStyle]} collapsable={false}>
|
|
10
|
+
{children}
|
|
11
|
+
</View>
|
|
12
|
+
</NativeBottomSheet>);
|
|
22
13
|
});
|
|
23
14
|
const styles = StyleSheet.create({
|
|
24
15
|
fitToContents: {
|
package/package.json
CHANGED
package/src/index.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import { NativeSyntheticEvent,
|
|
2
|
+
import { NativeSyntheticEvent, requireNativeComponent, StyleSheet, View, ViewProps } from 'react-native'
|
|
3
3
|
import splitLayoutProps from './splitLayoutProps'
|
|
4
4
|
|
|
5
5
|
export interface OffsetChangedEventData {
|
|
@@ -20,6 +20,7 @@ interface NativeBottomSheetProps extends ViewProps {
|
|
|
20
20
|
onStateChanged?: (event: NativeSyntheticEvent<StateChangedEventData>) => void
|
|
21
21
|
peekHeight?: number
|
|
22
22
|
state?: BottomSheetState
|
|
23
|
+
contentContainerStyle?: ViewProps['style']
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
type BottomSheetProps = NativeBottomSheetProps & {
|
|
@@ -31,36 +32,30 @@ const NativeBottomSheet = requireNativeComponent<NativeBottomSheetProps>('Bottom
|
|
|
31
32
|
type NativeBottomSheetInstance = InstanceType<typeof NativeBottomSheet>
|
|
32
33
|
|
|
33
34
|
const BottomSheet = React.forwardRef<NativeBottomSheetInstance, BottomSheetProps>((props, ref) => {
|
|
34
|
-
const {
|
|
35
|
+
const {
|
|
36
|
+
style,
|
|
37
|
+
contentContainerStyle,
|
|
38
|
+
children,
|
|
39
|
+
peekHeight = 200,
|
|
40
|
+
state = 'collapsed',
|
|
41
|
+
fitToContents,
|
|
42
|
+
...rest
|
|
43
|
+
} = props
|
|
35
44
|
const { outer, inner } = splitLayoutProps(StyleSheet.flatten(style))
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
</View>
|
|
48
|
-
</NativeBottomSheet>
|
|
49
|
-
)
|
|
50
|
-
} else {
|
|
51
|
-
return (
|
|
52
|
-
<View style={[StyleSheet.absoluteFill, outer]} pointerEvents="box-none">
|
|
53
|
-
<NativeBottomSheet
|
|
54
|
-
style={[fitToContents ? styles.fitToContents : StyleSheet.absoluteFill, inner]}
|
|
55
|
-
peekHeight={peekHeight}
|
|
56
|
-
state={state}
|
|
57
|
-
{...rest}
|
|
58
|
-
ref={ref}>
|
|
59
|
-
{children}
|
|
60
|
-
</NativeBottomSheet>
|
|
45
|
+
return (
|
|
46
|
+
<NativeBottomSheet
|
|
47
|
+
style={[StyleSheet.absoluteFill, outer]}
|
|
48
|
+
peekHeight={peekHeight}
|
|
49
|
+
state={state}
|
|
50
|
+
{...rest}
|
|
51
|
+
ref={ref}>
|
|
52
|
+
<View
|
|
53
|
+
style={[fitToContents ? styles.fitToContents : StyleSheet.absoluteFill, inner, contentContainerStyle]}
|
|
54
|
+
collapsable={false}>
|
|
55
|
+
{children}
|
|
61
56
|
</View>
|
|
62
|
-
|
|
63
|
-
|
|
57
|
+
</NativeBottomSheet>
|
|
58
|
+
)
|
|
64
59
|
})
|
|
65
60
|
|
|
66
61
|
const styles = StyleSheet.create({
|