@lodev09/react-native-true-sheet 3.9.9 → 3.10.0-beta.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/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 +8 -4
- 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/TrueSheetModule.mm +5 -0
- package/ios/TrueSheetView.mm +25 -26
- 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 {
|
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,18 +244,20 @@ 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
|
|
|
@@ -489,7 +484,11 @@ using namespace facebook::react;
|
|
|
489
484
|
}
|
|
490
485
|
|
|
491
486
|
- (void)emitDismissedPosition {
|
|
492
|
-
[
|
|
487
|
+
[TrueSheetStateEvents emitPositionChange:_eventEmitter
|
|
488
|
+
index:-1
|
|
489
|
+
position:_controller.screenHeight
|
|
490
|
+
detent:0
|
|
491
|
+
realtime:NO];
|
|
493
492
|
}
|
|
494
493
|
|
|
495
494
|
- (void)dismissAnimated:(BOOL)animated completion:(nullable TrueSheetCompletionBlock)completion {
|
|
@@ -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
|
|
@@ -8,9 +8,22 @@
|
|
|
8
8
|
|
|
9
9
|
#import "TrueSheetGrabberView.h"
|
|
10
10
|
|
|
11
|
+
@implementation GrabberOptions
|
|
12
|
+
|
|
13
|
+
- (instancetype)init {
|
|
14
|
+
if (self = [super init]) {
|
|
15
|
+
_adaptive = YES;
|
|
16
|
+
}
|
|
17
|
+
return self;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@end
|
|
21
|
+
|
|
11
22
|
static const CGFloat kDefaultGrabberWidth = 36.0;
|
|
12
23
|
static const CGFloat kDefaultGrabberHeight = 5.0;
|
|
13
24
|
static const CGFloat kDefaultGrabberTopMargin = 5.0;
|
|
25
|
+
static const CGFloat kHitPaddingHorizontal = 20.0;
|
|
26
|
+
static const CGFloat kHitPaddingVertical = 10.0;
|
|
14
27
|
|
|
15
28
|
@implementation TrueSheetGrabberView {
|
|
16
29
|
UIVisualEffectView *_vibrancyView;
|
|
@@ -51,17 +64,19 @@ static const CGFloat kDefaultGrabberTopMargin = 5.0;
|
|
|
51
64
|
#pragma mark - Setup
|
|
52
65
|
|
|
53
66
|
- (void)setupView {
|
|
54
|
-
self.clipsToBounds =
|
|
67
|
+
self.clipsToBounds = NO;
|
|
68
|
+
self.isAccessibilityElement = YES;
|
|
69
|
+
self.accessibilityLabel = @"Sheet Grabber";
|
|
70
|
+
self.accessibilityTraits = UIAccessibilityTraitAdjustable | UIAccessibilityTraitButton;
|
|
71
|
+
self.accessibilityHint = @"Double-tap to expand. Swipe up or down to resize the sheet";
|
|
55
72
|
|
|
56
73
|
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap)];
|
|
57
74
|
[self addGestureRecognizer:tap];
|
|
58
75
|
|
|
59
76
|
_vibrancyView = [[UIVisualEffectView alloc] initWithEffect:nil];
|
|
60
|
-
_vibrancyView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
|
61
77
|
[self addSubview:_vibrancyView];
|
|
62
78
|
|
|
63
79
|
_fillView = [[UIView alloc] init];
|
|
64
|
-
_fillView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
|
65
80
|
_fillView.backgroundColor = [UIColor.darkGrayColor colorWithAlphaComponent:0.7];
|
|
66
81
|
[_vibrancyView.contentView addSubview:_fillView];
|
|
67
82
|
}
|
|
@@ -74,8 +89,35 @@ static const CGFloat kDefaultGrabberTopMargin = 5.0;
|
|
|
74
89
|
}
|
|
75
90
|
}
|
|
76
91
|
|
|
92
|
+
- (void)accessibilityIncrement {
|
|
93
|
+
if (_onIncrement) {
|
|
94
|
+
_onIncrement();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
- (void)accessibilityDecrement {
|
|
99
|
+
if (_onDecrement) {
|
|
100
|
+
_onDecrement();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
77
104
|
#pragma mark - Public
|
|
78
105
|
|
|
106
|
+
- (void)updateAccessibilityValueWithIndex:(NSInteger)index detentCount:(NSInteger)count {
|
|
107
|
+
if (index < 0 || count <= 0) {
|
|
108
|
+
self.accessibilityValue = nil;
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (index >= count - 1) {
|
|
113
|
+
self.accessibilityValue = @"Expanded";
|
|
114
|
+
} else if (index == 0) {
|
|
115
|
+
self.accessibilityValue = @"Collapsed";
|
|
116
|
+
} else {
|
|
117
|
+
self.accessibilityValue = [NSString stringWithFormat:@"Detent %ld of %ld", (long)(index + 1), (long)count];
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
79
121
|
- (void)addToView:(UIView *)parentView {
|
|
80
122
|
if (self.superview == parentView) {
|
|
81
123
|
return;
|
|
@@ -87,17 +129,22 @@ static const CGFloat kDefaultGrabberTopMargin = 5.0;
|
|
|
87
129
|
}
|
|
88
130
|
|
|
89
131
|
- (void)applyConfiguration {
|
|
90
|
-
CGFloat
|
|
91
|
-
CGFloat
|
|
132
|
+
CGFloat pillWidth = [self effectiveWidth];
|
|
133
|
+
CGFloat pillHeight = [self effectiveHeight];
|
|
92
134
|
CGFloat topMargin = [self effectiveTopMargin];
|
|
93
135
|
CGFloat parentWidth = self.superview ? self.superview.bounds.size.width : UIScreen.mainScreen.bounds.size.width;
|
|
94
136
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
137
|
+
CGFloat frameWidth = pillWidth + kHitPaddingHorizontal * 2;
|
|
138
|
+
CGFloat frameHeight = pillHeight + kHitPaddingVertical * 2;
|
|
139
|
+
CGFloat frameY = topMargin - kHitPaddingVertical;
|
|
140
|
+
|
|
141
|
+
self.frame = CGRectMake((parentWidth - frameWidth) / 2.0, frameY, frameWidth, frameHeight);
|
|
142
|
+
self.backgroundColor = UIColor.clearColor;
|
|
98
143
|
|
|
99
|
-
|
|
100
|
-
_vibrancyView.frame =
|
|
144
|
+
CGRect pillRect = CGRectMake(kHitPaddingHorizontal, kHitPaddingVertical, pillWidth, pillHeight);
|
|
145
|
+
_vibrancyView.frame = pillRect;
|
|
146
|
+
_vibrancyView.layer.cornerRadius = [self effectiveCornerRadius];
|
|
147
|
+
_vibrancyView.clipsToBounds = YES;
|
|
101
148
|
_fillView.frame = _vibrancyView.contentView.bounds;
|
|
102
149
|
|
|
103
150
|
if (self.isAdaptive) {
|
|
@@ -108,9 +155,8 @@ static const CGFloat kDefaultGrabberTopMargin = 5.0;
|
|
|
108
155
|
_fillView.hidden = NO;
|
|
109
156
|
} else {
|
|
110
157
|
_vibrancyView.effect = nil;
|
|
111
|
-
_vibrancyView.backgroundColor = nil;
|
|
112
158
|
_fillView.hidden = YES;
|
|
113
|
-
|
|
159
|
+
_vibrancyView.backgroundColor = _color ?: [UIColor.darkGrayColor colorWithAlphaComponent:0.7];
|
|
114
160
|
}
|
|
115
161
|
}
|
|
116
162
|
|