自定義控件:利用 3D Touch 確認 Button 操作

xtymmms102 7年前發布 | 8K 次閱讀 iOS開發 移動開發

在我看來,3D Touch 是能夠追蹤用戶按壓屏幕力度、并且是 iOS 的觸碰處理中最有意思且未被充分挖掘的一個能力特性。

通過這個教程,我們會創建一個自定義的按鈕,并且要求用戶通過 3D Touch 操作進行確認。如果用戶的設備不支持 3D Touch,控件對用戶的處理也會回退到備選方案。下面是預覽視頻,它能夠讓你快速了解這個自定義控件是如何工作的:

  1. 當用戶開始點擊屏幕時,一個圓形的進度條就會跟蹤用戶按壓屏幕的力度。用戶按壓屏幕的力度會影響圓形視圖填充進度,按得越用力,圓就被填充得越多(稍后我會展示在不支持 3D Touch 的設備上模擬該行為)。

  2. 當圓形被填充滿的時候,它會變成一個處于激活狀態的按鈕,圓形進度條里的標簽內容會變成 “OK” 且顏色變成綠色,這暗示著當前操作可以被確認。此時用戶可以通過向上滑動手指并在圓圈上松開手指的方式來確認此操作。

通常,我們會通過彈窗的方式來詢問用戶是否想進行一個刪除操作。我很樂意做一些 UX 交互方面的嘗試,而且我認為 3D Touch 這種新的交互方式可以很好的替代原有的 “標準” 交互流程。你真的應該在一個實體機上體驗一下 3D Touch,馬上你就會了解到交互的便利性。:grinning:

代碼擼起來

如果你還不知道自定義控件的工作原理,我強烈建議你閱讀一下之前我寫的一篇關于 創建自定義控件 的教程,下載配套的工程文件。這樣你就能輕松 hold 住接下來的內容了。

UI 畫起來

當用戶與按鈕控件進行交互的時候會繪制圓形控件和標簽控件,實現這個需求的代碼很簡單,讓我們一起看下:

private let circle = CAShapeLayer()
private let msgLabel = CATextLayer()
private let container = CALayer()
.
.
.

private func drawControl(){

    // Circle
    var transform = CGAffineTransform.identity
    circle.frame = CGRect(x: 0, y: 0, width: size.width, height: size.height)
    circle.path = CGPath(ellipseIn: CGRect(x: 0,y: 0,width: size.width, height: size.height),
                         transform: &transform)

    circle.strokeColor = UIColor.white.cgColor
    circle.fillColor = UIColor.clear.cgColor
    circle.lineWidth = 1
    circle.lineCap = kCALineCapRound
    circle.strokeEnd = 0 // initially set to 0
    circle.shadowColor = UIColor.white.cgColor
    circle.shadowRadius = 2.0
    circle.shadowOpacity = 1.0
    circle.shadowOffset = CGSize.zero
    circle.contentsScale = UIScreen.main.scale

    // Label
    msgLabel.font = UIFont.systemFont(ofSize: 3.0)
    msgLabel.fontSize = 12
    msgLabel.foregroundColor = UIColor.white.cgColor
    msgLabel.string = ""
    msgLabel.alignmentMode = "center"
    msgLabel.frame = CGRect(x: 0, y: (size.height / 2) - 8.0, width: size.width, height: 12)
    msgLabel.contentsScale = UIScreen.main.scale

    // Put it all together
    container.frame = CGRect(x: 0, y: 0, width: size.width, height: size.height)
    container.addSublayer(msgLabel)
    container.addSublayer(circle)

    layer.addSublayer(container)
}

圓形 和 標簽 的 layer 在這段代碼中初始化后,被添加到了 容器 的 layer 中。這段代碼沒有需要特別關注的地方,僅需要注意的是圓形的 strokeEnd 屬性值是 0。

對于任意一個圖形的 layer,可以使用這個屬性來對其進行動畫操作。簡單地說,系統會在 strokeStart 和 strokeEnd 之間渲染圖形 layer 的路徑,而這兩個屬性的默認值是 0 和 1,所以利用這個值區間是可以玩出許多漂亮的動畫效果。但對于當前這個控件,我們設置 strokeEnd 的值為 0 ,因為我們要使用用戶按壓屏幕的力度來做動畫。

控件的狀態

