在动画移动对象(UIView)上未检测到点击手势
Tap gesture not detect on animating-moving object(UIView)
我尝试使用动画简单地移动 UIView
。这是我的示例代码。
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.tap1(gesture:)))
tapGesture.numberOfTapsRequired = 1
tapGesture.numberOfTouchesRequired = 1
self.tempView.addGestureRecognizer(tapGesture)
self.tempView.frame = CGRect(x: self.tempView.frame.origin.x, y: 0, width: self.tempView.frame.width, height: self.tempView.frame.height)
UIView.animate(withDuration: 15, delay: 0.0, options: [.allowUserInteraction, .allowAnimatedContent], animations: {
self.tempView.frame = CGRect(x: self.tempView.frame.origin.x, y: UIScreen.main.bounds.height - 100, width: self.tempView.frame.width, height: self.tempView.frame.height)
}) { (_) in
}
这里提一下点击手势的方法:
@objc func tap1(gesture: UITapGestureRecognizer) {
print("tap with object")
}
问题是,当 UIView
正在动画并从一个位置移动到另一个位置时,此时点击手势不起作用。
虽然视觉视图改变了位置,但它并没有这样做。如果您在动画内部检查它的帧,则该帧将始终是您的结束帧。所以你可以在它的结束位置的任何地方点击它。 (这还假设您在制作动画时启用了用户交互)。
您的案例需要的是使用计时器手动移动视图。这需要更多的努力,但很容易做到。计时器应该以一些不错的 FPS(例如 60)触发,并且每次触发帧时都应更新到其插值位置。
要进行插值,您可以简单地按组件进行:
func interpolateRect(from: CGRect, to: CGRect, scale: CGFloat) -> CGRect {
return CGRect(x: from.minX + (to.minX - from.minX) * scale, y: from.minY + (to.minY - from.minY) * scale, width: from.width + (to.width - from.width) * scale, height: from.height + (to.height - from.height) * scale)
}
在大多数情况下,比例自然必须介于 0 和 1 之间。
现在您需要有一个方法来使用计时器制作动画,例如:
func animateFrame(to frame: CGRect, animationDuration duration: TimeInterval) {
self.animationStartFrame = tempView.frame // Assign new values
self.animationEndFrame = frame // Assign new values
self.animationStartTime = Date() // Assign new values
self.currentAnimationTimer.invalidate() // Remove current timer if any
self.currentAnimationTimer = Timer.scheduledTimer(withTimeInterval: 1.0/60.0, repeats: true) { timer in
let timeElapsed = Date().timeIntervalSince(self.animationStartTime)
let scale = timeElapsed/duration
self.tempView.frame = self.interpolateRect(from: self.animationStartFrame, to: self.animationEndFrame, scale: CGFloat(max(0.0, min(scale, 1.0))))
if(scale >= 1.0) { // Animation ended. Remove timer
timer.invalidate()
self.currentAnimationTimer = nil
}
}
}
公平地说,这段代码仍然可以减少,因为我们正在使用带块的计时器:
func animateFrame(to frame: CGRect, animationDuration duration: TimeInterval) {
let startFrame = tempView.frame
let endFrame = frame
let animationStartTime = Date()
self.currentAnimationTimer.invalidate() // Remove current timer if any
self.currentAnimationTimer = Timer.scheduledTimer(withTimeInterval: 1.0/60.0, repeats: true) { timer in
let timeElapsed = Date().timeIntervalSince(animationStartTime)
let scale = timeElapsed/duration
self.tempView.frame = self.interpolateRect(from: startFrame, to: endFrame, scale: CGFloat(max(0.0, min(scale, 1.0))))
if(scale >= 1.0) { // Animation ended. Remove timer
timer.invalidate()
if(timer === self.currentAnimationTimer) {
self.currentAnimationTimer = nil // Remove reference only if this is the current timer
}
}
}
}
这样我们就不需要在 class 中存储那么多值,而是将所有值都保存在方法中。
提到如何进行缓动可能会很有趣。诀窍只是操纵我们的scale
。考虑这样的事情:
func easeInScaleFromLinearScale(_ scale: CGFloat, factor: CGFloat = 0.2) -> CGFloat {
return pow(scale, 1.0+factor)
}
func easeOutScaleFromLinearScale(_ scale: CGFloat, factor: CGFloat = 0.2) -> CGFloat {
return pow(scale, 1.0/(1.0+factor))
}
现在我们需要做的就是在插值时使用它:
self.tempView.frame = self.interpolateRect(from: startFrame, to: endFrame, scale: easeInScaleFromLinearScale(CGFloat(max(0.0, min(scale, 1.0)))))
修改因子会改变效果。系数越高,效果越好。使用零系数意味着线性曲线。
有了它,您几乎可以制作任何类型的动画。唯一的规则是您的函数必须遵循 f(0) = 0
和 f(1) = 1
。这意味着它从开始位置开始到结束位置结束。
虽然有些曲线有点棘手。 EaseInOut
看似简单,实则不然。我们可能希望在 [-PI/2, PI/2]
范围内实现类似 sin
的东西。然后用线性函数平衡这个函数。这是我发现的实现之一:
return (sin((inputScale-0.5) * .pi)+1.0)/2.0 * (magnitude) + (inputScale * (1.0-magnitude))
如果你尝试一些 "online graphing calculator" 来找到你的方程式,然后将结果转换成你的函数,这会很有帮助。
我尝试使用动画简单地移动 UIView
。这是我的示例代码。
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.tap1(gesture:)))
tapGesture.numberOfTapsRequired = 1
tapGesture.numberOfTouchesRequired = 1
self.tempView.addGestureRecognizer(tapGesture)
self.tempView.frame = CGRect(x: self.tempView.frame.origin.x, y: 0, width: self.tempView.frame.width, height: self.tempView.frame.height)
UIView.animate(withDuration: 15, delay: 0.0, options: [.allowUserInteraction, .allowAnimatedContent], animations: {
self.tempView.frame = CGRect(x: self.tempView.frame.origin.x, y: UIScreen.main.bounds.height - 100, width: self.tempView.frame.width, height: self.tempView.frame.height)
}) { (_) in
}
这里提一下点击手势的方法:
@objc func tap1(gesture: UITapGestureRecognizer) {
print("tap with object")
}
问题是,当 UIView
正在动画并从一个位置移动到另一个位置时,此时点击手势不起作用。
虽然视觉视图改变了位置,但它并没有这样做。如果您在动画内部检查它的帧,则该帧将始终是您的结束帧。所以你可以在它的结束位置的任何地方点击它。 (这还假设您在制作动画时启用了用户交互)。
您的案例需要的是使用计时器手动移动视图。这需要更多的努力,但很容易做到。计时器应该以一些不错的 FPS(例如 60)触发,并且每次触发帧时都应更新到其插值位置。
要进行插值,您可以简单地按组件进行:
func interpolateRect(from: CGRect, to: CGRect, scale: CGFloat) -> CGRect {
return CGRect(x: from.minX + (to.minX - from.minX) * scale, y: from.minY + (to.minY - from.minY) * scale, width: from.width + (to.width - from.width) * scale, height: from.height + (to.height - from.height) * scale)
}
在大多数情况下,比例自然必须介于 0 和 1 之间。
现在您需要有一个方法来使用计时器制作动画,例如:
func animateFrame(to frame: CGRect, animationDuration duration: TimeInterval) {
self.animationStartFrame = tempView.frame // Assign new values
self.animationEndFrame = frame // Assign new values
self.animationStartTime = Date() // Assign new values
self.currentAnimationTimer.invalidate() // Remove current timer if any
self.currentAnimationTimer = Timer.scheduledTimer(withTimeInterval: 1.0/60.0, repeats: true) { timer in
let timeElapsed = Date().timeIntervalSince(self.animationStartTime)
let scale = timeElapsed/duration
self.tempView.frame = self.interpolateRect(from: self.animationStartFrame, to: self.animationEndFrame, scale: CGFloat(max(0.0, min(scale, 1.0))))
if(scale >= 1.0) { // Animation ended. Remove timer
timer.invalidate()
self.currentAnimationTimer = nil
}
}
}
公平地说,这段代码仍然可以减少,因为我们正在使用带块的计时器:
func animateFrame(to frame: CGRect, animationDuration duration: TimeInterval) {
let startFrame = tempView.frame
let endFrame = frame
let animationStartTime = Date()
self.currentAnimationTimer.invalidate() // Remove current timer if any
self.currentAnimationTimer = Timer.scheduledTimer(withTimeInterval: 1.0/60.0, repeats: true) { timer in
let timeElapsed = Date().timeIntervalSince(animationStartTime)
let scale = timeElapsed/duration
self.tempView.frame = self.interpolateRect(from: startFrame, to: endFrame, scale: CGFloat(max(0.0, min(scale, 1.0))))
if(scale >= 1.0) { // Animation ended. Remove timer
timer.invalidate()
if(timer === self.currentAnimationTimer) {
self.currentAnimationTimer = nil // Remove reference only if this is the current timer
}
}
}
}
这样我们就不需要在 class 中存储那么多值,而是将所有值都保存在方法中。
提到如何进行缓动可能会很有趣。诀窍只是操纵我们的scale
。考虑这样的事情:
func easeInScaleFromLinearScale(_ scale: CGFloat, factor: CGFloat = 0.2) -> CGFloat {
return pow(scale, 1.0+factor)
}
func easeOutScaleFromLinearScale(_ scale: CGFloat, factor: CGFloat = 0.2) -> CGFloat {
return pow(scale, 1.0/(1.0+factor))
}
现在我们需要做的就是在插值时使用它:
self.tempView.frame = self.interpolateRect(from: startFrame, to: endFrame, scale: easeInScaleFromLinearScale(CGFloat(max(0.0, min(scale, 1.0)))))
修改因子会改变效果。系数越高,效果越好。使用零系数意味着线性曲线。
有了它,您几乎可以制作任何类型的动画。唯一的规则是您的函数必须遵循 f(0) = 0
和 f(1) = 1
。这意味着它从开始位置开始到结束位置结束。
虽然有些曲线有点棘手。 EaseInOut
看似简单,实则不然。我们可能希望在 [-PI/2, PI/2]
范围内实现类似 sin
的东西。然后用线性函数平衡这个函数。这是我发现的实现之一:
return (sin((inputScale-0.5) * .pi)+1.0)/2.0 * (magnitude) + (inputScale * (1.0-magnitude))
如果你尝试一些 "online graphing calculator" 来找到你的方程式,然后将结果转换成你的函数,这会很有帮助。