UIGestureRecognizer共有8種:
- UITapGestureRecognizer
- UIPanGestureRecognizerpan
- UIScreenEdgePanRecognizer
- UIPinchGestureRecognizer
- UIRotationGestureRecognizer
- UILongPressGestureRecognizer
- UISwipeGestureRecognizer
- UIGestureRecognizer即自定義手勢
其用法大同小異,比較常用的主要有tap和pan,主要使用recognizer.location
和recognizer.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等事件方法。
- 新建OC-Header橋接文件,并導入頭文件。
#import <UIKit/UIGestureRecognizerSubclass.h>
- 新建類,實現(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
}
}
}
- 使用自定義手勢
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
}
}
}