2025-04-21 17:02:37 +04:00

744 lines
34 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)
}
} else if let selectionLayer = self.selectionLayer {
self.selectionLayer = nil
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) {
guard let component = self.component else {
return
}
self.scrollView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
let buttonFrame = buttonView.convert(buttonView.bounds, to: self)
let fromPoint = CGPoint(x: buttonFrame.center.x - self.scrollView.center.x, y: buttonFrame.center.y - self.scrollView.center.y)
self.scrollView.layer.animatePosition(from: fromPoint, to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.scrollView.layer.animateBounds(from: CGRect(origin: CGPoint(x: buttonFrame.minX - self.scrollView.frame.minX, y: buttonFrame.minY - self.scrollView.frame.minY), size: buttonFrame.size), to: self.scrollView.bounds, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
self.backgroundMaskPanelView.layer.animatePosition(from: fromPoint, to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.backgroundMaskPanelView.layer.animate(from: NSNumber(value: Float(16.5)), to: NSNumber(value: Float(self.backgroundMaskPanelView.layer.cornerRadius)), keyPath: "cornerRadius", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.4)
self.backgroundMaskPanelView.layer.animateBounds(from: CGRect(origin: .zero, size: CGSize(width: 33.0, height: 33.0)), to: self.backgroundMaskPanelView.bounds, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
let mainCircleDelay: Double = 0.02
let backgroundWidth = self.backgroundMaskPanelView.frame.width
for item in component.items {
guard let itemView = self.itemViews[item.asset.localIdentifier] else {
continue
}
let distance = abs(itemView.frame.center.x - backgroundWidth)
let distanceNorm = distance / backgroundWidth
let adjustedDistanceNorm = distanceNorm
let itemDelay = mainCircleDelay + adjustedDistanceNorm * 0.14
itemView.isHidden = true
Queue.mainQueue().after(itemDelay * UIView.animationDurationFactor()) { [weak itemView] in
guard let itemView else {
return
}
itemView.isHidden = false
itemView.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4)
}
}
}
func animateOut(to buttonView: SelectionPanelButtonContentComponent.View, completion: @escaping () -> Void) {
guard let component = self.component else {
completion()
return
}
self.scrollView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
let buttonFrame = buttonView.convert(buttonView.bounds, to: self)
let scrollButtonFrame = buttonView.convert(buttonView.bounds, to: self.scrollView)
let toPoint = CGPoint(x: buttonFrame.center.x - self.scrollView.center.x, y: buttonFrame.center.y - self.scrollView.center.y)
self.scrollView.layer.animatePosition(from: .zero, to: toPoint, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.scrollView.layer.animateBounds(from: self.scrollView.bounds, to: CGRect(origin: CGPoint(x: (buttonFrame.minX - self.scrollView.frame.minX) / 2.0, y: (buttonFrame.minY - self.scrollView.frame.minY) / 2.0), size: buttonFrame.size), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
self.backgroundMaskPanelView.layer.animatePosition(from: .zero, to: toPoint, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true)
self.backgroundMaskPanelView.layer.animate(from: NSNumber(value: Float(self.backgroundMaskPanelView.layer.cornerRadius)), to: NSNumber(value: Float(16.5)), keyPath: "cornerRadius", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.4, removeOnCompletion: false)
self.backgroundMaskPanelView.layer.animateBounds(from: self.backgroundMaskPanelView.bounds, to: CGRect(origin: .zero, size: CGSize(width: 33.0, height: 33.0)), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { finished in
if finished {
completion()
self.backgroundMaskPanelView.layer.removeAllAnimations()
for (_, itemView) in self.itemViews {
itemView.layer.removeAllAnimations()
}
}
})
let mainCircleDelay: Double = 0.0
let backgroundWidth = self.backgroundMaskPanelView.frame.width
for item in component.items {
guard let itemView = self.itemViews[item.asset.localIdentifier] else {
continue
}
let distance = abs(itemView.frame.center.x - backgroundWidth)
let distanceNorm = distance / backgroundWidth
let adjustedDistanceNorm = distanceNorm
let itemDelay = mainCircleDelay + adjustedDistanceNorm * 0.05
Queue.mainQueue().after(itemDelay * UIView.animationDurationFactor()) { [weak itemView] in
guard let itemView else {
return
}
itemView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
}
itemView.layer.animatePosition(from: itemView.center, to: scrollButtonFrame.center, duration: 0.4)
}
}
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
}
}
}
}