iOS Review | UIGestureRecognizers手勢

UIGestureRecognizer共有8種:

  • UITapGestureRecognizer
  • UIPanGestureRecognizerpan
  • UIScreenEdgePanRecognizer
  • UIPinchGestureRecognizer
  • UIRotationGestureRecognizer
  • UILongPressGestureRecognizer
  • UISwipeGestureRecognizer
  • UIGestureRecognizer即自定義手勢

其用法大同小異,比較常用的主要有tap和pan,主要使用recognizer.locationrecognizer.view屬性。

Storyboard上添加手勢

UIPanGestureRecognizer

利用UIPanGesgtureRecognizer讓view跟隨touch移動,通常有兩種處理邏輯。

  • 第一種利用recognizer.location
var offset: CGSize! // 記住一開始的touch point距離center的偏移值

// 事件回調(diào)
@IBAction func handlePan(recognizer : UIPanGestureRecognizer) {
    let location = recognizer.location(in: view)
    guard let view = recognizer.view else {
        return
    }
    switch recognizer.state {
        case .began:
// 計算并存儲偏移值
            offset = CGSize(width: view.center.x - location.x, height: view.center.y - location.y)
// 偏移值 + location值即可做到跟隨移動
        case .changed:
            view.center = CGPoint(x: offset.width + location.x, y: offset.height + location.y)
        default: break
    }
}
recognizer.location

這種方式雖然理解起來簡單,但處理邏輯稍微繁瑣點,不推薦使用。

  • 第一種利用recognizer.translation
@IBAction func handlePan(recognizer : UIPanGestureRecognizer) { 
// pan的移動偏移量--相對began時的點
    let translation = recognizer.translation(in: view)
    if let view = recognizer.view {
        view.center = CGPoint(x: view.center.x + translation.x, y: view.center.y + translation.y)
    }
// 在移動changed時,重置pan的上一次移動點為zero
    recognizer.setTranslation(.zero, in: view)
}

這種方式雖然使用簡單,但要注意每次recognizer.setTranslation(.zero)歸零,不然,一下子就讓view移出了屏幕,因為translation每次就compound疊加的。

  • pan手勢滑動的加速度velocity
if recognizer.state == .ended {
// 加速度
    let velocity = recognizer.velocity(in: self.view)
    let magnitude = sqrt(velocity.x * velocity.x + velocity.y * velocity.y)
    let slideMultiplier = magnitude / 200
    let slideFactor = 0.1 * slideMultiplier
    
// 最終點
    var finalPoint = CGPoint(x: view.center.x + (velocity.x * slideFactor), y: view.center.y + (velocity.y * slideFactor))
    let halfWidth = panView.bounds.width / 2
    let halfHeight = panView.bounds.height / 2
    finalPoint.x = min(self.view.bounds.width - halfWidth, max(halfWidth, finalPoint.x))
    finalPoint.y = min(self.view.bounds.height - halfHeight, max(halfHeight, finalPoint.y))
    
// 動畫
    UIView.animate(withDuration: Double(slideFactor * 2), delay: 0, options: .curveEaseInOut, animations: {
        panView.center = finalPoint
    }, completion: nil)
}
加速度動畫

UIPinchGestureRecognizer

  • 使用比較簡單,利用recognizer.scale值即可transform要scale的view,但注意scale值也是連續(xù)變化的,注意隨時將recognizer.scale歸零。
  @IBAction func handlePinch(recognizer : UIPinchGestureRecognizer) {
    guard let pinchView = recognizer.view else {
        return
    }
    let scale = recognizer.scale
    pinchView.transform = pinchView.transform.scaledBy(x: scale, y: scale)
    recognizer.scale = 1 // 歸零
  }

UIRotationGestureRecognizer

  • 使用和UIPinchGestureRecognizer一樣,利用recognizer.rotation值即可transform要rotate的view,但注意rotation值也是連續(xù)變化的,注意隨時將recognizer.rotation歸零。
  @IBAction func handleRotate(recognizer : UIRotationGestureRecognizer) {
    guard let rotateView = recognizer.view else {
        return
    }
    let rotation = recognizer.rotation
    rotateView.transform = rotateView.transform.rotated(by: rotation)
    recognizer.rotation = 0
  }

Simultaneous Gesture Recognizers

  • 一般情況下,每個手勢只能被單獨使用,并不能在執(zhí)行一個手勢如rotation的同時執(zhí)行scale手勢,但可以設置UIGestureRecognizer的delegate,來配置是否允許手勢同時執(zhí)行。
extension ViewController: UIGestureRecognizerDelegate{
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return true
    }
}

UITapGestureRecognizer

  • 這個手勢相對來說是最常用的一個,實現(xiàn)方式也簡單,常用recognizer.location屬性和recognizer.view判斷點擊的view是否是目標view,然后處理不同的邏輯。
var chompPlayer: AVAudioPlayer? = nil
    
  override func viewDidLoad() {
    super.viewDidLoad()
    
    let filteredSubviews = view.subviews.filter{
        $0 is UIImageView
    }
// 給所有UIImageView添加tap手勢
    for subview in filteredSubviews {
        let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap(recognizer:)))
        tapGestureRecognizer.delegate = self
        subview.addGestureRecognizer(tapGestureRecognizer)

// TODO:
    }
    
    chompPlayer = loadSound(filename: "chomp")
  }

