mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
671 lines
29 KiB
Swift
671 lines
29 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import Display
|
|
import ComponentFlow
|
|
import SwiftSignalKit
|
|
import AccountContext
|
|
import MediaEditor
|
|
import MediaAssetsContext
|
|
import CheckNode
|
|
import TelegramPresentationData
|
|
|
|
final class SelectionPanelComponent: Component {
|
|
let previewContainerView: PortalSourceView
|
|
let frame: CGRect
|
|
let items: [MediaEditorScreenImpl.EditingItem]
|
|
let selectedItemId: String
|
|
let itemTapped: (String?) -> Void
|
|
let itemSelectionToggled: (String) -> Void
|
|
let itemReordered: (String, String) -> Void
|
|
|
|
init(
|
|
previewContainerView: PortalSourceView,
|
|
frame: CGRect,
|
|
items: [MediaEditorScreenImpl.EditingItem],
|
|
selectedItemId: String,
|
|
itemTapped: @escaping (String?) -> Void,
|
|
itemSelectionToggled: @escaping (String) -> Void,
|
|
itemReordered: @escaping (String, String) -> Void
|
|
) {
|
|
self.previewContainerView = previewContainerView
|
|
self.frame = frame
|
|
self.items = items
|
|
self.selectedItemId = selectedItemId
|
|
self.itemTapped = itemTapped
|
|
self.itemSelectionToggled = itemSelectionToggled
|
|
self.itemReordered = itemReordered
|
|
}
|
|
|
|
static func ==(lhs: SelectionPanelComponent, rhs: SelectionPanelComponent) -> Bool {
|
|
return lhs.frame == rhs.frame && lhs.items == rhs.items && lhs.selectedItemId == rhs.selectedItemId
|
|
}
|
|
|
|
final class View: UIView, UIGestureRecognizerDelegate {
|
|
final class ItemView: UIView {
|
|
private let backgroundNode: ASImageNode
|
|
private let imageNode: ImageNode
|
|
private let checkNode: InteractiveCheckNode
|
|
private var selectionLayer: SimpleShapeLayer?
|
|
|
|
var toggleSelection: () -> Void = {}
|
|
|
|
override init(frame: CGRect) {
|
|
self.backgroundNode = ASImageNode()
|
|
self.backgroundNode.displaysAsynchronously = false
|
|
|
|
self.imageNode = ImageNode()
|
|
self.imageNode.contentMode = .scaleAspectFill
|
|
|
|
self.checkNode = InteractiveCheckNode(theme: CheckNodeTheme(theme: defaultDarkColorPresentationTheme, style: .overlay))
|
|
|
|
super.init(frame: frame)
|
|
|
|
self.clipsToBounds = true
|
|
self.layer.cornerRadius = 6.0
|
|
|
|
self.addSubview(self.backgroundNode.view)
|
|
self.addSubview(self.imageNode.view)
|
|
self.addSubview(self.checkNode.view)
|
|
|
|
self.checkNode.valueChanged = { [weak self] value in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.toggleSelection()
|
|
}
|
|
}
|
|
|
|
required init?(coder aDecoder: NSCoder) {
|
|
preconditionFailure()
|
|
}
|
|
|
|
fileprivate var item: MediaEditorScreenImpl.EditingItem?
|
|
func update(item: MediaEditorScreenImpl.EditingItem, number: Int, isSelected: Bool, isEnabled: Bool, size: CGSize, portalView: PortalView?, transition: ComponentTransition) {
|
|
let previousItem = self.item
|
|
self.item = item
|
|
|
|
if previousItem?.asset.localIdentifier != item.asset.localIdentifier || previousItem?.version != item.version {
|
|
let imageSignal: Signal<UIImage?, NoError>
|
|
if let thumbnail = item.thumbnail {
|
|
imageSignal = .single(thumbnail)
|
|
self.imageNode.contentMode = .scaleAspectFill
|
|
} else {
|
|
imageSignal = assetImage(asset: item.asset, targetSize:CGSize(width: 128.0 * UIScreenScale, height: 128.0 * UIScreenScale), exact: false, synchronous: true)
|
|
self.imageNode.contentUpdated = { [weak self] image in
|
|
if let self {
|
|
if self.backgroundNode.image == nil {
|
|
if let image, image.size.width > image.size.height {
|
|
self.imageNode.contentMode = .scaleAspectFit
|
|
Queue.concurrentDefaultQueue().async {
|
|
let colors = mediaEditorGetGradientColors(from: image)
|
|
let gradientImage = mediaEditorGenerateGradientImage(size: CGSize(width: 3.0, height: 128.0), colors: colors.array)
|
|
Queue.mainQueue().async {
|
|
self.backgroundNode.image = gradientImage
|
|
}
|
|
}
|
|
} else {
|
|
self.imageNode.contentMode = .scaleAspectFill
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
self.imageNode.setSignal(imageSignal)
|
|
}
|
|
|
|
let backgroundSize = CGSize(width: size.width, height: floorToScreenPixels(size.width / 9.0 * 16.0))
|
|
self.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((size.height - backgroundSize.height) / 2.0)), size: backgroundSize)
|
|
|
|
self.imageNode.frame = CGRect(origin: .zero, size: size)
|
|
|
|
//self.checkNode.content = .counter(number)
|
|
self.checkNode.setSelected(isEnabled, animated: previousItem != nil)
|
|
|
|
let checkSize = CGSize(width: 29.0, height: 29.0)
|
|
self.checkNode.frame = CGRect(origin: CGPoint(x: size.width - checkSize.width - 4.0, y: 4.0), size: checkSize)
|
|
|
|
if isSelected, let portalView {
|
|
portalView.view.frame = CGRect(origin: .zero, size: size)
|
|
self.insertSubview(portalView.view, aboveSubview: self.imageNode.view)
|
|
}
|
|
|
|
let lineWidth: CGFloat = 2.0 - UIScreenPixel
|
|
let selectionFrame = CGRect(origin: .zero, size: size)
|
|
if isSelected {
|
|
let selectionLayer: SimpleShapeLayer
|
|
if let current = self.selectionLayer {
|
|
selectionLayer = current
|
|
} else {
|
|
selectionLayer = SimpleShapeLayer()
|
|
self.selectionLayer = selectionLayer
|
|
self.layer.addSublayer(selectionLayer)
|
|
|
|
selectionLayer.fillColor = UIColor.clear.cgColor
|
|
selectionLayer.strokeColor = UIColor.white.cgColor
|
|
selectionLayer.lineWidth = lineWidth
|
|
selectionLayer.frame = selectionFrame
|
|
selectionLayer.path = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0), cornerWidth: 6.0, cornerHeight: 6.0, transform: nil)
|
|
|
|
// if !transition.animation.isImmediate {
|
|
// let initialPath = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: 0.0, dy: 0.0), cornerWidth: 6.0, cornerHeight: 6.0, transform: nil)
|
|
// selectionLayer.animate(from: initialPath, to: selectionLayer.path as AnyObject, keyPath: "path", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2)
|
|
// selectionLayer.animateShapeLineWidth(from: 0.0, to: lineWidth, duration: 0.2)
|
|
// }
|
|
}
|
|
|
|
} else if let selectionLayer = self.selectionLayer {
|
|
self.selectionLayer = nil
|
|
selectionLayer.removeFromSuperlayer()
|
|
|
|
// let targetPath = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: 0.0, dy: 0.0), cornerWidth: 6.0, cornerHeight: 6.0, transform: nil)
|
|
// selectionLayer.animate(from: selectionLayer.path, to: targetPath, keyPath: "path", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2, removeOnCompletion: false)
|
|
// selectionLayer.animateShapeLineWidth(from: selectionLayer.lineWidth, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
|
|
// selectionLayer.removeFromSuperlayer()
|
|
// })
|
|
}
|
|
}
|
|
}
|
|
|
|
private let backgroundView: BlurredBackgroundView
|
|
private let backgroundMaskView: UIView
|
|
private let backgroundMaskPanelView: UIView
|
|
|
|
private let scrollView: UIScrollView
|
|
private var itemViews: [AnyHashable: ItemView] = [:]
|
|
private var portalView: PortalView?
|
|
|
|
private var reorderRecognizer: ReorderGestureRecognizer?
|
|
private var reorderingItem: (id: AnyHashable, initialPosition: CGPoint, position: CGPoint, snapshotView: UIView)?
|
|
|
|
private var tapRecognizer: UITapGestureRecognizer?
|
|
|
|
private var component: SelectionPanelComponent?
|
|
private var state: EmptyComponentState?
|
|
|
|
override init(frame: CGRect) {
|
|
self.backgroundView = BlurredBackgroundView(color: UIColor(white: 0.2, alpha: 0.45), enableBlur: true)
|
|
self.backgroundMaskView = UIView(frame: .zero)
|
|
|
|
self.backgroundMaskPanelView = UIView(frame: .zero)
|
|
self.backgroundMaskPanelView.backgroundColor = UIColor.white
|
|
self.backgroundMaskPanelView.clipsToBounds = true
|
|
self.backgroundMaskPanelView.layer.cornerRadius = 10.0
|
|
|
|
self.scrollView = UIScrollView(frame: .zero)
|
|
self.scrollView.contentInsetAdjustmentBehavior = .never
|
|
self.scrollView.showsHorizontalScrollIndicator = false
|
|
self.scrollView.showsVerticalScrollIndicator = false
|
|
self.scrollView.layer.cornerRadius = 10.0
|
|
|
|
super.init(frame: frame)
|
|
|
|
self.backgroundView.mask = self.backgroundMaskView
|
|
|
|
let reorderRecognizer = ReorderGestureRecognizer(
|
|
shouldBegin: { [weak self] point in
|
|
guard let self, let item = self.item(at: point) else {
|
|
return (allowed: false, requiresLongPress: false, item: nil)
|
|
}
|
|
|
|
return (allowed: true, requiresLongPress: true, item: item)
|
|
},
|
|
willBegin: { point in
|
|
},
|
|
began: { [weak self] item in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.setReorderingItem(item: item)
|
|
},
|
|
ended: { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.setReorderingItem(item: nil)
|
|
},
|
|
moved: { [weak self] distance in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.moveReorderingItem(distance: distance)
|
|
},
|
|
isActiveUpdated: { _ in
|
|
}
|
|
)
|
|
reorderRecognizer.delegate = self
|
|
self.reorderRecognizer = reorderRecognizer
|
|
self.addGestureRecognizer(reorderRecognizer)
|
|
|
|
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap))
|
|
self.tapRecognizer = tapRecognizer
|
|
self.addGestureRecognizer(tapRecognizer)
|
|
|
|
self.addSubview(self.backgroundView)
|
|
self.addSubview(self.scrollView)
|
|
|
|
self.backgroundMaskView.addSubview(self.backgroundMaskPanelView)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
preconditionFailure()
|
|
}
|
|
|
|
deinit {
|
|
}
|
|
|
|
@objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) {
|
|
guard let component = self.component else {
|
|
return
|
|
}
|
|
self.reorderRecognizer?.isEnabled = false
|
|
self.reorderRecognizer?.isEnabled = true
|
|
|
|
let location = gestureRecognizer.location(in: self)
|
|
if let itemView = self.item(at: location), let item = itemView.item, item.asset.localIdentifier != component.selectedItemId {
|
|
component.itemTapped(item.asset.localIdentifier)
|
|
} else {
|
|
component.itemTapped(nil)
|
|
}
|
|
}
|
|
|
|
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
if otherGestureRecognizer is UITapGestureRecognizer {
|
|
return true
|
|
}
|
|
if otherGestureRecognizer is UIPanGestureRecognizer {
|
|
if gestureRecognizer === self.reorderRecognizer, ![.began, .changed].contains(gestureRecognizer.state) {
|
|
gestureRecognizer.isEnabled = false
|
|
gestureRecognizer.isEnabled = true
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func item(at point: CGPoint) -> ItemView? {
|
|
let point = self.convert(point, to: self.scrollView)
|
|
for (_, itemView) in self.itemViews {
|
|
if itemView.frame.contains(point) {
|
|
return itemView
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func setReorderingItem(item: ItemView?) {
|
|
self.tapRecognizer?.isEnabled = false
|
|
self.tapRecognizer?.isEnabled = true
|
|
|
|
var mappedItem: (AnyHashable, ItemView)?
|
|
if let item {
|
|
for (id, visibleItem) in self.itemViews {
|
|
if visibleItem === item {
|
|
mappedItem = (id, visibleItem)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if self.reorderingItem?.id != mappedItem?.0 {
|
|
let transition: ComponentTransition = .spring(duration: 0.4)
|
|
if let (id, itemView) = mappedItem, let snapshotView = itemView.snapshotView(afterScreenUpdates: false) {
|
|
itemView.isHidden = true
|
|
|
|
let position = self.scrollView.convert(itemView.center, to: self)
|
|
snapshotView.center = position
|
|
transition.setScale(view: snapshotView, scale: 0.9)
|
|
self.addSubview(snapshotView)
|
|
|
|
self.reorderingItem = (id, position, position, snapshotView)
|
|
} else {
|
|
if let (id, _, _, snapshotView) = self.reorderingItem {
|
|
if let itemView = self.itemViews[id] {
|
|
if let innerSnapshotView = snapshotView.snapshotView(afterScreenUpdates: false) {
|
|
innerSnapshotView.center = self.convert(snapshotView.center, to: self.scrollView)
|
|
innerSnapshotView.transform = CGAffineTransformMakeScale(0.9, 0.9)
|
|
self.scrollView.addSubview(innerSnapshotView)
|
|
|
|
transition.setPosition(view: innerSnapshotView, position: itemView.center, completion: { [weak innerSnapshotView] _ in
|
|
innerSnapshotView?.removeFromSuperview()
|
|
itemView.isHidden = false
|
|
})
|
|
transition.setScale(view: innerSnapshotView, scale: 1.0)
|
|
}
|
|
|
|
transition.setPosition(view: snapshotView, position: self.scrollView.convert(itemView.center, to: self), completion: { [weak snapshotView] _ in
|
|
snapshotView?.removeFromSuperview()
|
|
})
|
|
transition.setScale(view: snapshotView, scale: 1.0)
|
|
transition.setAlpha(view: snapshotView, alpha: 0.0)
|
|
}
|
|
}
|
|
self.reorderingItem = nil
|
|
}
|
|
self.state?.updated(transition: transition)
|
|
}
|
|
}
|
|
|
|
func moveReorderingItem(distance: CGPoint) {
|
|
guard let component = self.component else {
|
|
return
|
|
}
|
|
if let (id, initialPosition, _, snapshotView) = self.reorderingItem {
|
|
let targetPosition = CGPoint(x: initialPosition.x + distance.x, y: initialPosition.y + distance.y)
|
|
self.reorderingItem = (id, initialPosition, targetPosition, snapshotView)
|
|
snapshotView.center = targetPosition
|
|
|
|
let mappedPosition = self.convert(targetPosition, to: self.scrollView)
|
|
|
|
if let visibleReorderingItem = self.itemViews[id], let fromId = self.itemViews[id]?.item?.asset.localIdentifier {
|
|
for (_, visibleItem) in self.itemViews {
|
|
if visibleItem === visibleReorderingItem {
|
|
continue
|
|
}
|
|
if visibleItem.frame.contains(mappedPosition), let toId = visibleItem.item?.asset.localIdentifier {
|
|
component.itemReordered(fromId, toId)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func animateIn(from buttonView: SelectionPanelButtonContentComponent.View) {
|
|
|
|
}
|
|
|
|
func animateOut(to buttonView: SelectionPanelButtonContentComponent.View, completion: @escaping () -> Void) {
|
|
completion()
|
|
}
|
|
|
|
func update(component: SelectionPanelComponent, availableSize: CGSize, state: EmptyComponentState, transition: ComponentTransition) -> CGSize {
|
|
self.component = component
|
|
self.state = state
|
|
|
|
if self.portalView == nil {
|
|
if let portalView = PortalView(matchPosition: false) {
|
|
portalView.view.layer.rasterizationScale = UIScreenScale
|
|
|
|
let scale = 95.0 / component.previewContainerView.frame.width
|
|
portalView.view.transform = CGAffineTransformMakeScale(scale, scale)
|
|
|
|
component.previewContainerView.addPortal(view: portalView)
|
|
self.portalView = portalView
|
|
}
|
|
}
|
|
|
|
var validIds = Set<AnyHashable>()
|
|
|
|
let itemSize = CGSize(width: 95.0, height: 112.0)
|
|
let spacing: CGFloat = 4.0
|
|
|
|
var itemFrame: CGRect = CGRect(origin: CGPoint(x: spacing, y: spacing), size: itemSize)
|
|
|
|
var index = 1
|
|
for item in component.items {
|
|
let id = item.asset.localIdentifier
|
|
validIds.insert(id)
|
|
|
|
var itemTransition = transition
|
|
let itemView: ItemView
|
|
if let current = self.itemViews[id] {
|
|
itemView = current
|
|
} else {
|
|
itemView = ItemView(frame: itemFrame)
|
|
self.scrollView.addSubview(itemView)
|
|
self.itemViews[id] = itemView
|
|
|
|
itemTransition = .immediate
|
|
}
|
|
itemView.toggleSelection = { [weak self] in
|
|
guard let self, let component = self.component else {
|
|
return
|
|
}
|
|
component.itemSelectionToggled(id)
|
|
}
|
|
itemView.update(item: item, number: index, isSelected: item.asset.localIdentifier == component.selectedItemId, isEnabled: item.isEnabled, size: itemFrame.size, portalView: self.portalView, transition: itemTransition)
|
|
|
|
itemTransition.setBounds(view: itemView, bounds: CGRect(origin: .zero, size: itemFrame.size))
|
|
itemTransition.setPosition(view: itemView, position: itemFrame.center)
|
|
|
|
itemFrame.origin.x += itemSize.width + spacing
|
|
index += 1
|
|
}
|
|
|
|
var removeIds: [AnyHashable] = []
|
|
for (id, itemView) in self.itemViews {
|
|
if !validIds.contains(id) {
|
|
removeIds.append(id)
|
|
transition.setAlpha(view: itemView, alpha: 0.0, completion: { [weak itemView] _ in
|
|
itemView?.removeFromSuperview()
|
|
})
|
|
}
|
|
}
|
|
for id in removeIds {
|
|
self.itemViews.removeValue(forKey: id)
|
|
}
|
|
|
|
let contentSize = CGSize(width: itemFrame.minX, height: itemSize.height + spacing * 2.0)
|
|
if self.scrollView.contentSize != contentSize {
|
|
self.scrollView.contentSize = contentSize
|
|
}
|
|
|
|
let backgroundSize = CGSize(width: min(availableSize.width - 24.0, contentSize.width), height: contentSize.height)
|
|
self.backgroundView.frame = CGRect(origin: .zero, size: availableSize)
|
|
self.backgroundView.update(size: availableSize, transition: .immediate)
|
|
|
|
let contentFrame = CGRect(origin: CGPoint(x: availableSize.width - 12.0 - backgroundSize.width, y: component.frame.minY), size: backgroundSize)
|
|
self.backgroundMaskPanelView.frame = contentFrame
|
|
self.scrollView.frame = contentFrame
|
|
|
|
return availableSize
|
|
}
|
|
}
|
|
|
|
func makeView() -> View {
|
|
return View()
|
|
}
|
|
|
|
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
|
return view.update(component: self, availableSize: availableSize, state: state, transition: transition)
|
|
}
|
|
}
|
|
|
|
private final class ReorderGestureRecognizer: UIGestureRecognizer {
|
|
private let shouldBegin: (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, item: SelectionPanelComponent.View.ItemView?)
|
|
private let willBegin: (CGPoint) -> Void
|
|
private let began: (SelectionPanelComponent.View.ItemView) -> Void
|
|
private let ended: () -> Void
|
|
private let moved: (CGPoint) -> Void
|
|
private let isActiveUpdated: (Bool) -> Void
|
|
|
|
private var initialLocation: CGPoint?
|
|
private var longTapTimer: SwiftSignalKit.Timer?
|
|
private var longPressTimer: SwiftSignalKit.Timer?
|
|
|
|
private var itemView: SelectionPanelComponent.View.ItemView?
|
|
|
|
public init(shouldBegin: @escaping (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, item: SelectionPanelComponent.View.ItemView?), willBegin: @escaping (CGPoint) -> Void, began: @escaping (SelectionPanelComponent.View.ItemView) -> Void, ended: @escaping () -> Void, moved: @escaping (CGPoint) -> Void, isActiveUpdated: @escaping (Bool) -> Void) {
|
|
self.shouldBegin = shouldBegin
|
|
self.willBegin = willBegin
|
|
self.began = began
|
|
self.ended = ended
|
|
self.moved = moved
|
|
self.isActiveUpdated = isActiveUpdated
|
|
|
|
super.init(target: nil, action: nil)
|
|
}
|
|
|
|
deinit {
|
|
self.longTapTimer?.invalidate()
|
|
self.longPressTimer?.invalidate()
|
|
}
|
|
|
|
private func startLongTapTimer() {
|
|
self.longTapTimer?.invalidate()
|
|
let longTapTimer = SwiftSignalKit.Timer(timeout: 0.25, repeat: false, completion: { [weak self] in
|
|
self?.longTapTimerFired()
|
|
}, queue: Queue.mainQueue())
|
|
self.longTapTimer = longTapTimer
|
|
longTapTimer.start()
|
|
}
|
|
|
|
private func stopLongTapTimer() {
|
|
self.itemView = nil
|
|
self.longTapTimer?.invalidate()
|
|
self.longTapTimer = nil
|
|
}
|
|
|
|
private func startLongPressTimer() {
|
|
self.longPressTimer?.invalidate()
|
|
let longPressTimer = SwiftSignalKit.Timer(timeout: 0.6, repeat: false, completion: { [weak self] in
|
|
self?.longPressTimerFired()
|
|
}, queue: Queue.mainQueue())
|
|
self.longPressTimer = longPressTimer
|
|
longPressTimer.start()
|
|
}
|
|
|
|
private func stopLongPressTimer() {
|
|
self.itemView = nil
|
|
self.longPressTimer?.invalidate()
|
|
self.longPressTimer = nil
|
|
}
|
|
|
|
override public func reset() {
|
|
super.reset()
|
|
|
|
self.itemView = nil
|
|
self.stopLongTapTimer()
|
|
self.stopLongPressTimer()
|
|
self.initialLocation = nil
|
|
|
|
self.isActiveUpdated(false)
|
|
}
|
|
|
|
private func longTapTimerFired() {
|
|
guard let location = self.initialLocation else {
|
|
return
|
|
}
|
|
|
|
self.longTapTimer?.invalidate()
|
|
self.longTapTimer = nil
|
|
|
|
self.willBegin(location)
|
|
}
|
|
|
|
private func longPressTimerFired() {
|
|
guard let _ = self.initialLocation else {
|
|
return
|
|
}
|
|
|
|
self.isActiveUpdated(true)
|
|
self.state = .began
|
|
self.longPressTimer?.invalidate()
|
|
self.longPressTimer = nil
|
|
self.longTapTimer?.invalidate()
|
|
self.longTapTimer = nil
|
|
if let itemView = self.itemView {
|
|
self.began(itemView)
|
|
}
|
|
self.isActiveUpdated(true)
|
|
}
|
|
|
|
override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
|
super.touchesBegan(touches, with: event)
|
|
|
|
if self.numberOfTouches > 1 {
|
|
self.isActiveUpdated(false)
|
|
self.state = .failed
|
|
self.ended()
|
|
return
|
|
}
|
|
|
|
if self.state == .possible {
|
|
if let location = touches.first?.location(in: self.view) {
|
|
let (allowed, requiresLongPress, itemView) = self.shouldBegin(location)
|
|
if allowed {
|
|
self.isActiveUpdated(true)
|
|
|
|
self.itemView = itemView
|
|
self.initialLocation = location
|
|
if requiresLongPress {
|
|
self.startLongTapTimer()
|
|
self.startLongPressTimer()
|
|
} else {
|
|
self.state = .began
|
|
if let itemView = self.itemView {
|
|
self.began(itemView)
|
|
}
|
|
}
|
|
} else {
|
|
self.isActiveUpdated(false)
|
|
self.state = .failed
|
|
}
|
|
} else {
|
|
self.isActiveUpdated(false)
|
|
self.state = .failed
|
|
}
|
|
}
|
|
}
|
|
|
|
override public func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
|
|
super.touchesEnded(touches, with: event)
|
|
|
|
self.initialLocation = nil
|
|
|
|
self.stopLongTapTimer()
|
|
if self.longPressTimer != nil {
|
|
self.stopLongPressTimer()
|
|
self.isActiveUpdated(false)
|
|
self.state = .failed
|
|
}
|
|
if self.state == .began || self.state == .changed {
|
|
self.isActiveUpdated(false)
|
|
self.ended()
|
|
self.state = .failed
|
|
}
|
|
}
|
|
|
|
override public func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
|
|
super.touchesCancelled(touches, with: event)
|
|
|
|
self.initialLocation = nil
|
|
|
|
self.stopLongTapTimer()
|
|
if self.longPressTimer != nil {
|
|
self.isActiveUpdated(false)
|
|
self.stopLongPressTimer()
|
|
self.state = .failed
|
|
}
|
|
if self.state == .began || self.state == .changed {
|
|
self.isActiveUpdated(false)
|
|
self.ended()
|
|
self.state = .failed
|
|
}
|
|
}
|
|
|
|
override public func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
|
|
super.touchesMoved(touches, with: event)
|
|
|
|
if (self.state == .began || self.state == .changed), let initialLocation = self.initialLocation, let location = touches.first?.location(in: self.view) {
|
|
self.state = .changed
|
|
let offset = CGPoint(x: location.x - initialLocation.x, y: location.y - initialLocation.y)
|
|
self.moved(offset)
|
|
} else if let touch = touches.first, let initialTapLocation = self.initialLocation, self.longPressTimer != nil {
|
|
let touchLocation = touch.location(in: self.view)
|
|
let dX = touchLocation.x - initialTapLocation.x
|
|
let dY = touchLocation.y - initialTapLocation.y
|
|
|
|
if dX * dX + dY * dY > 3.0 * 3.0 {
|
|
self.stopLongTapTimer()
|
|
self.stopLongPressTimer()
|
|
self.initialLocation = nil
|
|
self.isActiveUpdated(false)
|
|
self.state = .failed
|
|
}
|
|
}
|
|
}
|
|
}
|