@onekeyfe/react-native-bottom-tabs 6.7.0-19 → 6.7.0-21
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/onekey/bottomtabs/BottomBarBlurRenderer.kt +50 -30
- package/android/src/main/java/com/onekey/bottomtabs/BottomTabsDrawerManager.kt +55 -0
- package/android/src/main/java/com/onekey/bottomtabs/BottomTabsDrawerView.kt +496 -0
- package/android/src/main/java/com/onekey/bottomtabs/BottomTabsPackage.kt +1 -1
- package/android/src/main/java/com/onekey/bottomtabs/BottomTabsView.kt +24 -4
- package/android/src/main/java/com/onekey/bottomtabs/BottomTabsViewManager.kt +3 -0
- package/docs/SPEC.md +184 -20
- package/ios/RCTBottomTabsComponentView.mm +37 -0
- package/ios/RCTBottomTabsContainerView.swift +192 -7
- package/ios/RCTBottomTabsDrawerComponentView.h +8 -0
- package/ios/RCTBottomTabsDrawerComponentView.mm +79 -0
- package/ios/RCTBottomTabsDrawerContainerView.swift +327 -0
- package/lib/module/BottomTabs.js +144 -87
- package/lib/module/BottomTabsDrawerNativeComponent.ts +19 -0
- package/lib/module/BottomTabsDrawerNativeComponent.web.js +13 -0
- package/lib/module/BottomTabsNativeComponent.ts +13 -0
- package/lib/typescript/src/BottomTabs.d.ts +36 -10
- package/lib/typescript/src/BottomTabsDrawerNativeComponent.d.ts +15 -0
- package/lib/typescript/src/BottomTabsDrawerNativeComponent.web.d.ts +4 -0
- package/lib/typescript/src/BottomTabsNativeComponent.d.ts +8 -0
- package/lib/typescript/src/index.d.ts +1 -1
- package/lib/typescript/src/types.d.ts +1 -0
- package/package.json +3 -2
- package/react-native.config.js +4 -1
- package/src/BottomTabs.tsx +235 -115
- package/src/BottomTabsDrawerNativeComponent.ts +19 -0
- package/src/BottomTabsDrawerNativeComponent.web.tsx +9 -0
- package/src/BottomTabsNativeComponent.ts +13 -0
- package/src/index.tsx +1 -1
- package/src/types.ts +3 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#import "RCTBottomTabsDrawerComponentView.h"
|
|
2
|
+
#import <react/renderer/components/RNBottomTabs/ComponentDescriptors.h>
|
|
3
|
+
#import <react/renderer/components/RNBottomTabs/EventEmitters.h>
|
|
4
|
+
#import <react/renderer/components/RNBottomTabs/Props.h>
|
|
5
|
+
#import <React/RCTFabricComponentsPlugins.h>
|
|
6
|
+
|
|
7
|
+
// Swift keeps the host internal to this module, so its framework compatibility
|
|
8
|
+
// header does not export the class. Its explicit @objc name and selectors form
|
|
9
|
+
// this private boundary, matching the existing tab container's runtime lookup.
|
|
10
|
+
@protocol RNBottomTabsDrawerContainer <NSObject>
|
|
11
|
+
@property(nonatomic) BOOL enabled;
|
|
12
|
+
@property(nonatomic) BOOL open;
|
|
13
|
+
@property(nonatomic) BOOL swipeEnabled;
|
|
14
|
+
@property(nonatomic) BOOL hapticFeedbackEnabled;
|
|
15
|
+
@property(nonatomic) double bottomBarHeight;
|
|
16
|
+
@property(nonatomic, copy) void (^onDrawerOpenChange)(BOOL open);
|
|
17
|
+
- (void)insertChild:(UIView *)child atIndex:(NSInteger)index;
|
|
18
|
+
- (void)removeChildAtIndex:(NSInteger)index;
|
|
19
|
+
@end
|
|
20
|
+
|
|
21
|
+
using namespace facebook::react;
|
|
22
|
+
|
|
23
|
+
@implementation RCTBottomTabsDrawerComponentView {
|
|
24
|
+
UIView<RNBottomTabsDrawerContainer> *_drawer;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
+ (ComponentDescriptorProvider)componentDescriptorProvider
|
|
28
|
+
{
|
|
29
|
+
return concreteComponentDescriptorProvider<RNBottomTabsDrawerComponentDescriptor>();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
+ (BOOL)shouldBeRecycled { return NO; }
|
|
33
|
+
|
|
34
|
+
- (instancetype)initWithFrame:(CGRect)frame
|
|
35
|
+
{
|
|
36
|
+
if (self = [super initWithFrame:frame]) {
|
|
37
|
+
_props = std::make_shared<const RNBottomTabsDrawerProps>();
|
|
38
|
+
Class containerClass = NSClassFromString(@"RCTBottomTabsDrawerContainerView");
|
|
39
|
+
_drawer = (UIView<RNBottomTabsDrawerContainer> *)[[containerClass alloc] initWithFrame:frame];
|
|
40
|
+
self.contentView = _drawer;
|
|
41
|
+
__weak RCTBottomTabsDrawerComponentView *weakSelf = self;
|
|
42
|
+
_drawer.onDrawerOpenChange = ^(BOOL open) {
|
|
43
|
+
auto strongSelf = weakSelf;
|
|
44
|
+
if (!strongSelf) return;
|
|
45
|
+
auto emitter = std::static_pointer_cast<const RNBottomTabsDrawerEventEmitter>(strongSelf->_eventEmitter);
|
|
46
|
+
if (emitter) emitter->onDrawerOpenChange({.open = (bool)open});
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return self;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)child index:(NSInteger)index
|
|
53
|
+
{
|
|
54
|
+
[_drawer insertChild:child atIndex:index];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)child index:(NSInteger)index
|
|
58
|
+
{
|
|
59
|
+
[_drawer removeChildAtIndex:index];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
|
|
63
|
+
{
|
|
64
|
+
const auto &next = *std::static_pointer_cast<const RNBottomTabsDrawerProps>(props);
|
|
65
|
+
const auto &previous = *std::static_pointer_cast<const RNBottomTabsDrawerProps>(_props);
|
|
66
|
+
if (next.enabled != previous.enabled) _drawer.enabled = next.enabled;
|
|
67
|
+
if (next.open != previous.open) _drawer.open = next.open;
|
|
68
|
+
if (next.swipeEnabled != previous.swipeEnabled) _drawer.swipeEnabled = next.swipeEnabled;
|
|
69
|
+
if (next.hapticFeedbackEnabled != previous.hapticFeedbackEnabled)
|
|
70
|
+
_drawer.hapticFeedbackEnabled = next.hapticFeedbackEnabled;
|
|
71
|
+
if (next.bottomBarHeight != previous.bottomBarHeight) _drawer.bottomBarHeight = next.bottomBarHeight;
|
|
72
|
+
[super updateProps:props oldProps:oldProps];
|
|
73
|
+
}
|
|
74
|
+
@end
|
|
75
|
+
|
|
76
|
+
Class<RCTComponentViewProtocol> RNBottomTabsDrawerCls(void)
|
|
77
|
+
{
|
|
78
|
+
return RCTBottomTabsDrawerComponentView.class;
|
|
79
|
+
}
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
/// Internal native host. React owns two fixed slots: menu, then foreground.
|
|
4
|
+
@objc(RCTBottomTabsDrawerContainerView)
|
|
5
|
+
final class RCTBottomTabsDrawerContainerView: UIView, UIGestureRecognizerDelegate {
|
|
6
|
+
@objc var enabled = false {
|
|
7
|
+
didSet {
|
|
8
|
+
guard enabled != oldValue else { return }
|
|
9
|
+
interruptGesture()
|
|
10
|
+
stopAnimation()
|
|
11
|
+
stableOpen = false
|
|
12
|
+
setProgress(0)
|
|
13
|
+
if enabled && open { settle(to: true, velocity: 0, user: false) }
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
@objc var open = false {
|
|
17
|
+
didSet {
|
|
18
|
+
guard open != oldValue else { return }
|
|
19
|
+
interruptGesture()
|
|
20
|
+
settle(to: enabled && open, velocity: 0, user: false)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
@objc var swipeEnabled = false
|
|
24
|
+
@objc var hapticFeedbackEnabled = false
|
|
25
|
+
@objc var bottomBarHeight: Double = 0
|
|
26
|
+
@objc var onDrawerOpenChange: ((Bool) -> Void)?
|
|
27
|
+
|
|
28
|
+
private let menu = UIView()
|
|
29
|
+
private let menuShade = UIView()
|
|
30
|
+
private let foreground = UIView()
|
|
31
|
+
private let foregroundContent = UIView()
|
|
32
|
+
private let dim = UIButton(type: .custom)
|
|
33
|
+
private var slots: [UIView] = []
|
|
34
|
+
private var pan: UIPanGestureRecognizer!
|
|
35
|
+
private var progress: CGFloat = 0
|
|
36
|
+
private var stableOpen = false
|
|
37
|
+
private var gestureStartProgress: CGFloat = 0
|
|
38
|
+
private var gestureReturnOpen = false
|
|
39
|
+
private weak var touchTarget: UIView?
|
|
40
|
+
private var suppressCancellation = false
|
|
41
|
+
private var displayLink: CADisplayLink?
|
|
42
|
+
private var animationStart: CFTimeInterval = 0
|
|
43
|
+
private var animationFrom: CGFloat = 0
|
|
44
|
+
private var animationVelocity: CGFloat = 0
|
|
45
|
+
private var animationTarget = false
|
|
46
|
+
private var animationUser = false
|
|
47
|
+
private var previousSize = CGSize.zero
|
|
48
|
+
|
|
49
|
+
override init(frame: CGRect) {
|
|
50
|
+
super.init(frame: frame)
|
|
51
|
+
clipsToBounds = true
|
|
52
|
+
addSubview(menu)
|
|
53
|
+
menuShade.backgroundColor = .black
|
|
54
|
+
menuShade.isUserInteractionEnabled = false
|
|
55
|
+
menu.addSubview(menuShade)
|
|
56
|
+
addSubview(foreground)
|
|
57
|
+
foreground.addSubview(foregroundContent)
|
|
58
|
+
dim.backgroundColor = UIColor(white: 28.0 / 255.0, alpha: 1)
|
|
59
|
+
dim.accessibilityLabel = NSLocalizedString("Close menu", comment: "Close the tab drawer")
|
|
60
|
+
dim.addTarget(self, action: #selector(closeFromUser), for: .touchUpInside)
|
|
61
|
+
foreground.addSubview(dim)
|
|
62
|
+
foreground.layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner]
|
|
63
|
+
foreground.layer.cornerCurve = .circular
|
|
64
|
+
foreground.clipsToBounds = true
|
|
65
|
+
pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
|
|
66
|
+
pan.maximumNumberOfTouches = 1
|
|
67
|
+
pan.delegate = self
|
|
68
|
+
addGestureRecognizer(pan)
|
|
69
|
+
setProgress(0)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
|
73
|
+
|
|
74
|
+
@objc func insertChild(_ child: UIView, atIndex index: Int) {
|
|
75
|
+
guard index >= 0 && index <= slots.count && slots.count < 2 else { return }
|
|
76
|
+
slots.insert(child, at: index)
|
|
77
|
+
updateSlots()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@objc func removeChild(atIndex index: Int) {
|
|
81
|
+
guard slots.indices.contains(index) else { return }
|
|
82
|
+
slots.remove(at: index).removeFromSuperview()
|
|
83
|
+
updateSlots()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private func updateSlots() {
|
|
87
|
+
for (index, child) in slots.enumerated() {
|
|
88
|
+
(index == 0 ? menu : foregroundContent).insertSubview(child, at: 0)
|
|
89
|
+
}
|
|
90
|
+
menu.bringSubviewToFront(menuShade)
|
|
91
|
+
setNeedsLayout()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
override func layoutSubviews() {
|
|
95
|
+
super.layoutSubviews()
|
|
96
|
+
if previousSize != bounds.size {
|
|
97
|
+
let dragging = pan.state == .began || pan.state == .changed
|
|
98
|
+
let target = displayLink != nil ? animationTarget : (dragging ? gestureReturnOpen : stableOpen)
|
|
99
|
+
let user = displayLink != nil && animationUser && previousSize != .zero
|
|
100
|
+
interruptGesture()
|
|
101
|
+
stopAnimation()
|
|
102
|
+
finish(target: enabled && (previousSize == .zero ? open : target), user: user)
|
|
103
|
+
previousSize = bounds.size
|
|
104
|
+
}
|
|
105
|
+
menu.transform = .identity
|
|
106
|
+
foreground.transform = .identity
|
|
107
|
+
menu.frame = CGRect(x: 0, y: 0, width: bounds.width * 0.78, height: bounds.height)
|
|
108
|
+
foreground.frame = bounds
|
|
109
|
+
foregroundContent.frame = foreground.bounds
|
|
110
|
+
dim.frame = foreground.bounds
|
|
111
|
+
menuShade.frame = menu.bounds
|
|
112
|
+
if slots.count > 0 { slots[0].frame = menu.bounds }
|
|
113
|
+
if slots.count > 1 { slots[1].frame = foreground.bounds }
|
|
114
|
+
setProgress(progress)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
override func didMoveToWindow() {
|
|
118
|
+
super.didMoveToWindow()
|
|
119
|
+
if window == nil {
|
|
120
|
+
interruptGesture()
|
|
121
|
+
stopAnimation()
|
|
122
|
+
setProgress(stableOpen && enabled ? 1 : 0)
|
|
123
|
+
} else if enabled && stableOpen != open {
|
|
124
|
+
settle(to: open, velocity: 0, user: false)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
override func accessibilityPerformEscape() -> Bool {
|
|
129
|
+
guard enabled && progress > 0 else { return false }
|
|
130
|
+
closeFromUser()
|
|
131
|
+
return true
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
|
|
135
|
+
guard enabled else { return false }
|
|
136
|
+
let start = touch.location(in: self)
|
|
137
|
+
guard start.y < bounds.height - CGFloat(max(0, bottomBarHeight)) else { return false }
|
|
138
|
+
touchTarget = touch.view
|
|
139
|
+
if progress > 0 { return start.x >= bounds.width * 0.78 * progress }
|
|
140
|
+
return swipeEnabled && !touchPathContainsExclusiveControl()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
144
|
+
guard enabled, bounds.width > 0 else { return false }
|
|
145
|
+
let velocity = pan.velocity(in: self)
|
|
146
|
+
guard abs(velocity.x) > abs(velocity.y) else { return false }
|
|
147
|
+
let location = pan.location(in: self)
|
|
148
|
+
let translation = pan.translation(in: self)
|
|
149
|
+
let start = CGPoint(x: location.x - translation.x, y: location.y - translation.y)
|
|
150
|
+
guard start.y < bounds.height - CGFloat(max(0, bottomBarHeight)) else { return false }
|
|
151
|
+
if progress > 0 {
|
|
152
|
+
return start.x >= bounds.width * 0.78 * progress
|
|
153
|
+
}
|
|
154
|
+
return swipeEnabled && velocity.x > 0 && !touchPathCanScrollRight()
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Let this recognizer make its one-time direction/boundary decision before
|
|
158
|
+
// descendant scroll pans. Returning false from shouldBegin releases them.
|
|
159
|
+
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
|
|
160
|
+
shouldBeRequiredToFailBy other: UIGestureRecognizer) -> Bool {
|
|
161
|
+
guard gestureRecognizer === pan,
|
|
162
|
+
let scroll = other.view as? UIScrollView,
|
|
163
|
+
other === scroll.panGestureRecognizer else { return false }
|
|
164
|
+
return scroll.isDescendant(of: foregroundContent)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
|
|
168
|
+
shouldRequireFailureOf other: UIGestureRecognizer) -> Bool {
|
|
169
|
+
guard gestureRecognizer === pan else { return false }
|
|
170
|
+
// UIKit's navigation edge gesture retains priority over the drawer.
|
|
171
|
+
return other is UIScreenEdgePanGestureRecognizer
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private func touchPathContainsExclusiveControl() -> Bool {
|
|
175
|
+
var view = touchTarget
|
|
176
|
+
while let current = view, current !== self {
|
|
177
|
+
if current is UISlider || current is UISwitch || current is UISegmentedControl ||
|
|
178
|
+
current is UITextField || current is UITextView { return true }
|
|
179
|
+
if let webClass = NSClassFromString("WKWebView"), current.isKind(of: webClass) { return true }
|
|
180
|
+
if current.layer is CAMetalLayer { return true }
|
|
181
|
+
view = current.superview
|
|
182
|
+
}
|
|
183
|
+
return false
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private func pageController(for scroll: UIScrollView) -> UIPageViewController? {
|
|
187
|
+
var responder: UIResponder? = scroll.next
|
|
188
|
+
while let current = responder, current !== self {
|
|
189
|
+
if let page = current as? UIPageViewController, scroll.superview === page.view { return page }
|
|
190
|
+
responder = current.next
|
|
191
|
+
}
|
|
192
|
+
return nil
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private func touchPathCanScrollRight() -> Bool {
|
|
196
|
+
var view = touchTarget
|
|
197
|
+
while let current = view, current !== self {
|
|
198
|
+
if let scroll = current as? UIScrollView, scroll.isScrollEnabled {
|
|
199
|
+
// The native smooth pager exposes this existing header exclusion. Read
|
|
200
|
+
// its getter instead of invoking UIKit's recognition lifecycle hook.
|
|
201
|
+
let selector = NSSelectorFromString("excludedHeaderView")
|
|
202
|
+
if scroll.responds(to: selector),
|
|
203
|
+
let header = scroll.perform(selector)?.takeUnretainedValue() as? UIView,
|
|
204
|
+
let target = touchTarget, target.isDescendant(of: header) {
|
|
205
|
+
view = current.superview
|
|
206
|
+
continue
|
|
207
|
+
}
|
|
208
|
+
if let page = pageController(for: scroll) {
|
|
209
|
+
// UIPageViewController keeps its private scroll view centered even at
|
|
210
|
+
// the first page. Its public before query describes physical right.
|
|
211
|
+
if page.navigationOrientation == .horizontal,
|
|
212
|
+
let displayed = page.viewControllers?.first,
|
|
213
|
+
page.dataSource?.pageViewController(page, viewControllerBefore: displayed) != nil { return true }
|
|
214
|
+
} else {
|
|
215
|
+
let inset = scroll.adjustedContentInset
|
|
216
|
+
let horizontal = scroll.contentSize.width + inset.left + inset.right > scroll.bounds.width + 0.5
|
|
217
|
+
if horizontal && scroll.contentOffset.x > -inset.left + 0.5 { return true }
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
view = current.superview
|
|
221
|
+
}
|
|
222
|
+
return false
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
@objc private func handlePan(_ recognizer: UIPanGestureRecognizer) {
|
|
226
|
+
switch recognizer.state {
|
|
227
|
+
case .began:
|
|
228
|
+
gestureReturnOpen = displayLink != nil && !animationUser ? animationTarget : stableOpen
|
|
229
|
+
stopAnimation()
|
|
230
|
+
gestureStartProgress = progress
|
|
231
|
+
case .changed:
|
|
232
|
+
guard recognizer.numberOfTouches <= 1 else {
|
|
233
|
+
interruptGesture()
|
|
234
|
+
settle(to: gestureReturnOpen, velocity: 0, user: false)
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
setProgress(gestureStartProgress + recognizer.translation(in: self).x / max(1, bounds.width * 0.78))
|
|
238
|
+
case .ended:
|
|
239
|
+
let velocity = min(8, max(-8, recognizer.velocity(in: self).x / max(1, bounds.width * 0.78)))
|
|
240
|
+
settle(to: progress + velocity * 0.12 >= 0.5, velocity: velocity, user: true)
|
|
241
|
+
case .cancelled, .failed:
|
|
242
|
+
if !suppressCancellation { settle(to: gestureReturnOpen, velocity: 0, user: false) }
|
|
243
|
+
default: break
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private func interruptGesture() {
|
|
248
|
+
guard pan != nil else { return }
|
|
249
|
+
suppressCancellation = true
|
|
250
|
+
pan.isEnabled = false
|
|
251
|
+
pan.isEnabled = true
|
|
252
|
+
touchTarget = nil
|
|
253
|
+
suppressCancellation = false
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
@objc private func closeFromUser() {
|
|
257
|
+
interruptGesture()
|
|
258
|
+
settle(to: false, velocity: 0, user: true)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private func setProgress(_ value: CGFloat) {
|
|
262
|
+
progress = min(1, max(0, enabled ? value : 0))
|
|
263
|
+
foreground.transform = CGAffineTransform(translationX: bounds.width * 0.78 * progress, y: 0)
|
|
264
|
+
foreground.layer.cornerRadius = 64 * progress
|
|
265
|
+
dim.alpha = 0.7 * progress
|
|
266
|
+
dim.isHidden = progress == 0
|
|
267
|
+
foregroundContent.isUserInteractionEnabled = progress == 0
|
|
268
|
+
foregroundContent.accessibilityElementsHidden = progress > 0
|
|
269
|
+
menu.transform = CGAffineTransform(translationX: -0.12 * bounds.width * 0.78 * (1 - progress), y: 0)
|
|
270
|
+
menuShade.alpha = 0.12 * (1 - progress)
|
|
271
|
+
menu.isHidden = progress == 0
|
|
272
|
+
menu.accessibilityElementsHidden = progress == 0
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private func settle(to target: Bool, velocity: CGFloat, user: Bool) {
|
|
276
|
+
stopAnimation()
|
|
277
|
+
let target = enabled && target
|
|
278
|
+
guard bounds.width > 0, window != nil else {
|
|
279
|
+
stableOpen = target
|
|
280
|
+
setProgress(target ? 1 : 0)
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
if progress == (target ? 1 : 0) {
|
|
284
|
+
finish(target: target, user: user)
|
|
285
|
+
return
|
|
286
|
+
}
|
|
287
|
+
animationFrom = progress
|
|
288
|
+
animationVelocity = min(8, max(-8, velocity))
|
|
289
|
+
animationTarget = target
|
|
290
|
+
animationUser = user
|
|
291
|
+
animationStart = CACurrentMediaTime()
|
|
292
|
+
let link = CADisplayLink(target: self, selector: #selector(tick))
|
|
293
|
+
link.add(to: .main, forMode: .common)
|
|
294
|
+
displayLink = link
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
@objc private func tick() {
|
|
298
|
+
let elapsed = CGFloat(CACurrentMediaTime() - animationStart)
|
|
299
|
+
let target: CGFloat = animationTarget ? 1 : 0
|
|
300
|
+
let a = animationFrom - target
|
|
301
|
+
let b = animationVelocity + 22 * a
|
|
302
|
+
setProgress(target + (a + b * elapsed) * exp(-22 * elapsed))
|
|
303
|
+
if elapsed >= 0.5 {
|
|
304
|
+
let target = animationTarget
|
|
305
|
+
let user = animationUser
|
|
306
|
+
stopAnimation()
|
|
307
|
+
finish(target: target, user: user)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private func finish(target: Bool, user: Bool) {
|
|
312
|
+
let changed = stableOpen != target
|
|
313
|
+
stableOpen = target
|
|
314
|
+
setProgress(target ? 1 : 0)
|
|
315
|
+
if user && (changed || target != open) {
|
|
316
|
+
if hapticFeedbackEnabled && changed { UIImpactFeedbackGenerator(style: .light).impactOccurred() }
|
|
317
|
+
onDrawerOpenChange?(target)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
private func stopAnimation() {
|
|
322
|
+
displayLink?.invalidate()
|
|
323
|
+
displayLink = nil
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
deinit { displayLink?.invalidate() }
|
|
327
|
+
}
|