在第十八章中,您通過實(shí)現(xiàn) UIResponder 的方法來處理原始觸摸。 有時(shí)你想檢測(cè)一個(gè)特定的觸摸模式——手勢(shì),如捏或滑動(dòng)。 您可以使用 UIGestureRecognizer 的實(shí)例而不用自己寫代碼來檢測(cè)常用手勢(shì)。
UIGestureRecognizer 攔截由視圖處理的觸摸。 當(dāng)它識(shí)別出一個(gè)特定的手勢(shì)時(shí),它就會(huì)根據(jù)你選擇的對(duì)象調(diào)用一個(gè)方法。 SDK 中內(nèi)置了多種類型的手勢(shì)識(shí)別器。 在本章中,您將使用其中三個(gè)來允許 TouchTracker 用戶選擇,移動(dòng)和刪除線(圖19.1)。 您還將看到如何使用另一個(gè)有趣的 iOS 類,UIMenuController。
圖19.1 本章末尾的TouchTracker
UIGestureRecognizer子類
您不需要自己去實(shí)例化 UIGestureRecognizer。 相反, UIGestureRecognizer 有很多子類,每個(gè)子類負(fù)責(zé)識(shí)別一個(gè)特定的手勢(shì)。
要使用 UIGestureRecognizer 子類的實(shí)例,請(qǐng)給它一個(gè) 目標(biāo)動(dòng)作對(duì) 并將其與視圖相關(guān)聯(lián)。 每當(dāng)手勢(shì)識(shí)別器在視圖上識(shí)別其手勢(shì)時(shí),它將向其目標(biāo)發(fā)送動(dòng)作消息。 所有 UIGestureRecognizer 動(dòng)作消息具有相同的形式:
func action(_ gestureRecognizer: UIGestureRecognizer) { }
當(dāng)識(shí)別手勢(shì)時(shí),手勢(shì)識(shí)別器截取視圖內(nèi)指定的觸摸(圖19.2)。 因此,在使用了手勢(shì)識(shí)別器的視圖上可能不會(huì)調(diào)用像 touchesBegan(_:with :) 這樣的典型的 UIResponder 方法。
圖19.2 手勢(shì)識(shí)別器攔截
用 UITapGestureRecognizer 檢測(cè)點(diǎn)擊
您將使用的第一個(gè) UIGestureRecognizer 子類是 UITapGestureRecognizer。 當(dāng)用戶點(diǎn)擊屏幕兩次時(shí),屏幕上的所有線將被清除。
打開 TouchTracker.xcodeproj
和 DrawView.swift
。 添加一個(gè) init?(coder :) 方法并實(shí)例化一個(gè) UITapGestureRecognizer,需要兩次點(diǎn)擊來觸發(fā)并調(diào)用其目標(biāo)上的動(dòng)作方法。
required init?(coder aDecoder: NSCoder) {
??super.init(coder: aDecoder)
??let doubleTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(DrawView.doubleTap(_:)))
??doubleTapRecognizer.numberOfTapsRequired = 2
??addGestureRecognizer(doubleTapRecognizer)
}
現(xiàn)在當(dāng)在 DrawView 的一個(gè)實(shí)例上發(fā)生雙擊時(shí),將在該實(shí)例上調(diào)用 doubleTap(_ :) 方法。 在 DrawView.swift
中實(shí)現(xiàn)此方法。
func doubleTap(_ gestureRecognizer: UIGestureRecognizer) {
??print("Recognized a double tap")
??currentLines.removeAll()
??finishedLines.removeAll()
??setNeedsDisplay()
}
請(qǐng)注意,手勢(shì)識(shí)別器的動(dòng)作方法的參數(shù)是調(diào)用該方法的 UIGestureRecognizer 的實(shí)例。 在雙擊的情況下,您不需要識(shí)別器的任何信息,但在本章后面您將需要用到其他識(shí)別器的信息。
構(gòu)建并運(yùn)行應(yīng)用程序,繪制幾條線,然后雙擊屏幕以清除它們。
您可能已經(jīng)注意到(特別是在模擬器上),第一次點(diǎn)擊雙擊會(huì)導(dǎo)致繪制一個(gè)小紅點(diǎn)。 之所以出現(xiàn)這個(gè)點(diǎn),是因?yàn)樵诘谝淮吸c(diǎn)擊時(shí),我們?cè)?DrawView 上調(diào)用了 touchesBegan(_:with :),創(chuàng)建了一條很短的線。 檢查控制臺(tái),您將看到以下事件序列:
touchesBegan(_:with:)
Recognized a double tap
touchesCancelled(_:with:)
手勢(shì)識(shí)別器通過檢查觸摸事件來確定他們的特定手勢(shì)是否發(fā)生。 在識(shí)別手勢(shì)之前,手勢(shì)識(shí)別器攔截所有的 UIResponder 方法調(diào)用。 如果它沒有識(shí)別出是某個(gè)手勢(shì),那么每個(gè)調(diào)用都將轉(zhuǎn)發(fā)回到視圖中去。
識(shí)別點(diǎn)擊需要觸摸的開始和結(jié)束。 這意味著當(dāng)最初調(diào)用 touchesBegan(_:with :) 時(shí),UITapGestureRecognizer 不知道該觸摸是否是點(diǎn)擊,因此還是會(huì)在視圖上調(diào)用該方法。 當(dāng)觸摸結(jié)束時(shí),識(shí)別出為點(diǎn)擊,手勢(shì)識(shí)別器會(huì)將該觸摸聲明為自己所有。 它通過在視圖上調(diào)用 touchesCancelled(_:with :) 來實(shí)現(xiàn)。 之后,其它的 UIResponder 方法將無(wú)法在該視圖中調(diào)用。
要防止這個(gè)紅點(diǎn)暫時(shí)出現(xiàn),您必須防止在視圖中調(diào)用 touchesBegan(_:with :)。 您可以告訴 UIGestureRecognizer 在其視圖上延遲調(diào)用 touchesBegan(_:with :),因?yàn)樵撚|摸有可能會(huì)被識(shí)別為手勢(shì)。
在 DrawView.swift
中,修改 init?(coder :) 來實(shí)現(xiàn)。
required init?(coder aDecoder: NSCoder) {
??super.init(coder: aDecoder)
??let doubleTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(DrawView.doubleTap(_:)))
??doubleTapRecognizer.numberOfTapsRequired = 2
??doubleTapRecognizer.delaysTouchesBegan = true
??addGestureRecognizer(doubleTapRecognizer)
}
構(gòu)建并運(yùn)行應(yīng)用程序,繪制一些線,然后雙擊以清除它們。 雙擊時(shí),您將不會(huì)再看到紅點(diǎn)。
多種手勢(shì)識(shí)別器
下一步是添加另一個(gè)允許用戶選擇一條線的手勢(shì)識(shí)別器。 (稍后,用戶將能夠刪除所選行。)您將在 DrawView 上添加只需要一次點(diǎn)擊就觸發(fā)的另一個(gè) UITapGestureRecognizer。
在 DrawView.swift
中,修改 init?(coder :) 來添加這個(gè)手勢(shì)識(shí)別器。
required init?(coder aDecoder: NSCoder) {
??super.init(coder: aDecoder)
??let doubleTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(DrawView.doubleTap(_:)))
??doubleTapRecognizer.numberOfTapsRequired = 2
??doubleTapRecognizer.delaysTouchesBegan = true
??addGestureRecognizer(doubleTapRecognizer)
??let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(DrawView.tap(_:)))
??tapRecognizer.delaysTouchesBegan = true
??addGestureRecognizer(tapRecognizer)
}
現(xiàn)在,在 DrawView.swift
中實(shí)現(xiàn) tap(_ :),將點(diǎn)擊記錄到控制臺(tái)。
func tap(_ gestureRecognizer: UIGestureRecognizer) {
??print("Recognized a tap")
}
構(gòu)建并運(yùn)行應(yīng)用程序。 嘗試點(diǎn)擊和雙擊。 點(diǎn)擊一次將適當(dāng)?shù)南⒂涗浀娇刂婆_(tái)。 然而,雙擊可以觸發(fā) tap(_:) 和 doubleTap(_ :) 兩個(gè)方法。
在具有多個(gè)手勢(shì)識(shí)別器的情況下,一個(gè)手勢(shì)識(shí)別器識(shí)別到該觸摸并處理,但其實(shí)你想要讓另一個(gè)手勢(shì)識(shí)別器去處理這個(gè)手勢(shì),這是很常見的。 在這些情況下,您可以在識(shí)別器之間設(shè)置依賴關(guān)系,這些依賴關(guān)系就像:“等等你先別急,這個(gè)觸摸可能是我的!”
在 init?(coder:) 中,使 tapDecognizer
等待直到 doubleTapRecognizer
無(wú)法識(shí)別雙擊才能聲明為其自身的點(diǎn)擊。
required init?(coder aDecoder: NSCoder) {
??super.init(coder: aDecoder)
??let doubleTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(DrawView.doubleTap(_:)))
??doubleTapRecognizer.numberOfTapsRequired = 2
??doubleTapRecognizer.delaysTouchesBegan = true
??addGestureRecognizer(doubleTapRecognizer)
??let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(DrawView.tap(_:)))
??tapRecognizer.delaysTouchesBegan = true
??tapRecognizer.require(toFail: doubleTapRecognizer)
??addGestureRecognizer(tapRecognizer)
}
再次構(gòu)建并運(yùn)行應(yīng)用程序,并嘗試一些點(diǎn)擊。 點(diǎn)擊發(fā)生后,單擊現(xiàn)在需要少量時(shí)間去觸發(fā),但雙擊不再觸發(fā) tap(_:) 消息。
接下來,讓我們?cè)?DrawView 上構(gòu)建,以便用戶可以在點(diǎn)擊時(shí)選擇一條線。 首先,在 DrawView.swift
的頂部添加一個(gè)屬性來保存所選線的索引。
class DrawView: UIView {
??var currentLines = [NSValue:Line]()
??var finishedLines = [Line]()
??var selectedLineIndex: Int?
現(xiàn)在修改 draw(_ :) 將所選線畫成綠色。
override func draw(_ rect: CGRect) {
??finishedLineColor.setStroke()
??for line in finishedLines {
????stroke(line)
??}
??currentLineColor.setStroke()
??for (_,line) in currentLines {
????stroke(line)
??}
??if let index = selectedLineIndex {
????UIColor.green.setStroke()
????let selectedLine = finishedLines[index]
????stroke(selectedLine)
??}
}
仍然在 DrawView.swift
中,添加一個(gè) indexOfLine(at :) 方法返回離給定點(diǎn)最接近的 Line 的索引。
func indexOfLine(at point: CGPoint) -> Int? {
??// Find a line close to point
??for (index, line) in finishedLines.enumerated() {
????let begin = line.begin
????let end = line.end
????// Check a few points on the line
????for t in stride(from: CGFloat(0), to: 1.0, by: 0.05) {
??????let x = begin.x + ((end.x - begin.x) * t)
??????let y = begin.y + ((end.y - begin.y) * t)
??????// If the tapped point is within 20 points, let's return this line
??????if hypot(x - point.x, y - point.y) < 20.0 {
????????return index
??????}
????}
??}
??// If nothing is close enough to the tapped point, then we did not select a line
??return nil
}
stride(from:to:by:) 方法將允許 t 從 from
值開始,并且增加到(但不達(dá)到) to
值,每次增量為 by
。
還有其他更好的方法可以確定最接近某個(gè)點(diǎn)的線條,但是這個(gè)簡(jiǎn)單的實(shí)現(xiàn)已經(jīng)達(dá)到目的了。
要傳遞的 point
是手指點(diǎn)擊的點(diǎn)。 您可以輕松獲取此信息。 每個(gè) UIGestureRecognizer 都有一個(gè) location(in :) 方法。 在手勢(shì)識(shí)別器上調(diào)用此方法將給出在作為參數(shù)傳遞的視圖的坐標(biāo)系中觸發(fā)手勢(shì)的坐標(biāo)。
在 DrawView.swift
中,更新 tap(_ :) 來調(diào)用手勢(shì)識(shí)別器的 location(in:),將結(jié)果傳遞給 indexOfLine(in:),并將返回的索引賦給 selectedLineIndex
。
func tap(_ gestureRecognizer: UIGestureRecognizer) {
??print("Recognized a tap")
??let point = gestureRecognizer.location(in: self)
??selectedLineIndex = indexOfLine(at: point)
setNeedsDisplay()
}
如果用戶在選擇一條線時(shí)使用了雙擊來清除所有線,應(yīng)用程序?qū)⒈罎ⅰ?要解決這個(gè)問題,請(qǐng)更新 doubleTap(_ :) 將 selectedLineIndex
設(shè)置為 nil
。
func doubleTap(_ gestureRecognizer: UIGestureRecognizer) {
??print("Recognized a double tap")
??selectedLineIndex = nil
??currentLines.removeAll()
??finishedLines.removeAll()
??setNeedsDisplay()
}
構(gòu)建并運(yùn)行應(yīng)用程序。 畫幾條線,然后點(diǎn)擊其中一條。 點(diǎn)擊的線應(yīng)顯示為綠色(請(qǐng)記住,在點(diǎn)擊而不是雙擊被識(shí)別出來之前需要一些時(shí)間)。
UIMenuController
接下來,您需要做到當(dāng)用戶選擇一條線時(shí),具有刪除該行的選項(xiàng)的菜單將顯示在用戶點(diǎn)擊的位置。 有一個(gè)內(nèi)置的類用于提供這種菜單,稱為 UIMenuController(圖19.3)。 菜單控制器具有 UIMenuItem 對(duì)象的列表,并且在現(xiàn)有視圖中呈現(xiàn)。 每個(gè) item 都有一個(gè)標(biāo)題(菜單中顯示的內(nèi)容)和一個(gè)動(dòng)作(它發(fā)送到窗口的第一響應(yīng)者的消息)。
圖19.3 UIMenuController
每個(gè)應(yīng)用程序只有一個(gè) UIMenuController。 當(dāng)你想呈現(xiàn)這個(gè)實(shí)例時(shí),你可以用菜單項(xiàng)來填充它,給它一個(gè)矩形來呈現(xiàn),并將其設(shè)置為可見。
在 DrawView.swift
的 tap(_ :) 方法中執(zhí)行如下操作,如果用戶已經(jīng)點(diǎn)擊了一條線。 如果用戶點(diǎn)擊不在該線附近的某處,則當(dāng)前選中的行將被取消選擇,并且菜單控制器將被隱藏。
func tap(_ gestureRecognizer: UIGestureRecognizer) {
??print("Recognized a tap")
??let point = gestureRecognizer.location(in: self)
??selectedLineIndex = indexOfLine(at: point)
??// Grab the menu controller
??let menu = UIMenuController.shared
??if selectedLineIndex != nil {
????// Make DrawView the target of menu item action messages
????becomeFirstResponder()
????// Create a new "Delete" UIMenuItem
????let deleteItem = UIMenuItem(title: "Delete",action: #selector(DrawView.deleteLine(_:)))
????menu.menuItems = [deleteItem]
????// Tell the menu where it should come from and show it
????let targetRect = CGRect(x: point.x, y: point.y, width: 2, height: 2)
????menu.setTargetRect(targetRect, in: self)
????menu.setMenuVisible(true, animated: true)
??} else {
????// Hide the menu if no line is selected
????menu.setMenuVisible(false, animated: true)
??}
??setNeedsDisplay()
}
要顯示菜單控制器,在 UIMenuController 的菜單項(xiàng)中響應(yīng)至少一個(gè)動(dòng)作消息的視圖必須是窗口的第一響應(yīng)者——這就是為什么在設(shè)置菜單控制器之前,要在 DrawView 上調(diào)用方法 becomeFirstResponder()。
如果您的自定義視圖類要成為第一響應(yīng)者,則還必須覆蓋 canBecomeFirstResponder
。 在 DrawView.swift
中,覆蓋此屬性以返回 true
。
override var canBecomeFirstResponder: Bool {
??return true
}
最后,在 DrawView.swift
中實(shí)現(xiàn) deleteLine(_ :)。
func deleteLine(_ sender: UIMenuController) {
??// Remove the selected line from the list of finishedLines
??if let index = selectedLineIndex {
????finishedLines.remove(at: index)
????selectedLineIndex = nil
????// Redraw everything
????setNeedsDisplay()
??}
}
當(dāng)被呈現(xiàn)時(shí),菜單控制器通過每個(gè)菜單項(xiàng)并且詢問第一響應(yīng)者是否實(shí)現(xiàn)該項(xiàng)的動(dòng)作方法。 如果第一響應(yīng)者沒有實(shí)現(xiàn)該方法,則菜單控制器將不會(huì)顯示相關(guān)的菜單項(xiàng)。 如果菜單項(xiàng)沒有由第一響應(yīng)者實(shí)現(xiàn)的動(dòng)作方法,則完全不顯示該菜單。
構(gòu)建并運(yùn)行應(yīng)用程序。 畫一條線,點(diǎn)擊它,然后從菜單項(xiàng)中選擇 Delete
。
選擇一條線,然后雙擊以清除所有行,而菜單控制器仍然可見。 如果 selectedLineIndex
為 nil
,菜單控制器應(yīng)該為不可見才對(duì)。
在 DrawView.swift
中向 propertiesLineIndex
添加屬性觀察器,如果索引設(shè)置為 nil
,則將菜單控制器設(shè)置為不可見。
var selectedLineIndex: Int? {
??didSet {
????if selectedLineIndex == nil {
??????let menu = UIMenuController.shared
??????menu.setMenuVisible(false, animated: true)
????}
??}
}
構(gòu)建并運(yùn)行應(yīng)用程序。 畫一條線,選擇它,然后雙擊背景。 線和菜單控制器將不再可見。
更多手勢(shì)識(shí)別器
在本節(jié)中,您將添加用戶通過長(zhǎng)按來選擇線的功能,然后通過平移移動(dòng)所選線。 這將需要用到另外兩個(gè) UIGestureRecognizer 子類:
UILongPressGestureRecognizer 和 UIPanGestureRecognizer。
UILongPressGestureRecognizer
在 DrawView.swift
中,在 init?(coder:) 中實(shí)例化一個(gè) UILongPressGestureRecognizer 并將其添加到 DrawView。
...
??addGestureRecognizer(tapRecognizer)
??let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(DrawView.longPress(_:)))
??addGestureRecognizer(longPressRecognizer)
}
當(dāng)用戶按住 DrawView 時(shí),將會(huì)調(diào)用 longPress(_ :) 方法。 默認(rèn)情況下,觸摸必須持續(xù) 0.5 秒才被視為長(zhǎng)按,但如果你想修改的話,可以更改手勢(shì)識(shí)別器的 minimumPressDuration
。
到目前為止,您已經(jīng)使用過手勢(shì)了。 點(diǎn)擊 是一個(gè) 獨(dú)立(discrete
) 的手勢(shì)。 當(dāng)被識(shí)別時(shí),手勢(shì)已經(jīng)結(jié)束,并且已經(jīng)傳送了動(dòng)作消息。 另一方面,長(zhǎng)按是一個(gè) 持續(xù)(
continuous) 的手勢(shì)。 持續(xù)的手勢(shì)隨著時(shí)間推移而發(fā)生,為了跟蹤持續(xù)的手勢(shì)發(fā)生了什么,您可以檢查識(shí)別器的
state` 屬性。
例如,考慮一個(gè)典型的長(zhǎng)按:
- 當(dāng)用戶觸摸視圖時(shí),長(zhǎng)按識(shí)別器會(huì)注意到 可能(
possible
) 是一個(gè)長(zhǎng)按,但是它必須等待觀看觸摸是否持續(xù)足夠長(zhǎng)以成為長(zhǎng)按手勢(shì)。 識(shí)別器的狀態(tài)是UIGestureRecognizerState.possible
。 - 一旦用戶持續(xù)足夠長(zhǎng)的時(shí)間,長(zhǎng)按可以被識(shí)別并且手勢(shì)已經(jīng) 開始(
began
)。 識(shí)別器的狀態(tài)是UIGestureRecognizerState.began
。 - 當(dāng)用戶移除手指時(shí),手勢(shì)已經(jīng) 結(jié)束(
ended
)。 識(shí)別器的狀態(tài)是UIGestureRecognizerState.ended
。
當(dāng)長(zhǎng)按手勢(shì)識(shí)別器從 possible 轉(zhuǎn)移到 began 和從 began 到 ended 時(shí),它將其動(dòng)作消息發(fā)送到其目標(biāo)。 要確定在哪個(gè)轉(zhuǎn)換中觸發(fā)動(dòng)作,可以查看手勢(shì)識(shí)別器的 state
。
請(qǐng)記住,長(zhǎng)按是較大功能的一部分。 在下一節(jié)中,您將使用戶可以通過用長(zhǎng)按開始的同一個(gè)手指拖動(dòng)所選直線來移動(dòng)所選直線線。 所以這里是實(shí)現(xiàn) longPress(_ :) 動(dòng)作方法的計(jì)劃:當(dāng)識(shí)別器處于 began 狀態(tài)時(shí),您將選擇距離手勢(shì)發(fā)生的最近的直線。 當(dāng)識(shí)別器處于 ended 狀態(tài)時(shí),您將取消選擇該線。
在 DrawView.swift
中,實(shí)現(xiàn) longPress(_ :)。
func longPress(_ gestureRecognizer: UIGestureRecognizer) {
??print("Recognized a long press")
??if gestureRecognizer.state == .began {
????let point = gestureRecognizer.location(in: self)
????selectedLineIndex = indexOfLine(at: point)
????if selectedLineIndex != nil {
??????currentLines.removeAll()
????}
??} else if gestureRecognizer.state == .ended {
????selectedLineIndex = nil
??}
??setNeedsDisplay()
}
構(gòu)建并運(yùn)行應(yīng)用程序。 畫一條直線,然后按住它; 該行將變?yōu)榫G色并成為所選行。 當(dāng)您放開時(shí),該行將恢復(fù)為其之前的顏色,并且將不再是所選行。
UIPanGestureRecognizer 和 同時(shí)識(shí)別器
在 DrawView.swift
中,聲明一個(gè) UIPanGestureRecognizer 作為一個(gè)屬性,以便您可以在所有方法中訪問它。
class DrawView: UIView {
??var currentLines = [NSValue:Line]()
??var finishedLines = [Line]()
??var selectedLineIndex: Int? {
????...
??}
??var moveRecognizer: UIPanGestureRecognizer!
接下來,在 DrawView.swift
中,添加代碼到 init?(coder :) 來實(shí)例化一個(gè) UIPanGestureRecognizer,設(shè)置其中的一個(gè)屬性,并將其添加到 DrawView。
let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(DrawView.longPress(_:)))
??addGestureRecognizer(longPressRecognizer)
??moveRecognizer = UIPanGestureRecognizer(target: self, action: #selector(DrawView.moveLine(_:)))
??moveRecognizer.cancelsTouchesInView = false
??addGestureRecognizer(moveRecognizer)
}
cancelsTouchesInView
是什么? 每個(gè) UIGestureRecognizer 都具有此屬性,默認(rèn)為 true
。 當(dāng) cancelsTouchesInView
為 true
時(shí),手勢(shì)識(shí)別器將 “吃” 掉任何可識(shí)別的觸摸,并且視圖將無(wú)法通過傳統(tǒng)的 UIResponder 方法,例如 touchesBegan(_:with :) 來處理觸摸。
通常,這是你想要的,但不總是。 在這種情況下,如果平移手勢(shì)識(shí)別器要觸摸它,則用戶將無(wú)法繪制線條。 當(dāng)您將 cancelsTouchesInView
設(shè)置為 false
時(shí),您確保手勢(shì)識(shí)別器識(shí)別的任何觸摸也將通過 UIResponder 方法傳遞到視圖。
在 DrawView.swift
中,為動(dòng)作方法添加一個(gè)簡(jiǎn)單的實(shí)現(xiàn):
func moveLine(_ gestureRecognizer: UIPanGestureRecognizer) {
??print("Recognized a pan")
}
構(gòu)建并運(yùn)行應(yīng)用程序并繪制一些直線。 因?yàn)?cancelsTouchesInView
為 false
,所以可以識(shí)別平移手勢(shì),也可以繪制線條。 您可以注釋掉設(shè)置 cancelsTouchesInView
的代碼行,再次運(yùn)行以查看差異。
接下來,當(dāng)用戶的手指移動(dòng)到屏幕上時(shí),您將更新 moveLine(_ :) 來重繪所選線。 但首先,您需要兩個(gè)手勢(shì)識(shí)別器才能處理相同的觸摸。 通常,當(dāng)手勢(shì)識(shí)別器識(shí)別出其手勢(shì)時(shí),它會(huì)吃掉它,而其他識(shí)別器沒有機(jī)會(huì)處理該觸摸。 嘗試一下:運(yùn)行應(yīng)用程序,畫一條線,按住選擇該線,然后移動(dòng)你的手指。 控制臺(tái)報(bào)告是長(zhǎng)按而不是平移。
在這種情況下,默認(rèn)行為是有問題的:為了按住并選擇一條線,然后平移以移動(dòng)線,在這過程中您的用戶是不會(huì)將手指抬起的。 因此,兩個(gè)手勢(shì)應(yīng)該同時(shí)發(fā)生,并且即使長(zhǎng)按手勢(shì)已經(jīng)識(shí)別長(zhǎng)按,也必須允許平移手勢(shì)識(shí)別器識(shí)別平移。
為了允許手勢(shì)識(shí)別器與其他手勢(shì)識(shí)別器同時(shí)識(shí)別其手勢(shì),您可以從 UIGestureRecognizerDelegate 協(xié)議實(shí)現(xiàn)一種方法:
optional func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool
第一個(gè)參數(shù)是要求引導(dǎo)的手勢(shì)識(shí)別器。 它對(duì)其 委托 說:“如果在我和另一個(gè)的識(shí)別器之中的某一個(gè)剛剛識(shí)別出了一個(gè)手勢(shì)。 那那個(gè)不識(shí)別的應(yīng)該保持在 possible 的狀態(tài),并繼續(xù)追蹤這個(gè)觸摸嗎?”
請(qǐng)注意,調(diào)用本身并沒有告訴您兩個(gè)識(shí)別器中哪一個(gè)已經(jīng)識(shí)別出它的手勢(shì),因此,有的可能被剝奪了識(shí)別其手勢(shì)的機(jī)會(huì)。
默認(rèn)情況下,該方法返回 false
,并且手勢(shì)識(shí)別器仍處于 possible 狀態(tài),使手勢(shì)中的觸摸處于 recognized 狀態(tài)。 您可以實(shí)現(xiàn)返回 true
的方法,以允許兩個(gè)識(shí)別器在相同的觸摸中識(shí)別各自的手勢(shì)。 (如果您需要確定兩個(gè)識(shí)別器中的哪一個(gè)識(shí)別其手勢(shì),則可以檢查它們的 state 屬性。)
要長(zhǎng)時(shí)間啟用平移功能,您將要給平移手勢(shì)識(shí)別器一個(gè)委托(DrawView)。 然后,當(dāng)長(zhǎng)按識(shí)別器識(shí)別其手勢(shì)時(shí),平移手勢(shì)識(shí)別器將在其委托上調(diào)用同時(shí)識(shí)別方法。 您將在 DrawView 中實(shí)現(xiàn)此方法返回 true
。 這將允許平移手勢(shì)識(shí)別器識(shí)別在長(zhǎng)按進(jìn)行過程中發(fā)生的任何平移。
首先,在 DrawView.swift
中,聲明 DrawView 符合 UIGestureRecognizerDelegate 協(xié)議。
class DrawView: UIView,
UIGestureRecognizerDelegate {
??var currentLines = [NSValue:Line]()
??var finishedLines = [Line]()
??var selectedLineIndex: Int? {
??...
}
var moveRecognizer: UIPanGestureRecognizer!
接下來,在 init?(coder :) 中,將 DrawView 設(shè)置為 UIPanGestureRecognizer 的委托。
let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(DrawView.longPress(_:)))
??addGestureRecognizer(longPressRecognizer)
??moveRecognizer = UIPanGestureRecognizer(target: self, action: #selector(DrawView.moveLine(_:)))
??moveRecognizer.delegate = self
??moveRecognizer.cancelsTouchesInView = false
??addGestureRecognizer(moveRecognizer)
}
最后,在 DrawView.swift
中,實(shí)現(xiàn)委托方法并返回 true
。
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
??return true
}
對(duì)于這種情況,只有您的平移手勢(shì)識(shí)別器有一個(gè)委托,除了返回 true
則不需要做更多的事情。 在更復(fù)雜的情況下,您可以使用傳入的手勢(shì)識(shí)別器來更仔細(xì)地控制同時(shí)識(shí)別。
現(xiàn)在,當(dāng)長(zhǎng)按開始時(shí),UIPanGestureRecognizer 將繼續(xù)跟蹤觸摸,如果用戶的手指開始移動(dòng),則識(shí)別器將識(shí)別平移。 要看到差異,運(yùn)行應(yīng)用程序,畫一條線,選擇它,然后平移。 控制臺(tái)將報(bào)告兩種手勢(shì)。(UIGestureRecognizerDelegate 協(xié)議包括其他方法來幫助您調(diào)整手勢(shì)識(shí)別器的行為。訪問協(xié)議參考頁(yè)面了解更多信息。)
除了您已經(jīng)看到的狀態(tài)之外,平移手勢(shì)識(shí)別器支持 changed
狀態(tài)。 當(dāng)手指開始移動(dòng)時(shí),識(shí)別器進(jìn)入 began 狀態(tài)并調(diào)用其目標(biāo)上的方法。 當(dāng)手指圍繞屏幕移動(dòng)時(shí),識(shí)別器轉(zhuǎn)換到 changed 的狀態(tài),并重復(fù)地在其目標(biāo)上調(diào)用動(dòng)作方法。 當(dāng)手指離開屏幕時(shí),識(shí)別器的狀態(tài)被設(shè)置為 ended,并且最終在目標(biāo)上調(diào)用該方法。
下一步是實(shí)現(xiàn)平移識(shí)別器在其目標(biāo)上調(diào)用的 moveLine(_ :) 方法。 在這個(gè)實(shí)現(xiàn)中,您將在平移識(shí)別器上調(diào)用 translationInView(_ :) 方法。 該 UIPanGestureRecognizer 方法返回在作為參數(shù)傳遞的視圖的坐標(biāo)系中,平移點(diǎn)作為 CGPoint 移動(dòng)的距離。 當(dāng)平移手勢(shì)開始時(shí),該屬性設(shè)置為零點(diǎn)(其中 x
和 y
為0)。 隨著不斷的平移,這個(gè)值被更新——如果向右平移,它具有高 x 值; 如果平移返回到它開始的位置,它的 translation 將回到零點(diǎn)。
在 DrawView.swift
中,實(shí)現(xiàn) moveLine(_ :)。 請(qǐng)注意,因?yàn)槟鷮?UIPanGestureRecognizer 類發(fā)送手勢(shì)識(shí)別器方法,所以該方法的參數(shù)必須是 UIPanGestureRecognizer 的實(shí)例的引用,而不是 UIGestureRecognizer 。
func moveLine(_ gestureRecognizer: UIPanGestureRecognizer) {
??print("Recognized a pan")
??// If a line is selected...
??if let index = selectedLineIndex {
????// When the pan recognizer changes its position...
????if gestureRecognizer.state == .changed {
??????// How far has the pan moved?
??????let translation = gestureRecognizer.translation(in: self)
??????// Add the translation to the current beginning and end points of the line
??????// Make sure there are no copy and paste typos!
??????finishedLines[index].begin.x += translation.x
??????finishedLines[index].begin.y += translation.y
??????finishedLines[index].end.x += translation.x
??????finishedLines[index].end.y += translation.y
??????// Redraw the screen
??????setNeedsDisplay()
????}
??} else {
????// If no line is selected, do not do anything
return
??}
}
構(gòu)建并運(yùn)行應(yīng)用程序。 觸摸并按住一條線并開始拖動(dòng)——您會(huì)發(fā)現(xiàn)該線和您的手指不同步。 這到底是怎么回事?
您正在將該當(dāng)前的 translation 重復(fù)添加到該線的原始終點(diǎn)。 當(dāng)您最后一次調(diào)用此方法時(shí),您實(shí)際上需要讓手勢(shì)識(shí)別器來報(bào)告 translation 中的更改。 幸運(yùn)的是,你可以做到這一點(diǎn)。 每次報(bào)告更改時(shí),您可以將平移手勢(shì)識(shí)別器的 translation 設(shè)置回零點(diǎn)。 然后,下一次報(bào)告更改時(shí),它將獲得自上次發(fā)生以來的 translation。
在 DrawView.swift
中的 moveLine(_ :) 底部附近添加以下代碼行。
finishedLines[index].end.x += translation.x
finishedLines[index].end.y += translation.y
gestureRecognizer.setTranslation(CGPoint.zero, in: self)
// Redraw the screen
setNeedsDisplay()
構(gòu)建并運(yùn)行應(yīng)用程序并移動(dòng)一條線。 效果還不錯(cuò)
關(guān)于 UIGestureRecognizer 的更多信息##
你只是用到了 UIGestureRecognizer 的一些基礎(chǔ)知識(shí)。 它還有更多的子類,更多的屬性和更多的委托方法——你甚至可以創(chuàng)建自己的識(shí)別器。 本節(jié)將為講解關(guān)于 UIGestureRecognizer 其它的信息。 您可以查看文檔以了解更多信息。
當(dāng)手勢(shì)識(shí)別器在視圖中時(shí),它確實(shí)會(huì)為您處理所有的 UIResponder 方法,如 touchesBegan(_:with :)。 手勢(shì)識(shí)別器是非常貪心的,所以他們通常不會(huì)讓視圖接觸到觸摸事件,或者他們至少延遲交付這些事件。 您可以在識(shí)別器上設(shè)置屬性,如 delayedTouchesBegan
,delaysTouchesEnded
,cancelsTouchesInView
以更改此行為。 如果您需要比這種全無(wú)變化的方法更好的控制,您可以為識(shí)別器實(shí)現(xiàn)委托方法。
有時(shí),您可能會(huì)有兩個(gè)手勢(shì)識(shí)別器尋找非常相似的手勢(shì)。 您可以將識(shí)別器鏈接在一起,以便一個(gè)失敗后下一個(gè)才能開始使用 require(toFail :) 方法。 你在 init?(coder :) 中使用了這個(gè)方法,使得點(diǎn)擊識(shí)別器等待雙擊識(shí)別器失敗。
掌握手勢(shì)識(shí)別器必須了解的一件事是他們?nèi)绾谓忉屗麄兊臓顟B(tài)。 總體來說,識(shí)別器可以輸入七個(gè)狀態(tài):
UIGestureRecognizerState.possible
UIGestureRecognizerState.failed
UIGestureRecognizerState.began
UIGestureRecognizerState.cancelled
UIGestureRecognizerState.changed
UIGestureRecognizerState.recognized
UIGestureRecognizerState.ended
識(shí)別器花費(fèi)大部分時(shí)間在 possible 狀態(tài)。 當(dāng)手勢(shì)轉(zhuǎn)換到 possible 狀態(tài)或 failed 狀態(tài)之外的任何狀態(tài)時(shí),識(shí)別器的動(dòng)作消息被發(fā)送,并且可以查看其 state
屬性以了解原因。
failed 狀態(tài)用于識(shí)別器等待多點(diǎn)觸控手勢(shì)。 在某些時(shí)候,用戶的手指可能會(huì)達(dá)到一個(gè)位置,從這個(gè)位置他們?cè)僖膊荒軌蜃R(shí)別識(shí)別器的手勢(shì)。 那么手勢(shì)識(shí)別器識(shí)別失敗。 識(shí)別器在中斷時(shí)進(jìn)入 canceled 的狀態(tài),例如通過來電。
如果手勢(shì)是連續(xù)的如平移,那手勢(shì)識(shí)別器將進(jìn)入 began 狀態(tài),然后進(jìn)入 changed 狀態(tài),直到手勢(shì)結(jié)束。 當(dāng)手勢(shì)結(jié)束(或 canceled 狀態(tài) )時(shí),識(shí)別器進(jìn)入 ended(或 canceled)狀態(tài),并在返回到 possible 狀態(tài)之前最后發(fā)送其動(dòng)作消息。
對(duì)于識(shí)別離散手勢(shì)(如點(diǎn)擊)手勢(shì)識(shí)別器,您只能看到 recognized 狀態(tài)(與 ended 狀態(tài)具有相同的值)。
您在本章中未實(shí)現(xiàn)的四個(gè)內(nèi)置識(shí)別器是 UIPinchGestureRecognizer,UISwipeGestureRecognizer,UIScreenEdgePanGestureRecognizer 和 UIRotationGestureRecognizer。 每個(gè)都具有允許您微調(diào)其行為的屬性。 文檔將向您展示怎么做。
最后,如果您想要識(shí)別的手勢(shì)不是由 UIGestureRecognizer 的內(nèi)置子類實(shí)現(xiàn)的,那么您可以自己對(duì) UIGestureRecognizer 進(jìn)行子類化。 這不在本書的范圍之內(nèi)。 您可以閱讀 UIGestureRecognizer 文檔的 Methods for Subclassing
部分,了解需要的內(nèi)容。
白銀挑戰(zhàn):神秘線
應(yīng)用程序中有一個(gè) bug。 如果您點(diǎn)擊一條線,然后在菜單可見時(shí)開始繪制新的,您將拖動(dòng)所選行并同時(shí)繪制新線。 修復(fù)這個(gè)bug。
黃金挑戰(zhàn):速度和大小
當(dāng)你在畫一條線的時(shí)候,你可以用手勢(shì)識(shí)別器來記錄下平移的速度。根據(jù)這個(gè)速度調(diào)整線的厚度。不要只靠猜測(cè) pan 識(shí)別器的速度值可以設(shè)為多大或能有多大。(換句話說,將各種速度記錄到控制臺(tái)。)
白金挑戰(zhàn):顏色
用三指向上滑動(dòng)可以彈出一個(gè)顯示顏色的面板。 選擇這些顏色之一應(yīng)該使您以后繪制的任何線條以該顏色顯示。 通過放置該面板不應(yīng)該畫出額外的線,或者當(dāng)應(yīng)用程序意識(shí)到處理三指滑動(dòng)時(shí),應(yīng)立即刪除任何繪制的線。
更多:UIMenuController 和 UIResponderStandardEditActions
UIMenuController 通常負(fù)責(zé)在顯示時(shí)向用戶展示一個(gè)“編輯”菜單。 (當(dāng)您按住文本字段或文本視圖時(shí)。)因此,未修改的菜單控制器(您未設(shè)置菜單項(xiàng)的控件)已經(jīng)具有其所呈現(xiàn)的默認(rèn)菜單項(xiàng),如 剪切(Cut
),復(fù)制(Copy
) 和其他熟悉的選項(xiàng)。 每項(xiàng)都有一個(gè)相關(guān)聯(lián)的動(dòng)作消息。 例如,當(dāng) Cut
菜單項(xiàng)被點(diǎn)擊時(shí),cut: 被發(fā)送到呈現(xiàn)菜單控制器的視圖。
UIResponder 的所有實(shí)例都會(huì)實(shí)現(xiàn)這些方法,但是默認(rèn)情況下,這些方法不會(huì)執(zhí)行任何操作。 像 UITextField 這樣的子類會(huì)覆蓋這些方法,以便對(duì)其上下文進(jìn)行適當(dāng)?shù)牟僮鳎缂羟挟?dāng)前選擇的文本。 這些方法都在 UIResponderStandardEditActions 協(xié)議中聲明。
如果您在視圖中覆蓋了 UIResponderStandardEditActions 的方法,則其菜單項(xiàng)將自動(dòng)顯示在您為該視圖顯示的任何菜單中。 這是因?yàn)椴藛慰刂破髟谄湟晥D上調(diào)用 canPerformAction(_:withSender :) 方法,它根據(jù)視圖是否實(shí)現(xiàn)此方法返回 true
或 false
。
如果要實(shí)現(xiàn)這些方法之一,但不希望它出現(xiàn)在菜單中,可以覆蓋 canPerformAction(_:withSender :) 返回 false
:
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
??if action == #selector(copy(_:)) {
????return false
??} else {
????// Else return the default behavior
????return super.canPerformAction(action, withSender: sender)
??}
}