使用 ConfirmActionButtonState 枚舉類型來定義當前控制器的 UI 和行為狀態。

enum ConfirmActionButtonState {
    case idle
    case updating
    case selected
    case confirmed
}

當該控件上沒有任何操作時,它的狀態是 idle ;當用戶開始進行交互時,它的狀態會變成 updating ;當圓形控件被填充完畢的時候,它的狀態會變成 selected ;如果此時用戶將手指移動到綠色的圓圈中,它的狀態又會繼續變成 confirmed 。

如果用戶手指離開了屏幕,且控件的狀態已經是 confirmed 的時候,我們會繼續傳遞這個按鍵操作。因為這時按鈕已處于確認狀態;反之,按鈕又會回到 idle 態。

處理用戶的點擊

我們重寫了 beginTracking 、 continueTracking 和 endTracking 三個方法來響應用戶的點擊并為自定義控件提供所有的信息。

通過這些方法我們會跟蹤三個元素:

  1. 觸摸點位置(touch Location) 。用于決定應該在哪里繪制容器視圖的 layer 層(它包含了圓形視圖和信息標簽)。
  2. 按壓屏幕的力度(touch force) 值。需要根據這個值來設置圓形控件的動畫并且通過它來決定自定義控件的狀態是否應該設置成 updating 、 selected 或者 confirmed 。
  3. 更新后的觸摸點的位置(updated touch location) 。我們必須跟蹤用戶的觸摸點來驗證它是否在容器視圖 layer 層的 bounds 內,如果滿足這個條件,我們就需要更新狀態為 confirmed 或者 updating 。

首先看看 begingTracking 方法的代碼。

  override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
      super.beginTracking(touch, with: event)

      if traitCollection.forceTouchCapability != UIForceTouchCapability.available{
// fallback code ….
      }

      let initialLocation = touch.location(in: self)

      CATransaction.begin()
      CATransaction.setDisableActions(true)
      container.position = initialLocation ++ CGPoint(x: 0, y: -size.height)
      CATransaction.commit()

      return true
  }

首先我們檢查設備是否可以使用 3D Touch,如果不支持這個特性的話,我們會執行一個備選代碼(在后面會具體討論備選代碼的事情)。然后通過觸摸點的位置減去自定義控件高度的方式來計算容器視圖 layer 的位置。 ++ 操作符的定義在文件的最下面,它的作用就是允許 CGPoint 類型的元素進行加法計算。

為了避免系統的隱式動畫,需要在 setDisableActions 方法后設置容器視圖的位置。

在 continueTracking 這個函數中,我們執行所有必要的操作來確認控件的狀態。

override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
    super.continueTracking(touch, with: event)
    lastTouchPosition = touch
    updateSelection(with:touch)

    return true
}

當 updateSelection 方法中的 touch 更新后,不支持 3D Touch 的設備會使用到 lastTouchPosition 做一些處理,具體的內容后面會詳細介紹。

updateSelection 的代碼如下所示:

private func updateSelection(with touch: UITouch) {

    if self.traitCollection.forceTouchCapability == UIForceTouchCapability.available{
        intention = 1.0 * (min(touch.force, 3.0) / min(touch.maximumPossibleForce, 3.0))
    }

    if intention > 0.97 {
        if container.frame.contains(touch.location(in:self)){
            selectionState = .confirmed
        }else{
            selectionState = .selected
        }
        updateUI(with: 1.0)
    }
    else{
        if !container.frame.contains(touch.location(in:self)){
            selectionState = .updating
            updateUI(with: intention)
        }
    }
}

同樣,首先要檢查設備是否支持 3D Touch 特性,如果支持這個特性,我們會計算當前的“用戶意向”,這個 intention 屬性的值區間在 0(沒有觸摸事件被檢測到)到 1(按壓屏幕的力度達到了所需的最大值)之間。獲取這個屬性值的方法很簡單:用當前壓力除以最大壓力的值作為 intention 的值即可。經過真機調試后,我發現如果使用這種方式實現的話,用戶需要用很大的力量來按壓屏幕才能達到最大值,出于節省力氣的考慮,我對壓力值做了一個 3.0 的上限。

(事實上,我不太確定使用 “intention” 作為命名是不是一個好的選擇…使用英語做母語的朋友們,請讓我知道這個命名是否明確的表達了這個屬性的作用:stuck_out_tongue_closed_eyes:)

