@swmansion/react-native-bottom-sheet 0.6.2 → 0.7.0-next.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/ReactNativeBottomSheet.podspec +24 -0
- package/android/build.gradle +68 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/swmansion/reactnativebottomsheet/BottomSheetPackage.kt +14 -0
- package/android/src/main/java/com/swmansion/reactnativebottomsheet/BottomSheetView.kt +412 -0
- package/android/src/main/java/com/swmansion/reactnativebottomsheet/BottomSheetViewManager.kt +105 -0
- package/ios/BottomSheetComponentView.h +10 -0
- package/ios/BottomSheetComponentView.mm +100 -0
- package/ios/BottomSheetContentView.h +25 -0
- package/ios/BottomSheetContentView.mm +73 -0
- package/ios/RNSBottomSheetHostingView.swift +294 -0
- package/lib/module/BottomSheet.js +76 -5
- package/lib/module/BottomSheet.js.map +1 -1
- package/lib/module/BottomSheetNativeComponent.ts +24 -0
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/useBottomSheetPanGesture.js +7 -2
- package/lib/module/useBottomSheetPanGesture.js.map +1 -1
- package/lib/typescript/src/BottomSheet.d.ts +16 -3
- package/lib/typescript/src/BottomSheet.d.ts.map +1 -1
- package/lib/typescript/src/BottomSheetNativeComponent.d.ts +19 -0
- package/lib/typescript/src/BottomSheetNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +2 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/useBottomSheetPanGesture.d.ts.map +1 -1
- package/package.json +14 -1
- package/react-native.config.js +1 -0
- package/src/BottomSheet.tsx +91 -6
- package/src/BottomSheetNativeComponent.ts +24 -0
- package/src/index.tsx +2 -2
- package/src/useBottomSheetPanGesture.ts +7 -2
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#import "BottomSheetComponentView.h"
|
|
2
|
+
#import "BottomSheetContentView.h"
|
|
3
|
+
|
|
4
|
+
#import <React/RCTConversions.h>
|
|
5
|
+
#import <React/RCTFabricComponentsPlugins.h>
|
|
6
|
+
#import <react/renderer/components/ReactNativeBottomSheetSpec/ComponentDescriptors.h>
|
|
7
|
+
#import <react/renderer/components/ReactNativeBottomSheetSpec/EventEmitters.h>
|
|
8
|
+
#import <react/renderer/components/ReactNativeBottomSheetSpec/Props.h>
|
|
9
|
+
#import <react/renderer/components/ReactNativeBottomSheetSpec/RCTComponentViewHelpers.h>
|
|
10
|
+
|
|
11
|
+
using namespace facebook::react;
|
|
12
|
+
|
|
13
|
+
@interface BottomSheetComponentView () <BottomSheetContentViewDelegate>
|
|
14
|
+
@end
|
|
15
|
+
|
|
16
|
+
@implementation BottomSheetComponentView {
|
|
17
|
+
BottomSheetContentView *_sheetView;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
+ (ComponentDescriptorProvider)componentDescriptorProvider
|
|
21
|
+
{
|
|
22
|
+
return concreteComponentDescriptorProvider<BottomSheetViewComponentDescriptor>();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
- (instancetype)initWithFrame:(CGRect)frame
|
|
26
|
+
{
|
|
27
|
+
if (self = [super initWithFrame:frame]) {
|
|
28
|
+
static const auto defaultProps = std::make_shared<const BottomSheetViewProps>();
|
|
29
|
+
_props = defaultProps;
|
|
30
|
+
|
|
31
|
+
_sheetView = [[BottomSheetContentView alloc] initWithFrame:CGRectZero];
|
|
32
|
+
_sheetView.delegate = self;
|
|
33
|
+
_sheetView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
|
34
|
+
self.contentView = _sheetView;
|
|
35
|
+
}
|
|
36
|
+
return self;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
- (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps
|
|
40
|
+
{
|
|
41
|
+
const auto &newViewProps = static_cast<const BottomSheetViewProps &>(*props);
|
|
42
|
+
const auto &oldViewProps = static_cast<const BottomSheetViewProps &>(*_props);
|
|
43
|
+
|
|
44
|
+
// Always update detents — the codegen struct lacks operator==.
|
|
45
|
+
{
|
|
46
|
+
NSMutableArray<NSDictionary *> *detentsArray = [NSMutableArray new];
|
|
47
|
+
for (const auto &detent : newViewProps.detents) {
|
|
48
|
+
[detentsArray addObject:@{
|
|
49
|
+
@"height": @(detent.height),
|
|
50
|
+
@"programmatic": @(detent.programmatic),
|
|
51
|
+
}];
|
|
52
|
+
}
|
|
53
|
+
[_sheetView setDetents:detentsArray];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (newViewProps.index != oldViewProps.index) {
|
|
57
|
+
[_sheetView setDetentIndex:newViewProps.index];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (newViewProps.animateIn != oldViewProps.animateIn) {
|
|
61
|
+
_sheetView.animateIn = newViewProps.animateIn;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
[super updateProps:props oldProps:oldProps];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
|
|
68
|
+
{
|
|
69
|
+
[_sheetView mountChildComponentView:childComponentView atIndex:index];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
|
|
73
|
+
{
|
|
74
|
+
[_sheetView unmountChildComponentView:childComponentView];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
#pragma mark - BottomSheetContentViewDelegate
|
|
78
|
+
|
|
79
|
+
- (void)bottomSheetView:(BottomSheetContentView *)view didChangeIndex:(NSInteger)index
|
|
80
|
+
{
|
|
81
|
+
if (_eventEmitter) {
|
|
82
|
+
auto emitter = std::static_pointer_cast<const BottomSheetViewEventEmitter>(_eventEmitter);
|
|
83
|
+
emitter->onIndexChange({.index = static_cast<int>(index)});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
- (void)bottomSheetView:(BottomSheetContentView *)view didChangePosition:(CGFloat)position
|
|
88
|
+
{
|
|
89
|
+
if (_eventEmitter) {
|
|
90
|
+
auto emitter = std::static_pointer_cast<const BottomSheetViewEventEmitter>(_eventEmitter);
|
|
91
|
+
emitter->onPositionChange({.position = static_cast<double>(position)});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@end
|
|
96
|
+
|
|
97
|
+
Class<RCTComponentViewProtocol> BottomSheetViewCls(void)
|
|
98
|
+
{
|
|
99
|
+
return BottomSheetComponentView.class;
|
|
100
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#import <UIKit/UIKit.h>
|
|
2
|
+
|
|
3
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
4
|
+
|
|
5
|
+
@class BottomSheetContentView;
|
|
6
|
+
|
|
7
|
+
@protocol BottomSheetContentViewDelegate <NSObject>
|
|
8
|
+
- (void)bottomSheetView:(BottomSheetContentView *)view didChangeIndex:(NSInteger)index;
|
|
9
|
+
- (void)bottomSheetView:(BottomSheetContentView *)view didChangePosition:(CGFloat)position;
|
|
10
|
+
@end
|
|
11
|
+
|
|
12
|
+
@interface BottomSheetContentView : UIView
|
|
13
|
+
|
|
14
|
+
@property (nonatomic, weak, nullable) id<BottomSheetContentViewDelegate> delegate;
|
|
15
|
+
@property (nonatomic) BOOL animateIn;
|
|
16
|
+
@property (nonatomic, readonly) UIView *sheetContainer;
|
|
17
|
+
|
|
18
|
+
- (void)setDetents:(NSArray<NSDictionary *> *)raw;
|
|
19
|
+
- (void)setDetentIndex:(NSInteger)newIndex;
|
|
20
|
+
- (void)mountChildComponentView:(UIView *)childView atIndex:(NSInteger)index;
|
|
21
|
+
- (void)unmountChildComponentView:(UIView *)childView;
|
|
22
|
+
|
|
23
|
+
@end
|
|
24
|
+
|
|
25
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#import "BottomSheetContentView.h"
|
|
2
|
+
|
|
3
|
+
#if __has_include("ReactNativeBottomSheet-Swift.h")
|
|
4
|
+
#import "ReactNativeBottomSheet-Swift.h"
|
|
5
|
+
#else
|
|
6
|
+
#import <ReactNativeBottomSheet/ReactNativeBottomSheet-Swift.h>
|
|
7
|
+
#endif
|
|
8
|
+
|
|
9
|
+
@interface BottomSheetContentView () <RNSBottomSheetHostingViewDelegate>
|
|
10
|
+
@end
|
|
11
|
+
|
|
12
|
+
@implementation BottomSheetContentView {
|
|
13
|
+
RNSBottomSheetHostingView *_impl;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
- (instancetype)initWithFrame:(CGRect)frame
|
|
17
|
+
{
|
|
18
|
+
if (self = [super initWithFrame:frame]) {
|
|
19
|
+
_impl = [[RNSBottomSheetHostingView alloc] initWithFrame:self.bounds];
|
|
20
|
+
_impl.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
|
21
|
+
_impl.eventDelegate = self;
|
|
22
|
+
[self addSubview:_impl];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return self;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
- (BOOL)animateIn
|
|
29
|
+
{
|
|
30
|
+
return _impl.animateIn;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
- (void)setAnimateIn:(BOOL)animateIn
|
|
34
|
+
{
|
|
35
|
+
_impl.animateIn = animateIn;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
- (UIView *)sheetContainer
|
|
39
|
+
{
|
|
40
|
+
return _impl.sheetContainer;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
- (void)setDetents:(NSArray<NSDictionary *> *)raw
|
|
44
|
+
{
|
|
45
|
+
[_impl setDetents:raw];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
- (void)setDetentIndex:(NSInteger)newIndex
|
|
49
|
+
{
|
|
50
|
+
[_impl setDetentIndex:newIndex];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
- (void)mountChildComponentView:(UIView *)childView atIndex:(NSInteger)index
|
|
54
|
+
{
|
|
55
|
+
[_impl mountChildComponentView:childView atIndex:index];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
- (void)unmountChildComponentView:(UIView *)childView
|
|
59
|
+
{
|
|
60
|
+
[_impl unmountChildComponentView:childView];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
- (void)bottomSheetHostingView:(RNSBottomSheetHostingView *)view didChangeIndex:(NSInteger)index
|
|
64
|
+
{
|
|
65
|
+
[self.delegate bottomSheetView:self didChangeIndex:index];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
- (void)bottomSheetHostingView:(RNSBottomSheetHostingView *)view didChangePosition:(CGFloat)position
|
|
69
|
+
{
|
|
70
|
+
[self.delegate bottomSheetView:self didChangePosition:position];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@end
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
@objc public protocol RNSBottomSheetHostingViewDelegate: AnyObject {
|
|
4
|
+
func bottomSheetHostingView(_ view: RNSBottomSheetHostingView, didChangeIndex index: Int)
|
|
5
|
+
func bottomSheetHostingView(_ view: RNSBottomSheetHostingView, didChangePosition position: CGFloat)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
private struct DetentSpec {
|
|
9
|
+
let height: CGFloat
|
|
10
|
+
let programmatic: Bool
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@objcMembers
|
|
14
|
+
public final class RNSBottomSheetHostingView: UIView {
|
|
15
|
+
public weak var eventDelegate: RNSBottomSheetHostingViewDelegate?
|
|
16
|
+
|
|
17
|
+
private var detentSpecs: [DetentSpec] = [] {
|
|
18
|
+
didSet { setNeedsLayout() }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private var targetIndex: Int = 0
|
|
22
|
+
public var animateIn: Bool = true
|
|
23
|
+
|
|
24
|
+
public let sheetContainer = UIView()
|
|
25
|
+
private var panGesture: UIPanGestureRecognizer!
|
|
26
|
+
private var activeAnimator: UIViewPropertyAnimator?
|
|
27
|
+
private var displayLink: CADisplayLink?
|
|
28
|
+
private var pendingIndex: Int?
|
|
29
|
+
private var hasLaidOut = false
|
|
30
|
+
private var isPanning = false
|
|
31
|
+
|
|
32
|
+
public override init(frame: CGRect) {
|
|
33
|
+
super.init(frame: frame)
|
|
34
|
+
backgroundColor = .clear
|
|
35
|
+
clipsToBounds = false
|
|
36
|
+
|
|
37
|
+
sheetContainer.backgroundColor = .clear
|
|
38
|
+
sheetContainer.clipsToBounds = false
|
|
39
|
+
addSubview(sheetContainer)
|
|
40
|
+
|
|
41
|
+
panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
|
|
42
|
+
panGesture.delegate = self
|
|
43
|
+
sheetContainer.addGestureRecognizer(panGesture)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public required init?(coder: NSCoder) {
|
|
47
|
+
fatalError("init(coder:) has not been implemented")
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public override func layoutSubviews() {
|
|
51
|
+
super.layoutSubviews()
|
|
52
|
+
guard bounds.width > 0, bounds.height > 0 else { return }
|
|
53
|
+
|
|
54
|
+
let maxHeight = detentSpecs.last?.height ?? bounds.height
|
|
55
|
+
sheetContainer.bounds = CGRect(x: 0, y: 0, width: bounds.width, height: maxHeight)
|
|
56
|
+
sheetContainer.center = CGPoint(x: bounds.width / 2, y: bounds.height - maxHeight / 2)
|
|
57
|
+
|
|
58
|
+
if !hasLaidOut && !detentSpecs.isEmpty {
|
|
59
|
+
hasLaidOut = true
|
|
60
|
+
let indexToApply = pendingIndex ?? targetIndex
|
|
61
|
+
pendingIndex = nil
|
|
62
|
+
targetIndex = max(0, min(detentSpecs.count - 1, indexToApply))
|
|
63
|
+
|
|
64
|
+
if animateIn {
|
|
65
|
+
let closedTy = detentSpecs.last?.height ?? bounds.height
|
|
66
|
+
sheetContainer.transform = CGAffineTransform(translationX: 0, y: closedTy)
|
|
67
|
+
emitPosition()
|
|
68
|
+
snapToIndex(targetIndex, velocity: 0)
|
|
69
|
+
} else {
|
|
70
|
+
sheetContainer.transform = CGAffineTransform(translationX: 0, y: translationY(for: targetIndex))
|
|
71
|
+
emitPosition()
|
|
72
|
+
eventDelegate?.bottomSheetHostingView(self, didChangeIndex: targetIndex)
|
|
73
|
+
}
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if activeAnimator != nil || isPanning { return }
|
|
78
|
+
sheetContainer.transform = CGAffineTransform(translationX: 0, y: translationY(for: targetIndex))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
82
|
+
let containerPoint = convert(point, to: sheetContainer)
|
|
83
|
+
guard sheetContainer.bounds.contains(containerPoint) else { return nil }
|
|
84
|
+
return sheetContainer.hitTest(containerPoint, with: event)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
public func setDetents(_ raw: [NSDictionary]) {
|
|
88
|
+
detentSpecs = raw.compactMap { dict in
|
|
89
|
+
guard let height = dict["height"] as? Double ?? (dict["height"] as? NSNumber)?.doubleValue else {
|
|
90
|
+
return nil
|
|
91
|
+
}
|
|
92
|
+
let programmatic = (dict["programmatic"] as? Bool) ?? (dict["programmatic"] as? NSNumber)?.boolValue ?? false
|
|
93
|
+
return DetentSpec(height: CGFloat(height), programmatic: programmatic)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
public func setDetentIndex(_ newIndex: Int) {
|
|
98
|
+
guard newIndex >= 0 else { return }
|
|
99
|
+
|
|
100
|
+
if !hasLaidOut {
|
|
101
|
+
pendingIndex = newIndex
|
|
102
|
+
targetIndex = newIndex
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
guard newIndex < detentSpecs.count, newIndex != targetIndex else { return }
|
|
107
|
+
snapToIndex(newIndex, velocity: 0)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
public func mountChildComponentView(_ childView: UIView, atIndex index: Int) {
|
|
111
|
+
sheetContainer.insertSubview(childView, at: index)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
public func unmountChildComponentView(_ childView: UIView) {
|
|
115
|
+
childView.removeFromSuperview()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private func detent(at index: Int) -> DetentSpec {
|
|
119
|
+
guard detentSpecs.indices.contains(index) else {
|
|
120
|
+
return DetentSpec(height: 0, programmatic: false)
|
|
121
|
+
}
|
|
122
|
+
return detentSpecs[index]
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private func translationY(for index: Int) -> CGFloat {
|
|
126
|
+
let maxHeight = detentSpecs.last?.height ?? bounds.height
|
|
127
|
+
let snapHeight = detent(at: index).height
|
|
128
|
+
return maxHeight - snapHeight
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private var draggableRange: (minTy: CGFloat, maxTy: CGFloat) {
|
|
132
|
+
let draggable = detentSpecs.enumerated().filter { !$0.element.programmatic }
|
|
133
|
+
let highestIndex = draggable.last?.offset ?? 0
|
|
134
|
+
let lowestIndex = draggable.first?.offset ?? 0
|
|
135
|
+
return (minTy: translationY(for: highestIndex), maxTy: translationY(for: lowestIndex))
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private func emitPosition() {
|
|
139
|
+
let maxHeight = detentSpecs.last?.height ?? bounds.height
|
|
140
|
+
let ty = sheetContainer.layer.presentation()?.affineTransform().ty ?? sheetContainer.transform.ty
|
|
141
|
+
eventDelegate?.bottomSheetHostingView(self, didChangePosition: maxHeight - ty)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private func startDisplayLink() {
|
|
145
|
+
guard displayLink == nil else { return }
|
|
146
|
+
let link = CADisplayLink(target: self, selector: #selector(displayLinkFired))
|
|
147
|
+
link.add(to: .main, forMode: .common)
|
|
148
|
+
displayLink = link
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private func stopDisplayLink() {
|
|
152
|
+
displayLink?.invalidate()
|
|
153
|
+
displayLink = nil
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@objc private func displayLinkFired() {
|
|
157
|
+
emitPosition()
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private func snapToIndex(_ index: Int, velocity: CGFloat) {
|
|
161
|
+
guard index >= 0, index < detentSpecs.count else { return }
|
|
162
|
+
targetIndex = index
|
|
163
|
+
|
|
164
|
+
let currentTy = sheetContainer.transform.ty
|
|
165
|
+
let targetTy = translationY(for: index)
|
|
166
|
+
let distance = targetTy - currentTy
|
|
167
|
+
let velocityRatio = distance != 0 ? velocity / distance : 0
|
|
168
|
+
let clampedRatio = min(max(velocityRatio, -5), 5)
|
|
169
|
+
let initialVelocity = CGVector(dx: 0, dy: clampedRatio)
|
|
170
|
+
|
|
171
|
+
activeAnimator?.stopAnimation(true)
|
|
172
|
+
|
|
173
|
+
let spring = UISpringTimingParameters(dampingRatio: 1.0, initialVelocity: initialVelocity)
|
|
174
|
+
let animator = UIViewPropertyAnimator(duration: 0.45, timingParameters: spring)
|
|
175
|
+
|
|
176
|
+
animator.addAnimations {
|
|
177
|
+
self.sheetContainer.transform = CGAffineTransform(translationX: 0, y: targetTy)
|
|
178
|
+
}
|
|
179
|
+
animator.addCompletion { [weak self] position in
|
|
180
|
+
guard let self, position == .end else { return }
|
|
181
|
+
self.stopDisplayLink()
|
|
182
|
+
self.emitPosition()
|
|
183
|
+
self.activeAnimator = nil
|
|
184
|
+
self.eventDelegate?.bottomSheetHostingView(self, didChangeIndex: index)
|
|
185
|
+
}
|
|
186
|
+
animator.startAnimation()
|
|
187
|
+
activeAnimator = animator
|
|
188
|
+
startDisplayLink()
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
@objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
|
|
192
|
+
let maxHeight = detentSpecs.last?.height ?? bounds.height
|
|
193
|
+
|
|
194
|
+
switch gesture.state {
|
|
195
|
+
case .began:
|
|
196
|
+
isPanning = true
|
|
197
|
+
gesture.setTranslation(.zero, in: self)
|
|
198
|
+
if let animator = activeAnimator {
|
|
199
|
+
stopDisplayLink()
|
|
200
|
+
let visual = sheetContainer.layer.presentation()?.affineTransform() ?? sheetContainer.transform
|
|
201
|
+
animator.stopAnimation(true)
|
|
202
|
+
sheetContainer.transform = visual
|
|
203
|
+
activeAnimator = nil
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
case .changed:
|
|
207
|
+
let delta = gesture.translation(in: self).y
|
|
208
|
+
gesture.setTranslation(.zero, in: self)
|
|
209
|
+
let minTy = draggableRange.minTy
|
|
210
|
+
let maxTy = draggableRange.maxTy
|
|
211
|
+
let newTy = max(minTy, min(maxTy, sheetContainer.transform.ty + delta))
|
|
212
|
+
sheetContainer.transform = CGAffineTransform(translationX: 0, y: newTy)
|
|
213
|
+
emitPosition()
|
|
214
|
+
|
|
215
|
+
case .ended, .cancelled:
|
|
216
|
+
isPanning = false
|
|
217
|
+
let velocity = gesture.velocity(in: self).y
|
|
218
|
+
let currentHeight = maxHeight - sheetContainer.transform.ty
|
|
219
|
+
let index = bestSnapIndex(for: currentHeight, velocity: velocity)
|
|
220
|
+
snapToIndex(index, velocity: velocity)
|
|
221
|
+
|
|
222
|
+
case .failed:
|
|
223
|
+
isPanning = false
|
|
224
|
+
|
|
225
|
+
default:
|
|
226
|
+
break
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private func bestSnapIndex(for height: CGFloat, velocity: CGFloat) -> Int {
|
|
231
|
+
let draggable = detentSpecs.enumerated().filter { !$0.element.programmatic }
|
|
232
|
+
guard !draggable.isEmpty else { return targetIndex }
|
|
233
|
+
|
|
234
|
+
let flickThreshold: CGFloat = 600
|
|
235
|
+
|
|
236
|
+
if velocity < -flickThreshold {
|
|
237
|
+
return draggable.first(where: { $0.element.height > height })?.offset
|
|
238
|
+
?? draggable.last?.offset ?? targetIndex
|
|
239
|
+
}
|
|
240
|
+
if velocity > flickThreshold {
|
|
241
|
+
return draggable.last(where: { $0.element.height < height })?.offset
|
|
242
|
+
?? draggable.first?.offset ?? targetIndex
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return draggable.min(by: {
|
|
246
|
+
abs($0.element.height - height) < abs($1.element.height - height)
|
|
247
|
+
})?.offset ?? targetIndex
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private func firstScrollView(in view: UIView) -> UIScrollView? {
|
|
251
|
+
for subview in view.subviews {
|
|
252
|
+
if let scrollView = subview as? UIScrollView {
|
|
253
|
+
return scrollView
|
|
254
|
+
}
|
|
255
|
+
if let found = firstScrollView(in: subview) {
|
|
256
|
+
return found
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return nil
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
263
|
+
guard gestureRecognizer === panGesture else { return true }
|
|
264
|
+
|
|
265
|
+
let velocity = panGesture.velocity(in: self)
|
|
266
|
+
guard abs(velocity.y) > abs(velocity.x) else { return false }
|
|
267
|
+
|
|
268
|
+
let maxDraggableIndex = detentSpecs.indices.last(where: { !detentSpecs[$0].programmatic }) ?? 0
|
|
269
|
+
guard targetIndex >= maxDraggableIndex else { return true }
|
|
270
|
+
|
|
271
|
+
if velocity.y < 0 {
|
|
272
|
+
return false
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
let scrollAtTop = (firstScrollView(in: sheetContainer)?.contentOffset.y ?? 0) <= 0
|
|
276
|
+
return scrollAtTop
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
extension RNSBottomSheetHostingView: UIGestureRecognizerDelegate {
|
|
281
|
+
public func gestureRecognizer(
|
|
282
|
+
_ gestureRecognizer: UIGestureRecognizer,
|
|
283
|
+
shouldBeRequiredToFailBy other: UIGestureRecognizer
|
|
284
|
+
) -> Bool {
|
|
285
|
+
return gestureRecognizer === panGesture && other is UIPanGestureRecognizer
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
public func gestureRecognizer(
|
|
289
|
+
_ gestureRecognizer: UIGestureRecognizer,
|
|
290
|
+
shouldRecognizeSimultaneouslyWith other: UIGestureRecognizer
|
|
291
|
+
) -> Bool {
|
|
292
|
+
return false
|
|
293
|
+
}
|
|
294
|
+
}
|
|
@@ -1,8 +1,79 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { StyleSheet, View, useWindowDimensions } from 'react-native';
|
|
5
|
+
import { runOnUI } from 'react-native-reanimated';
|
|
6
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
7
|
+
import BottomSheetNativeComponent from './BottomSheetNativeComponent';
|
|
8
|
+
import { resolveDetent } from "./bottomSheetUtils.js";
|
|
9
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
10
|
+
export { programmatic } from "./bottomSheetUtils.js";
|
|
11
|
+
export const BottomSheet = ({
|
|
12
|
+
children,
|
|
13
|
+
style,
|
|
14
|
+
detents = [0, 'max'],
|
|
15
|
+
index,
|
|
16
|
+
animateIn = true,
|
|
17
|
+
onIndexChange,
|
|
18
|
+
position
|
|
19
|
+
}) => {
|
|
20
|
+
const {
|
|
21
|
+
height: screenHeight
|
|
22
|
+
} = useWindowDimensions();
|
|
23
|
+
const insets = useSafeAreaInsets();
|
|
24
|
+
const maxHeight = screenHeight - insets.top;
|
|
25
|
+
const [contentHeight, setContentHeight] = useState(0);
|
|
26
|
+
const resolvedDetents = detents.map(detent => {
|
|
27
|
+
const value = resolveDetent(detent, contentHeight, maxHeight);
|
|
28
|
+
return {
|
|
29
|
+
height: Math.max(0, Math.min(value, maxHeight)),
|
|
30
|
+
programmatic: isDetentProgrammatic(detent)
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
const handleSentinelLayout = event => {
|
|
34
|
+
setContentHeight(event.nativeEvent.layout.y);
|
|
35
|
+
};
|
|
36
|
+
const clampedIndex = Math.max(0, Math.min(index, resolvedDetents.length - 1));
|
|
37
|
+
const isCollapsed = (resolvedDetents[clampedIndex]?.height ?? 0) === 0;
|
|
38
|
+
const sheetPointerEvents = isCollapsed ? 'none' : 'box-none';
|
|
39
|
+
const handleIndexChange = event => {
|
|
40
|
+
onIndexChange?.(event.nativeEvent.index);
|
|
41
|
+
};
|
|
42
|
+
const handlePositionChange = event => {
|
|
43
|
+
if (position !== undefined) {
|
|
44
|
+
const height = event.nativeEvent.position;
|
|
45
|
+
runOnUI(() => {
|
|
46
|
+
'worklet';
|
|
47
|
+
|
|
48
|
+
position.set(height);
|
|
49
|
+
})();
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
return /*#__PURE__*/_jsx(View, {
|
|
53
|
+
pointerEvents: "box-none",
|
|
54
|
+
style: StyleSheet.absoluteFill,
|
|
55
|
+
children: /*#__PURE__*/_jsx(BottomSheetNativeComponent, {
|
|
56
|
+
pointerEvents: sheetPointerEvents,
|
|
57
|
+
style: [StyleSheet.absoluteFill, style],
|
|
58
|
+
detents: resolvedDetents,
|
|
59
|
+
index: index,
|
|
60
|
+
animateIn: animateIn,
|
|
61
|
+
onIndexChange: handleIndexChange,
|
|
62
|
+
onPositionChange: handlePositionChange,
|
|
63
|
+
children: /*#__PURE__*/_jsxs(View, {
|
|
64
|
+
pointerEvents: "box-none",
|
|
65
|
+
children: [children, /*#__PURE__*/_jsx(View, {
|
|
66
|
+
onLayout: handleSentinelLayout,
|
|
67
|
+
pointerEvents: "none"
|
|
68
|
+
})]
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
function isDetentProgrammatic(detent) {
|
|
74
|
+
if (typeof detent === 'object' && detent !== null) {
|
|
75
|
+
return detent.programmatic === true;
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
8
79
|
//# sourceMappingURL=BottomSheet.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["
|
|
1
|
+
{"version":3,"names":["useState","StyleSheet","View","useWindowDimensions","runOnUI","useSafeAreaInsets","BottomSheetNativeComponent","resolveDetent","jsx","_jsx","jsxs","_jsxs","programmatic","BottomSheet","children","style","detents","index","animateIn","onIndexChange","position","height","screenHeight","insets","maxHeight","top","contentHeight","setContentHeight","resolvedDetents","map","detent","value","Math","max","min","isDetentProgrammatic","handleSentinelLayout","event","nativeEvent","layout","y","clampedIndex","length","isCollapsed","sheetPointerEvents","handleIndexChange","handlePositionChange","undefined","set","pointerEvents","absoluteFill","onPositionChange","onLayout"],"sourceRoot":"../../src","sources":["BottomSheet.tsx"],"mappings":"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","ignoreList":[]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {
|
|
2
|
+
codegenNativeComponent,
|
|
3
|
+
type CodegenTypes,
|
|
4
|
+
type ViewProps,
|
|
5
|
+
} from 'react-native';
|
|
6
|
+
|
|
7
|
+
type NativeDetent = Readonly<{
|
|
8
|
+
height: CodegenTypes.Double;
|
|
9
|
+
programmatic: boolean;
|
|
10
|
+
}>;
|
|
11
|
+
|
|
12
|
+
export interface NativeProps extends ViewProps {
|
|
13
|
+
detents: ReadonlyArray<NativeDetent>;
|
|
14
|
+
index: CodegenTypes.Int32;
|
|
15
|
+
animateIn: boolean;
|
|
16
|
+
onIndexChange?: CodegenTypes.DirectEventHandler<
|
|
17
|
+
Readonly<{ index: CodegenTypes.Int32 }>
|
|
18
|
+
>;
|
|
19
|
+
onPositionChange?: CodegenTypes.DirectEventHandler<
|
|
20
|
+
Readonly<{ position: CodegenTypes.Double }>
|
|
21
|
+
>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default codegenNativeComponent<NativeProps>('BottomSheetView');
|
package/lib/module/index.js
CHANGED
|
@@ -5,6 +5,6 @@ export { ModalBottomSheet } from "./ModalBottomSheet.js";
|
|
|
5
5
|
export { BottomSheetProvider } from "./BottomSheetProvider.js";
|
|
6
6
|
export { BottomSheetFlatList } from "./BottomSheetFlatList.js";
|
|
7
7
|
export { BottomSheetScrollView } from "./BottomSheetScrollView.js";
|
|
8
|
-
export { programmatic } from "./
|
|
8
|
+
export { programmatic } from "./bottomSheetUtils.js";
|
|
9
9
|
export { bottomSheetScrollable } from "./bottomSheetScrollable.js";
|
|
10
10
|
//# sourceMappingURL=index.js.map
|
package/lib/module/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["BottomSheet","ModalBottomSheet","BottomSheetProvider","BottomSheetFlatList","BottomSheetScrollView","programmatic","bottomSheetScrollable"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,SAASA,WAAW,QAAQ,kBAAe;AAE3C,SAASC,gBAAgB,QAAQ,uBAAoB;AAErD,SAASC,mBAAmB,QAAQ,0BAAuB;AAC3D,SAASC,mBAAmB,QAAQ,0BAAuB;AAE3D,SAASC,qBAAqB,QAAQ,4BAAyB;AAG/D,SAASC,YAAY,QAAQ,
|
|
1
|
+
{"version":3,"names":["BottomSheet","ModalBottomSheet","BottomSheetProvider","BottomSheetFlatList","BottomSheetScrollView","programmatic","bottomSheetScrollable"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,SAASA,WAAW,QAAQ,kBAAe;AAE3C,SAASC,gBAAgB,QAAQ,uBAAoB;AAErD,SAASC,mBAAmB,QAAQ,0BAAuB;AAC3D,SAASC,mBAAmB,QAAQ,0BAAuB;AAE3D,SAASC,qBAAqB,QAAQ,4BAAyB;AAG/D,SAASC,YAAY,QAAQ,uBAAoB;AACjD,SAASC,qBAAqB,QAAQ,4BAAyB","ignoreList":[]}
|
|
@@ -130,16 +130,21 @@ export const useBottomSheetPanGesture = ({
|
|
|
130
130
|
const resolvedDetents = detentsValue.value;
|
|
131
131
|
const draggable = isDraggableValue.value;
|
|
132
132
|
let maxDraggableTranslateY = sheetHeight.value;
|
|
133
|
+
let minDraggableTranslateY = 0;
|
|
133
134
|
let foundDraggable = false;
|
|
134
135
|
for (let i = 0; i < resolvedDetents.length; i++) {
|
|
135
136
|
if (!(draggable[i] ?? true)) continue;
|
|
136
137
|
const t = sheetHeight.value - (resolvedDetents[i] ?? 0);
|
|
137
|
-
if (!foundDraggable
|
|
138
|
+
if (!foundDraggable) {
|
|
138
139
|
maxDraggableTranslateY = t;
|
|
140
|
+
minDraggableTranslateY = t;
|
|
139
141
|
foundDraggable = true;
|
|
142
|
+
} else {
|
|
143
|
+
if (t > maxDraggableTranslateY) maxDraggableTranslateY = t;
|
|
144
|
+
if (t < minDraggableTranslateY) minDraggableTranslateY = t;
|
|
140
145
|
}
|
|
141
146
|
}
|
|
142
|
-
const nextTranslate = Math.min(Math.max(rawTranslate,
|
|
147
|
+
const nextTranslate = Math.min(Math.max(rawTranslate, minDraggableTranslateY), maxDraggableTranslateY);
|
|
143
148
|
translateY.set(nextTranslate);
|
|
144
149
|
if (isDraggingSheet.value && rawTranslate < 0 && hasActive) {
|
|
145
150
|
isDraggingSheet.set(false);
|