一种使用 UIPinchGestureRecognizer UIRotationGestureRecognizer 和 UIPanGestureRecognizer 添加类似 Instagram 的布

Posted

技术标签:

【中文标题】一种使用 UIPinchGestureRecognizer UIRotationGestureRecognizer 和 UIPanGestureRecognizer 添加类似 Instagram 的布局指南的方法?【英文标题】:A way to add Instagram like layout guide with UIPinchGestureRecognizer UIRotationGestureRecognizer & UIPanGestureRecognizer? 【发布时间】:2018-12-29 05:56:13 【问题描述】:

我使用 UIPinchGestureRecognizer UIPanGestureRecognizer & UIRotationGestureRecognizerUILabel 来实现 Instagram 之类的缩放和拖动功能。现在我想显示布局指南,例如当UILabel 被拖到中心时,它应该显示布局指南,如下例所示。当您旋转UILabel 时,它还应该显示布局指南。

实现此功能的最佳且准确的方法是什么?

这是我已经拥有的

(图片取自this question@Skiddswarmik)

这是我用于简单拖动和缩放功能的代码(取自 this answer @lbsweek)

SnapGesture 类

import UIKit

/*
 usage:

    add gesture:
        yourObjToStoreMe.snapGesture = SnapGesture(view: your_view)
    remove gesture:
        yourObjToStoreMe.snapGesture = nil
    disable gesture:
        yourObjToStoreMe.snapGesture.isGestureEnabled = false
    advanced usage:
        view to receive gesture(usually superview) is different from view to be transformed,
        thus you can zoom the view even if it is too small to be touched.
        yourObjToStoreMe.snapGesture = SnapGesture(transformView: your_view_to_transform, gestureView: your_view_to_recieve_gesture)

 */

