UIGestureRecognizer共有8种:
- UITapGestureRecognizer
- UIPanGestureRecognizerpan
- UIScreenEdgePanRecognizer
- UIPinchGestureRecognizer
- UIRotationGestureRecognizer
- UILongPressGestureRecognizer
- UISwipeGestureRecognizer
- UIGestureRecognizer即自定义手势
其用法大同小异,比较常用的主要有tap和pan,主要使用recognizer.location
和recognizer.view
属性。
UIPanGestureRecognizer
利用
UIPanGesgtureRecognizer
让view跟随touch移动,通常有两种处理逻辑。
- 第一种利用
recognizer.location
:
var offset: CGSize! // 记住一开始的touch point距离center的偏移值
// 事件回调
@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.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值也是连续变化的,注意随时将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值也是连续变化的,注意随时将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
- 一般情况下,每个手势只能被单独使用,并不能在执行一个手势如rotation的同时执行scale手势,但可以设置UIGestureRecognizer的delegate,来配置是否允许手势同时执行。
extension ViewController: UIGestureRecognizerDelegate{
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}
UITapGestureRecognizer
- 这个手势相对来说是最常用的一个,实现方式也简单,常用
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手势会同时执行,在pan很小值得时候,tap手势也会被触发,这种情况下可以用
recognizer.require(toFail:)
让2个冲突的手势只能执行一个。
// TODO:
tapGestureRecognizer.require(toFail: panGestureRecognizer)
Custom UIGestureRecognizer
- 基于UIGestureRecognizer的自定义手势,注意在Swift中,需要借助OC桥接.h文件,才能重写touches等事件方法。
- 新建OC-Header桥接文件,并导入头文件。
#import <UIKit/UIGestureRecognizerSubclass.h>
- 新建类,实现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()
}
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
}
}
}