// tap手勢處理
@objc func handleTap(recognizer: UITapGestureRecognizer) {
    chompPlayer?.play()
  }
  • 但注意以上tap手勢和pan手勢會同時執(zhí)行,在pan很小值得時候,tap手勢也會被觸發(fā),這種情況下可以用recognizer.require(toFail:)讓2個沖突的手勢只能執(zhí)行一個。
// TODO:
tapGestureRecognizer.require(toFail: panGestureRecognizer)

Custom UIGestureRecognizer

  • 基于UIGestureRecognizer的自定義手勢,注意在Swift中,需要借助OC橋接.h文件,才能重寫touches等事件方法。
  1. 新建OC-Header橋接文件,并導入頭文件。
#import <UIKit/UIGestureRecognizerSubclass.h>
  1. 新建類,實現(xiàn)touchesBegan、moved、ended、canceled等方法。

"撓癢癢"自定義手勢

class TickleGestureRecognizer: UIGestureRecognizer {
    enum Direction: String {
        case unknown = "DirectionUnknown", 
        left = "DirectionLeft", 
        right = "DirectionRight"
    }
    var requiredTickles = 2
    var distanceForTickleGesture: CGFloat = 25
    
    var tickleCount = 0
    var lastDirection: Direction = .unknown
    var curTickleStart: CGPoint = .zero
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
        guard let touch = touches.first else {
            return
        }
        curTickleStart = touch.location(in: view)
    }
    
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
        guard let touch = touches.first else {
            return
        }
        let ticklePoint = touch.location(in: view)
        let moveAmt = ticklePoint.x - curTickleStart.x 
        var curDirection: Direction = .unknown
        if moveAmt < 0 {
            curDirection = .left
        }
        else{
            curDirection = .right
        }
        if fabs(moveAmt) < distanceForTickleGesture {
            return
        }
        
        if (lastDirection == .left && curDirection == .right) ||
            (lastDirection == .right && curDirection == .left) || 
            lastDirection == .unknown{
            tickleCount += 1
            curTickleStart = ticklePoint
            lastDirection = curDirection
            
            if state == .possible && tickleCount > requiredTickles{
                print("He He He...")
                state = .ended
            }
        }
        
        print("\(curDirection.rawValue)")
    }
    
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
        reset()
    }
    
    override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
        reset()
    }
    
    override func reset() {
        curTickleStart = .zero
        lastDirection = .unknown
        tickleCount = 0
        if state == .possible {
            state = .failed
        }
    }
}
  1. 使用自定義手勢
let tickleGestureRecognizer = TickleGestureRecognizer(target: self, action: #selector(handleTickle(recognizer:)))
subview.addGestureRecognizer(tickleGestureRecognizer)

@objc func handleTickle(recognizer: TickleGestureRecognizer) {
    hehePlayer?.play()
}
也可以在storyboard上使用自定義手勢

UILongPressGestureRecognizer

長按手勢

UIScreenEdgePanGestureRecognizer

屏幕邊緣滑動手勢

UISwipeGestureRecognizer

掃除手勢

  • 支持單點和多點手勢,設置numberOfTouchesRequired屬性。
  • 判斷方向通過direction屬性,主要有up、down、left、right
  • 可以通過recognizer.location進行子view的translation變換。

Demo Side Panel Nav Gesture

extension ContainerViewController: UIGestureRecognizerDelegate {
  @objc func handleTapGesture(_ recognizer: UIPanGestureRecognizer) {
    if currentState == .leftPanelExpanded {
      animateLeftPanel(shouldExpand: false)
    }
    else if currentState == .rightPanelExpanded {
      animateRightPanel(shouldExpand: false)
    }
  }
  
  @objc func handlePanGesture(_ recognizer: UIPanGestureRecognizer) {
    let gestureIsDraggingFromLeftToRight = (recognizer.velocity(in: view).x > 0)

    switch recognizer.state {
    case .began:
      if currentState == .bothCollapsed {
        if gestureIsDraggingFromLeftToRight {
          addLeftPanelViewController()
        } else {
          addRightPanelViewController()
        }
        
        showShadowForCenterViewController(true)
      }
      
    case .changed:
      if let rview = recognizer.view {
        rview.center.x = rview.center.x + recognizer.translation(in: view).x
        recognizer.setTranslation(CGPoint.zero, in: view)
      }
      
    case .ended:
      let velocity = recognizer.velocity(in: recognizer.view)
      if let _ = leftViewController,
        let rview = recognizer.view {
        // animate the side panel open or closed based on whether the view
        // has moved more or less than halfway
        let hasMovedGreaterThanHalfway = rview.center.x > view.bounds.size.width
        if currentState == .bothCollapsed, velocity.x > 200 {
          animateLeftPanel(shouldExpand: true)
        }
        else if currentState == .leftPanelExpanded, velocity.x < -200 {
          animateLeftPanel(shouldExpand: false)
        }
        else {
          animateLeftPanel(shouldExpand: hasMovedGreaterThanHalfway)
        }
      } else if let _ = rightViewController,
        let rview = recognizer.view {
        let hasMovedGreaterThanHalfway = rview.center.x < 0
        if currentState == .bothCollapsed, velocity.x < -200 {
          animateRightPanel(shouldExpand: true)
        }
        else if currentState == .leftPanelExpanded, velocity.x > 200 {
          animateRightPanel(shouldExpand: false)
        }
        else {
          animateRightPanel(shouldExpand: hasMovedGreaterThanHalfway)
        }
      }
      
    default:
      break
    }
  }
}
最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容