mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-16 19:30:29 +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 Display
|
||||||
import TelegramPresentationData
|
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 {
|
private enum ContextItemNode {
|
||||||
case action(ContextActionNode)
|
case action(ContextActionNode)
|
||||||
case itemSeparator(ASDisplayNode)
|
case itemSeparator(ASDisplayNode)
|
||||||
@ -12,8 +41,14 @@ private enum ContextItemNode {
|
|||||||
final class ContextActionsContainerNode: ASDisplayNode {
|
final class ContextActionsContainerNode: ASDisplayNode {
|
||||||
private var effectView: UIVisualEffectView?
|
private var effectView: UIVisualEffectView?
|
||||||
private var itemNodes: [ContextItemNode]
|
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] = []
|
var itemNodes: [ContextItemNode] = []
|
||||||
for i in 0 ..< items.count {
|
for i in 0 ..< items.count {
|
||||||
switch items[i] {
|
switch items[i] {
|
||||||
@ -43,6 +78,7 @@ final class ContextActionsContainerNode: ASDisplayNode {
|
|||||||
self.itemNodes.forEach({ itemNode in
|
self.itemNodes.forEach({ itemNode in
|
||||||
switch itemNode {
|
switch itemNode {
|
||||||
case let .action(actionNode):
|
case let .action(actionNode):
|
||||||
|
actionNode.isUserInteractionEnabled = false
|
||||||
self.addSubnode(actionNode)
|
self.addSubnode(actionNode)
|
||||||
case let .itemSeparator(separatorNode):
|
case let .itemSeparator(separatorNode):
|
||||||
self.addSubnode(separatorNode)
|
self.addSubnode(separatorNode)
|
||||||
@ -50,6 +86,36 @@ final class ContextActionsContainerNode: ASDisplayNode {
|
|||||||
self.addSubnode(separatorNode)
|
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 {
|
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 var items: Signal<[ContextMenuItem], NoError>
|
||||||
private let beginDismiss: (ContextMenuActionResult) -> Void
|
private let beginDismiss: (ContextMenuActionResult) -> Void
|
||||||
private let reactionSelected: (String) -> Void
|
private let reactionSelected: (String) -> Void
|
||||||
|
private let beganAnimatingOut: () -> Void
|
||||||
private let getController: () -> ContextController?
|
private let getController: () -> ContextController?
|
||||||
private weak var gesture: ContextGesture?
|
private weak var gesture: ContextGesture?
|
||||||
|
|
||||||
@ -105,13 +106,14 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
|||||||
|
|
||||||
private let itemsDisposable = MetaDisposable()
|
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.theme = theme
|
||||||
self.strings = strings
|
self.strings = strings
|
||||||
self.source = source
|
self.source = source
|
||||||
self.items = items
|
self.items = items
|
||||||
self.beginDismiss = beginDismiss
|
self.beginDismiss = beginDismiss
|
||||||
self.reactionSelected = reactionSelected
|
self.reactionSelected = reactionSelected
|
||||||
|
self.beganAnimatingOut = beganAnimatingOut
|
||||||
self.gesture = gesture
|
self.gesture = gesture
|
||||||
|
|
||||||
self.getController = { [weak controller] in
|
self.getController = { [weak controller] in
|
||||||
@ -152,10 +154,14 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
|||||||
|
|
||||||
self.contentContainerNode = ContextContentContainerNode()
|
self.contentContainerNode = ContextContentContainerNode()
|
||||||
|
|
||||||
|
var feedbackTap: (() -> Void)?
|
||||||
|
|
||||||
self.actionsContainerNode = ContextActionsContainerNode(theme: theme, items: [], getController: { [weak controller] in
|
self.actionsContainerNode = ContextActionsContainerNode(theme: theme, items: [], getController: { [weak controller] in
|
||||||
return controller
|
return controller
|
||||||
}, actionSelected: { result in
|
}, actionSelected: { result in
|
||||||
beginDismiss(result)
|
beginDismiss(result)
|
||||||
|
}, feedbackTap: {
|
||||||
|
feedbackTap?()
|
||||||
})
|
})
|
||||||
|
|
||||||
if !reactionItems.isEmpty {
|
if !reactionItems.isEmpty {
|
||||||
@ -167,6 +173,10 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
|||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
|
feedbackTap = { [weak self] in
|
||||||
|
self?.hapticFeedback.tap()
|
||||||
|
}
|
||||||
|
|
||||||
self.scrollNode.view.delegate = self
|
self.scrollNode.view.delegate = self
|
||||||
|
|
||||||
self.view.addSubview(self.effectView)
|
self.view.addSubview(self.effectView)
|
||||||
@ -586,6 +596,8 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
|||||||
}
|
}
|
||||||
|
|
||||||
func animateOut(result initialResult: ContextMenuActionResult, completion: @escaping () -> Void) {
|
func animateOut(result initialResult: ContextMenuActionResult, completion: @escaping () -> Void) {
|
||||||
|
self.beganAnimatingOut()
|
||||||
|
|
||||||
var transitionDuration: Double = 0.2
|
var transitionDuration: Double = 0.2
|
||||||
var transitionCurve: ContainedViewLayoutTransitionCurve = .easeInOut
|
var transitionCurve: ContainedViewLayoutTransitionCurve = .easeInOut
|
||||||
|
|
||||||
@ -945,6 +957,8 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
|||||||
return self?.getController()
|
return self?.getController()
|
||||||
}, actionSelected: { [weak self] result in
|
}, actionSelected: { [weak self] result in
|
||||||
self?.beginDismiss(result)
|
self?.beginDismiss(result)
|
||||||
|
}, feedbackTap: { [weak self] in
|
||||||
|
self?.hapticFeedback.tap()
|
||||||
})
|
})
|
||||||
self.scrollNode.insertSubnode(self.actionsContainerNode, aboveSubnode: previousActionsContainerNode)
|
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 isInitialLayout = self.actionsContainerNode.frame.size.width.isZero
|
||||||
let previousContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view)
|
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 constrainedWidth: CGFloat
|
||||||
let contentScale = (layout.size.width - actionsSideInset * 2.0) / layout.size.width
|
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
|
var contentUnscaledSize: CGSize
|
||||||
if case .compact = layout.metrics.widthClass {
|
if case .compact = layout.metrics.widthClass {
|
||||||
let proposedContentHeight = layout.size.height - topEdge - contentActionsSpacing - actionsSize.height - layout.intrinsicInsets.bottom - actionsBottomInset
|
let proposedContentHeight: CGFloat
|
||||||
contentUnscaledSize = CGSize(width: layout.size.width, height: max(400.0, proposedContentHeight))
|
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)) {
|
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
|
contentUnscaledSize = preferredSize
|
||||||
@ -1115,16 +1141,22 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
|||||||
var originalContentFrame: CGRect
|
var originalContentFrame: CGRect
|
||||||
var contentHeight: CGFloat
|
var contentHeight: CGFloat
|
||||||
if case .compact = layout.metrics.widthClass {
|
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)
|
if layout.size.width < layout.size.height {
|
||||||
originalContentFrame = CGRect(origin: CGPoint(x: actionsSideInset, y: originalActionsFrame.minY - contentActionsSpacing - contentSize.height), size: contentSize)
|
originalActionsFrame = CGRect(origin: CGPoint(x: actionsSideInset, y: min(maximumActionsFrameOrigin, floor((layout.size.height - contentActionsSpacing - contentSize.height) / 2.0) + contentSize.height + contentActionsSpacing)), size: actionsSize)
|
||||||
if originalContentFrame.minY < topEdge {
|
originalContentFrame = CGRect(origin: CGPoint(x: actionsSideInset, y: originalActionsFrame.minY - contentActionsSpacing - contentSize.height), size: contentSize)
|
||||||
let requiredOffset = topEdge - originalContentFrame.minY
|
if originalContentFrame.minY < topEdge {
|
||||||
let availableOffset = max(0.0, layout.size.height - layout.intrinsicInsets.bottom - actionsBottomInset - originalActionsFrame.maxY)
|
let requiredOffset = topEdge - originalContentFrame.minY
|
||||||
let offset = min(requiredOffset, availableOffset)
|
let availableOffset = max(0.0, layout.size.height - layout.intrinsicInsets.bottom - actionsBottomInset - originalActionsFrame.maxY)
|
||||||
originalActionsFrame = originalActionsFrame.offsetBy(dx: 0.0, dy: offset)
|
let offset = min(requiredOffset, availableOffset)
|
||||||
originalContentFrame = originalContentFrame.offsetBy(dx: 0.0, dy: offset)
|
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 {
|
} 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 = 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)
|
originalContentFrame.origin.x = min(originalContentFrame.origin.x, layout.size.width - actionsSideInset - contentSize.width)
|
||||||
@ -1342,6 +1374,8 @@ public final class ContextController: ViewController, StandalonePresentableContr
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
strongSelf.reactionSelected?(value)
|
strongSelf.reactionSelected?(value)
|
||||||
|
}, beganAnimatingOut: { [weak self] in
|
||||||
|
self?.statusBar.statusBarStyle = .Ignore
|
||||||
})
|
})
|
||||||
|
|
||||||
self.displayNodeDidLoad()
|
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 {
|
public final class ContextGesture: UIGestureRecognizer, UIGestureRecognizerDelegate {
|
||||||
private var currentProgress: CGFloat = 0.0
|
private var currentProgress: CGFloat = 0.0
|
||||||
private var delayTimer: Timer?
|
private var delayTimer: Timer?
|
||||||
@ -104,6 +117,9 @@ public final class ContextGesture: UIGestureRecognizer, UIGestureRecognizerDeleg
|
|||||||
strongSelf.animator?.invalidate()
|
strongSelf.animator?.invalidate()
|
||||||
strongSelf.activated?(strongSelf)
|
strongSelf.activated?(strongSelf)
|
||||||
if let view = strongSelf.view?.superview {
|
if let view = strongSelf.view?.superview {
|
||||||
|
if let window = view.window {
|
||||||
|
cancelOtherGestures(gesture: strongSelf, view: window)
|
||||||
|
}
|
||||||
cancelParentGestures(view: view)
|
cancelParentGestures(view: view)
|
||||||
}
|
}
|
||||||
strongSelf.state = .began
|
strongSelf.state = .began
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user