iOS 深入理解手势识别器
1,手势识别器简介
在iOS中由于手势识别器的存在,我们可以非常容易的识别出用户的交互手势。 系统提供的手势识别器如下:
- UITapGestureRecognizer(点一下)
- UIPinchGestureRecognizer(二指往內或往外拨动,平时经常用到的缩放)
- UIRotationGestureRecognizer(旋转)
- UISwipeGestureRecognizer(滑动,快速移动)
- UIPanGestureRecognizer(拖移,慢速移动)
- UILongPressGestureRecognizer(长按)
大家对上面的手势识别器肯定不陌生, 那么问题来了:
1,手势识别器是怎样识别出用户手势的?2,如何使用手势识别器?3,各手势识别器状态,及各状态间如何进展?4,多个手势识别器作用在同一个UIView会发生什么?5,如何通过继承现有手势识别器来自定义?
围绕着这几个问题, 咱们一起深入的学习一下 GestureRecognizer。
2, 解释触摸
2.1手势识别和Touch event 的关系:
手势识别通过分析 Touch events 中的数据, 识别出当前当前手指的动作。 成功识别出手势后,发送 Action message。了解手势识别器如何解释触摸还是有好处的: 你可以利用Touch event中的数据直接解释触摸; 继承现有的手势识别器满足特定的识别需求。
2.2通过几段代码解释如何解释触摸:
自定义一个view, 然后重写下面的 touches... 方法。 然后将这个view 添加到superView中。 为了简单,只考虑一个手指。
2.2.1,手指拖动视图
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) { let loc = (touches as NSSet).anyObject()?.locationInView(self.superview) let oldP = (touches as NSSet).anyObject()?.previousLocationInView(self.superview) let deltaX = (loc?.x)! - (oldP?.x)! let deltaY = (loc?.y)! - (oldP?.y)! var c = self.center c.x += deltaX c.y += deltaY self.center = c }
2.2.2, 添加限制, 只可以水平或者垂直的拖动视图。
//定义两个属性,存储识别时的状态 var decided = false //是否确定移动方向了 var horiz = false //是否水平移动 override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) { decided = false } override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) { if !decided { //第一次调用, 确定移动的方向。 decided = true let then = (touches as NSSet).anyObject()?.previousLocationInView(self.superview) let now = (touches as NSSet).anyObject()?.locationInView(self.superview) let deltaX = fabs((now?.x)! - (then?.x)!) let deltaY = fabs((now?.y)! - (then?.y)!) horiz = deltaX>=deltaY } let loc = (touches as NSSet).anyObject()?.locationInView(self.superview) let oldP = (touches as NSSet).anyObject()?.previousLocationInView(self.superview) let deltaX = (loc?.x)! - (oldP?.x)! let deltaY = (loc?.y)! - (oldP?.y)! var c = self.center if horiz{ c.x += deltaX }else{ c.y += deltaY } self.center = c } override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) { }
2.2.3,区分长按和短按
区分长按和短按主要根据 touchesBegan,touchesEnded之间的时间间隔确定。 UITouch 对象的timestamp属性能够得到点击时的时间间隔。 代码如下:
var time:Double = 0 //记录时间开始时的时间 override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) { time = (touches.first?.timestamp)! } override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) { } override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) { let diff = (touches.first?.timestamp)! - time //计算间隔 if diff > 0.5{ print("long") }else{ print("short") } }
注意: 上面的代码存在一个问题: 只有当touchesEnded 时才能判断出是长按还是短按。事实是: 如果时间超过了0.5, 就可以做出判断了,没必要再等了。 代码:
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) { time = (touches.first?.timestamp)! performSelector("longPress", withObject: nil, afterDelay: 0.5) //延迟执行 } override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) { } override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) { let diff = (touches.first?.timestamp)! - time //当时间间隔<=0.5时,判断为短按。另外还要取消 performSelector...指定的延迟消息。 不然longPress()总会调用 if diff <= 0.5{ print("short") NSObject.cancelPreviousPerformRequestsWithTarget(self, selector: "longPress", object: nil) } } func longPress(){ print("longPress") }
2.2.4,区分单击和双击
上面判断长按和短按的方法也可以应用在 单击和双击。虽然 UITouch的tapCount属性可以实现这个目标,但是并不能对单击,双击做出不同的响应。
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) { let ct = touches.first!.tapCount if(ct==2){ //取消点击一次的延时执行函数 NSObject.cancelPreviousPerformRequestsWithTarget(self, selector: "singTap", object: nil) } } override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) { } override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) { let ct = touches.first!.tapCount if ct==1{// 点第一下0.3秒后 不点击第二下, 就会执行singTap() self.performSelector("singTap", withObject: nil, afterDelay: 0.3) } if ct==2{ print("doubleTap") } } func singTap(){ print("singTap") }
现在可以实现: 长按,单击,双击,拖动,在水平, 垂直方向移动。 每种模式的代码有些不好理解了。 那么将上面这些代码组和起来,使view可以同时进行 长按,单击,双击,拖动,在水平, 垂直方向移动。。。。这是相当可怕的。写出来的代码完全不具备可读性,还有难以理解的逻辑。这就是发明手势识别器的原因之一。
3,手势识别器
3.1 手势识别器类的介绍
- UIGestureRecognizer : 手势识别器抽象类
// 系统提供的手势识别器都是继承 UIGestureRecognizer类。 - initWithTarget:action: //初始化方法,识别到手势后发送消息action到target - addTarget:action: //添加 target-action - removeTarget:action: //移除 target-action - locationInView: //触摸点在指定view上的坐标。如果是单点触摸,就是这个点;如果是多点触摸,是这几个点的中点。(重心) - numberOfTouches //识别到的手势对象中包含 UITouch对象的个数 - locationOfTouch:inView: //指定touch在指定view上的坐标。touch通过index指定。 - requireGestureRecognizerToFail: //只有当指定的识别器识别失败,自己才去识别 state //当前的状态 delegate //代理 enabled //关闭手势识别器 view //手势识别器所从属的view cancelsTouchesInView // 默认为YES,这种情况下当手势识别器识别到touch之后,会发送touchesCancelled给hit-testview以取消hit-test view对touch的响应,这个时候只有手势识别器响应touch。当设置成NO时,手势识别器识别到touch之后不会发送touchesCancelled给hit-test,这个时候手势识别器和hit-test view均响应touch。 delaysTouchesBegan //默认是NO,这种情况下当发生一个touch时,手势识别器先捕捉到到touch,然后发给hit-testview,两者各自做出响应。如果设置为YES,手势识别器在识别的过程中(注意是识别过程),不会将touch发给hit-test view,即hit-testview不会有任何触摸事件。只有在识别失败之后才会将touch发给hit-testview,这种情况下hit-test view的响应会延迟约0.15ms。 delaysTouchesEnded //默认为YES。这种情况下发生一个touch时,在手势识别成功后,发送给touchesCancelled消息给hit-testview,手势识别失败时,会延迟大概0.15ms,期间没有接收到别的touch才会发送touchesEnded。如果设置为NO,则不会延迟,即会立即发送touchesEnded以结束当前触摸。
- UITapGestureRecognizer
numberOfTapsRequired //点击次数 numberOfTouchesRequired //触摸点数
- UIPinchGestureRecognizer
scale //缩放比例 velocity //缩放速度
- UIRotationGestureRecognizer
rotation //旋转角度 velocity //缩放速度
- UISwipeGestureRecognizer
direction //允许的方向 numberOfTouchesRequired //触摸点数
- UIPanGestureRecognizer
maximumNumberOfTouches //最多触摸点数 minimumNumberOfTouches //最少触摸点数
- UILongPressGestureRecognizer
minimumPressDuration //手势识别的最小时长 numberOfTouchesRequired numberOfTapsRequired allowableMovement //补偿用户手指在长期按压无法保持平稳的事实。eg:确认长按后可以进行拖动。
3.2 status
识别器可以分为两类: 离散的, 连续的。状态之间的转换上图中很明了。
3.3 多重手势识别器
一个视图上面可以添加多个手势识别器。同时当触摸一个视图时,不仅本视图的识别器在进行识别操作,视图层次结构中的父视图中的识别器也在工作。所以可以认为一个视图被很多手势识别器包围。在这些手势识别器中, 一旦一个识别器识别到了它的手势,任何和它的触摸关联的其他手势识别器强制设置为失败状态。有时这不是我们想要的,比如 识别单击和双击。
override init(frame: CGRect) { super.init(frame: frame) self.backgroundColor = UIColor.blueColor() let t2 = UITapGestureRecognizer(target: self, action: "doubleTap") t2.numberOfTapsRequired = 2 addGestureRecognizer(t2) let t1 = UITapGestureRecognizer(target: self, action: "singleTap") t1.requireGestureRecognizerToFail(t2) //只有t2识别失败后,t1才进行识别操作 addGestureRecognizer(t1) } func doubleTap(){ print("doubleTap") } func singleTap(){ print("singleTap") }
4 继承手势识别器
继承手势识别器,就是重写touches,和相关方法。 在方法中改变手势状态信息和一些属性信息。一般都会调用父类的touches方法,毕竟父类的touches中做了大量的计算识别工作。 下面通过一个只能识别水平移动的手势来举例:
// // HerizonalPanGestureRecognizer.swift // GestureRecognizerDemo // // Created by 贺俊孟 on 16/5/13. // Copyright © 2016年 贺俊孟. All rights reserved. // 只能够水平拖动 import UIKit import UIKit.UIGestureRecognizerSubclass //这个extension可以使手势可以继承。否则你没法重写必要的方法。 class HerizonalPanGestureRecognizer: UIPanGestureRecognizer { var origLoc:CGPoint! //记录开始时的坐标 override init(target: AnyObject?, action: Selector) { super.init(target: target, action: action) } override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent) { origLoc = touches.first?.locationInView(view?.superview) super.touchesBegan(touches, withEvent: event) } //所有的识别逻辑都是在这里进行。第一次调用时状态是 Possible override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent) { if self.state == .Possible{ let loc:CGPoint! = touches.first?.locationInView(view?.superview) let deltaX = fabs(loc.x-origLoc.x) let deltaY = fabs(loc.y - origLoc.y) //开始识别时, 如果竖直移动距离>水平移动距离,直接Failed if deltaX <= deltaY{ state = .Failed } } super.touchesMoved(touches, withEvent: event) } //通过重写。现在只有x 产生偏移。 override func translationInView(view: UIView?) -> CGPoint { var proposedTranslation = super.translationInView(view) proposedTranslation.y = 0 return proposedTranslation } } // 使用 import UIKit class BlueView: UIView { override init(frame: CGRect) { super.init(frame: frame) self.backgroundColor = UIColor.blueColor() //添加水平手势识别器 let herizonalPan = HerizonalPanGestureRecognizer(target: self, action: "herizonalPan:") addGestureRecognizer(herizonalPan) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } // 进行水平移动 func herizonalPan(hp:HerizonalPanGestureRecognizer){ if hp.state == .Began || hp.state == .Changed{ let delta = hp.translationInView(superview) var c = center c.x += delta.x c.y += delta.y center = c hp.setTranslation(CGPoint.zero, inView: superview) //将移动的值清零 } } }
5 手势识别器委托
1, gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool //在手势识别器超越Possible状态前发送给委托。返回No,强制识别器进入Failed状态。 2,gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool //当一个手势识别器打算声明他识别的手势时,如果这个手势会强制使另一个手势识别器失败,失败的手势识别器会发送这个消息给他的代理。 返回yes,阻止这个失败。这时两个识别器同时工作。 3, gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldReceiveTouch touch: UITouch) -> Bool //在手势识别器开始touchesBegan:withEvent:之前调用。 返回false,忽略这个手势。 4,gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOfGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool //协调两个同时发生的手势识别 5,gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailByGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool //识别两个同时发生的手势识别。
下面举个例子: 使用委托消息来组合 UILongPressGestureRecognizer和UIPanGestureRecognizer. 通过长按使一个view抖动, 只有在抖动的过程中才可以拖动该view。
// // BlueView.swift // GestureRecognizerDemo // // Created by 贺俊孟 on 16/5/12. // Copyright © 2016年 贺俊孟. All rights reserved. // import UIKit class BlueView: UIView { var longP:UILongPressGestureRecognizer! override init(frame: CGRect) { super.init(frame: frame) self.backgroundColor = UIColor.blueColor() //拖动 let pan = UIPanGestureRecognizer(target: self, action: "panning:") pan.delegate = self addGestureRecognizer(pan) //长按 longP = UILongPressGestureRecognizer(target: self, action: "longPress:") longP.delegate = self addGestureRecognizer(longP) } func longPress(lp:UILongPressGestureRecognizer){ if lp.state == .Began{ //开始动画 let anim = CABasicAnimation(keyPath: "transform") anim.toValue = NSValue(CATransform3D: CATransform3DMakeScale(1.1, 1.1, 1.1)) anim.fromValue = NSValue(CATransform3D:CATransform3DIdentity) anim.repeatCount = HUGE anim.autoreverses = true lp.view?.layer.addAnimation(anim, forKey: nil) return } if lp.state == .Ended || lp.state == .Cancelled{ //结束动画 lp.view?.layer.removeAllAnimations() } } func panning(p:UIPanGestureRecognizer){ if p.state == .Began || p.state == .Changed{ let delta = p.translationInView(superview) var c = center c.x += delta.x c.y += delta.y center = c p.setTranslation(CGPoint.zero, inView: superview) //将移动的值清零 } } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } } extension BlueView:UIGestureRecognizerDelegate{ //长按和拖动可以同时进行 func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool { return true } //只有在长按状态时,才可以进行拖拽 override func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool { if gestureRecognizer == longP{ return true }else{ if longP.state == .Possible || longP.state == .Failed{ return false } return true } } }
上面的代码仅仅是为了说明代理如何使用。要实现上面的效果, 只使用UILongPressGestureRecognizer就够了:
//长按 let longP = UILongPressGestureRecognizer(target: self, action: "longPress:") addGestureRecognizer(longP) func longPress(lp:UILongPressGestureRecognizer){ //开始动画 if lp.state == .Began{ let anim = CABasicAnimation(keyPath: "transform") anim.toValue = NSValue(CATransform3D: CATransform3DMakeScale(1.1, 1.1, 1.1)) anim.fromValue = NSValue(CATransform3D:CATransform3DIdentity) anim.repeatCount = HUGE anim.autoreverses = true lp.view?.layer.addAnimation(anim, forKey: nil) //获取触摸点相对于center的偏移 origOffset = CGPointMake(CGRectGetMidX(bounds)-lp.locationInView(self).x, CGRectGetMidY(bounds)-lp.locationInView(self).y) } //进行移动 if lp.state == .Changed{ var c = lp.locationInView(superview) c.x += origOffset.x c.y += origOffset.y center = c } //结束动画 if lp.state == .Ended || lp.state == .Cancelled{ lp.view?.layer.removeAllAnimations() } }
Demo在这里
上面的抖动动画使用了 Core Animation. 内容挺多的,这里就不说了。提供一下学习资料:CALayer官方文档
OK ,关于手势识别,就说这么多。 这里只是抛砖引玉,为定制更加复杂的识别说一下思路。如果哪里存在问题,欢迎提出。 如果感觉这篇博文对你有帮助,记得点赞哟!