現在通過這個觸摸循環可以計算出 intention 的具體值,從而就可以利用它來更新 UI 和控件的狀態。如果 intention 的值大于 0.97 且用戶的觸摸點已經在綠色圓形區域內,這個控件的狀態就會變為 confirmed ,否則,即使用戶一直按壓刪除按鈕,控件的狀態也只是停留在 selected 。如果 intention 的值小于 0.97,控件的狀態會處于 updating 的狀態。

updateUI 方法會拿到當前的 intention 值,并把它賦值給圓形視圖 layer 的 endStroke 屬性。任何與 intention 相關的 UI 操作都可以放在這個方法中進行。

private func updateUI(with value:CGFloat){
    circle.strokeEnd = value
}

最后,我們重寫了 endTracking 方法。當控件狀態為 confirmed 的時候,該方法可以觸發 valueChanged 事件。

override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
    super.endTracking(touch, with: event)
    intention = 0

    if selectionState == .confirmed{
        self.sendActions(for: UIControlEvents.valueChanged)
    }else{
        selectionState = .idle
        circle.strokeEnd = 0
    }
}

如果你仔細查看了 Main.storyboard 文件,你就會發現刪除按鈕的 valueChanged 動作已經與 ViewController 的 confirmDelete 方法相關聯了。并且比較容易發現這個刪除按鈕的 class 屬性已經設置成 ConfirmActionButton 了。

控件狀態和 UI

這個控件的 UI 是與與其自身狀態息息相關的。為了簡化,我們將更新 UI 的代碼直接放在了 selectionState 屬性的 didSet 方法中。

這段代碼很簡單,它包含了根據狀態來更新圓形視圖顏色和標簽文字內容的操作,以及對圓形視圖調用 setNeedLayout 方法進行重繪的操作。

private var selectionState:ConfirmActionButtonState = .idle {
    didSet{
        switch self.selectionState {
        case .idle, .updating:
            if oldValue != .updating || oldValue != .idle {
                circle.strokeColor = UIColor.white.cgColor
                circle.shadowColor = UIColor.white.cgColor
                circle.transform = CATransform3DIdentity
                msgLabel.string = ""
            }

        case .selected:
            if oldValue != .selected{
                circle.strokeColor = UIColor.red.cgColor
                circle.shadowColor = UIColor.red.cgColor
                circle.transform = CATransform3DMakeScale(1.1, 1.1, 1)
                msgLabel.string = "CONFIRM"
            }

        case .confirmed:
            if oldValue != .confirmed{
                circle.strokeColor = UIColor.green.cgColor
                circle.shadowColor = UIColor.green.cgColor
                circle.transform = CATransform3DMakeScale(1.3, 1.3, 1)
                msgLabel.string = "OK"
            }
        }
        circle.setNeedsLayout()
    }
}

備選代碼

我們快速瀏覽一下為不支持 3D Touch 特性的設備而提供的備選代碼。由于我想在所有的設備上保持相同的設計效果,所以在不支持 3D Touch 特性的設備中,我讓 intention 屬性與時間關聯在了一起,而不再是按壓屏幕的力度值。其他方面的邏輯與我們之前所說的保持一致,但是當用戶按壓 delete 按鈕時, intention 屬性會以 0.1 秒的速度更新。下面就是在 beginTracking 中如何定義計時器的了:

if traitCollection.forceTouchCapability != UIForceTouchCapability.available{
    timer = Timer.scheduledTimer(timeInterval: 0.1,
                                 target: self,
                                 selector: #selector(ConfirmActionButton.updateTimedIntention),
                                 userInfo: nil,
                                 repeats: true)
    timer?.fire()
}

updateTimedIntention 方法能在兩秒內將 intention 的值更新到最大值(1.0):

func updateTimedIntention(){
    intention += CGFloat(0.1 / 2.0)
    updateSelection(with: lastTouchPosition)
}

小結

我十分享受寫這段代碼的過程,而且在后面的日子里我還會討論其他自定義控件。在我看來,利用設備的新特性來改進自定義 UI 和提升用戶體驗的工作還有很大的進步空間…我希望這個教程能對你有所啟發:grinning:。

 

來自:http://swift.gg/2017/03/20/custom-controls-3d-touch-confirm/

 

 本文由用戶 xtymmms102 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!