class SnapGesture: NSObject, UIGestureRecognizerDelegate 

    // MARK: - init and deinit
    convenience init(view: UIView) 
        self.init(transformView: view, gestureView: view)
    
    init(transformView: UIView, gestureView: UIView) 
        super.init()

        self.addGestures(v: gestureView)
        self.weakTransformView = transformView
    
    deinit 
        self.cleanGesture()
    

    // MARK: - private method
    private weak var weakGestureView: UIView?
    private weak var weakTransformView: UIView?

    private var panGesture: UIPanGestureRecognizer?
    private var pinchGesture: UIPinchGestureRecognizer?
    private var rotationGesture: UIRotationGestureRecognizer?

    private func addGestures(v: UIView) 

        panGesture = UIPanGestureRecognizer(target: self, action: #selector(panProcess(_:)))
        v.isUserInteractionEnabled = true
        panGesture?.delegate = self     // for simultaneous recog
        v.addGestureRecognizer(panGesture!)

        pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(pinchProcess(_:)))
        //view.isUserInteractionEnabled = true
        pinchGesture?.delegate = self   // for simultaneous recog
        v.addGestureRecognizer(pinchGesture!)

        rotationGesture = UIRotationGestureRecognizer(target: self, action: #selector(rotationProcess(_:)))
        rotationGesture?.delegate = self
        v.addGestureRecognizer(rotationGesture!)

        self.weakGestureView = v
    

    private func cleanGesture() 
        if let view = self.weakGestureView 
            //for recognizer in view.gestureRecognizers ?? [] 
            //    view.removeGestureRecognizer(recognizer)
            //
            if panGesture != nil 
                view.removeGestureRecognizer(panGesture!)
                panGesture = nil
            
            if pinchGesture != nil 
                view.removeGestureRecognizer(pinchGesture!)
                pinchGesture = nil
            
            if rotationGesture != nil 
                view.removeGestureRecognizer(rotationGesture!)
                rotationGesture = nil
            
        
        self.weakGestureView = nil
        self.weakTransformView = nil
    




    // MARK: - API

    private func setView(view:UIView?) 
        self.setTransformView(view, gestgureView: view)
    

    private func setTransformView(_ transformView: UIView?, gestgureView:UIView?) 
        self.cleanGesture()

        if let v = gestgureView  
            self.addGestures(v: v)
        
        self.weakTransformView = transformView
    

    open func resetViewPosition() 
        UIView.animate(withDuration: 0.4) 
            self.weakTransformView?.transform = CGAffineTransform.identity
        
    

    open var isGestureEnabled = true

    // MARK: - gesture handle

    // location will jump when finger number change
    private var initPanFingerNumber:Int = 1
    private var isPanFingerNumberChangedInThisSession = false
    private var lastPanPoint:CGPoint = CGPoint(x: 0, y: 0)
    @objc func panProcess(_ recognizer:UIPanGestureRecognizer) 
        if isGestureEnabled 
            //guard let view = recognizer.view else  return 
            guard let view = self.weakTransformView else  return 

            // init
            if recognizer.state == .began 
                lastPanPoint = recognizer.location(in: view)
                initPanFingerNumber = recognizer.numberOfTouches
                isPanFingerNumberChangedInThisSession = false
            

            // judge valid
            if recognizer.numberOfTouches != initPanFingerNumber 
                isPanFingerNumberChangedInThisSession = true
            
            if isPanFingerNumberChangedInThisSession 
                return
            

            // perform change
            let point = recognizer.location(in: view)
            view.transform = view.transform.translatedBy(x: point.x - lastPanPoint.x, y: point.y - lastPanPoint.y)
            lastPanPoint = recognizer.location(in: view)
        
    



    private var lastScale:CGFloat = 1.0
    private var lastPinchPoint:CGPoint = CGPoint(x: 0, y: 0)
    @objc func pinchProcess(_ recognizer:UIPinchGestureRecognizer) 
        if isGestureEnabled 
            guard let view = self.weakTransformView else  return 

            // init
            if recognizer.state == .began 
                lastScale = 1.0;
                lastPinchPoint = recognizer.location(in: view)
            

            // judge valid
            if recognizer.numberOfTouches < 2 
                lastPinchPoint = recognizer.location(in: view)
                return
            

            // Scale
            let scale = 1.0 - (lastScale - recognizer.scale);
            view.transform = view.transform.scaledBy(x: scale, y: scale)
            lastScale = recognizer.scale;

            // Translate
            let point = recognizer.location(in: view)
            view.transform = view.transform.translatedBy(x: point.x - lastPinchPoint.x, y: point.y - lastPinchPoint.y)
            lastPinchPoint = recognizer.location(in: view)
        
    


    @objc func rotationProcess(_ recognizer: UIRotationGestureRecognizer) 
        if isGestureEnabled 
            guard let view = self.weakTransformView else  return 

            view.transform = view.transform.rotated(by: recognizer.rotation)
            recognizer.rotation = 0
        
    


    //MARK:- UIGestureRecognizerDelegate Methods
    func gestureRecognizer(_: UIGestureRecognizer,
                           shouldRecognizeSimultaneouslyWith shouldRecognizeSimultaneouslyWithGestureRecognizer:UIGestureRecognizer) -> Bool 
        return true
    


UILabel中添加手势

// define 
var snapGesture: SnapGesture?

// add gesture
self.snapGesture = SnapGesture(view: self.myLabel!)

【问题讨论】:

您说希望在以下情况下出现指南: a) 文本在屏幕上水平居中。 b)当你旋转物体时?您能否添加更多关于 b) 的信息?谢谢 添加了 gif 以便您获得更多详细信息。 【参考方案1】:

您将在下面找到您所描述的课程的更新版本。

大部分更新的代码位于接近末尾的最后一部分(指南),但我已经稍微更新了您的 UIGestureRecognizer 操作以及您的主要 init 方法。

特点:

- 用于水平居中视图位置的垂直参考线。

- 用于将视图旋转居中于 0 度的水平参考线。

- 位置和旋转捕捉到具有公差值的参考线(snapToleranceDistancesnapToleranceAngle 属性)。

- 指南的动画外观/消失(animateGuidesguideAnimationDuration 属性)。

- 可以根据用例更改的指南视图(movementGuideViewrotationGuideView 属性)

