Two-finger message selection

This commit is contained in:
Ilya Laktyushin 2019-11-29 17:53:54 +04:00
parent 4a05d43645
commit e9ea1b57bc
5 changed files with 223 additions and 25 deletions

View File

@ -4020,7 +4020,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
case .down:
var contentOffset = initialOffset
contentOffset.y += distance
contentOffset.y = max(self.scroller.contentInset.top, min(contentOffset.y, self.scroller.contentSize.height - self.visibleSize.height - self.insets.bottom - self.insets.top))
contentOffset.y = max(self.scroller.contentInset.top, min(contentOffset.y, self.scroller.contentSize.height - self.scroller.frame.height))
if contentOffset.y > initialOffset.y {
self.ignoreScrollingEvents = true
self.scroller.setContentOffset(contentOffset, animated: false)

View File

@ -23,6 +23,19 @@ class ListViewScroller: UIScrollView, UIGestureRecognizerDelegate {
return false
}
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer is UIPanGestureRecognizer, let gestureRecognizers = gestureRecognizer.view?.gestureRecognizers {
for otherGestureRecognizer in gestureRecognizers {
if otherGestureRecognizer !== gestureRecognizer, let panGestureRecognizer = otherGestureRecognizer as? UIPanGestureRecognizer, panGestureRecognizer.minimumNumberOfTouches == 2 {
return gestureRecognizer.numberOfTouches < 2
}
}
return true
} else {
return true
}
}
#if os(iOS)
override func touchesShouldCancel(in view: UIView) -> Bool {
return true

View File

@ -650,7 +650,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
return transaction.getPeer(peerId).flatMap(RenderedPeer.init(peer:))
} |> deliverOnMainQueue).start(next: { [weak self] peer in
if let strongSelf = self, let peer = peer {
strongSelf.controllerInteraction?.togglePeer(peer, true)
strongSelf.controllerInteraction?.togglePeer(peer, peer.peerId != account.peerId)
}
})
}

View File

