第十九章——UIGestureRecognizer 和 UIMenuController【譯】

在第十八章中,您通過實(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.xcodeprojDrawView.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.swifttap(_ :) 方法中執(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。

選擇一條線,然后雙擊以清除所有行,而菜單控制器仍然可見。 如果 selectedLineIndexnil,菜單控制器應(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 子類:

UILongPressGestureRecognizerUIPanGestureRecognizer。

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) cancelsTouchesInViewtrue 時(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)?cancelsTouchesInViewfalse,所以可以識(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)(其中 xy 為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è)置屬性,如 delayedTouchesBegandelaysTouchesEndedcancelsTouchesInView 以更改此行為。 如果您需要比這種全無(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,UIScreenEdgePanGestureRecognizerUIRotationGestureRecognizer。 每個(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)此方法返回 truefalse。

如果要實(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)
??}
}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 手勢(shì)識(shí)別器是附加到視圖的對(duì)象,將低級(jí)別事件處理代碼轉(zhuǎn)換為更高級(jí)別的操作,它允許視圖以控件執(zhí)行的方式響應(yīng)操作。 手勢(shì)...
    坤坤同學(xué)閱讀 4,139評(píng)論 0 9
  • 手勢(shì)識(shí)別器(Gesture Recognizer)用于識(shí)別觸摸序列并觸發(fā)響應(yīng)事件。當(dāng)手勢(shì)識(shí)別器識(shí)別到一個(gè)手勢(shì)或手勢(shì)...
    pro648閱讀 6,156評(píng)論 0 13
  • 0、緣起 之所以要寫這篇文章,是因?yàn)榘l(fā)現(xiàn)在實(shí)際編程處理點(diǎn)擊事件的過程中,知道響應(yīng)鏈和探測(cè)鏈根本沒有一點(diǎn)用處。 即使...
    吳佩在天涯閱讀 44,292評(píng)論 33 127
  • block對(duì)象簡(jiǎn)介及語(yǔ)法 什么是block? block對(duì)象是一組指令,可以像調(diào)用函數(shù)指令那樣調(diào)用block對(duì)象。...
    dreamCatcher閱讀 1,208評(píng)論 0 14
  • 1.今天早上學(xué)習(xí)了時(shí)間成本 2.今天早上七點(diǎn)出門去地鐵蘇州街B口發(fā)資料(只是發(fā)資料),有2個(gè)人主動(dòng)加我,有兩個(gè)人意...
    鵬鵬YH閱讀 234評(píng)論 1 2