@onekeyfe/react-native-native-overlay 3.0.162-alpha.268
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/LICENSE +21 -0
- package/NativeOverlay.podspec +30 -0
- package/README.md +35 -0
- package/android/build.gradle +82 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayAnimation.kt +176 -0
- package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayEntryRootView.kt +415 -0
- package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayEvents.kt +38 -0
- package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayHost.kt +174 -0
- package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayPackage.kt +16 -0
- package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayPageHostView.kt +122 -0
- package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayPageHostViewManager.kt +40 -0
- package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayView.kt +465 -0
- package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayViewManager.kt +156 -0
- package/android/src/main/jni/CMakeLists.txt +44 -0
- package/android/src/main/jni/RNCNativeOverlay.h +19 -0
- package/common/cpp/react/renderer/components/RNCNativeOverlay/NativeOverlayComponentDescriptor.h +12 -0
- package/common/cpp/react/renderer/components/RNCNativeOverlay/NativeOverlayShadowNode.cpp +12 -0
- package/common/cpp/react/renderer/components/RNCNativeOverlay/NativeOverlayShadowNode.h +41 -0
- package/common/cpp/react/renderer/components/RNCNativeOverlay/NativeOverlayState.h +40 -0
- package/docs/SPEC.md +693 -0
- package/ios/NativeOverlayAnimation.swift +160 -0
- package/ios/NativeOverlayContainerView.swift +688 -0
- package/ios/NativeOverlayKeyboard.swift +88 -0
- package/ios/NativeOverlayPageHostView.swift +98 -0
- package/ios/NativeOverlaySheetController.swift +391 -0
- package/ios/NativeOverlayWindowManager.swift +194 -0
- package/ios/RCTNativeOverlayComponentView.h +11 -0
- package/ios/RCTNativeOverlayComponentView.mm +193 -0
- package/ios/RCTNativeOverlayPageHostComponentView.h +11 -0
- package/ios/RCTNativeOverlayPageHostComponentView.mm +64 -0
- package/lib/module/NativeOverlayNativeComponent.ts +61 -0
- package/lib/module/NativeOverlayPageHostNativeComponent.ts +13 -0
- package/lib/module/OverlayErrors.js +6 -0
- package/lib/module/OverlayErrors.js.map +1 -0
- package/lib/module/OverlayLevelContext.js +27 -0
- package/lib/module/OverlayLevelContext.js.map +1 -0
- package/lib/module/OverlayLevels.js +33 -0
- package/lib/module/OverlayLevels.js.map +1 -0
- package/lib/module/OverlayPageHost.js +24 -0
- package/lib/module/OverlayPageHost.js.map +1 -0
- package/lib/module/OverlayPageHost.web.js +49 -0
- package/lib/module/OverlayPageHost.web.js.map +1 -0
- package/lib/module/OverlayPageScope.js +41 -0
- package/lib/module/OverlayPageScope.js.map +1 -0
- package/lib/module/OverlayStore.js +305 -0
- package/lib/module/OverlayStore.js.map +1 -0
- package/lib/module/OverlayView.js +141 -0
- package/lib/module/OverlayView.js.map +1 -0
- package/lib/module/OverlayView.web.js +290 -0
- package/lib/module/OverlayView.web.js.map +1 -0
- package/lib/module/OverlayViewTypes.js +4 -0
- package/lib/module/OverlayViewTypes.js.map +1 -0
- package/lib/module/animation/motionPresets.js +57 -0
- package/lib/module/animation/motionPresets.js.map +1 -0
- package/lib/module/animation/platformMotionPresets.js +4 -0
- package/lib/module/animation/platformMotionPresets.js.map +1 -0
- package/lib/module/animation/platformMotionPresets.web.js +4 -0
- package/lib/module/animation/platformMotionPresets.web.js.map +1 -0
- package/lib/module/animation/resolveAnimation.js +80 -0
- package/lib/module/animation/resolveAnimation.js.map +1 -0
- package/lib/module/animation/spring.js +69 -0
- package/lib/module/animation/spring.js.map +1 -0
- package/lib/module/animation/types.js +2 -0
- package/lib/module/animation/types.js.map +1 -0
- package/lib/module/index.js +16 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/useOverlayController.js +127 -0
- package/lib/module/useOverlayController.js.map +1 -0
- package/lib/module/useSuspendedPageOwners.js +16 -0
- package/lib/module/useSuspendedPageOwners.js.map +1 -0
- package/lib/module/web/animateTransition.js +115 -0
- package/lib/module/web/animateTransition.js.map +1 -0
- package/lib/module/web/overlayLayers.js +147 -0
- package/lib/module/web/overlayLayers.js.map +1 -0
- package/lib/module/web/useKeyboardShift.js +110 -0
- package/lib/module/web/useKeyboardShift.js.map +1 -0
- package/lib/module/web/useSheetDrag.js +126 -0
- package/lib/module/web/useSheetDrag.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeOverlayNativeComponent.d.ts +41 -0
- package/lib/typescript/src/NativeOverlayNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/NativeOverlayPageHostNativeComponent.d.ts +9 -0
- package/lib/typescript/src/NativeOverlayPageHostNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/OverlayErrors.d.ts +4 -0
- package/lib/typescript/src/OverlayErrors.d.ts.map +1 -0
- package/lib/typescript/src/OverlayLevelContext.d.ts +12 -0
- package/lib/typescript/src/OverlayLevelContext.d.ts.map +1 -0
- package/lib/typescript/src/OverlayLevels.d.ts +13 -0
- package/lib/typescript/src/OverlayLevels.d.ts.map +1 -0
- package/lib/typescript/src/OverlayPageHost.d.ts +9 -0
- package/lib/typescript/src/OverlayPageHost.d.ts.map +1 -0
- package/lib/typescript/src/OverlayPageHost.web.d.ts +11 -0
- package/lib/typescript/src/OverlayPageHost.web.d.ts.map +1 -0
- package/lib/typescript/src/OverlayPageScope.d.ts +17 -0
- package/lib/typescript/src/OverlayPageScope.d.ts.map +1 -0
- package/lib/typescript/src/OverlayStore.d.ts +72 -0
- package/lib/typescript/src/OverlayStore.d.ts.map +1 -0
- package/lib/typescript/src/OverlayView.d.ts +3 -0
- package/lib/typescript/src/OverlayView.d.ts.map +1 -0
- package/lib/typescript/src/OverlayView.web.d.ts +3 -0
- package/lib/typescript/src/OverlayView.web.d.ts.map +1 -0
- package/lib/typescript/src/OverlayViewTypes.d.ts +68 -0
- package/lib/typescript/src/OverlayViewTypes.d.ts.map +1 -0
- package/lib/typescript/src/animation/motionPresets.d.ts +13 -0
- package/lib/typescript/src/animation/motionPresets.d.ts.map +1 -0
- package/lib/typescript/src/animation/platformMotionPresets.d.ts +2 -0
- package/lib/typescript/src/animation/platformMotionPresets.d.ts.map +1 -0
- package/lib/typescript/src/animation/platformMotionPresets.web.d.ts +2 -0
- package/lib/typescript/src/animation/platformMotionPresets.web.d.ts.map +1 -0
- package/lib/typescript/src/animation/resolveAnimation.d.ts +23 -0
- package/lib/typescript/src/animation/resolveAnimation.d.ts.map +1 -0
- package/lib/typescript/src/animation/spring.d.ts +24 -0
- package/lib/typescript/src/animation/spring.d.ts.map +1 -0
- package/lib/typescript/src/animation/types.d.ts +56 -0
- package/lib/typescript/src/animation/types.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +18 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +59 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/useOverlayController.d.ts +29 -0
- package/lib/typescript/src/useOverlayController.d.ts.map +1 -0
- package/lib/typescript/src/useSuspendedPageOwners.d.ts +7 -0
- package/lib/typescript/src/useSuspendedPageOwners.d.ts.map +1 -0
- package/lib/typescript/src/web/animateTransition.d.ts +18 -0
- package/lib/typescript/src/web/animateTransition.d.ts.map +1 -0
- package/lib/typescript/src/web/overlayLayers.d.ts +10 -0
- package/lib/typescript/src/web/overlayLayers.d.ts.map +1 -0
- package/lib/typescript/src/web/useKeyboardShift.d.ts +29 -0
- package/lib/typescript/src/web/useKeyboardShift.d.ts.map +1 -0
- package/lib/typescript/src/web/useSheetDrag.d.ts +13 -0
- package/lib/typescript/src/web/useSheetDrag.d.ts.map +1 -0
- package/package.json +188 -0
- package/react-native.config.js +16 -0
- package/src/NativeOverlayNativeComponent.ts +61 -0
- package/src/NativeOverlayPageHostNativeComponent.ts +13 -0
- package/src/OverlayErrors.ts +3 -0
- package/src/OverlayLevelContext.tsx +32 -0
- package/src/OverlayLevels.ts +40 -0
- package/src/OverlayPageHost.tsx +21 -0
- package/src/OverlayPageHost.web.tsx +53 -0
- package/src/OverlayPageScope.tsx +42 -0
- package/src/OverlayStore.ts +401 -0
- package/src/OverlayView.tsx +165 -0
- package/src/OverlayView.web.tsx +356 -0
- package/src/OverlayViewTypes.ts +81 -0
- package/src/animation/motionPresets.ts +52 -0
- package/src/animation/platformMotionPresets.ts +1 -0
- package/src/animation/platformMotionPresets.web.ts +1 -0
- package/src/animation/resolveAnimation.ts +103 -0
- package/src/animation/spring.ts +80 -0
- package/src/animation/types.ts +71 -0
- package/src/index.ts +55 -0
- package/src/types.ts +79 -0
- package/src/useOverlayController.ts +199 -0
- package/src/useSuspendedPageOwners.ts +26 -0
- package/src/web/animateTransition.ts +137 -0
- package/src/web/overlayLayers.ts +182 -0
- package/src/web/useKeyboardShift.ts +130 -0
- package/src/web/useSheetDrag.ts +143 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
/// Tracks the software keyboard frame for every overlay entry, so an entry
|
|
4
|
+
/// presented while the keyboard is already up avoids it too.
|
|
5
|
+
final class NativeOverlayKeyboard {
|
|
6
|
+
static let shared = NativeOverlayKeyboard()
|
|
7
|
+
|
|
8
|
+
static let didChange = Notification.Name("NativeOverlayKeyboardDidChange")
|
|
9
|
+
|
|
10
|
+
/// Screen coordinates; `.zero` while hidden.
|
|
11
|
+
private(set) var frame: CGRect = .zero
|
|
12
|
+
private(set) var duration: TimeInterval = 0.25
|
|
13
|
+
private(set) var curve: UIView.AnimationCurve = .easeInOut
|
|
14
|
+
|
|
15
|
+
private init() {
|
|
16
|
+
let center = NotificationCenter.default
|
|
17
|
+
center.addObserver(
|
|
18
|
+
self,
|
|
19
|
+
selector: #selector(keyboardWillChange(_:)),
|
|
20
|
+
name: UIResponder.keyboardWillChangeFrameNotification,
|
|
21
|
+
object: nil
|
|
22
|
+
)
|
|
23
|
+
center.addObserver(
|
|
24
|
+
self,
|
|
25
|
+
selector: #selector(keyboardWillHide(_:)),
|
|
26
|
+
name: UIResponder.keyboardWillHideNotification,
|
|
27
|
+
object: nil
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@objc private func keyboardWillChange(_ notification: Notification) {
|
|
32
|
+
readAnimation(notification)
|
|
33
|
+
let end = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect ?? .zero
|
|
34
|
+
frame = end
|
|
35
|
+
NotificationCenter.default.post(name: Self.didChange, object: self)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@objc private func keyboardWillHide(_ notification: Notification) {
|
|
39
|
+
readAnimation(notification)
|
|
40
|
+
frame = .zero
|
|
41
|
+
NotificationCenter.default.post(name: Self.didChange, object: self)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private func readAnimation(_ notification: Notification) {
|
|
45
|
+
let info = notification.userInfo
|
|
46
|
+
duration = info?[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval ?? 0.25
|
|
47
|
+
if let raw = info?[UIResponder.keyboardAnimationCurveUserInfoKey] as? Int,
|
|
48
|
+
let value = UIView.AnimationCurve(rawValue: raw) {
|
|
49
|
+
curve = value
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// Keyboard top in `view`'s coordinates, or nil when it does not cover it.
|
|
54
|
+
func top(in view: UIView) -> CGFloat? {
|
|
55
|
+
guard frame.height > 0, let window = view.window else { return nil }
|
|
56
|
+
let inWindow = window.convert(frame, from: window.screen.coordinateSpace)
|
|
57
|
+
let local = view.convert(inWindow, from: window)
|
|
58
|
+
guard local.minY < view.bounds.maxY else { return nil }
|
|
59
|
+
return local.minY
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
enum NativeOverlayKeyboardShift {
|
|
64
|
+
/// Gap between centered content and the keyboard.
|
|
65
|
+
static let margin: CGFloat = 16
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
How far to lift an entry's content (points, >= 0).
|
|
69
|
+
|
|
70
|
+
- Sheets rise until their content bottom clears the keyboard.
|
|
71
|
+
- Other content rises until its bottom is `margin` above the keyboard.
|
|
72
|
+
- Nothing rises above the top safe area; full-window content never moves.
|
|
73
|
+
*/
|
|
74
|
+
static func compute(
|
|
75
|
+
keyboardTop: CGFloat?,
|
|
76
|
+
contentFrame: CGRect,
|
|
77
|
+
contentBottom: CGFloat,
|
|
78
|
+
isSheet: Bool,
|
|
79
|
+
safeTop: CGFloat
|
|
80
|
+
) -> CGFloat {
|
|
81
|
+
guard let keyboardTop else { return 0 }
|
|
82
|
+
let needed = isSheet
|
|
83
|
+
? contentBottom - keyboardTop
|
|
84
|
+
: contentFrame.maxY + margin - keyboardTop
|
|
85
|
+
let room = contentFrame.minY - safeTop
|
|
86
|
+
return max(0, min(needed, room))
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
/// Page-scope overlays of one root route render inside this view. It sits
|
|
4
|
+
/// in the React tree (last child of the root-route screen), so the surface's
|
|
5
|
+
/// own touch handler already dispatches touches to the moved content.
|
|
6
|
+
@objc public final class NativeOverlayPageHostView: UIView {
|
|
7
|
+
private struct WeakHost {
|
|
8
|
+
weak var value: NativeOverlayPageHostView?
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
private static var registry: [String: WeakHost] = [:]
|
|
12
|
+
|
|
13
|
+
static func host(for key: String) -> NativeOverlayPageHostView? {
|
|
14
|
+
registry[key]?.value
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@objc public var hostKey = "" {
|
|
18
|
+
didSet {
|
|
19
|
+
guard hostKey != oldValue else { return }
|
|
20
|
+
if !oldValue.isEmpty, Self.registry[oldValue]?.value === self {
|
|
21
|
+
Self.registry[oldValue] = nil
|
|
22
|
+
}
|
|
23
|
+
if !hostKey.isEmpty {
|
|
24
|
+
Self.registry[hostKey] = WeakHost(value: self)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private var suspendedOwners: Set<String> = []
|
|
30
|
+
|
|
31
|
+
@objc public func setSuspendedOwners(_ joined: String) {
|
|
32
|
+
let owners = Set(joined.split(separator: "\n").map(String.init))
|
|
33
|
+
guard owners != suspendedOwners else { return }
|
|
34
|
+
suspendedOwners = owners
|
|
35
|
+
entries().forEach(applySuspension)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
override public init(frame: CGRect) {
|
|
39
|
+
super.init(frame: frame)
|
|
40
|
+
backgroundColor = .clear
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@available(*, unavailable)
|
|
44
|
+
required init?(coder: NSCoder) {
|
|
45
|
+
fatalError("init(coder:) has not been implemented")
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@objc public func invalidate() {
|
|
49
|
+
if !hostKey.isEmpty, Self.registry[hostKey]?.value === self {
|
|
50
|
+
Self.registry[hostKey] = nil
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
55
|
+
let hit = super.hitTest(point, with: event)
|
|
56
|
+
return hit === self ? nil : hit
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
override public func layoutSubviews() {
|
|
60
|
+
super.layoutSubviews()
|
|
61
|
+
entries().forEach { $0.frame = bounds }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// Orders page entries by level, then by request order.
|
|
65
|
+
func insert(_ entry: NativeOverlayEntryView) {
|
|
66
|
+
if entry.superview !== self {
|
|
67
|
+
entry.removeFromSuperview()
|
|
68
|
+
entry.frame = bounds
|
|
69
|
+
let above = entries().first { Self.sortKey($0) > Self.sortKey(entry) }
|
|
70
|
+
if let above {
|
|
71
|
+
insertSubview(entry, belowSubview: above)
|
|
72
|
+
} else {
|
|
73
|
+
addSubview(entry)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
applySuspension(entry)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private func applySuspension(_ entry: NativeOverlayEntryView) {
|
|
80
|
+
let hidden = suspendedOwners.contains(entry.ownerKey)
|
|
81
|
+
guard entry.isHidden != hidden else { return }
|
|
82
|
+
if hidden {
|
|
83
|
+
entry.isHidden = true
|
|
84
|
+
} else {
|
|
85
|
+
entry.alpha = 0
|
|
86
|
+
entry.isHidden = false
|
|
87
|
+
UIView.animate(withDuration: 0.15) { entry.alpha = 1 }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private func entries() -> [NativeOverlayEntryView] {
|
|
92
|
+
subviews.compactMap { $0 as? NativeOverlayEntryView }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private static func sortKey(_ entry: NativeOverlayEntryView) -> (Int, Int) {
|
|
96
|
+
(entry.levelOrder, entry.stackOrder)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
/// Callbacks from a presented sheet back to its `NativeOverlayContainerView`.
|
|
4
|
+
protocol NativeOverlaySheetHost: AnyObject {
|
|
5
|
+
var dismissOnPanDown: Bool { get }
|
|
6
|
+
func sheetDidLoad(_ controller: NativeOverlaySheetController)
|
|
7
|
+
/// The sheet's frame settled (presentation, detent or keyboard change).
|
|
8
|
+
func sheetDidLayout(_ controller: NativeOverlaySheetController)
|
|
9
|
+
func sheetDidDismissInteractively(_ controller: NativeOverlaySheetController)
|
|
10
|
+
/// UIKit dismissed the sheet without being asked, e.g. because a sheet
|
|
11
|
+
/// below it in the same level window was dismissed.
|
|
12
|
+
func sheetDidDisappearUnexpectedly(_ controller: NativeOverlaySheetController)
|
|
13
|
+
func sheetRequestedDismiss(_ controller: NativeOverlaySheetController, reason: String)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
struct NativeOverlaySheetStyle {
|
|
17
|
+
var height: CGFloat
|
|
18
|
+
var backgroundColor: UIColor
|
|
19
|
+
var cornerRadius: CGFloat
|
|
20
|
+
var showHandle: Bool
|
|
21
|
+
var backdropColor: UIColor?
|
|
22
|
+
var dismissOnBackdropPress: Bool
|
|
23
|
+
var enterMotion: NativeOverlayMotion
|
|
24
|
+
var exitMotion: NativeOverlayMotion
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// Slides the sheet with the overlay's own motion instead of UIKit's curve
|
|
28
|
+
/// and fades the custom dimming view on the same clock.
|
|
29
|
+
private final class NativeOverlaySheetTransitionAnimator: NSObject,
|
|
30
|
+
UIViewControllerAnimatedTransitioning {
|
|
31
|
+
private let presenting: Bool
|
|
32
|
+
private let motion: NativeOverlayMotion
|
|
33
|
+
private var animator: UIViewPropertyAnimator?
|
|
34
|
+
|
|
35
|
+
init(presenting: Bool, motion: NativeOverlayMotion) {
|
|
36
|
+
self.presenting = presenting
|
|
37
|
+
self.motion = motion
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
|
|
41
|
+
motion.makeAnimator().duration
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
|
45
|
+
interruptibleAnimator(using: transitionContext).startAnimation()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
func interruptibleAnimator(
|
|
49
|
+
using transitionContext: UIViewControllerContextTransitioning
|
|
50
|
+
) -> UIViewImplicitlyAnimating {
|
|
51
|
+
if let animator {
|
|
52
|
+
return animator
|
|
53
|
+
}
|
|
54
|
+
let viewKey: UITransitionContextViewKey = presenting ? .to : .from
|
|
55
|
+
let controllerKey: UITransitionContextViewControllerKey = presenting ? .to : .from
|
|
56
|
+
guard let sheetView = transitionContext.view(forKey: viewKey),
|
|
57
|
+
let sheetController = transitionContext.viewController(forKey: controllerKey)
|
|
58
|
+
as? NativeOverlaySheetController else {
|
|
59
|
+
transitionContext.completeTransition(false)
|
|
60
|
+
return motion.makeAnimator()
|
|
61
|
+
}
|
|
62
|
+
let container = transitionContext.containerView
|
|
63
|
+
let finalFrame = presenting ? transitionContext.finalFrame(for: sheetController) : sheetView.frame
|
|
64
|
+
let travel = max(container.bounds.maxY - finalFrame.minY, sheetView.bounds.height)
|
|
65
|
+
let offscreen = CGAffineTransform(translationX: 0, y: travel)
|
|
66
|
+
let reduceMotion = UIAccessibility.isReduceMotionEnabled
|
|
67
|
+
|
|
68
|
+
if presenting {
|
|
69
|
+
sheetView.frame = finalFrame
|
|
70
|
+
if reduceMotion {
|
|
71
|
+
sheetView.alpha = 0
|
|
72
|
+
} else {
|
|
73
|
+
sheetView.transform = offscreen
|
|
74
|
+
}
|
|
75
|
+
sheetController.setDimmingAlpha(0)
|
|
76
|
+
if sheetView.superview == nil {
|
|
77
|
+
container.addSubview(sheetView)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let animator = motion.makeAnimator()
|
|
82
|
+
animator.addAnimations {
|
|
83
|
+
if reduceMotion {
|
|
84
|
+
sheetView.alpha = self.presenting ? 1 : 0
|
|
85
|
+
} else {
|
|
86
|
+
sheetView.transform = self.presenting ? .identity : offscreen
|
|
87
|
+
}
|
|
88
|
+
sheetController.setDimmingAlpha(self.presenting ? 1 : 0)
|
|
89
|
+
}
|
|
90
|
+
animator.addCompletion { position in
|
|
91
|
+
let completed = position == .end && !transitionContext.transitionWasCancelled
|
|
92
|
+
if !completed {
|
|
93
|
+
sheetView.transform = self.presenting ? offscreen : .identity
|
|
94
|
+
sheetController.setDimmingAlpha(self.presenting ? 0 : 1)
|
|
95
|
+
}
|
|
96
|
+
transitionContext.completeTransition(completed)
|
|
97
|
+
}
|
|
98
|
+
self.animator = animator
|
|
99
|
+
return animator
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
func animationEnded(_ transitionCompleted: Bool) {
|
|
103
|
+
animator = nil
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/// A `.pageSheet` with one custom detent, ported from native-sheet. It is
|
|
108
|
+
/// presented from its level window, so sheets of different levels never
|
|
109
|
+
/// share a presentation chain.
|
|
110
|
+
final class NativeOverlaySheetController: UIViewController,
|
|
111
|
+
UIAdaptivePresentationControllerDelegate,
|
|
112
|
+
UIViewControllerTransitioningDelegate {
|
|
113
|
+
private weak var host: NativeOverlaySheetHost?
|
|
114
|
+
private let detentIdentifier = UISheetPresentationController.Detent.Identifier(
|
|
115
|
+
"onekey.nativeOverlay.sheet"
|
|
116
|
+
)
|
|
117
|
+
private var style: NativeOverlaySheetStyle
|
|
118
|
+
private let backgroundView = UIView()
|
|
119
|
+
private var dimmingView: UIView?
|
|
120
|
+
private var backdropTap: UITapGestureRecognizer?
|
|
121
|
+
private var heightAnimator: UIViewPropertyAnimator?
|
|
122
|
+
private var shadowSuppressionDisplayLink: CADisplayLink?
|
|
123
|
+
/// Set while the host itself asked UIKit to dismiss.
|
|
124
|
+
var dismissRequestedByHost = false
|
|
125
|
+
|
|
126
|
+
init(host: NativeOverlaySheetHost, style: NativeOverlaySheetStyle) {
|
|
127
|
+
self.host = host
|
|
128
|
+
self.style = style
|
|
129
|
+
super.init(nibName: nil, bundle: nil)
|
|
130
|
+
modalPresentationStyle = .pageSheet
|
|
131
|
+
transitioningDelegate = self
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
@available(*, unavailable)
|
|
135
|
+
required init?(coder: NSCoder) {
|
|
136
|
+
fatalError("init(coder:) has not been implemented")
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
override func loadView() {
|
|
140
|
+
let rootView = UIView()
|
|
141
|
+
rootView.backgroundColor = style.backgroundColor
|
|
142
|
+
rootView.clipsToBounds = true
|
|
143
|
+
if #available(iOS 26.0, *) {
|
|
144
|
+
backgroundView.frame = rootView.bounds
|
|
145
|
+
backgroundView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
146
|
+
backgroundView.backgroundColor = style.backgroundColor
|
|
147
|
+
backgroundView.isUserInteractionEnabled = false
|
|
148
|
+
rootView.addSubview(backgroundView)
|
|
149
|
+
}
|
|
150
|
+
view = rootView
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
deinit {
|
|
154
|
+
shadowSuppressionDisplayLink?.invalidate()
|
|
155
|
+
dimmingView?.removeFromSuperview()
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
override func viewDidLoad() {
|
|
159
|
+
super.viewDidLoad()
|
|
160
|
+
host?.sheetDidLoad(self)
|
|
161
|
+
guard let sheet = sheetPresentationController else { return }
|
|
162
|
+
let detent = UISheetPresentationController.Detent.custom(
|
|
163
|
+
identifier: detentIdentifier
|
|
164
|
+
) { [weak self] context in
|
|
165
|
+
min(max(self?.style.height ?? 1, 1), context.maximumDetentValue)
|
|
166
|
+
}
|
|
167
|
+
if #available(iOS 26.1, *) {
|
|
168
|
+
let effect = UIColorEffect(color: style.backgroundColor)
|
|
169
|
+
sheet.backgroundEffect = effect
|
|
170
|
+
detent.backgroundEffect = effect
|
|
171
|
+
}
|
|
172
|
+
sheet.detents = [detent]
|
|
173
|
+
sheet.selectedDetentIdentifier = detentIdentifier
|
|
174
|
+
sheet.largestUndimmedDetentIdentifier = detentIdentifier
|
|
175
|
+
sheet.prefersGrabberVisible = style.showHandle
|
|
176
|
+
sheet.preferredCornerRadius = style.cornerRadius
|
|
177
|
+
sheet.prefersScrollingExpandsWhenScrolledToEdge = false
|
|
178
|
+
sheet.prefersEdgeAttachedInCompactHeight = true
|
|
179
|
+
sheet.widthFollowsPreferredContentSizeWhenEdgeAttached = false
|
|
180
|
+
presentationController?.delegate = self
|
|
181
|
+
isModalInPresentation = !(host?.dismissOnPanDown ?? true)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
func update(style next: NativeOverlaySheetStyle, animated: Bool) {
|
|
185
|
+
let previousHeight = style.height
|
|
186
|
+
style.height = max(next.height, 1)
|
|
187
|
+
style.dismissOnBackdropPress = next.dismissOnBackdropPress
|
|
188
|
+
style.backdropColor = next.backdropColor
|
|
189
|
+
style.exitMotion = next.exitMotion
|
|
190
|
+
dimmingView?.backgroundColor = next.backdropColor ?? .clear
|
|
191
|
+
isModalInPresentation = !(host?.dismissOnPanDown ?? true)
|
|
192
|
+
guard abs(style.height - previousHeight) >= 0.5,
|
|
193
|
+
isViewLoaded,
|
|
194
|
+
let sheet = sheetPresentationController else { return }
|
|
195
|
+
let changes = { [weak self, weak sheet] in
|
|
196
|
+
guard let self, let sheet else { return }
|
|
197
|
+
sheet.invalidateDetents()
|
|
198
|
+
sheet.selectedDetentIdentifier = self.detentIdentifier
|
|
199
|
+
sheet.containerView?.layoutIfNeeded()
|
|
200
|
+
}
|
|
201
|
+
guard animated else {
|
|
202
|
+
changes()
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
if let heightAnimator {
|
|
206
|
+
heightAnimator.stopAnimation(false)
|
|
207
|
+
heightAnimator.finishAnimation(at: .current)
|
|
208
|
+
}
|
|
209
|
+
sheet.containerView?.layoutIfNeeded()
|
|
210
|
+
let animator = style.enterMotion.makeAnimator()
|
|
211
|
+
heightAnimator = animator
|
|
212
|
+
animator.addAnimations(changes)
|
|
213
|
+
animator.addCompletion { [weak self, weak animator] _ in
|
|
214
|
+
guard let self, self.heightAnimator === animator else { return }
|
|
215
|
+
self.heightAnimator = nil
|
|
216
|
+
}
|
|
217
|
+
animator.startAnimation()
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// MARK: Transitions
|
|
221
|
+
|
|
222
|
+
func animationController(
|
|
223
|
+
forPresented presented: UIViewController,
|
|
224
|
+
presenting: UIViewController,
|
|
225
|
+
source: UIViewController
|
|
226
|
+
) -> UIViewControllerAnimatedTransitioning? {
|
|
227
|
+
NativeOverlaySheetTransitionAnimator(presenting: true, motion: style.enterMotion)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
func animationController(
|
|
231
|
+
forDismissed dismissed: UIViewController
|
|
232
|
+
) -> UIViewControllerAnimatedTransitioning? {
|
|
233
|
+
NativeOverlaySheetTransitionAnimator(presenting: false, motion: style.exitMotion)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
override func viewWillAppear(_ animated: Bool) {
|
|
237
|
+
super.viewWillAppear(animated)
|
|
238
|
+
installDimmingView()
|
|
239
|
+
startSuppressingPresentationShadow()
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
override func viewDidAppear(_ animated: Bool) {
|
|
243
|
+
super.viewDidAppear(animated)
|
|
244
|
+
stopSuppressingPresentationShadow()
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
override func viewWillDisappear(_ animated: Bool) {
|
|
248
|
+
super.viewWillDisappear(animated)
|
|
249
|
+
stopSuppressingPresentationShadow()
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
override func viewDidDisappear(_ animated: Bool) {
|
|
253
|
+
super.viewDidDisappear(animated)
|
|
254
|
+
if let backdropTap {
|
|
255
|
+
dimmingView?.removeGestureRecognizer(backdropTap)
|
|
256
|
+
}
|
|
257
|
+
backdropTap = nil
|
|
258
|
+
dimmingView?.removeFromSuperview()
|
|
259
|
+
dimmingView = nil
|
|
260
|
+
if presentingViewController == nil && !dismissRequestedByHost {
|
|
261
|
+
host?.sheetDidDisappearUnexpectedly(self)
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
override func viewDidLayoutSubviews() {
|
|
266
|
+
super.viewDidLayoutSubviews()
|
|
267
|
+
host?.sheetDidLayout(self)
|
|
268
|
+
removePresentationShadow()
|
|
269
|
+
if #available(iOS 26.0, *) {
|
|
270
|
+
DispatchQueue.main.async { [weak self] in
|
|
271
|
+
self?.removePresentationShadow()
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
override func accessibilityPerformEscape() -> Bool {
|
|
277
|
+
host?.sheetRequestedDismiss(self, reason: "back")
|
|
278
|
+
return true
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
|
|
282
|
+
host?.dismissOnPanDown ?? true
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
|
|
286
|
+
dismissRequestedByHost = true
|
|
287
|
+
host?.sheetDidDismissInteractively(self)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// MARK: Backdrop
|
|
291
|
+
|
|
292
|
+
func setDimmingAlpha(_ alpha: CGFloat) {
|
|
293
|
+
dimmingView?.alpha = alpha
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
private func installDimmingView() {
|
|
297
|
+
guard dimmingView == nil, let container = presentationController?.containerView else { return }
|
|
298
|
+
let dimmingView = UIView(frame: container.bounds)
|
|
299
|
+
dimmingView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
300
|
+
dimmingView.backgroundColor = style.backdropColor ?? .clear
|
|
301
|
+
dimmingView.alpha = 0
|
|
302
|
+
// Always swallow backdrop touches; a sheet is blocking even without a tap-to-close.
|
|
303
|
+
dimmingView.isUserInteractionEnabled = true
|
|
304
|
+
container.insertSubview(dimmingView, at: 0)
|
|
305
|
+
let recognizer = UITapGestureRecognizer(target: self, action: #selector(handleBackdropTap(_:)))
|
|
306
|
+
recognizer.cancelsTouchesInView = true
|
|
307
|
+
dimmingView.addGestureRecognizer(recognizer)
|
|
308
|
+
backdropTap = recognizer
|
|
309
|
+
self.dimmingView = dimmingView
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
@objc private func handleBackdropTap(_ recognizer: UITapGestureRecognizer) {
|
|
313
|
+
guard recognizer.state == .ended, style.dismissOnBackdropPress else { return }
|
|
314
|
+
host?.sheetRequestedDismiss(self, reason: "backdrop")
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// MARK: Shadow suppression (same approach as native-sheet)
|
|
318
|
+
|
|
319
|
+
private func removePresentationShadow() {
|
|
320
|
+
guard let container = presentationController?.containerView else { return }
|
|
321
|
+
var ancestor = view.superview
|
|
322
|
+
while let current = ancestor, current !== container {
|
|
323
|
+
clearShadow(on: current)
|
|
324
|
+
ancestor = current.superview
|
|
325
|
+
}
|
|
326
|
+
removeDropShadowViews(in: container)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private func removeDropShadowViews(in candidate: UIView) {
|
|
330
|
+
guard candidate !== view else { return }
|
|
331
|
+
let className = NSStringFromClass(type(of: candidate))
|
|
332
|
+
guard className.hasPrefix("UI") || className.hasPrefix("_UI") else { return }
|
|
333
|
+
if className.contains("DropShadowView") {
|
|
334
|
+
clearShadow(on: candidate)
|
|
335
|
+
}
|
|
336
|
+
candidate.subviews.forEach(removeDropShadowViews(in:))
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private func clearShadow(on shadowView: UIView) {
|
|
340
|
+
CATransaction.begin()
|
|
341
|
+
CATransaction.setDisableActions(true)
|
|
342
|
+
if #available(iOS 26.0, *),
|
|
343
|
+
NSStringFromClass(type(of: shadowView)).contains("UIDropShadowView") {
|
|
344
|
+
compensateForPresentationScale(in: shadowView)
|
|
345
|
+
}
|
|
346
|
+
shadowView.layer.shadowOpacity = 0
|
|
347
|
+
shadowView.layer.shadowRadius = 0
|
|
348
|
+
shadowView.layer.shadowColor = UIColor.clear.cgColor
|
|
349
|
+
shadowView.layer.shadowPath = nil
|
|
350
|
+
CATransaction.commit()
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
@available(iOS 26.0, *)
|
|
354
|
+
private func compensateForPresentationScale(in shadowView: UIView) {
|
|
355
|
+
guard var surfaceView = view else { return }
|
|
356
|
+
while let parent = surfaceView.superview, parent !== shadowView {
|
|
357
|
+
surfaceView = parent
|
|
358
|
+
}
|
|
359
|
+
guard surfaceView.superview === shadowView else { return }
|
|
360
|
+
let transform = shadowView.transform
|
|
361
|
+
guard abs(transform.a) > 0.001, abs(transform.d) > 0.001 else { return }
|
|
362
|
+
surfaceView.transform = CGAffineTransform(scaleX: 1 / transform.a, y: 1 / transform.d)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private func startSuppressingPresentationShadow() {
|
|
366
|
+
shadowSuppressionDisplayLink?.invalidate()
|
|
367
|
+
removePresentationShadow()
|
|
368
|
+
let displayLink = CADisplayLink(target: self, selector: #selector(suppressShadowForFrame))
|
|
369
|
+
displayLink.add(to: .main, forMode: .common)
|
|
370
|
+
shadowSuppressionDisplayLink = displayLink
|
|
371
|
+
if let transitionCoordinator {
|
|
372
|
+
transitionCoordinator.animate(alongsideTransition: nil) { [weak self] _ in
|
|
373
|
+
self?.stopSuppressingPresentationShadow()
|
|
374
|
+
}
|
|
375
|
+
} else {
|
|
376
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
|
|
377
|
+
self?.stopSuppressingPresentationShadow()
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
private func stopSuppressingPresentationShadow() {
|
|
383
|
+
shadowSuppressionDisplayLink?.invalidate()
|
|
384
|
+
shadowSuppressionDisplayLink = nil
|
|
385
|
+
removePresentationShadow()
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
@objc private func suppressShadowForFrame() {
|
|
389
|
+
removePresentationShadow()
|
|
390
|
+
}
|
|
391
|
+
}
|