@ -350,30 +350,7 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode {
public override func didLoad() {
super.didLoad()
}
private var liveSelectingState: (selecting: Bool, currentMessageId: MessageId)?
@objc private func panGesture(_ recognizer: UIGestureRecognizer) -> Void {
guard let selectionState = controllerInteraction.selectionState else {return}
switch recognizer.state {
case .began:
if let itemNode = self.itemNodeAtPoint(recognizer.location(in: self.view)) as? GridMessageItemNode, let messageId = itemNode.messageId {
liveSelectingState = (selecting: !selectionState.selectedIds.contains(messageId), currentMessageId: messageId)
controllerInteraction.toggleMessagesSelection([messageId], !selectionState.selectedIds.contains(messageId))
}
case .changed:
if let liveSelectingState = liveSelectingState, let itemNode = self.itemNodeAtPoint(recognizer.location(in: self.view)) as? GridMessageItemNode, let messageId = itemNode.messageId, messageId != liveSelectingState.currentMessageId {
controllerInteraction.toggleMessagesSelection([messageId], liveSelectingState.selecting)
self.liveSelectingState?.currentMessageId = messageId
}
case .ended, .failed, .cancelled:
liveSelectingState = nil
case .possible:
break
}
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@ -509,4 +486,27 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode {
public func disconnect() {
self.historyDisposable.set(nil)
}
private var selectionPanState: (selecting: Bool, currentMessageId: MessageId)?
@objc private func panGesture(_ recognizer: UIGestureRecognizer) -> Void {
guard let selectionState = self.controllerInteraction.selectionState else {return}
switch recognizer.state {
case .began:
if let itemNode = self.itemNodeAtPoint(recognizer.location(in: self.view)) as? GridMessageItemNode, let messageId = itemNode.messageId {
self.selectionPanState = (selecting: !selectionState.selectedIds.contains(messageId), currentMessageId: messageId)
self.controllerInteraction.toggleMessagesSelection([messageId], !selectionState.selectedIds.contains(messageId))
}
case .changed:
if let selectionPanState = self.selectionPanState, let itemNode = self.itemNodeAtPoint(recognizer.location(in: self.view)) as? GridMessageItemNode, let messageId = itemNode.messageId, messageId != selectionPanState.currentMessageId {
self.controllerInteraction.toggleMessagesSelection([messageId], selectionPanState.selecting)
self.selectionPanState?.currentMessageId = messageId
}
case .ended, .failed, .cancelled:
self.selectionPanState = nil
case .possible:
break
}
}
}

View File

@ -14,6 +14,55 @@ import TemporaryCachedPeerDataManager
import ChatListSearchItemNode
import Emoji
private class ChatHistoryListSelectionRecognizer: UIPanGestureRecognizer {
private let selectionGestureActivationThreshold: CGFloat = 5.0
var recognized: Bool? = nil
var initialLocation: CGPoint = CGPoint()
var shouldBegin: (() -> Bool)?
override init(target: Any?, action: Selector?) {
super.init(target: target, action: action)
self.minimumNumberOfTouches = 2
self.maximumNumberOfTouches = 2
}
override func reset() {
super.reset()
self.recognized = nil
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
if let shouldBegin = self.shouldBegin, !shouldBegin() {
self.state = .failed
} else {
let touch = touches.first!
self.initialLocation = touch.location(in: self.view)
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
let location = touches.first!.location(in: self.view)
let translation = location.offsetBy(dx: -self.initialLocation.x, dy: -self.initialLocation.y)
if self.recognized == nil {
if (fabs(translation.y) >= selectionGestureActivationThreshold) {
self.recognized = true
}
}
if let recognized = self.recognized, recognized {
super.touchesMoved(touches, with: event)
}
}
}
private let historyMessageCount: Int = 100
public enum ChatHistoryListMode: Equatable {
@ -869,6 +918,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
self?.isInteractivelyScrollingValue = false
self?.isInteractivelyScrollingPromise.set(false)
}
let selectionRecognizer = ChatHistoryListSelectionRecognizer(target: self, action: #selector(self.selectionPanGesture(_:)))
self.view.addGestureRecognizer(selectionRecognizer)
}
deinit {
@ -1582,4 +1634,137 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
}
}
}
private func messagesAtPoint(_ point: CGPoint) -> [Message]? {
var resultMessages: [Message]?
self.forEachVisibleItemNode { itemNode in
if resultMessages == nil, let itemNode = itemNode as? ListViewItemNode, itemNode.frame.contains(point) {
if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item as? ChatMessageItem {
switch item.content {
case let .message(message, _, _ , _):
resultMessages = [message]
case let .group(messages):
resultMessages = messages.map { $0.0 }
}
}
}
}
return resultMessages
}
private var selectionPanState: (selecting: Bool, initialMessageId: MessageId, toggledMessageIds: [[MessageId]])?
private var selectionScrollActivationTimer: SwiftSignalKit.Timer?
private var selectionScrollDisplayLink: ConstantDisplayLinkAnimator?
private var selectionScrollDelta: CGFloat?
private var selectionLastLocation: CGPoint?
@objc private func selectionPanGesture(_ recognizer: UIGestureRecognizer) -> Void {
let location = recognizer.location(in: self.view)
switch recognizer.state {
case .began:
if let messages = self.messagesAtPoint(location), let message = messages.first {
let selecting = !(self.controllerInteraction.selectionState?.selectedIds.contains(message.id) ?? false)
self.selectionPanState = (selecting, message.id, [])
self.controllerInteraction.toggleMessagesSelection(messages.map { $0.id }, selecting)
}
case .changed:
self.handlePanSelection(location: location)
self.selectionLastLocation = location
case .ended, .failed, .cancelled:
self.selectionPanState = nil
self.selectionScrollDisplayLink = nil
self.selectionScrollActivationTimer?.invalidate()
self.selectionScrollActivationTimer = nil
self.selectionScrollDelta = nil
self.selectionLastLocation = nil
self.selectionScrollSkipUpdate = false
case .possible:
break
}
}
private func handlePanSelection(location: CGPoint) {
if let state = self.selectionPanState {
if let messages = self.messagesAtPoint(location), let message = messages.first {
if message.id == state.initialMessageId {
if !state.toggledMessageIds.isEmpty {
self.controllerInteraction.toggleMessagesSelection(state.toggledMessageIds.flatMap { $0 }, !state.selecting)
self.selectionPanState = (state.selecting, state.initialMessageId, [])
}
} else if state.toggledMessageIds.last?.first != message.id {
var updatedToggledMessageIds: [[MessageId]] = []
var previouslyToggled = false
for i in (0 ..< state.toggledMessageIds.count) {
if let messageId = state.toggledMessageIds[i].first {
if messageId == message.id {
previouslyToggled = true
updatedToggledMessageIds = Array(state.toggledMessageIds.prefix(i + 1))
let messageIdsToToggle = Array(state.toggledMessageIds.suffix(state.toggledMessageIds.count - i - 1)).flatMap { $0 }
self.controllerInteraction.toggleMessagesSelection(messageIdsToToggle, !state.selecting)
break
}
}
}
if !previouslyToggled {
updatedToggledMessageIds = state.toggledMessageIds
let isSelected = (self.controllerInteraction.selectionState?.selectedIds.contains(message.id) ?? false)
if state.selecting != isSelected {
let messageIds = messages.map { $0.id }
updatedToggledMessageIds.append(messageIds)
self.controllerInteraction.toggleMessagesSelection(messageIds, state.selecting)
}
}
self.selectionPanState = (state.selecting, state.initialMessageId, updatedToggledMessageIds)
}
}
let scrollingAreaHeight: CGFloat = 50.0
if location.y < scrollingAreaHeight + self.insets.top || location.y > self.frame.height - scrollingAreaHeight - self.insets.bottom {
if location.y < self.frame.height / 2.0 {
self.selectionScrollDelta = (scrollingAreaHeight - (location.y - self.insets.top)) / scrollingAreaHeight
} else {
self.selectionScrollDelta = -(scrollingAreaHeight - min(scrollingAreaHeight, max(0.0, (self.frame.height - self.insets.bottom - location.y)))) / scrollingAreaHeight
}
if let displayLink = self.selectionScrollDisplayLink {
displayLink.isPaused = false
} else {
if let _ = self.selectionScrollActivationTimer {
} else {
let timer = SwiftSignalKit.Timer(timeout: 0.45, repeat: false, completion: { [weak self] in
self?.setupSelectionScrolling()
}, queue: .mainQueue())
timer.start()
self.selectionScrollActivationTimer = timer
}
}
} else {
self.selectionScrollDisplayLink?.isPaused = true
self.selectionScrollActivationTimer?.invalidate()
self.selectionScrollActivationTimer = nil
}
}
}
private var selectionScrollSkipUpdate = false
private func setupSelectionScrolling() {
self.selectionScrollDisplayLink = ConstantDisplayLinkAnimator(update: { [weak self] in
self?.selectionScrollActivationTimer = nil
if let strongSelf = self, let delta = strongSelf.selectionScrollDelta {
let distance: CGFloat = 10.0 * min(1.0, 0.15 + abs(delta * delta))
let direction: ListViewScrollDirection = delta > 0.0 ? .up : .down
strongSelf.scrollWithDirection(direction, distance: distance)
if let location = strongSelf.selectionLastLocation {
if !strongSelf.selectionScrollSkipUpdate {
strongSelf.handlePanSelection(location: location)
}
strongSelf.selectionScrollSkipUpdate = !strongSelf.selectionScrollSkipUpdate
}
}
})
self.selectionScrollDisplayLink?.isPaused = false
}
}