如何在动画过程中检测 UIImageView 上的点击?

How to detect a tap on an UIImageView while it is in the process of animation?

我尝试检测 UIImageView 在动画过程中的点击,但它不起作用。

我做什么(swift 4):

通过 StoryBoard 添加了 UIImageView:

@IBOutlet weak var myImageView: UIImageView!

做动画:

override func viewWillAppear (_ animated: Bool) {
        super.viewWillAppear (animated)

        myImageView.center.y + = view.bounds.height

    }


    override func viewDidAppear (_ animated: Bool) {
        super.viewDidAppear (animated)

        UIView.animate (withDuration: 10, delay: 0, options: [.repeat, .autoreverse, .allowUserInteraction], animations: {
            self.myImageView.center.y - = self.view.bounds.height
        })
    }

尝试检测点击:

override func viewDidLoad () {
        super.viewDidLoad ()

        let gestureSwift2AndHigher = UITapGestureRecognizer (target: self, action: #selector (self.actionUITapGestureRecognizer))
        myImageView.isUserInteractionEnabled = true
        myImageView.addGestureRecognizer (gestureSwift2AndHigher)

    }


    @objc func actionUITapGestureRecognizer () {

        print ("actionUITapGestureRecognizer - works!")

    }

请在对问题进行投票之前,确保这些问题没有正常公式化的答案,初学者可以理解并且写在 swift 以上版本 2 中,所以我不能将它们应用到我的案例.

研究这个问题,我意识到有必要调整框架!?但这对我来说仍然很难。请告诉我,我需要在下面的代码中添加或更改什么。

感谢您的帮助。

class ExampleViewController: UIViewController {

    @IBOutlet weak var myImageView: UIImageView!

    override func viewDidLoad() {
        super.viewDidLoad()

        // action by tap
        let gestureSwift2AndHigher = UITapGestureRecognizer(target: self, action:  #selector (self.actionUITapGestureRecognizer))
        myImageView.isUserInteractionEnabled = true
        myImageView.addGestureRecognizer(gestureSwift2AndHigher)

    }

    // action by tap
    @objc func actionUITapGestureRecognizer (){

        print("actionUITapGestureRecognizer - works!") // !!! IT IS DOES NOT WORK !!!

    }

    // hide UIImageView before appear
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        myImageView.center.y += view.bounds.height

    }

    // show UIImageView after appear with animation
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        UIView.animate(withDuration: 10, delay: 0, options: [.repeat, .autoreverse, .allowUserInteraction], animations: {
            self.myImageView.center.y -= self.view.bounds.height
        })
    }
}

你不能使用 UITapGestureRecognizer 做你想做的事,因为它使用基于 frame 的检测并通过检查它的框架来检测触摸是否在你的视图内..

这样做的问题是,动画甚至在动画开始之前就已经设置了视图的最终帧。然后它会在再次显示您的真实视图之前将视图的快照设置为动画。

因此,如果您要点击动画的最终位置,您会看到您的点击手势被击中,即使您的视图似乎还没有出现。您可以在下图中看到:

https://i.imgur.com/Wl9WRfV.png

(左侧是视图层次检查器)..(右侧是模拟器动画)。

要解决点击问题,您可以尝试一些粗略的代码(但有效):

import UIKit

protocol AnimationTouchDelegate {
    func onViewTapped(view: UIView)
}

protocol AniTouchable {
    var animationTouchDelegate: AnimationTouchDelegate? {
        get
        set
    }
}

extension UIView : AniTouchable {
    private struct Internal {
        static var key: String = "AniTouchable"
    }

    private class func getAllSubviews<T: UIView>(view: UIView) -> [T] {
        return view.subviews.flatMap { subView -> [T] in
            var result = getAllSubviews(view: subView) as [T]
            if let view = subView as? T {
                result.append(view)
            }
            return result
        }
    }

    private func getAllSubviews<T: UIView>() -> [T] {
        return UIView.getAllSubviews(view: self) as [T]
    }

    var animationTouchDelegate: AnimationTouchDelegate? {
        get {
            return objc_getAssociatedObject(self, &Internal.key) as? AnimationTouchDelegate
        }

        set {
            objc_setAssociatedObject(self, &Internal.key, newValue, .OBJC_ASSOCIATION_ASSIGN)
        }
    }


    override open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touch = touches.first else { return }
        let touchLocation = touch.location(in: self)

        var didTouch: Bool = false
        let views = self.getAllSubviews() as [UIView]
        for view in views {
            if view.layer.presentation()?.hitTest(touchLocation) != nil {
                if let delegate = view.animationTouchDelegate {
                    didTouch = true
                    delegate.onViewTapped(view: view)
                }
            }
        }

        if !didTouch {
            super.touchesBegan(touches, with: event)
        }
    }
}

class ViewController : UIViewController, AnimationTouchDelegate {

    @IBOutlet weak var myImageView: UIImageView!

    deinit {
        self.myImageView.animationTouchDelegate = nil
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        self.myImageView.isUserInteractionEnabled = true
        self.myImageView.animationTouchDelegate = self
    }

    func onViewTapped(view: UIView) {
        print("Works!")
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        myImageView.center.y += view.bounds.height
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        UIView.animate(withDuration: 5, delay: 0, options: [.repeat, .autoreverse, .allowUserInteraction], animations: {
            self.myImageView.center.y -= self.view.bounds.height
        })
    }
}

它的工作原理是覆盖 UIView 上的 touchesBegan,然后检查是否有任何触摸落在该视图内。

一个更好的方法 就是在 UIViewController 中进行..

import UIKit

protocol AnimationTouchDelegate : class {
    func onViewTapped(view: UIView)
}

extension UIView {
    private class func getAllSubviews<T: UIView>(view: UIView) -> [T] {
        return view.subviews.flatMap { subView -> [T] in
            var result = getAllSubviews(view: subView) as [T]
            if let view = subView as? T {
                result.append(view)
            }
            return result
        }
    }

    func getAllSubviews<T: UIView>() -> [T] {
        return UIView.getAllSubviews(view: self) as [T]
    }
}

class ViewController : UIViewController, AnimationTouchDelegate {

    @IBOutlet weak var myImageView: UIImageView!

    override func viewDidLoad() {
        super.viewDidLoad()

        self.myImageView.isUserInteractionEnabled = true
    }

    func onViewTapped(view: UIView) {
        print("Works!")
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        myImageView.center.y += view.bounds.height
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        UIView.animate(withDuration: 5, delay: 0, options: [.repeat, .autoreverse, .allowUserInteraction], animations: {
            self.myImageView.center.y -= self.view.bounds.height
        })
    }

    override open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touch = touches.first else { return }
        let touchLocation = touch.location(in: self.view)

        var didTouch: Bool = false
        for view in self.view.getAllSubviews() {
            if view.isUserInteractionEnabled && !view.isHidden && view.alpha > 0.0 && view.layer.presentation()?.hitTest(touchLocation) != nil {

                didTouch = true
                self.onViewTapped(view: view)
            }
        }

        if !didTouch {
            super.touchesBegan(touches, with: event)
        }
    }
}

要检测移动(动画)视图上的触摸,只需使用表示层覆盖 hitTest

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    
    return (layer.presentation()!.frame)
                      .contains(self.convert(point, to: superview!)) ? self : nil
}

在手头的例子中

  1. 它适用于所有手势识别器

  2. 请勿在视图控制器级别

    修改任何框架或任何其他内容
  3. 简单地继承视图本身,在上面添加覆盖

自然不要忘记,如果您想在抓取项目后停止动画,请(在您的视图控制器中)使用 yourPropertyAnimator?.stopAnimation(true) , yourPropertyAnimator = nil