@lodev09/react-native-true-sheet 3.9.9 → 3.10.0-beta.1
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 +1 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +1 -2
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +41 -14
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetModule.kt +10 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +10 -12
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +49 -43
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +11 -2
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetBehavior.kt +36 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetView.kt +14 -3
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +3 -3
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDimView.kt +3 -1
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetGrabberView.kt +51 -0
- package/android/src/main/java/com/lodev09/truesheet/events/TrueSheetLifecycleEvents.kt +8 -5
- package/android/src/main/java/com/lodev09/truesheet/utils/ViewUtils.kt +17 -0
- package/ios/TrueSheetContainerView.h +8 -1
- package/ios/TrueSheetContainerView.mm +14 -7
- package/ios/TrueSheetContentView.mm +10 -7
- package/ios/TrueSheetModule.mm +5 -0
- package/ios/TrueSheetView.mm +40 -40
- package/ios/TrueSheetViewController.h +3 -1
- package/ios/TrueSheetViewController.mm +67 -18
- package/ios/core/TrueSheetGrabberView.h +20 -0
- package/ios/core/TrueSheetGrabberView.mm +58 -12
- package/lib/module/TrueSheet.js +27 -7
- package/lib/module/TrueSheet.js.map +1 -1
- package/lib/module/fabric/TrueSheetViewNativeComponent.ts +2 -1
- package/lib/module/specs/NativeTrueSheetModule.js.map +1 -1
- package/lib/typescript/src/TrueSheet.d.ts +5 -1
- package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.types.d.ts +9 -3
- package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +4 -1
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/specs/NativeTrueSheetModule.d.ts +6 -0
- package/lib/typescript/src/specs/NativeTrueSheetModule.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/TrueSheet.tsx +40 -8
- package/src/TrueSheet.types.ts +10 -3
- package/src/fabric/TrueSheetViewNativeComponent.ts +2 -1
- package/src/specs/NativeTrueSheetModule.ts +7 -0
|
@@ -5,8 +5,10 @@ import android.content.Context
|
|
|
5
5
|
import android.content.res.Configuration
|
|
6
6
|
import android.graphics.Color
|
|
7
7
|
import android.graphics.drawable.GradientDrawable
|
|
8
|
+
import android.os.Bundle
|
|
8
9
|
import android.view.Gravity
|
|
9
10
|
import android.view.View
|
|
11
|
+
import android.view.accessibility.AccessibilityNodeInfo
|
|
10
12
|
import android.widget.FrameLayout
|
|
11
13
|
import androidx.core.graphics.ColorUtils
|
|
12
14
|
import com.facebook.react.uimanager.PixelUtil.dpToPx
|
|
@@ -58,6 +60,9 @@ class TrueSheetGrabberView(context: Context, private val options: GrabberOptions
|
|
|
58
60
|
private val grabberColor: Int
|
|
59
61
|
get() = if (isAdaptive) getAdaptiveColor(options?.color) else options?.color ?: DEFAULT_COLOR
|
|
60
62
|
|
|
63
|
+
var onAccessibilityIncrement: (() -> Unit)? = null
|
|
64
|
+
var onAccessibilityDecrement: (() -> Unit)? = null
|
|
65
|
+
|
|
61
66
|
init {
|
|
62
67
|
val hitboxWidth = grabberWidth + (HITBOX_PADDING_HORIZONTAL * 2)
|
|
63
68
|
val hitboxHeight = grabberHeight + (HITBOX_PADDING_VERTICAL * 2)
|
|
@@ -86,6 +91,52 @@ class TrueSheetGrabberView(context: Context, private val options: GrabberOptions
|
|
|
86
91
|
}
|
|
87
92
|
|
|
88
93
|
addView(pillView)
|
|
94
|
+
|
|
95
|
+
isFocusable = true
|
|
96
|
+
contentDescription = "Sheet Grabber"
|
|
97
|
+
|
|
98
|
+
accessibilityDelegate = object : View.AccessibilityDelegate() {
|
|
99
|
+
override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfo) {
|
|
100
|
+
super.onInitializeAccessibilityNodeInfo(host, info)
|
|
101
|
+
info.addAction(
|
|
102
|
+
AccessibilityNodeInfo.AccessibilityAction(
|
|
103
|
+
AccessibilityNodeInfo.ACTION_SCROLL_FORWARD,
|
|
104
|
+
"Expand"
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
info.addAction(
|
|
108
|
+
AccessibilityNodeInfo.AccessibilityAction(
|
|
109
|
+
AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD,
|
|
110
|
+
"Collapse"
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
info.className = "android.widget.SeekBar"
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
override fun performAccessibilityAction(host: View, action: Int, args: Bundle?): Boolean =
|
|
117
|
+
when (action) {
|
|
118
|
+
AccessibilityNodeInfo.ACTION_SCROLL_FORWARD -> {
|
|
119
|
+
onAccessibilityIncrement?.invoke()
|
|
120
|
+
true
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD -> {
|
|
124
|
+
onAccessibilityDecrement?.invoke()
|
|
125
|
+
true
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
else -> super.performAccessibilityAction(host, action, args)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
fun updateAccessibilityValue(index: Int, detentCount: Int) {
|
|
134
|
+
stateDescription = when {
|
|
135
|
+
index < 0 || detentCount <= 0 -> null
|
|
136
|
+
index >= detentCount - 1 -> "Expanded"
|
|
137
|
+
index == 0 -> "Collapsed"
|
|
138
|
+
else -> "Detent ${index + 1} of $detentCount"
|
|
139
|
+
}
|
|
89
140
|
}
|
|
90
141
|
|
|
91
142
|
private fun getAdaptiveColor(baseColor: Int? = null): Int {
|
|
@@ -94,16 +94,19 @@ class DidDismissEvent(surfaceId: Int, viewId: Int) : Event<DidDismissEvent>(surf
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
/**
|
|
97
|
-
* Fired when the
|
|
97
|
+
* Fired when the sheet visibility changes due to screen transitions
|
|
98
98
|
*/
|
|
99
|
-
class
|
|
99
|
+
class VisibilityChangeEvent(surfaceId: Int, viewId: Int, private val visible: Boolean) : Event<VisibilityChangeEvent>(surfaceId, viewId) {
|
|
100
100
|
|
|
101
101
|
override fun getEventName(): String = EVENT_NAME
|
|
102
102
|
|
|
103
|
-
override fun getEventData(): WritableMap =
|
|
103
|
+
override fun getEventData(): WritableMap =
|
|
104
|
+
Arguments.createMap().apply {
|
|
105
|
+
putBoolean("visible", visible)
|
|
106
|
+
}
|
|
104
107
|
|
|
105
108
|
companion object {
|
|
106
|
-
const val EVENT_NAME = "
|
|
107
|
-
const val REGISTRATION_NAME = "
|
|
109
|
+
const val EVENT_NAME = "topVisibilityChange"
|
|
110
|
+
const val REGISTRATION_NAME = "onVisibilityChange"
|
|
108
111
|
}
|
|
109
112
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
package com.lodev09.truesheet.utils
|
|
2
2
|
|
|
3
3
|
import android.view.View
|
|
4
|
+
import android.view.ViewGroup
|
|
5
|
+
import android.widget.ScrollView
|
|
6
|
+
import androidx.core.widget.NestedScrollView
|
|
4
7
|
|
|
5
8
|
fun View.isDescendantOf(ancestor: View): Boolean {
|
|
6
9
|
if (!isAttachedToWindow) return false
|
|
@@ -11,3 +14,17 @@ fun View.isDescendantOf(ancestor: View): Boolean {
|
|
|
11
14
|
}
|
|
12
15
|
return false
|
|
13
16
|
}
|
|
17
|
+
|
|
18
|
+
fun ViewGroup.smoothScrollBy(dx: Int, dy: Int) {
|
|
19
|
+
when (this) {
|
|
20
|
+
is ScrollView -> smoothScrollBy(dx, dy)
|
|
21
|
+
is NestedScrollView -> smoothScrollBy(dx, dy)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
fun ViewGroup.smoothScrollTo(x: Int, y: Int) {
|
|
26
|
+
when (this) {
|
|
27
|
+
is ScrollView -> smoothScrollTo(x, y)
|
|
28
|
+
is NestedScrollView -> smoothScrollTo(x, y)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -13,6 +13,13 @@
|
|
|
13
13
|
|
|
14
14
|
NS_ASSUME_NONNULL_BEGIN
|
|
15
15
|
|
|
16
|
+
@interface ScrollableOptions : NSObject
|
|
17
|
+
|
|
18
|
+
@property (nonatomic, assign) CGFloat keyboardScrollOffset;
|
|
19
|
+
@property (nonatomic, assign) BOOL scrollingExpandsSheet;
|
|
20
|
+
|
|
21
|
+
@end
|
|
22
|
+
|
|
16
23
|
@protocol TrueSheetContainerViewDelegate <NSObject>
|
|
17
24
|
|
|
18
25
|
- (void)containerViewContentDidChangeSize:(CGSize)newSize;
|
|
@@ -44,7 +51,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
44
51
|
/**
|
|
45
52
|
* Options for scrollable behavior
|
|
46
53
|
*/
|
|
47
|
-
@property (nonatomic, strong, nullable)
|
|
54
|
+
@property (nonatomic, strong, nullable) ScrollableOptions *scrollableOptions;
|
|
48
55
|
|
|
49
56
|
/**
|
|
50
57
|
* Returns the current content height
|
|
@@ -27,6 +27,18 @@
|
|
|
27
27
|
|
|
28
28
|
using namespace facebook::react;
|
|
29
29
|
|
|
30
|
+
@implementation ScrollableOptions
|
|
31
|
+
|
|
32
|
+
- (instancetype)init {
|
|
33
|
+
if (self = [super init]) {
|
|
34
|
+
_keyboardScrollOffset = 0;
|
|
35
|
+
_scrollingExpandsSheet = YES;
|
|
36
|
+
}
|
|
37
|
+
return self;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@end
|
|
41
|
+
|
|
30
42
|
@interface TrueSheetContainerView () <TrueSheetContentViewDelegate, TrueSheetHeaderViewDelegate>
|
|
31
43
|
@end
|
|
32
44
|
|
|
@@ -87,14 +99,9 @@ using namespace facebook::react;
|
|
|
87
99
|
_scrollableSet = YES;
|
|
88
100
|
}
|
|
89
101
|
|
|
90
|
-
- (void)setScrollableOptions:(
|
|
102
|
+
- (void)setScrollableOptions:(ScrollableOptions *)scrollableOptions {
|
|
91
103
|
_scrollableOptions = scrollableOptions;
|
|
92
|
-
|
|
93
|
-
NSNumber *offset = scrollableOptions[@"keyboardScrollOffset"];
|
|
94
|
-
_contentView.keyboardScrollOffset = offset ? [offset floatValue] : 0;
|
|
95
|
-
} else {
|
|
96
|
-
_contentView.keyboardScrollOffset = 0;
|
|
97
|
-
}
|
|
104
|
+
_contentView.keyboardScrollOffset = scrollableOptions ? scrollableOptions.keyboardScrollOffset : 0;
|
|
98
105
|
}
|
|
99
106
|
|
|
100
107
|
- (void)setupScrollable {
|
|
@@ -216,15 +216,18 @@ using namespace facebook::react;
|
|
|
216
216
|
animations:^{
|
|
217
217
|
[self setScrollViewContentInset:height
|
|
218
218
|
indicatorInset:self->_originalIndicatorBottomInset + height];
|
|
219
|
-
|
|
220
|
-
if (firstResponder) {
|
|
221
|
-
CGRect responderFrame = [firstResponder convertRect:firstResponder.bounds
|
|
222
|
-
toView:self->_pinnedScrollView.scrollView];
|
|
223
|
-
responderFrame.size.height += self.keyboardScrollOffset;
|
|
224
|
-
[self->_pinnedScrollView.scrollView scrollRectToVisible:responderFrame animated:NO];
|
|
225
|
-
}
|
|
226
219
|
}
|
|
227
220
|
completion:nil];
|
|
221
|
+
|
|
222
|
+
// Defer scroll until the next run loop so content insets are applied first
|
|
223
|
+
if (firstResponder) {
|
|
224
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
225
|
+
CGRect responderFrame = [firstResponder convertRect:firstResponder.bounds
|
|
226
|
+
toView:self->_pinnedScrollView.scrollView];
|
|
227
|
+
responderFrame.size.height += self.keyboardScrollOffset;
|
|
228
|
+
[self->_pinnedScrollView.scrollView scrollRectToVisible:responderFrame animated:YES];
|
|
229
|
+
});
|
|
230
|
+
}
|
|
228
231
|
}
|
|
229
232
|
|
|
230
233
|
- (void)keyboardWillHide:(NSTimeInterval)duration curve:(UIViewAnimationOptions)curve {
|
package/ios/TrueSheetModule.mm
CHANGED
|
@@ -140,6 +140,11 @@ RCT_EXPORT_MODULE(TrueSheetModule)
|
|
|
140
140
|
});
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
- (void)handleBackPress:(double)viewTag resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
|
144
|
+
// No-op on iOS — no hardware back button
|
|
145
|
+
resolve(nil);
|
|
146
|
+
}
|
|
147
|
+
|
|
143
148
|
- (void)dismissAll:(BOOL)animated resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
|
144
149
|
RCTExecuteOnMainQueue(^{
|
|
145
150
|
@synchronized(viewRegistry) {
|
package/ios/TrueSheetView.mm
CHANGED
|
@@ -53,7 +53,7 @@ using namespace facebook::react;
|
|
|
53
53
|
NSInteger _initialDetentIndex;
|
|
54
54
|
NSInteger _insetAdjustment;
|
|
55
55
|
BOOL _scrollable;
|
|
56
|
-
|
|
56
|
+
ScrollableOptions *_scrollableOptions;
|
|
57
57
|
BOOL _initialDetentAnimated;
|
|
58
58
|
BOOL _isSheetUpdatePending;
|
|
59
59
|
BOOL _pendingLayoutUpdate;
|
|
@@ -213,25 +213,18 @@ using namespace facebook::react;
|
|
|
213
213
|
grabberOpts.cornerRadius >= 0 || grabberColor != nil || !grabberOpts.adaptive;
|
|
214
214
|
|
|
215
215
|
if (hasGrabberOptions) {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (grabberOpts.
|
|
222
|
-
options
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
options[@"cornerRadius"] = @(grabberOpts.cornerRadius);
|
|
229
|
-
}
|
|
230
|
-
if (grabberColor) {
|
|
231
|
-
options[@"color"] = grabberColor;
|
|
232
|
-
}
|
|
233
|
-
options[@"adaptive"] = @(grabberOpts.adaptive);
|
|
234
|
-
|
|
216
|
+
GrabberOptions *options = [[GrabberOptions alloc] init];
|
|
217
|
+
if (grabberOpts.width > 0)
|
|
218
|
+
options.width = @(grabberOpts.width);
|
|
219
|
+
if (grabberOpts.height > 0)
|
|
220
|
+
options.height = @(grabberOpts.height);
|
|
221
|
+
if (grabberOpts.topMargin > 0)
|
|
222
|
+
options.topMargin = @(grabberOpts.topMargin);
|
|
223
|
+
if (grabberOpts.cornerRadius >= 0)
|
|
224
|
+
options.cornerRadius = @(grabberOpts.cornerRadius);
|
|
225
|
+
if (grabberColor)
|
|
226
|
+
options.color = grabberColor;
|
|
227
|
+
options.adaptive = grabberOpts.adaptive;
|
|
235
228
|
_controller.grabberOptions = options;
|
|
236
229
|
} else {
|
|
237
230
|
_controller.grabberOptions = nil;
|
|
@@ -251,26 +244,24 @@ using namespace facebook::react;
|
|
|
251
244
|
_scrollable = newProps.scrollable;
|
|
252
245
|
|
|
253
246
|
const auto &scrollableOpts = newProps.scrollableOptions;
|
|
254
|
-
BOOL
|
|
247
|
+
BOOL scrollingExpandsSheet = scrollableOpts.scrollingExpandsSheet;
|
|
248
|
+
BOOL hasScrollableOptions = scrollableOpts.keyboardScrollOffset > 0 || !scrollingExpandsSheet;
|
|
255
249
|
|
|
256
250
|
if (hasScrollableOptions) {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
}
|
|
251
|
+
ScrollableOptions *options = [[ScrollableOptions alloc] init];
|
|
252
|
+
options.keyboardScrollOffset = scrollableOpts.keyboardScrollOffset;
|
|
253
|
+
options.scrollingExpandsSheet = scrollingExpandsSheet;
|
|
261
254
|
_scrollableOptions = options;
|
|
262
255
|
} else {
|
|
263
256
|
_scrollableOptions = nil;
|
|
264
257
|
}
|
|
265
258
|
|
|
259
|
+
_controller.scrollingExpandsSheet = scrollingExpandsSheet;
|
|
260
|
+
|
|
266
261
|
_insetAdjustment = (NSInteger)newProps.insetAdjustment;
|
|
267
262
|
_controller.insetAdjustment = _insetAdjustment;
|
|
268
263
|
|
|
269
|
-
|
|
270
|
-
_containerView.scrollableEnabled = _scrollable;
|
|
271
|
-
_containerView.insetAdjustment = _insetAdjustment;
|
|
272
|
-
_containerView.scrollableOptions = _scrollableOptions;
|
|
273
|
-
}
|
|
264
|
+
[self setupScrollable];
|
|
274
265
|
}
|
|
275
266
|
|
|
276
267
|
- (void)updateState:(const State::Shared &)state oldState:(const State::Shared &)oldState {
|
|
@@ -319,9 +310,7 @@ using namespace facebook::react;
|
|
|
319
310
|
if (!(updateMask & RNComponentViewUpdateMaskProps) || !_controller)
|
|
320
311
|
return;
|
|
321
312
|
|
|
322
|
-
|
|
323
|
-
[_containerView setupScrollable];
|
|
324
|
-
}
|
|
313
|
+
[self setupScrollable];
|
|
325
314
|
|
|
326
315
|
if (_controller.isPresented) {
|
|
327
316
|
[self applySheetPropsUpdate];
|
|
@@ -389,11 +378,6 @@ using namespace facebook::react;
|
|
|
389
378
|
_controller.headerHeight = @(headerHeight);
|
|
390
379
|
}
|
|
391
380
|
|
|
392
|
-
_containerView.scrollableEnabled = _scrollable;
|
|
393
|
-
_containerView.insetAdjustment = _insetAdjustment;
|
|
394
|
-
_containerView.scrollableOptions = _scrollableOptions;
|
|
395
|
-
[_containerView setupScrollable];
|
|
396
|
-
|
|
397
381
|
if (_eventEmitter) {
|
|
398
382
|
[TrueSheetLifecycleEvents emitMount:_eventEmitter];
|
|
399
383
|
} else {
|
|
@@ -454,6 +438,8 @@ using namespace facebook::react;
|
|
|
454
438
|
[_controller setupSheetDetents];
|
|
455
439
|
[_controller setupActiveDetentWithIndex:index];
|
|
456
440
|
|
|
441
|
+
[self setupScrollable];
|
|
442
|
+
|
|
457
443
|
[_screensEventObserver capturePresenterScreenFromView:self];
|
|
458
444
|
[_screensEventObserver startObservingWithState:_state.get()->getData()];
|
|
459
445
|
|
|
@@ -489,7 +475,11 @@ using namespace facebook::react;
|
|
|
489
475
|
}
|
|
490
476
|
|
|
491
477
|
- (void)emitDismissedPosition {
|
|
492
|
-
[
|
|
478
|
+
[TrueSheetStateEvents emitPositionChange:_eventEmitter
|
|
479
|
+
index:-1
|
|
480
|
+
position:_controller.screenHeight
|
|
481
|
+
detent:0
|
|
482
|
+
realtime:NO];
|
|
493
483
|
}
|
|
494
484
|
|
|
495
485
|
- (void)dismissAnimated:(BOOL)animated completion:(nullable TrueSheetCompletionBlock)completion {
|
|
@@ -576,7 +566,7 @@ using namespace facebook::react;
|
|
|
576
566
|
|
|
577
567
|
// When the ScrollView changes (e.g. conditional remount), re-pin the new ScrollView.
|
|
578
568
|
- (void)containerViewScrollViewDidChange {
|
|
579
|
-
[
|
|
569
|
+
[self setupScrollable];
|
|
580
570
|
}
|
|
581
571
|
|
|
582
572
|
#pragma mark - TrueSheetViewControllerDelegate
|
|
@@ -698,6 +688,16 @@ using namespace facebook::react;
|
|
|
698
688
|
|
|
699
689
|
#pragma mark - Private Helpers
|
|
700
690
|
|
|
691
|
+
- (void)setupScrollable {
|
|
692
|
+
if (!_containerView)
|
|
693
|
+
return;
|
|
694
|
+
|
|
695
|
+
_containerView.scrollableEnabled = _scrollable;
|
|
696
|
+
_containerView.insetAdjustment = _insetAdjustment;
|
|
697
|
+
_containerView.scrollableOptions = _scrollableOptions;
|
|
698
|
+
[_containerView setupScrollable];
|
|
699
|
+
}
|
|
700
|
+
|
|
701
701
|
- (void)applySheetPropsUpdate {
|
|
702
702
|
BOOL pendingLayoutUpdate = _pendingLayoutUpdate;
|
|
703
703
|
_pendingLayoutUpdate = NO;
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
#import <UIKit/UIKit.h>
|
|
10
10
|
#import "core/TrueSheetDetentCalculator.h"
|
|
11
|
+
#import "core/TrueSheetGrabberView.h"
|
|
11
12
|
|
|
12
13
|
#if __has_include(<RNScreens/RNSDismissibleModalProtocol.h>)
|
|
13
14
|
#import <RNScreens/RNSDismissibleModalProtocol.h>
|
|
@@ -58,7 +59,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
58
59
|
@property (nonatomic, strong, nullable) UIColor *backgroundColor;
|
|
59
60
|
@property (nonatomic, strong, nullable) NSNumber *cornerRadius;
|
|
60
61
|
@property (nonatomic, assign) BOOL grabber;
|
|
61
|
-
@property (nonatomic, strong, nullable)
|
|
62
|
+
@property (nonatomic, strong, nullable) GrabberOptions *grabberOptions;
|
|
62
63
|
@property (nonatomic, assign) BOOL draggable;
|
|
63
64
|
@property (nonatomic, assign) BOOL dimmed;
|
|
64
65
|
@property (nonatomic, strong, nullable) NSNumber *dimmedDetentIndex;
|
|
@@ -68,6 +69,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
68
69
|
@property (nonatomic, assign) BOOL pageSizing;
|
|
69
70
|
@property (nonatomic, assign) NSInteger anchor;
|
|
70
71
|
@property (nonatomic, assign) NSInteger insetAdjustment;
|
|
72
|
+
@property (nonatomic, assign) BOOL scrollingExpandsSheet;
|
|
71
73
|
@property (nonatomic, assign) BOOL dismissible;
|
|
72
74
|
@property (nonatomic, assign) BOOL isPresented;
|
|
73
75
|
@property (nonatomic, assign) NSInteger activeDetentIndex;
|
|
@@ -22,12 +22,22 @@
|
|
|
22
22
|
|
|
23
23
|
using namespace facebook::react;
|
|
24
24
|
|
|
25
|
+
typedef struct {
|
|
26
|
+
CGFloat position;
|
|
27
|
+
CGFloat detent;
|
|
28
|
+
CGFloat index;
|
|
29
|
+
} TrueSheetPositionState;
|
|
30
|
+
|
|
31
|
+
static BOOL TrueSheetPositionStateEquals(TrueSheetPositionState a, TrueSheetPositionState b) {
|
|
32
|
+
return fabs(a.position - b.position) <= 0.01 && fabs(a.detent - b.detent) <= 0.01 && fabs(a.index - b.index) <= 0.01;
|
|
33
|
+
}
|
|
34
|
+
|
|
25
35
|
@interface TrueSheetViewController ()
|
|
26
36
|
|
|
27
37
|
@end
|
|
28
38
|
|
|
29
39
|
@implementation TrueSheetViewController {
|
|
30
|
-
|
|
40
|
+
TrueSheetPositionState _lastEmittedPositionState;
|
|
31
41
|
CGFloat _lastWidth;
|
|
32
42
|
NSInteger _pendingDetentIndex;
|
|
33
43
|
BOOL _pendingContentSizeChange;
|
|
@@ -59,11 +69,12 @@ using namespace facebook::react;
|
|
|
59
69
|
_headerHeight = @(0);
|
|
60
70
|
_grabber = YES;
|
|
61
71
|
_draggable = YES;
|
|
72
|
+
_scrollingExpandsSheet = YES;
|
|
62
73
|
_dismissible = YES;
|
|
63
74
|
_dimmed = YES;
|
|
64
75
|
_dimmedDetentIndex = @(0);
|
|
65
76
|
_pageSizing = YES;
|
|
66
|
-
|
|
77
|
+
_lastEmittedPositionState = (TrueSheetPositionState){0, 0, 0};
|
|
67
78
|
_isDragging = NO;
|
|
68
79
|
_isPresented = NO;
|
|
69
80
|
_isTransitioning = NO;
|
|
@@ -223,6 +234,7 @@ using namespace facebook::react;
|
|
|
223
234
|
[self.delegate viewControllerDidPresentAtIndex:index position:self.currentPosition detent:detent];
|
|
224
235
|
[self.delegate viewControllerDidFocus];
|
|
225
236
|
|
|
237
|
+
[_grabberView updateAccessibilityValueWithIndex:index detentCount:_detents.count];
|
|
226
238
|
[self emitChangePositionDelegateWithPosition:self.currentPosition realtime:NO debug:@"did present"];
|
|
227
239
|
});
|
|
228
240
|
|
|
@@ -315,6 +327,7 @@ using namespace facebook::react;
|
|
|
315
327
|
[self learnOffsetForDetentIndex:pendingIndex];
|
|
316
328
|
CGFloat detent = [self detentValueForIndex:pendingIndex];
|
|
317
329
|
[self.delegate viewControllerDidChangeDetent:pendingIndex position:self.currentPosition detent:detent];
|
|
330
|
+
[self->_grabberView updateAccessibilityValueWithIndex:pendingIndex detentCount:self->_detents.count];
|
|
318
331
|
[self emitChangePositionDelegateWithPosition:self.currentPosition realtime:NO debug:@"pending detent change"];
|
|
319
332
|
});
|
|
320
333
|
}
|
|
@@ -389,7 +402,9 @@ using namespace facebook::react;
|
|
|
389
402
|
case UIGestureRecognizerStateCancelled: {
|
|
390
403
|
if (!_isTransitioning) {
|
|
391
404
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
392
|
-
|
|
405
|
+
NSInteger index = self.currentDetentIndex;
|
|
406
|
+
[self learnOffsetForDetentIndex:index];
|
|
407
|
+
[self->_grabberView updateAccessibilityValueWithIndex:index detentCount:self->_detents.count];
|
|
393
408
|
[self emitChangePositionDelegateWithPosition:self.currentPosition realtime:NO debug:@"drag end"];
|
|
394
409
|
});
|
|
395
410
|
}
|
|
@@ -458,7 +473,7 @@ using namespace facebook::react;
|
|
|
458
473
|
CGFloat layerPosition = layer.presentationLayer.frame.origin.y;
|
|
459
474
|
|
|
460
475
|
if (self.currentPosition >= self.screenHeight) {
|
|
461
|
-
CGFloat position = fmax(
|
|
476
|
+
CGFloat position = fmax(_lastEmittedPositionState.position, layerPosition);
|
|
462
477
|
|
|
463
478
|
[self emitWillDismissEvents];
|
|
464
479
|
[self emitChangePositionDelegateWithPosition:position realtime:YES debug:@"transition out"];
|
|
@@ -466,7 +481,8 @@ using namespace facebook::react;
|
|
|
466
481
|
} else {
|
|
467
482
|
CGFloat position = fmax(self.currentPosition, layerPosition);
|
|
468
483
|
// Detect drag → snap transition jump; stay non-realtime for the rest of the animation
|
|
469
|
-
if (!_isTransitionSnapping && _isPresented &&
|
|
484
|
+
if (!_isTransitionSnapping && _isPresented && _lastEmittedPositionState.position > 0 &&
|
|
485
|
+
fabs(_lastEmittedPositionState.position - position) > 20) {
|
|
470
486
|
_isTransitionSnapping = YES;
|
|
471
487
|
}
|
|
472
488
|
BOOL realtime = !_isTransitionSnapping;
|
|
@@ -485,13 +501,19 @@ using namespace facebook::react;
|
|
|
485
501
|
}
|
|
486
502
|
}
|
|
487
503
|
|
|
488
|
-
|
|
489
|
-
|
|
504
|
+
TrueSheetPositionState state = {
|
|
505
|
+
.position = position,
|
|
506
|
+
.detent = [self interpolatedDetentForPosition:position],
|
|
507
|
+
.index = [self interpolatedIndexForPosition:position],
|
|
508
|
+
};
|
|
490
509
|
|
|
491
|
-
|
|
492
|
-
|
|
510
|
+
if (!TrueSheetPositionStateEquals(_lastEmittedPositionState, state)) {
|
|
511
|
+
_lastEmittedPositionState = state;
|
|
493
512
|
|
|
494
|
-
[self.delegate viewControllerDidChangePosition:index
|
|
513
|
+
[self.delegate viewControllerDidChangePosition:state.index
|
|
514
|
+
position:state.position
|
|
515
|
+
detent:state.detent
|
|
516
|
+
realtime:realtime];
|
|
495
517
|
}
|
|
496
518
|
}
|
|
497
519
|
|
|
@@ -733,13 +755,13 @@ using namespace facebook::react;
|
|
|
733
755
|
if (self.grabberOptions) {
|
|
734
756
|
self.sheet.prefersGrabberVisible = NO;
|
|
735
757
|
|
|
736
|
-
|
|
737
|
-
_grabberView.grabberWidth = options
|
|
738
|
-
_grabberView.grabberHeight = options
|
|
739
|
-
_grabberView.topMargin = options
|
|
740
|
-
_grabberView.cornerRadius = options
|
|
741
|
-
_grabberView.color = options
|
|
742
|
-
_grabberView.adaptive = options
|
|
758
|
+
GrabberOptions *options = self.grabberOptions;
|
|
759
|
+
_grabberView.grabberWidth = options.width;
|
|
760
|
+
_grabberView.grabberHeight = options.height;
|
|
761
|
+
_grabberView.topMargin = options.topMargin;
|
|
762
|
+
_grabberView.cornerRadius = options.cornerRadius;
|
|
763
|
+
_grabberView.color = options.color;
|
|
764
|
+
_grabberView.adaptive = @(options.adaptive);
|
|
743
765
|
[_grabberView applyConfiguration];
|
|
744
766
|
_grabberView.hidden = !showGrabber;
|
|
745
767
|
|
|
@@ -747,12 +769,39 @@ using namespace facebook::react;
|
|
|
747
769
|
_grabberView.onTap = ^{
|
|
748
770
|
[weakSelf handleGrabberTap];
|
|
749
771
|
};
|
|
772
|
+
_grabberView.onIncrement = ^{
|
|
773
|
+
__strong __typeof(weakSelf) strongSelf = weakSelf;
|
|
774
|
+
if (!strongSelf)
|
|
775
|
+
return;
|
|
776
|
+
NSInteger current = strongSelf.currentDetentIndex;
|
|
777
|
+
NSInteger count = strongSelf->_detents.count;
|
|
778
|
+
if (current >= 0 && current < count - 1) {
|
|
779
|
+
[strongSelf.sheet animateChanges:^{
|
|
780
|
+
[strongSelf resizeToDetentIndex:current + 1];
|
|
781
|
+
}];
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
_grabberView.onDecrement = ^{
|
|
785
|
+
__strong __typeof(weakSelf) strongSelf = weakSelf;
|
|
786
|
+
if (!strongSelf)
|
|
787
|
+
return;
|
|
788
|
+
NSInteger current = strongSelf.currentDetentIndex;
|
|
789
|
+
if (current > 0) {
|
|
790
|
+
[strongSelf.sheet animateChanges:^{
|
|
791
|
+
[strongSelf resizeToDetentIndex:current - 1];
|
|
792
|
+
}];
|
|
793
|
+
} else if (strongSelf.dismissible) {
|
|
794
|
+
[strongSelf.presentingViewController dismissViewControllerAnimated:YES completion:nil];
|
|
795
|
+
}
|
|
796
|
+
};
|
|
750
797
|
|
|
751
798
|
[self.view bringSubviewToFront:_grabberView];
|
|
752
799
|
} else {
|
|
753
800
|
self.sheet.prefersGrabberVisible = showGrabber;
|
|
754
801
|
_grabberView.hidden = YES;
|
|
755
802
|
_grabberView.onTap = nil;
|
|
803
|
+
_grabberView.onIncrement = nil;
|
|
804
|
+
_grabberView.onDecrement = nil;
|
|
756
805
|
}
|
|
757
806
|
}
|
|
758
807
|
|
|
@@ -835,7 +884,7 @@ using namespace facebook::react;
|
|
|
835
884
|
|
|
836
885
|
sheet.delegate = self;
|
|
837
886
|
sheet.prefersEdgeAttachedInCompactHeight = YES;
|
|
838
|
-
sheet.prefersScrollingExpandsWhenScrolledToEdge = self.draggable;
|
|
887
|
+
sheet.prefersScrollingExpandsWhenScrolledToEdge = self.draggable && self.scrollingExpandsSheet;
|
|
839
888
|
|
|
840
889
|
if (self.cornerRadius) {
|
|
841
890
|
sheet.preferredCornerRadius = [self.cornerRadius floatValue];
|
|
@@ -10,6 +10,17 @@
|
|
|
10
10
|
|
|
11
11
|
NS_ASSUME_NONNULL_BEGIN
|
|
12
12
|
|
|
13
|
+
@interface GrabberOptions : NSObject
|
|
14
|
+
|
|
15
|
+
@property (nonatomic, strong, nullable) NSNumber *width;
|
|
16
|
+
@property (nonatomic, strong, nullable) NSNumber *height;
|
|
17
|
+
@property (nonatomic, strong, nullable) NSNumber *topMargin;
|
|
18
|
+
@property (nonatomic, strong, nullable) NSNumber *cornerRadius;
|
|
19
|
+
@property (nonatomic, strong, nullable) UIColor *color;
|
|
20
|
+
@property (nonatomic, assign) BOOL adaptive;
|
|
21
|
+
|
|
22
|
+
@end
|
|
23
|
+
|
|
13
24
|
/**
|
|
14
25
|
* Native grabber (drag handle) view for the bottom sheet.
|
|
15
26
|
* Uses UIVibrancyEffect to adapt color based on the background.
|
|
@@ -37,12 +48,21 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
37
48
|
/// Called when the grabber is tapped
|
|
38
49
|
@property (nonatomic, copy, nullable) void (^onTap)(void);
|
|
39
50
|
|
|
51
|
+
/// Called when VoiceOver user swipes up (expand)
|
|
52
|
+
@property (nonatomic, copy, nullable) void (^onIncrement)(void);
|
|
53
|
+
|
|
54
|
+
/// Called when VoiceOver user swipes down (collapse)
|
|
55
|
+
@property (nonatomic, copy, nullable) void (^onDecrement)(void);
|
|
56
|
+
|
|
40
57
|
/// Adds the grabber view to a parent view with proper constraints
|
|
41
58
|
- (void)addToView:(UIView *)parentView;
|
|
42
59
|
|
|
43
60
|
/// Applies the current configuration to the grabber view
|
|
44
61
|
- (void)applyConfiguration;
|
|
45
62
|
|
|
63
|
+
/// Updates the accessibility value based on the current detent position
|
|
64
|
+
- (void)updateAccessibilityValueWithIndex:(NSInteger)index detentCount:(NSInteger)count;
|
|
65
|
+
|
|
46
66
|
@end
|
|
47
67
|
|
|
48
68
|
NS_ASSUME_NONNULL_END
|