class SnapGesture: NSObject, UIGestureRecognizerDelegate 

    // MARK: - init and deinit
    convenience init(view: UIView) 
        self.init(transformView: view, gestureView: view)
    

    init(transformView: UIView, gestureView: UIView) 
        super.init()

        self.addGestures(v: gestureView)
        self.weakTransformView = transformView

        guard let transformView = self.weakTransformView, let superview = transformView.superview else 
            return
        

        // This is required in order to be able to snap the view to center later on,
        // using the `tx` property of its transform.
        transformView.center = superview.center
    
    deinit 
        self.cleanGesture()
    

    // MARK: - private method
    private weak var weakGestureView: UIView?
    private weak var weakTransformView: UIView?

    private var panGesture: UIPanGestureRecognizer?
    private var pinchGesture: UIPinchGestureRecognizer?
    private var rotationGesture: UIRotationGestureRecognizer?

    private func addGestures(v: UIView) 

        panGesture = UIPanGestureRecognizer(target: self, action: #selector(panProcess(_:)))
        v.isUserInteractionEnabled = true
        panGesture?.delegate = self     // for simultaneous recog
        v.addGestureRecognizer(panGesture!)

        pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(pinchProcess(_:)))
        //view.isUserInteractionEnabled = true
        pinchGesture?.delegate = self   // for simultaneous recog
        v.addGestureRecognizer(pinchGesture!)

        rotationGesture = UIRotationGestureRecognizer(target: self, action: #selector(rotationProcess(_:)))
        rotationGesture?.delegate = self
        v.addGestureRecognizer(rotationGesture!)

        self.weakGestureView = v
    

    private func cleanGesture() 
        if let view = self.weakGestureView 
            //for recognizer in view.gestureRecognizers ?? [] 
            //    view.removeGestureRecognizer(recognizer)
            //
            if panGesture != nil 
                view.removeGestureRecognizer(panGesture!)
                panGesture = nil
            
            if pinchGesture != nil 
                view.removeGestureRecognizer(pinchGesture!)
                pinchGesture = nil
            
            if rotationGesture != nil 
                view.removeGestureRecognizer(rotationGesture!)
                rotationGesture = nil
            
        
        self.weakGestureView = nil
        self.weakTransformView = nil
    

    // MARK: - API

    private func setView(view:UIView?) 
        self.setTransformView(view, gestgureView: view)
    

    private func setTransformView(_ transformView: UIView?, gestgureView:UIView?) 
        self.cleanGesture()

        if let v = gestgureView  
            self.addGestures(v: v)
        
        self.weakTransformView = transformView
    

    open func resetViewPosition() 
        UIView.animate(withDuration: 0.4) 
            self.weakTransformView?.transform = CGAffineTransform.identity
        
    

    open var isGestureEnabled = true

    // MARK: - gesture handle

    // location will jump when finger number change
    private var initPanFingerNumber:Int = 1
    private var isPanFingerNumberChangedInThisSession = false
    private var lastPanPoint:CGPoint = CGPoint(x: 0, y: 0)
    @objc func panProcess(_ recognizer:UIPanGestureRecognizer) 
        guard isGestureEnabled, let view = self.weakTransformView else  return 

        // init
        if recognizer.state == .began 
            lastPanPoint = recognizer.location(in: view)
            initPanFingerNumber = recognizer.numberOfTouches
            isPanFingerNumberChangedInThisSession = false
        

        // judge valid
        if recognizer.numberOfTouches != initPanFingerNumber 
            isPanFingerNumberChangedInThisSession = true
        

        if isPanFingerNumberChangedInThisSession 
            hideGuidesOnGestureEnd(recognizer)
            return
        

        // perform change
        let point = recognizer.location(in: view)
        view.transform = view.transform.translatedBy(x: point.x - lastPanPoint.x, y: point.y - lastPanPoint.y)
        lastPanPoint = recognizer.location(in: view)

        updateMovementGuide()
        hideGuidesOnGestureEnd(recognizer)
    

    private var lastScale:CGFloat = 1.0
    private var lastPinchPoint:CGPoint = CGPoint(x: 0, y: 0)
    @objc func pinchProcess(_ recognizer:UIPinchGestureRecognizer) 
        guard isGestureEnabled, let view = self.weakTransformView else  return 

        // init
        if recognizer.state == .began 
            lastScale = 1.0;
            lastPinchPoint = recognizer.location(in: view)
        

        // judge valid
        if recognizer.numberOfTouches < 2 
            lastPinchPoint = recognizer.location(in: view)
            hideGuidesOnGestureEnd(recognizer)
            return
        

        // Scale
        let scale = 1.0 - (lastScale - recognizer.scale);
        view.transform = view.transform.scaledBy(x: scale, y: scale)
        lastScale = recognizer.scale;

        // Translate
        let point = recognizer.location(in: view)
        view.transform = view.transform.translatedBy(x: point.x - lastPinchPoint.x, y: point.y - lastPinchPoint.y)
        lastPinchPoint = recognizer.location(in: view)

        updateMovementGuide()
        hideGuidesOnGestureEnd(recognizer)
    


    @objc func rotationProcess(_ recognizer: UIRotationGestureRecognizer) 
        guard isGestureEnabled, let view = self.weakTransformView else  return 

        view.transform = view.transform.rotated(by: recognizer.rotation)
        recognizer.rotation = 0
        updateRotationGuide()
        hideGuidesOnGestureEnd(recognizer)
    

    func hideGuidesOnGestureEnd(_ recognizer: UIGestureRecognizer) 
        if recognizer.state == .ended 
            showMovementGuide(false)
            showRotationGuide(false)
        
    

    // MARK:- UIGestureRecognizerDelegate Methods
    func gestureRecognizer(_: UIGestureRecognizer,
                           shouldRecognizeSimultaneouslyWith shouldRecognizeSimultaneouslyWithGestureRecognizer:UIGestureRecognizer) -> Bool 
        return true
    

    // MARK:- Guides

    var animateGuides = true
    var guideAnimationDuration: TimeInterval = 0.3

    var snapToleranceDistance: CGFloat = 5 // pts
    var snapToleranceAngle: CGFloat = 1    // degrees
                        * CGFloat.pi / 180 // (converted to radians)

    var movementGuideView: UIView = 
        let view = UIView()
        view.backgroundColor = UIColor.blue
        return view
     ()

    var rotationGuideView: UIView = 
        let view = UIView()
        view.backgroundColor = UIColor.red
        return view
     ()

    // MARK: Movement guide and snap

    func updateMovementGuide() 
        guard let transformView = weakTransformView, let superview = transformView.superview else 
            return
        

        let transformX = transformView.frame.midX
        let superX = superview.bounds.midX

        if transformX - snapToleranceDistance < superX && transformX + snapToleranceDistance > superX 
            transformView.transform.tx = 0
            showMovementGuide(true)
         else 
            showMovementGuide(false)
        

        updateGuideFrames()
    

    var isShowingMovementGuide = false

    func showMovementGuide(_ shouldShow: Bool) 
        guard isShowingMovementGuide != shouldShow,
            let transformView = weakTransformView,
            let superview = transformView.superview
            else  return 

        superview.insertSubview(movementGuideView, belowSubview: transformView)
        movementGuideView.frame = CGRect(
            x: superview.frame.midX,
            y: 0,
            width: 1,
            height: superview.frame.size.height
        )

        let duration = animateGuides ? guideAnimationDuration : 0
        isShowingMovementGuide = shouldShow
        UIView.animate(withDuration: duration)  [weak self] in
            self?.movementGuideView.alpha = shouldShow ? 1 : 0
        
    

    // MARK: Rotation guide and snap

    func updateRotationGuide() 
        guard let transformView = weakTransformView else 
            return
        

        let angle = atan2(transformView.transform.b, transformView.transform.a)
        if angle > -snapToleranceAngle && angle < snapToleranceAngle 
            transformView.transform = transformView.transform.rotated(by: angle * -1)
            showRotationGuide(true)
         else 
            showRotationGuide(false)
        
    

    var isShowingRotationGuide = false

    func showRotationGuide(_ shouldShow: Bool) 
        guard isShowingRotationGuide != shouldShow,
            let transformView = weakTransformView,
            let superview = transformView.superview
            else  return 

        superview.insertSubview(rotationGuideView, belowSubview: transformView)

        let duration = animateGuides ? guideAnimationDuration : 0
        isShowingRotationGuide = shouldShow
        UIView.animate(withDuration: duration)  [weak self] in
            self?.rotationGuideView.alpha = shouldShow ? 1 : 0
        
    

    func updateGuideFrames() 
        guard let transformView = weakTransformView,
            let superview = transformView.superview
            else  return 

        rotationGuideView.frame = CGRect(
            x: 0,
            y: transformView.frame.midY,
            width: superview.frame.size.width,
            height: 1
        )
    

对于任何感兴趣的人,这里有一个 test project 使用这个类。

【讨论】:

非常感谢您的出色工作。我有更新问题,当它被击中时,有一些问题标签框架被改变了。 谢谢。嗯,你能举个例子让我看看吗? 嗯,我无法在此处重现...您可能在其中一个视图中使用 AutoLayout 吗? 我已经尝试通过将标签视图的水平和垂直中心约束添加到其超级视图的中心,它似乎在这里工作正常,一直回到 ios 8.0。您能否提供一些有关您的约束的更多信息,或者您的代码的链接,以便我看一下? 不用担心,我会在最后修复它,非常感谢您的工作,它真的对我有很大帮助

以上是关于一种使用 UIPinchGestureRecognizer UIRotationGestureRecognizer 和 UIPanGestureRecognizer 添加类似 Instagram 的布的主要内容,如果未能解决你的问题,请参考以下文章

使用一种编译器创建的静态 c 库是不是与另一种兼容?

在设计师中使用一种资源,在生产中使用一种资源。

在 Typescript/React 中使用一种或另一种接口类型

jQuery Datepicker:向用户显示一种日期格式,在后台使用另一种日期格式

如何使用算法将一种类型的列表映射到现代 C++ 中的另一种类型的列表

xctest 如何在另一种测试方法中使用在一种测试方法中捕获的数据