简体   繁体   中英

Tap gesture not detect on animating-moving object(UIView)

I try to simply move the UIView using animation. Here is my sample code.

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
      
    }

Here I mention tap gesture method:

@objc func tap1(gesture: UITapGestureRecognizer) {
    print("tap with object")
}

The problem is, when UIView is animating and moves from one position to another position, at that time tap gesture is not working.

Although visually view changes position it does not do so. If you check it's frame while inside animation the frame will always be whatever is your end frame. So you can tap it wherever its end position will be. (This also assumes that you have enabled user interaction while animating which you did).

What you need for your case is to manually move your view with timer. It takes a bit more effort but can be easily doable. A timer should trigger with some nice FPS like 60 and each time it fires a frame should be updated to its interpolated position.

To do interpolation you can simply do it by component:

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)
}

Scale naturally has to be between 0 and 1 for most cases.

Now you need to have a method to animate with timer like:

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
        }
     }
 }

To be fair this code can still be reduced since we are using timer with block:

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
            }
        }
     }
 }

This way we don't need to store so many values in our class but keep the all inside the method.

It may be interesting to mention how to do easing. The trick is only to manipulate our scale . Consider something like this:

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))
}

Now all we need to do is use it when interpolating:

self.tempView.frame = self.interpolateRect(from: startFrame, to: endFrame, scale: easeInScaleFromLinearScale(CGFloat(max(0.0, min(scale, 1.0)))))

Modifying the factor will change the effect. Higher the factor, higher effect. And using a factor at zero would mean a linear curve.

With this you can do pretty much any type of animation. The only rule is that your function must follow f(0) = 0 and f(1) = 1 . This means it starts at starting position and end at ending position.

Some curves are a bit more tricky though. EaseInOut might seem simple but it is not. We would probably want to implement something like sin in range [-PI/2, PI/2] . Then balance this function with linear function. This is one of my implementations I found:

return (sin((inputScale-0.5) * .pi)+1.0)/2.0 * (magnitude) + (inputScale * (1.0-magnitude))

It helps if you play around with some "online graphing calculator" to find your equations and then convert results into your functions.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM