iOS動畫:UIViewPropertyAnimator動畫之ViewController Present過渡(17)

靜態ViewController過渡動畫

首先實現一個present的自定義動畫。
創建PresentTransition,實現UIViewControllerAnimatedTransitioning協議:

import UIKit

class PresentTransition: NSObject, UIViewControllerAnimatedTransitioning {
  func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
    return 0.75
  }
  func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
    transitionAnimator(using: transitionContext).startAnimation()
  }
  func transitionAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
    let duration = transitionDuration(using: transitionContext)
    let container = transitionContext.containerView
    let to = transitionContext.view(forKey: .to)!
    
    container.addSubview(to)
    to.transform = CGAffineTransform(scaleX: 1.33, y: 1.33)
    .concatenating(CGAffineTransform(translationX: 0.0, y: 200))
    to.alpha = 0
    
    let animator = UIViewPropertyAnimator(duration: duration, curve: .easeOut)
    animator.addAnimations({
      to.transform = CGAffineTransform(translationX: 0.0, y: 100)
    }, delayFactor: 0.15)
    
    animator.addAnimations({
      to.alpha = 1.0
    }, delayFactor: 0.5)
    animator.addCompletion { (_) in
      transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
    }
    return animator
  }
}

製作present動畫看上去很簡單,前面章節已經介紹過了,這裏不做贅述。
將動畫運用於UIViewControllerTransitioningDelegate的代理方法:

extension LockScreenViewController: UIViewControllerTransitioningDelegate {
  func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    return presentTransition
  }
}

present到編輯頁面:

  @IBAction func presentSettings(_ sender: Any? = nil) {
    //present the view controller
    settingsController = storyboard?.instantiateViewController(withIdentifier: "SettingsViewController") as! SettingsViewController
    settingsController.transitioningDelegate = self
    present(settingsController, animated: true, completion: nil)
  }

當present到編輯頁面時,因爲編輯頁面背景色是透明的,所以看上去效果是這樣的:
image_1
爲PresentTransition添加動畫屬性,讓外部可以注入自己的動畫

  var auxAnimations: (() -> Void)?
  var auxAnimationsCancel: (()->Void)?

在animator返回前加入:

    if let auxAnimations = auxAnimations {
      animator.addAnimations(auxAnimations)
    }

在LockScreenViewController的presentSettings方法中添加動畫:

    presentTransition.auxAnimations = blurAnimations(true)

運行效果:
image_2
當用戶點擊cancel時dismiss編輯界面:

    settingsController.didDismiss = { [unowned self] in
      self.toggleBlur(false)
    }

ViewController過渡交互

和UINavigationController動畫交互類似,所使用的類是一樣的,都是UIPercentDrivenInteractiveTransition。
替換PresentTrasition中的類繼承:

class PresentTransition: UIPercentDrivenInteractiveTransition, UIViewControllerAnimatedTransitioning

UIPercentDrivenInteractiveTransition的三個主要方法(update,cancel,finish)前面已經介紹過了。它還有一些其他的屬性及方法:
timingCurve:跟前面章節一樣,提供交互結束後的動畫曲線
wantsInteractiveStart:默認值爲true,如果設置爲false,你將不能進行交互,你可以通過pause()後繼續進行交互。
paused():調用這個方法將暫停非交互性的過渡動畫,並進入交互模式。
添加新的方法:

  func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
    return transitionAnimator(using: transitionContext)
  }

這個方法是UIViewControllerAnimatedTransitioning協議裏面的,它允許你提供一箇中斷的動畫。
你的過渡動畫現在有兩種不同的形式:
1.非交互性的動畫,UIKit將回調animateTransition(using:)代理方法用於實現動畫。
2.交互性的動畫,UIKit將回調interruptibleAnimator(using:)代理方法用於實現動畫。
流程圖爲:
image_3
切換到LockScreenViewController,添加UIViewControllerTransitioningDelegate的代理方法:

  func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
    return presentTransition
  }

添加屬性:

  var isDragging = false
  var isPresentingSettings = false

當用戶下拉table時,設置isDragging爲true,當下拉到足夠距離設置isPresentingSettings爲true,實現scroll代理方法:

extension LockScreenViewController: UIScrollViewDelegate {
  func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
    isDragging = true
  }
  func scrollViewDidScroll(_ scrollView: UIScrollView) {
    guard isDragging else {
      return
    }
    if !isPresentingSettings && scrollView.contentOffset.y < -30 {
      isPresentingSettings = true
      presentTransition.wantsInteractiveStart = true
      presentSettings()
      return
    }
  }
}

在scrollViewDidScroll(_:)中添加交互代碼:

    if isPresentingSettings {
      let progress = max(0.0, min(1.0, ((-scrollView.contentOffset.y) - 30) / 90.0))
      presentTransition.update(progress)
    }

當用戶慢慢下拉時,table慢慢變模糊,運行效果:
image_4
當完成和取消交互式過渡時,你需要做一些處理:

  func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
    let progress = max(0.0, min(1.0, ((-scrollView.contentOffset.y) - 30) / 90.0))
    if progress > 0.5 {
      presentTransition.finish()
    }else {
      presentTransition.cancel()
    }
    isPresentingSettings = false
    isDragging = false
  }

修改PresentTransition中動畫完成時的block:

    animator.addCompletion { position in
      switch position {
      case .end:
        transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
      default:
        transitionContext.completeTransition(false)
      }
    }

只有當動畫在end位置結束時,纔算present完成。其它都是未完成。
在iOS10下反覆拉列表,過渡開始後立馬取消會出現問題,iOS11下已經修復了這個問題:
image_5
這跟visual effect view有關,當這種view放在block動畫裏面,當動畫反轉或者取消時似乎不會被移除掉,所以看起來一團糟,所以我們需要手動將其移除,那就要用到我們前面定義的auxAnimationsCancel。
找到animator.addCompletion,添加代碼到default:

self.auxAnimationsCancel?()

當動畫沒有完成時,它將調用,所以我們在LockScreenViewController中將它移除

    presentTransition.auxAnimationsCancel = blurAnimations(false)

這就解決啦。
新的問題又出現了,因爲wantsInteractiveStart默認值是true,所以點擊edit時不會調用animateTransition(using:)中的方法,非交互式動畫將不會開始,所以在點擊edit時修改wantsInteractiveStart爲false

	self.presentTransition.wantsInteractiveStart = false

現在我們來考慮一種情況,在用戶點擊“Edit”時,動畫期間當用戶再次點擊屏幕,我們需要暫停轉換,這就需要考慮transition在交互式和非交互式之間切換。
切換到PresentTranstion.swift,你不僅需要分別處理交互式模式和非交互式模式,而且還要處理它們之間的切換,添加屬性來保存動畫上下文:

  var context: UIViewControllerContextTransitioning?
  var animator: UIViewPropertyAnimator?

在transitionAnimator(using:)裏面添加:

    self.animator = animator
    self.context = transitionContext

動畫完成時設置爲nil

    animator.addCompletion { [unowned self] _ in
      self.animator = nil
      self.context = nil
    }

現在你可以添加一個方法來中斷transition

  func interruptTransition() {
    guard let context = context else { return }
    context.pauseInteractiveTransition()//暫停animator
    pause()//將transition切換到交互模式
  }

爲了允許在非交互模式下能點擊,你需要設置animator響應手勢

    animator.isUserInteractionEnabled = true

當進入交互模式後,允許用戶取消或者完成transition,在LockScreenViewController中添加屬性

    var touchesStartPointY: CGFloat?

點擊屏幕時中斷transition

  override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    guard presentTransition.wantsInteractiveStart == false, presentTransition.animator != nil else {
      return
    }
    touchesStartPointY = touches.first!.location(in: view).y
    presentTransition.interruptTransition()
  }

當用戶進行拖動時,修改touchesMoved方法:

  override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
    guard let startY = touchesStartPointY else { return }
    let currentPoint = touches.first!.location(in: view).y
    if currentPoint < startY - 40 {
      touchesStartPointY = nil
      presentTransition.animator?.addCompletion({ (_) in
        self.blurView.effect = nil
      })
      presentTransition.cancel()
    }else if currentPoint > startY + 40 {
      touchesStartPointY = nil
      presentTransition.finish()
    }
  }

運行效果
image_6

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章