处理移动/动画 UiView 的接触
Processing touches on moving/ animating UiViews
我目前遇到的问题是触摸并不总是被正确识别,
我的目标是拥有3个手势,这3个手势是
- 用户可以点击一个视图,点击会被识别,
- 用户可以双击一个视图并且双击被识别,
- 用户可以在屏幕上移动手指,如果视图在屏幕下方
一个标签被识别。
但是我有多个视图,它们都在不断地动画化,它们可能会重叠,
目前,我按大小对视图进行排序,并将最小的视图放在较大的视图之上。
我通常会遇到一个问题,即在点击它们时无法识别 UIView。尤其是双击,滑动似乎大部分时间都很好,但整个体验非常不一致。
我目前用来解决问题的代码是:
class FinderrBoxView: UIView {
private var lastBox: String?
private var throttleDelay = 0.01
private var processQueue = DispatchQueue(label: "com.finderr.FinderrBoxView")
public var predictedObjects: [FinderrItem] = [] {
didSet {
predictedObjects.forEach { self.checkIfBoxIntersectCentre(prediction: [=10=]) }
drawBoxs(with: FinderrBoxView.sortBoxByeSize(predictedObjects))
setNeedsDisplay()
}
}
func drawBoxs(with predictions: [FinderrItem]) {
var newBoxes = Set(predictions)
var views = subviews.compactMap { [=10=] as? BoxView }
views = views.filter { view in
guard let closest = newBoxes.sorted(by: { x, y in
let xd = FinderrBoxView.distanceBetweenBoxes(view.frame, x.box)
let yd = FinderrBoxView.distanceBetweenBoxes(view.frame, y.box)
return xd < yd
}).first else { return false }
if FinderrBoxView.updateOrCreateNewBox(view.frame, closest.box)
{
newBoxes.remove(closest)
UIView.animate(withDuration: self.throttleDelay, delay: 0, options: .curveLinear, animations: {
view.frame = closest.box
}, completion: nil)
return false
} else {
return true
}
}
views.forEach { [=10=].removeFromSuperview() }
newBoxes.forEach { self.createLabelAndBox(prediction: [=10=]) }
accessibilityElements = subviews
}
func update(with predictions: [FinderrItem]) {
var newBoxes = Set(predictions)
var viewsToRemove = [UIView]()
for view in subviews {
var shouldRemoveView = true
for box in predictions {
if FinderrBoxView.updateOrCreateNewBox(view.frame, box.box)
{
UIView.animate(withDuration: throttleDelay, delay: 0, options: .curveLinear, animations: {
view.frame = box.box
}, completion: nil)
shouldRemoveView = false
newBoxes.remove(box)
}
}
if shouldRemoveView {
viewsToRemove.append(view)
}
}
viewsToRemove.forEach { [=10=].removeFromSuperview() }
for prediction in newBoxes {
createLabelAndBox(prediction: prediction)
}
accessibilityElements = subviews
}
func checkIfBoxIntersectCentre(prediction: FinderrItem) {
let centreX = center.x
let centreY = center.y
let maxX = prediction.box.maxX
let minX = prediction.box.midX
let maxY = prediction.box.maxY
let minY = prediction.box.minY
if centreX >= minX, centreX <= maxX, centreY >= minY, centreY <= maxY {
// NotificationCenter.default.post(name: .centreIntersectsWithBox, object: prediction.name)
}
}
func removeAllSubviews() {
UIView.animate(withDuration: throttleDelay, delay: 0, options: .curveLinear) {
for i in self.subviews {
i.frame = CGRect(x: i.frame.midX, y: i.frame.midY, width: 0, height: 0)
}
} completion: { _ in
self.subviews.forEach { [=10=].removeFromSuperview() }
}
}
static func getDistanceFromCloseBbox(touchAt p1: CGPoint, items: [FinderrItem]) -> Float {
var boxCenters = [Float]()
for i in items {
let distance = Float(sqrt(pow(i.box.midX - p1.x, 2) + pow(i.box.midY - p1.y, 2)))
boxCenters.append(distance)
}
boxCenters = boxCenters.sorted { [=10=] < }
return boxCenters.first ?? 0.0
}
static func sortBoxByeSize(_ items: [FinderrItem]) -> [FinderrItem] {
return items.sorted { i, j -> Bool in
let iC = sqrt(pow(i.box.height, 2) + pow(i.box.width, 2))
let jC = sqrt(pow(j.box.height, 2) + pow(j.box.width, 2))
return iC > jC
}
}
static func updateOrCreateNewBox(_ box1: CGRect, _ box2: CGRect) -> Bool {
let distance = sqrt(pow(box1.midX - box2.midX, 2) + pow(box1.midY - box2.midY, 2))
print(distance)
return distance < 50
}
static func distanceBetweenBoxes(_ box1: CGRect, _ box2: CGRect) -> Float {
return Float(sqrt(pow(box1.midX - box2.midX, 2) + pow(box1.midY - box2.midY, 2)))
}
func createLabelAndBox(prediction: FinderrItem) {
let bgRect = prediction.box
let boxView = BoxView(frame: bgRect ,itemName: "box")
addSubview(boxView)
}
@objc func handleTap(_ sender: UITapGestureRecognizer) {
// handling code
// NotificationCenter.default.post(name: .didDoubleTapOnObject, object: itemName)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
processTouches(touches, with: event)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
processTouches(touches, with: event)
}
func processTouches(_ touches: Set<UITouch>, with event: UIEvent?) {
if UIAccessibility.isVoiceOverRunning { return }
if predictedObjects.count == 0 { return }
if let touch = touches.first {
let hitView = hitTest(touch.location(in: self), with: event)
if hitView?.accessibilityLabel == lastBox { return }
lastBox = hitView?.accessibilityLabel
guard let boxView = hitView as? BoxView else {
return
}
UIView.animate(withDuration: 0.1, delay: 0, options: .curveLinear) {
boxView.backgroundColor = UIColor.yellow.withAlphaComponent(0.5)
} completion: { _ in
UIView.animate(withDuration: 0.1, delay: 0, options: .curveLinear, animations: {
boxView.backgroundColor = UIColor.clear
}, completion: nil)
}
}
}
}
class BoxView: UIView {
let id = UUID()
var itemName: String
init(frame: CGRect, itemName: String) {
self.itemName = itemName
super.init(frame: frame)
if !UIAccessibility.isVoiceOverRunning {
let singleDoubleTapRecognizer = SingleDoubleTapGestureRecognizer(
target: self,
singleAction: #selector(handleDoubleTapGesture),
doubleAction: #selector(handleDoubleTapGesture)
)
addGestureRecognizer(singleDoubleTapRecognizer)
}
}
@objc func navigateAction() -> Bool {
// NotificationCenter.default.post(name: .didDoubleTapOnObject, object: itemName)
return true
}
required init?(coder aDecoder: NSCoder) {
itemName = "error aDecoder"
super.init(coder: aDecoder)
}
@objc func handleDoubleTapGesture(_: UITapGestureRecognizer) {
// handling code
// NotificationCenter.default.post(name: .didDoubleTapOnObject, object: itemName)
}
}
public class SingleDoubleTapGestureRecognizer: UITapGestureRecognizer {
var targetDelegate: SingleDoubleTapGestureRecognizerDelegate
public var timeout: TimeInterval = 0.5 {
didSet {
targetDelegate.timeout = timeout
}
}
public init(target: AnyObject, singleAction: Selector, doubleAction: Selector) {
targetDelegate = SingleDoubleTapGestureRecognizerDelegate(target: target, singleAction: singleAction, doubleAction: doubleAction)
super.init(target: targetDelegate, action: #selector(targetDelegate.recognizerAction(recognizer:)))
}
}
class SingleDoubleTapGestureRecognizerDelegate: NSObject {
weak var target: AnyObject?
var singleAction: Selector
var doubleAction: Selector
var timeout: TimeInterval = 0.5
var tapCount = 0
var workItem: DispatchWorkItem?
init(target: AnyObject, singleAction: Selector, doubleAction: Selector) {
self.target = target
self.singleAction = singleAction
self.doubleAction = doubleAction
}
@objc func recognizerAction(recognizer: UITapGestureRecognizer) {
tapCount += 1
if tapCount == 1 {
workItem = DispatchWorkItem { [weak self] in
guard let weakSelf = self else { return }
weakSelf.target?.performSelector(onMainThread: weakSelf.singleAction, with: recognizer, waitUntilDone: false)
weakSelf.tapCount = 0
}
DispatchQueue.main.asyncAfter(
deadline: .now() + timeout,
execute: workItem!
)
} else {
workItem?.cancel()
DispatchQueue.main.async { [weak self] in
guard let weakSelf = self else { return }
weakSelf.target?.performSelector(onMainThread: weakSelf.doubleAction, with: recognizer, waitUntilDone: false)
weakSelf.tapCount = 0
}
}
}
}
class FinderrItem: Equatable, Hashable {
var box: CGRect
init(
box: CGRect)
{
self.box = box
}
func hash(into hasher: inout Hasher) {
hasher.combine(Float(box.origin.x))
hasher.combine(Float(box.origin.y))
hasher.combine(Float(box.width))
hasher.combine(Float(box.height))
hasher.combine(Float(box.minX))
hasher.combine(Float(box.maxY))
}
static func == (lhs: FinderrItem, rhs: FinderrItem) -> Bool {
return lhs.box == rhs.box
}
}
默认情况下,视图对象会在动画“运行中”时阻止用户交互。您需要使用一种“长格式”动画方法,并传入选项 .allowUserInteraction。像这样:
UIView.animate(withDuration: 0.5,
delay: 0.0,
options: .allowUserInteraction,
animations: {
myView.alpha = 0.5
})
我目前遇到的问题是触摸并不总是被正确识别, 我的目标是拥有3个手势,这3个手势是
- 用户可以点击一个视图,点击会被识别,
- 用户可以双击一个视图并且双击被识别,
- 用户可以在屏幕上移动手指,如果视图在屏幕下方 一个标签被识别。
但是我有多个视图,它们都在不断地动画化,它们可能会重叠, 目前,我按大小对视图进行排序,并将最小的视图放在较大的视图之上。 我通常会遇到一个问题,即在点击它们时无法识别 UIView。尤其是双击,滑动似乎大部分时间都很好,但整个体验非常不一致。
我目前用来解决问题的代码是:
class FinderrBoxView: UIView {
private var lastBox: String?
private var throttleDelay = 0.01
private var processQueue = DispatchQueue(label: "com.finderr.FinderrBoxView")
public var predictedObjects: [FinderrItem] = [] {
didSet {
predictedObjects.forEach { self.checkIfBoxIntersectCentre(prediction: [=10=]) }
drawBoxs(with: FinderrBoxView.sortBoxByeSize(predictedObjects))
setNeedsDisplay()
}
}
func drawBoxs(with predictions: [FinderrItem]) {
var newBoxes = Set(predictions)
var views = subviews.compactMap { [=10=] as? BoxView }
views = views.filter { view in
guard let closest = newBoxes.sorted(by: { x, y in
let xd = FinderrBoxView.distanceBetweenBoxes(view.frame, x.box)
let yd = FinderrBoxView.distanceBetweenBoxes(view.frame, y.box)
return xd < yd
}).first else { return false }
if FinderrBoxView.updateOrCreateNewBox(view.frame, closest.box)
{
newBoxes.remove(closest)
UIView.animate(withDuration: self.throttleDelay, delay: 0, options: .curveLinear, animations: {
view.frame = closest.box
}, completion: nil)
return false
} else {
return true
}
}
views.forEach { [=10=].removeFromSuperview() }
newBoxes.forEach { self.createLabelAndBox(prediction: [=10=]) }
accessibilityElements = subviews
}
func update(with predictions: [FinderrItem]) {
var newBoxes = Set(predictions)
var viewsToRemove = [UIView]()
for view in subviews {
var shouldRemoveView = true
for box in predictions {
if FinderrBoxView.updateOrCreateNewBox(view.frame, box.box)
{
UIView.animate(withDuration: throttleDelay, delay: 0, options: .curveLinear, animations: {
view.frame = box.box
}, completion: nil)
shouldRemoveView = false
newBoxes.remove(box)
}
}
if shouldRemoveView {
viewsToRemove.append(view)
}
}
viewsToRemove.forEach { [=10=].removeFromSuperview() }
for prediction in newBoxes {
createLabelAndBox(prediction: prediction)
}
accessibilityElements = subviews
}
func checkIfBoxIntersectCentre(prediction: FinderrItem) {
let centreX = center.x
let centreY = center.y
let maxX = prediction.box.maxX
let minX = prediction.box.midX
let maxY = prediction.box.maxY
let minY = prediction.box.minY
if centreX >= minX, centreX <= maxX, centreY >= minY, centreY <= maxY {
// NotificationCenter.default.post(name: .centreIntersectsWithBox, object: prediction.name)
}
}
func removeAllSubviews() {
UIView.animate(withDuration: throttleDelay, delay: 0, options: .curveLinear) {
for i in self.subviews {
i.frame = CGRect(x: i.frame.midX, y: i.frame.midY, width: 0, height: 0)
}
} completion: { _ in
self.subviews.forEach { [=10=].removeFromSuperview() }
}
}
static func getDistanceFromCloseBbox(touchAt p1: CGPoint, items: [FinderrItem]) -> Float {
var boxCenters = [Float]()
for i in items {
let distance = Float(sqrt(pow(i.box.midX - p1.x, 2) + pow(i.box.midY - p1.y, 2)))
boxCenters.append(distance)
}
boxCenters = boxCenters.sorted { [=10=] < }
return boxCenters.first ?? 0.0
}
static func sortBoxByeSize(_ items: [FinderrItem]) -> [FinderrItem] {
return items.sorted { i, j -> Bool in
let iC = sqrt(pow(i.box.height, 2) + pow(i.box.width, 2))
let jC = sqrt(pow(j.box.height, 2) + pow(j.box.width, 2))
return iC > jC
}
}
static func updateOrCreateNewBox(_ box1: CGRect, _ box2: CGRect) -> Bool {
let distance = sqrt(pow(box1.midX - box2.midX, 2) + pow(box1.midY - box2.midY, 2))
print(distance)
return distance < 50
}
static func distanceBetweenBoxes(_ box1: CGRect, _ box2: CGRect) -> Float {
return Float(sqrt(pow(box1.midX - box2.midX, 2) + pow(box1.midY - box2.midY, 2)))
}
func createLabelAndBox(prediction: FinderrItem) {
let bgRect = prediction.box
let boxView = BoxView(frame: bgRect ,itemName: "box")
addSubview(boxView)
}
@objc func handleTap(_ sender: UITapGestureRecognizer) {
// handling code
// NotificationCenter.default.post(name: .didDoubleTapOnObject, object: itemName)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
processTouches(touches, with: event)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
processTouches(touches, with: event)
}
func processTouches(_ touches: Set<UITouch>, with event: UIEvent?) {
if UIAccessibility.isVoiceOverRunning { return }
if predictedObjects.count == 0 { return }
if let touch = touches.first {
let hitView = hitTest(touch.location(in: self), with: event)
if hitView?.accessibilityLabel == lastBox { return }
lastBox = hitView?.accessibilityLabel
guard let boxView = hitView as? BoxView else {
return
}
UIView.animate(withDuration: 0.1, delay: 0, options: .curveLinear) {
boxView.backgroundColor = UIColor.yellow.withAlphaComponent(0.5)
} completion: { _ in
UIView.animate(withDuration: 0.1, delay: 0, options: .curveLinear, animations: {
boxView.backgroundColor = UIColor.clear
}, completion: nil)
}
}
}
}
class BoxView: UIView {
let id = UUID()
var itemName: String
init(frame: CGRect, itemName: String) {
self.itemName = itemName
super.init(frame: frame)
if !UIAccessibility.isVoiceOverRunning {
let singleDoubleTapRecognizer = SingleDoubleTapGestureRecognizer(
target: self,
singleAction: #selector(handleDoubleTapGesture),
doubleAction: #selector(handleDoubleTapGesture)
)
addGestureRecognizer(singleDoubleTapRecognizer)
}
}
@objc func navigateAction() -> Bool {
// NotificationCenter.default.post(name: .didDoubleTapOnObject, object: itemName)
return true
}
required init?(coder aDecoder: NSCoder) {
itemName = "error aDecoder"
super.init(coder: aDecoder)
}
@objc func handleDoubleTapGesture(_: UITapGestureRecognizer) {
// handling code
// NotificationCenter.default.post(name: .didDoubleTapOnObject, object: itemName)
}
}
public class SingleDoubleTapGestureRecognizer: UITapGestureRecognizer {
var targetDelegate: SingleDoubleTapGestureRecognizerDelegate
public var timeout: TimeInterval = 0.5 {
didSet {
targetDelegate.timeout = timeout
}
}
public init(target: AnyObject, singleAction: Selector, doubleAction: Selector) {
targetDelegate = SingleDoubleTapGestureRecognizerDelegate(target: target, singleAction: singleAction, doubleAction: doubleAction)
super.init(target: targetDelegate, action: #selector(targetDelegate.recognizerAction(recognizer:)))
}
}
class SingleDoubleTapGestureRecognizerDelegate: NSObject {
weak var target: AnyObject?
var singleAction: Selector
var doubleAction: Selector
var timeout: TimeInterval = 0.5
var tapCount = 0
var workItem: DispatchWorkItem?
init(target: AnyObject, singleAction: Selector, doubleAction: Selector) {
self.target = target
self.singleAction = singleAction
self.doubleAction = doubleAction
}
@objc func recognizerAction(recognizer: UITapGestureRecognizer) {
tapCount += 1
if tapCount == 1 {
workItem = DispatchWorkItem { [weak self] in
guard let weakSelf = self else { return }
weakSelf.target?.performSelector(onMainThread: weakSelf.singleAction, with: recognizer, waitUntilDone: false)
weakSelf.tapCount = 0
}
DispatchQueue.main.asyncAfter(
deadline: .now() + timeout,
execute: workItem!
)
} else {
workItem?.cancel()
DispatchQueue.main.async { [weak self] in
guard let weakSelf = self else { return }
weakSelf.target?.performSelector(onMainThread: weakSelf.doubleAction, with: recognizer, waitUntilDone: false)
weakSelf.tapCount = 0
}
}
}
}
class FinderrItem: Equatable, Hashable {
var box: CGRect
init(
box: CGRect)
{
self.box = box
}
func hash(into hasher: inout Hasher) {
hasher.combine(Float(box.origin.x))
hasher.combine(Float(box.origin.y))
hasher.combine(Float(box.width))
hasher.combine(Float(box.height))
hasher.combine(Float(box.minX))
hasher.combine(Float(box.maxY))
}
static func == (lhs: FinderrItem, rhs: FinderrItem) -> Bool {
return lhs.box == rhs.box
}
}
默认情况下,视图对象会在动画“运行中”时阻止用户交互。您需要使用一种“长格式”动画方法,并传入选项 .allowUserInteraction。像这样:
UIView.animate(withDuration: 0.5,
delay: 0.0,
options: .allowUserInteraction,
animations: {
myView.alpha = 0.5
})