mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-08 19:10:53 +00:00
Allow pan to select on context actions
This commit is contained in:
parent
f4a21f9729
commit
cbf1e62c46
@ -3,6 +3,35 @@ import AsyncDisplayKit
|
||||
import Display
|
||||
import TelegramPresentationData
|
||||
|
||||
private final class ContextActionsSelectionGestureRecognizer: UIPanGestureRecognizer {
|
||||
var updateLocation: ((CGPoint, Bool) -> Void)?
|
||||
var completed: ((Bool) -> Void)?
|
||||
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
|
||||
self.updateLocation?(touches.first!.location(in: self.view), false)
|
||||
}
|
||||
|
||||
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesMoved(touches, with: event)
|
||||
|
||||
self.updateLocation?(touches.first!.location(in: self.view), true)
|
||||
}
|
||||
|
||||
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesEnded(touches, with: event)
|
||||
|
||||
self.completed?(true)
|
||||
}
|
||||
|
||||
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesCancelled(touches, with: event)
|
||||
|
||||
self.completed?(false)
|
||||
}
|
||||
}
|
||||
|
||||
private enum ContextItemNode {
|
||||
case action(ContextActionNode)
|
||||
case itemSeparator(ASDisplayNode)
|
||||
@ -12,8 +41,14 @@ private enum ContextItemNode {
|
||||
final class ContextActionsContainerNode: ASDisplayNode {
|
||||
private var effectView: UIVisualEffectView?
|
||||
private var itemNodes: [ContextItemNode]
|
||||
private let feedbackTap: () -> Void
|
||||
|
||||
init(theme: PresentationTheme, items: [ContextMenuItem], getController: @escaping () -> ContextController?, actionSelected: @escaping (ContextMenuActionResult) -> Void) {
|
||||
private(set) var gesture: UIGestureRecognizer?
|
||||
private var currentHighlightedActionNode: ContextActionNode?
|
||||
|
||||
init(theme: PresentationTheme, items: [ContextMenuItem], getController: @escaping () -> ContextController?, actionSelected: @escaping (ContextMenuActionResult) -> Void, feedbackTap: @escaping () -> Void) {
|
||||
self.feedbackTap = feedbackTap
|
||||
|
||||
var itemNodes: [ContextItemNode] = []
|
||||
for i in 0 ..< items.count {
|
||||
switch items[i] {
|
||||
@ -43,6 +78,7 @@ final class ContextActionsContainerNode: ASDisplayNode {
|
||||
self.itemNodes.forEach({ itemNode in
|
||||
switch itemNode {
|
||||
case let .action(actionNode):
|
||||
actionNode.isUserInteractionEnabled = false
|
||||
self.addSubnode(actionNode)
|
||||
case let .itemSeparator(separatorNode):
|
||||
self.addSubnode(separatorNode)
|
||||
@ -50,6 +86,36 @@ final class ContextActionsContainerNode: ASDisplayNode {
|
||||
self.addSubnode(separatorNode)
|
||||
}
|
||||
})
|
||||
|
||||
let gesture = ContextActionsSelectionGestureRecognizer(target: nil, action: nil)
|
||||
self.gesture = gesture
|
||||
gesture.updateLocation = { [weak self] point, moved in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let actionNode = strongSelf.actionNode(at: point)
|
||||
if actionNode !== strongSelf.currentHighlightedActionNode {
|
||||
if actionNode != nil, moved {
|
||||
strongSelf.feedbackTap()
|
||||
}
|
||||
strongSelf.currentHighlightedActionNode?.setIsHighlighted(false)
|
||||
}
|
||||
strongSelf.currentHighlightedActionNode = actionNode
|
||||
actionNode?.setIsHighlighted(true)
|
||||
}
|
||||
gesture.completed = { [weak self] performAction in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if let currentHighlightedActionNode = strongSelf.currentHighlightedActionNode {
|
||||
strongSelf.currentHighlightedActionNode = nil
|
||||
currentHighlightedActionNode.setIsHighlighted(false)
|
||||
if performAction {
|
||||
currentHighlightedActionNode.performAction()
|
||||
}
|
||||
}
|
||||
}
|
||||
self.view.addGestureRecognizer(gesture)
|
||||
}
|
||||
|
||||
func updateLayout(widthClass: ContainerViewLayoutSizeClass, constrainedWidth: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
|
@ -65,6 +65,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
private var items: Signal<[ContextMenuItem], NoError>
|
||||
private let beginDismiss: (ContextMenuActionResult) -> Void
|
||||
private let reactionSelected: (String) -> Void
|
||||
private let beganAnimatingOut: () -> Void
|
||||
private let getController: () -> ContextController?
|
||||
private weak var gesture: ContextGesture?
|
||||
|
||||
@ -105,13 +106,14 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
|
||||
private let itemsDisposable = MetaDisposable()
|
||||
|
||||
init(account: Account, controller: ContextController, theme: PresentationTheme, strings: PresentationStrings, source: ContextContentSource, items: Signal<[ContextMenuItem], NoError>, reactionItems: [ReactionContextItem], beginDismiss: @escaping (ContextMenuActionResult) -> Void, recognizer: TapLongTapOrDoubleTapGestureRecognizer?, gesture: ContextGesture?, reactionSelected: @escaping (String) -> Void) {
|
||||
init(account: Account, controller: ContextController, theme: PresentationTheme, strings: PresentationStrings, source: ContextContentSource, items: Signal<[ContextMenuItem], NoError>, reactionItems: [ReactionContextItem], beginDismiss: @escaping (ContextMenuActionResult) -> Void, recognizer: TapLongTapOrDoubleTapGestureRecognizer?, gesture: ContextGesture?, reactionSelected: @escaping (String) -> Void, beganAnimatingOut: @escaping () -> Void) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.source = source
|
||||
self.items = items
|
||||
self.beginDismiss = beginDismiss
|
||||
self.reactionSelected = reactionSelected
|
||||
self.beganAnimatingOut = beganAnimatingOut
|
||||
self.gesture = gesture
|
||||
|
||||
self.getController = { [weak controller] in
|
||||
@ -152,10 +154,14 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
|
||||
self.contentContainerNode = ContextContentContainerNode()
|
||||
|
||||
var feedbackTap: (() -> Void)?
|
||||
|
||||
self.actionsContainerNode = ContextActionsContainerNode(theme: theme, items: [], getController: { [weak controller] in
|
||||
return controller
|
||||
}, actionSelected: { result in
|
||||
beginDismiss(result)
|
||||
}, feedbackTap: {
|
||||
feedbackTap?()
|
||||
})
|
||||
|
||||
if !reactionItems.isEmpty {
|
||||
@ -167,6 +173,10 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
|
||||
super.init()
|
||||
|
||||
feedbackTap = { [weak self] in
|
||||
self?.hapticFeedback.tap()
|
||||
}
|
||||
|
||||
self.scrollNode.view.delegate = self
|
||||
|
||||
self.view.addSubview(self.effectView)
|
||||
@ -586,6 +596,8 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
}
|
||||
|
||||
func animateOut(result initialResult: ContextMenuActionResult, completion: @escaping () -> Void) {
|
||||
self.beganAnimatingOut()
|
||||
|
||||
var transitionDuration: Double = 0.2
|
||||
var transitionCurve: ContainedViewLayoutTransitionCurve = .easeInOut
|
||||
|
||||
@ -945,6 +957,8 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
return self?.getController()
|
||||
}, actionSelected: { [weak self] result in
|
||||
self?.beginDismiss(result)
|
||||
}, feedbackTap: { [weak self] in
|
||||
self?.hapticFeedback.tap()
|
||||
})
|
||||
self.scrollNode.insertSubnode(self.actionsContainerNode, aboveSubnode: previousActionsContainerNode)
|
||||
|
||||
@ -1088,12 +1102,24 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
let isInitialLayout = self.actionsContainerNode.frame.size.width.isZero
|
||||
let previousContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view)
|
||||
|
||||
let actionsSize = self.actionsContainerNode.updateLayout(widthClass: layout.metrics.widthClass, constrainedWidth: layout.size.width - actionsSideInset * 2.0, transition: actionsContainerTransition)
|
||||
let contentScale = (layout.size.width - actionsSideInset * 2.0) / layout.size.width
|
||||
let constrainedWidth: CGFloat
|
||||
if layout.size.width < layout.size.height {
|
||||
constrainedWidth = layout.size.width
|
||||
} else {
|
||||
constrainedWidth = floor(layout.size.width / 2.0)
|
||||
}
|
||||
|
||||
let actionsSize = self.actionsContainerNode.updateLayout(widthClass: layout.metrics.widthClass, constrainedWidth: constrainedWidth - actionsSideInset * 2.0, transition: actionsContainerTransition)
|
||||
let contentScale = (constrainedWidth - actionsSideInset * 2.0) / constrainedWidth
|
||||
var contentUnscaledSize: CGSize
|
||||
if case .compact = layout.metrics.widthClass {
|
||||
let proposedContentHeight = layout.size.height - topEdge - contentActionsSpacing - actionsSize.height - layout.intrinsicInsets.bottom - actionsBottomInset
|
||||
contentUnscaledSize = CGSize(width: layout.size.width, height: max(400.0, proposedContentHeight))
|
||||
let proposedContentHeight: CGFloat
|
||||
if layout.size.width < layout.size.height {
|
||||
proposedContentHeight = layout.size.height - topEdge - contentActionsSpacing - actionsSize.height - layout.intrinsicInsets.bottom - actionsBottomInset
|
||||
} else {
|
||||
proposedContentHeight = layout.size.height - topEdge - topEdge
|
||||
}
|
||||
contentUnscaledSize = CGSize(width: constrainedWidth, height: max(100.0, proposedContentHeight))
|
||||
|
||||
if let preferredSize = contentParentNode.controller.preferredContentSizeForLayout(ContainerViewLayout(size: contentUnscaledSize, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false)) {
|
||||
contentUnscaledSize = preferredSize
|
||||
@ -1115,16 +1141,22 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
var originalContentFrame: CGRect
|
||||
var contentHeight: CGFloat
|
||||
if case .compact = layout.metrics.widthClass {
|
||||
originalActionsFrame = CGRect(origin: CGPoint(x: actionsSideInset, y: min(maximumActionsFrameOrigin, floor((layout.size.height - contentActionsSpacing - contentSize.height) / 2.0) + contentSize.height + contentActionsSpacing)), size: actionsSize)
|
||||
originalContentFrame = CGRect(origin: CGPoint(x: actionsSideInset, y: originalActionsFrame.minY - contentActionsSpacing - contentSize.height), size: contentSize)
|
||||
if originalContentFrame.minY < topEdge {
|
||||
let requiredOffset = topEdge - originalContentFrame.minY
|
||||
let availableOffset = max(0.0, layout.size.height - layout.intrinsicInsets.bottom - actionsBottomInset - originalActionsFrame.maxY)
|
||||
let offset = min(requiredOffset, availableOffset)
|
||||
originalActionsFrame = originalActionsFrame.offsetBy(dx: 0.0, dy: offset)
|
||||
originalContentFrame = originalContentFrame.offsetBy(dx: 0.0, dy: offset)
|
||||
if layout.size.width < layout.size.height {
|
||||
originalActionsFrame = CGRect(origin: CGPoint(x: actionsSideInset, y: min(maximumActionsFrameOrigin, floor((layout.size.height - contentActionsSpacing - contentSize.height) / 2.0) + contentSize.height + contentActionsSpacing)), size: actionsSize)
|
||||
originalContentFrame = CGRect(origin: CGPoint(x: actionsSideInset, y: originalActionsFrame.minY - contentActionsSpacing - contentSize.height), size: contentSize)
|
||||
if originalContentFrame.minY < topEdge {
|
||||
let requiredOffset = topEdge - originalContentFrame.minY
|
||||
let availableOffset = max(0.0, layout.size.height - layout.intrinsicInsets.bottom - actionsBottomInset - originalActionsFrame.maxY)
|
||||
let offset = min(requiredOffset, availableOffset)
|
||||
originalActionsFrame = originalActionsFrame.offsetBy(dx: 0.0, dy: offset)
|
||||
originalContentFrame = originalContentFrame.offsetBy(dx: 0.0, dy: offset)
|
||||
}
|
||||
contentHeight = max(layout.size.height, max(layout.size.height, originalActionsFrame.maxY + actionsBottomInset) - originalContentFrame.minY + contentTopInset)
|
||||
} else {
|
||||
originalContentFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - contentSize.width - actionsSideInset - actionsSize.width) / 2.0), y: floor((layout.size.height - contentSize.height) / 2.0)), size: contentSize)
|
||||
originalActionsFrame = CGRect(origin: CGPoint(x: originalContentFrame.maxX + actionsSideInset, y: max(topEdge, originalContentFrame.minY)), size: actionsSize)
|
||||
contentHeight = max(layout.size.height, max(originalContentFrame.maxY, originalActionsFrame.maxY))
|
||||
}
|
||||
contentHeight = max(layout.size.height, max(layout.size.height, originalActionsFrame.maxY + actionsBottomInset) - originalContentFrame.minY + contentTopInset)
|
||||
} else {
|
||||
originalContentFrame = CGRect(origin: CGPoint(x: floor(originalProjectedContentViewFrame.1.midX - contentSize.width / 2.0), y: floor(originalProjectedContentViewFrame.1.midY - contentSize.height / 2.0)), size: contentSize)
|
||||
originalContentFrame.origin.x = min(originalContentFrame.origin.x, layout.size.width - actionsSideInset - contentSize.width)
|
||||
@ -1342,6 +1374,8 @@ public final class ContextController: ViewController, StandalonePresentableContr
|
||||
return
|
||||
}
|
||||
strongSelf.reactionSelected?(value)
|
||||
}, beganAnimatingOut: { [weak self] in
|
||||
self?.statusBar.statusBarStyle = .Ignore
|
||||
})
|
||||
|
||||
self.displayNodeDidLoad()
|
||||
|
@ -37,6 +37,19 @@ private func cancelParentGestures(view: UIView) {
|
||||
}
|
||||
}
|
||||
|
||||
private func cancelOtherGestures(gesture: ContextGesture, view: UIView) {
|
||||
if let gestureRecognizers = view.gestureRecognizers {
|
||||
for recognizer in gestureRecognizers {
|
||||
if let recognizer = recognizer as? ContextGesture, recognizer !== gesture {
|
||||
recognizer.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
for subview in view.subviews {
|
||||
cancelOtherGestures(gesture: gesture, view: subview)
|
||||
}
|
||||
}
|
||||
|
||||
public final class ContextGesture: UIGestureRecognizer, UIGestureRecognizerDelegate {
|
||||
private var currentProgress: CGFloat = 0.0
|
||||
private var delayTimer: Timer?
|
||||
@ -104,6 +117,9 @@ public final class ContextGesture: UIGestureRecognizer, UIGestureRecognizerDeleg
|
||||
strongSelf.animator?.invalidate()
|
||||
strongSelf.activated?(strongSelf)
|
||||
if let view = strongSelf.view?.superview {
|
||||
if let window = view.window {
|
||||
cancelOtherGestures(gesture: strongSelf, view: window)
|
||||
}
|
||||
cancelParentGestures(view: view)
|
||||
}
|
||||
strongSelf.state = .began
|
||||
|
Loading…
x
Reference in New Issue
Block a user