在iOS中因爲手勢識別器的存在,咱們能夠很是容易的識別出用戶的交互手勢。 系統提供的手勢識別器以下:html
你們對上面的手勢識別器確定不陌生, 那麼問題來了:ios
1,手勢識別器是怎樣識別出用戶手勢的? 2,如何使用手勢識別器? 3,各手勢識別器狀態,及各狀態間如何進展? 4,多個手勢識別器做用在同一個UIView會發生什麼? 5,如何經過繼承現有手勢識別器來自定義?git
圍繞着這幾個問題, 我們一塊兒深刻的學習一下 GestureRecognizer。github
手勢識別經過分析 Touch events 中的數據, 識別出當前當前手指的動做。 成功識別出手勢後,發送 Action message。瞭解手勢識別器如何解釋觸摸仍是有好處的: 你能夠利用Touch event中的數據直接解釋觸摸; 繼承現有的手勢識別器知足特定的識別需求。swift
自定義一個view, 而後重寫下面的 touches... 方法。 而後將這個view 添加到superView中。 爲了簡單,只考慮一個手指。bash
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
}
複製代碼
//定義兩個屬性,存儲識別時的狀態
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?) {
}
複製代碼
區分長按和短按主要根據 touchesBegan,touchesEnded之間的時間間隔肯定。 UITouch 對象的timestamp屬性可以獲得點擊時的時間間隔。 代碼以下:app
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, 就能夠作出判斷了,不必再等了。 代碼:ide
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")
}
複製代碼
上面判斷長按和短按的方法也能夠應用在 單擊和雙擊。雖然 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能夠同時進行 長按,單擊,雙擊,拖動,在水平, 垂直方向移動。。。。這是至關可怕的。寫出來的代碼徹底不具有可讀性,還有難以理解的邏輯。這就是發明手勢識別器的緣由之一。性能
// 系統提供的手勢識別器都是繼承 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以結束當前觸摸。
複製代碼
numberOfTapsRequired //點擊次數
numberOfTouchesRequired //觸摸點數
複製代碼
scale //縮放比例
velocity //縮放速度
複製代碼
rotation //旋轉角度
velocity //縮放速度
複製代碼
direction //容許的方向
numberOfTouchesRequired //觸摸點數
複製代碼
maximumNumberOfTouches //最多觸摸點數
minimumNumberOfTouches //最少觸摸點數
複製代碼
minimumPressDuration //手勢識別的最小時長
numberOfTouchesRequired
numberOfTapsRequired
allowableMovement //補償用戶手指在長期按壓沒法保持平穩的事實。eg:確認長按後能夠進行拖動。
複製代碼
識別器能夠分爲兩類: 離散的, 連續的。狀態之間的轉換上圖中很明瞭。
一個視圖上面能夠添加多個手勢識別器。同時當觸摸一個視圖時,不只本視圖的識別器在進行識別操做,視圖層次結構中的父視圖中的識別器也在工做。因此能夠認爲一個視圖被不少手勢識別器包圍。 在這些手勢識別器中, 一旦一個識別器識別到了它的手勢,任何和它的觸摸關聯的其餘手勢識別器強制設置爲失敗狀態。 有時這不是咱們想要的,好比 識別單擊和雙擊。
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")
}
複製代碼
繼承手勢識別器,就是重寫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) //將移動的值清零
}
}
}
複製代碼
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()
}
}
複製代碼
上面的抖動動畫使用了 Core Animation. 內容挺多的,這裏就不說了。 提供一下學習資料: 官方文檔
OK ,關於手勢識別,就說這麼多。 這裏只是拋磚引玉,爲定製更加複雜的識別說一下思路。若是哪裏存在問題,歡迎提出。 若是感受這篇博文對你有幫助,記得點贊喲!