Pinch, Pan, and Rotate Text Simultaneously like Snapchat [SWIFT 3]

前端 未结 2 1642
情深已故
情深已故 2020-12-24 10:00

I\'m trying to make a TextView that you can move around like snapchat does. I have made something similar to it, although when you try and scale while rotates, it tends to s

相关标签:
2条回答
  • 2020-12-24 10:09

    I write a class for easy to use.

    Basic usage:

    // define 
    var snapGesture: SnapGesture?
    
    // add gesture
    self.snapGesture = SnapGesture(view: self.testView!)
    
    // remove gesture
    self.snapGesture = nil
    

    Advanced usage, for scenario that the view receiving gesture is background view:

    // add gesture
    self.snapGesture = SnapGesture(transformView: self.testView!, gestureView: self.view)
    
    // remove gesture
    self.snapGesture = nil
    

    class:

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

    Now, you reach this point, please continue. as you know, the gesture in superView may eat event of the subView, and in snapchat example, the gesture will eat event of the toolbar, we need prevent any gesture recognition from superview if we are touching the toolbar.

    The idea is to add a pseduo customized gesture to the toolbar, so any gesture will be prevented to superview, and this pseduo gesture do nothing but deliver the gesture or event to subviews or its own view.

    Here, I also write a class for easy to use.

    usage:

       toolbarView.addGestureRecognizer(SnapBlockGestureRecognizer)
    

    implementation:

    import UIKit
    
    class SnapBlockGestureRecognizer: UIGestureRecognizer {
    
        init() {
            //self.init(target: self, action: #selector(__dummyAction))
            super.init(target: nil, action: nil)
    
            self.addTarget(self, action: #selector(__dummyAction))
            self.cancelsTouchesInView = false
        }
    
        override init(target: Any?, action: Selector?) {
            super.init(target: target, action: action)
    
            self.cancelsTouchesInView = false
        }
    
        override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
            if self.state == .possible {
                self.state = .began
            }
        }
    
        override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
        }
    
        override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
            self.state = .recognized
        }
    
        override func canBePrevented(by preventingGestureRecognizer: UIGestureRecognizer) -> Bool {
            return self.isGestureRecognizerAllowed(gr:preventingGestureRecognizer)
        }
    
    
        override func canPrevent(_ preventedGestureRecognizer: UIGestureRecognizer) -> Bool {
            return !(self.isGestureRecognizerAllowed(gr: preventedGestureRecognizer))
        }
    
        override func shouldBeRequiredToFail(by otherGestureRecognizer: UIGestureRecognizer) -> Bool {
            return !(self.isGestureRecognizerAllowed(gr: otherGestureRecognizer))
        }
    
        override func shouldRequireFailure(of otherGestureRecognizer: UIGestureRecognizer) -> Bool {
            return false
        }
    
        func isGestureRecognizerAllowed(gr: UIGestureRecognizer) -> Bool {
            return gr.view!.isDescendant(of: self.view!)
        }
    
        @objc func __dummyAction() {
            // do nothing
            // print("dummyAction")
        }
    }
    
    0 讨论(0)
  • 2020-12-24 10:14

    By default, after one gesture recognizer on a view starts handling the gesture, other recognizers are ignored. This behaviour can be controlled by overriding the method (for Swift):

    gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:)

    to return true. The default implementation returns false. To override the function just add your implementation, returning true, to your ViewController source code file.

    Here is some sample Swift code (see https://developer.apple.com/documentation/uikit/uigesturerecognizerdelegate/1624208-gesturerecognizer?changes=_7):

    class ViewController: UIViewController,UIGestureRecognizerDelegate {
    
        @IBOutlet var textField: UITextField!
    
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view, typically from a nib.
    
            //add pan gesture
            let gestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
            gestureRecognizer.delegate = self
            textField.addGestureRecognizer(gestureRecognizer)
    
            //Enable multiple touch and user interaction for textfield
            textField.isUserInteractionEnabled = true
            textField.isMultipleTouchEnabled = true
    
            //add pinch gesture
            let pinchGesture = UIPinchGestureRecognizer(target: self, action:#selector(pinchRecognized(pinch:)))
            pinchGesture.delegate = self
            textField.addGestureRecognizer(pinchGesture)
    
            //add rotate gesture.
            let rotate = UIRotationGestureRecognizer.init(target: self, action: #selector(handleRotate(recognizer:)))
            rotate.delegate = self
            textField.addGestureRecognizer(rotate)
    
    
        }
    
        func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
            if gestureRecognizer.state == .began || gestureRecognizer.state == .changed {
    
                let translation = gestureRecognizer.translation(in: self.view)
                // note: 'view' is optional and need to be unwrapped
                gestureRecognizer.view!.center = CGPoint(x: gestureRecognizer.view!.center.x + translation.x, y: gestureRecognizer.view!.center.y + translation.y)
                gestureRecognizer.setTranslation(CGPoint.zero, in: self.view)
            }
    
        }
    
        func pinchRecognized(pinch: UIPinchGestureRecognizer) {
    
            if let view = pinch.view {
                view.transform = view.transform.scaledBy(x: pinch.scale, y: pinch.scale)
                pinch.scale = 1
            }
        }
    
        func handleRotate(recognizer : UIRotationGestureRecognizer) {
            if let view = recognizer.view {
                view.transform = view.transform.rotated(by: recognizer.rotation)
                recognizer.rotation = 0
            }
        }
    
        //MARK:- UIGestureRecognizerDelegate Methods
        func gestureRecognizer(_: UIGestureRecognizer,
            shouldRecognizeSimultaneouslyWith shouldRecognizeSimultaneouslyWithGestureRecognizer:UIGestureRecognizer) -> Bool {
            return true
        }
    }
    

    In Objective C the overriding function function is this (see https://developer.apple.com/documentation/uikit/uigesturerecognizerdelegate/1624208-gesturerecognizer?language=objc):

    -(BOOL)gestureRecognizer:(UIGestureRecognizer*)aR1 shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)aR2
        {
        return YES;
        }
    
    0 讨论(0)
提交回复
热议问题