【问题标题】:A way to add Instagram like layout guide with UIPinchGestureRecognizer UIRotationGestureRecognizer & UIPanGestureRecognizer?一种使用 UIPinchGestureRecognizer UIRotationGestureRecognizer 和 UIPanGestureRecognizer 添加类似 Instagram 的布局指南的方法?
【发布时间】: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 以便您获得更多详细信息。

标签: ios swift instagram uigesturerecognizer


【解决方案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。您能否提供一些有关您的约束的更多信息,或者您的代码的链接,以便我看一下?
  • 不用担心,我会在最后修复它,非常感谢您的工作,它真的对我有很大帮助
猜你喜欢
  • 2021-12-23
  • 2012-12-03
  • 2015-08-21
  • 2013-12-20
  • 1970-01-01
  • 1970-01-01
  • 2015-08-08
  • 2020-03-10
  • 1970-01-01
相关资源
最近更新 更多