Demo GIF
本篇主要講述動畫的實作,手勢的部分留在下篇
動畫的原理是使用 maskView 畫出一個圓,然後對圓進行放大縮小
這邊使用 maskView 的原因是:在背景比較複雜的情況下(非純色),變化的效果會比較清晰,比較符合個人的喜好(?
那在開始前先看一下,在 iOS 中是如何實現自訂的轉場動畫
看上圖可以發現,由A到B,似乎不是我們一般在手機中看到的,移動B畫面。
而是在中間一個 Container View 中實作動畫,當動畫完成的時候,Containter View 中看到的畫面會跟B畫面完全一致,這樣在把 Container View 移除,就可以神不知鬼不覺的,達到一種,B畫面在移動的錯覺
開始!
首先我們先建立一個 class 用來管理我們 present & dismiss 的動畫要透過誰來動。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class CustomTransition: NSObject, UIViewControllerTransitioningDelegate { // 擴散的中心點 var destinationPoint = CGPoint.zero private lazy var presentAnimation = CustomPresentAnimation(startPoint: destinationPoint) private lazy var dismissAnimation = CustomDismissAnimation(endPoint: destinationPoint) func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { return presentAnimation } func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { return dismissAnimation } } |
點UIViewControllerTransitioningDelegate
進去看,可以發現裡面有五個 func 都是 optional
,是這樣的,如果不實作,那系統將會使用原生的效果來顯示。
我在網路上查的多數範例,是直接在 class CustomTransition
實作這個 protocol 然後 return self
,但為了讓 code 看起來簡單一點,我選擇另外建立兩個 class confirm 這個 protocol
Present
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class CustomPresentAnimation: NSObject , UIViewControllerAnimatedTransitioning { var startPoint: CGPoint // 擴散的起始點 private let durationTime = 0.45 // 動畫時間 init(startPoint: CGPoint) { self.startPoint = startPoint super.init() } func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { return durationTime } func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { // ... } } |
接著我們在 func animateTransition
裡面實作我的們想要的動畫效果啦
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { // 取出 toView, 在上面的示意圖中代表的就是 A 畫面 guard let toView = transitionContext.viewController(forKey: .to)?.view else { return } // 取出 container view let containerView = transitionContext.containerView // 建立我們的 mask view,並坐初步設置 let maskView = UIView() maskView.frame.size = CGSize(width: 1, height: 1) maskView.center = startPoint maskView.backgroundColor = .black maskView.layer.cornerRadius = 0.5 toView.mask = maskView containerView.addSubview(toView) // 因為最終 mask view 的圓,需要覆蓋到整個畫面,所以計算出 mask view需要的大小 let containerFrame = containerView.frame let maxY = max(containerFrame.height - startPoint.y, startPoint.y) let maxX = max(containerFrame.width - startPoint.x, startPoint.x) let maxSize = max(maxY, maxX) * 2.1 // 最後我們使用 UIView.animation 顯式動畫,將我們的 mask view 擴散到整個畫面 UIView.animate( withDuration: durationTime, delay: 0, options: [.curveEaseOut], animations: { maskView.frame.size = CGSize(width: maxSize, height: maxSize) maskView.layer.cornerRadius = maxSize / 2.0 maskView.center = self.startPoint }) { (flag) in transitionContext.completeTransition(flag) } } |
反之,Dismiss 如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
class CustomDismissAnimation: NSObject , UIViewControllerAnimatedTransitioning { let endPoint: CGPoint var grayView: UIView! private let durationTime = 0.45 init(endPoint: CGPoint) { self.endPoint = endPoint super.init() } func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { return durationTime } func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { guard let fromView = transitionContext.viewController(forKey: .from)?.view else { return } let containerView = transitionContext.containerView let containerFrame = containerView.frame let maxY = max(containerFrame.height - endPoint.y, endPoint.y) let maxX = max(containerFrame.width - endPoint.x, endPoint.x) let maxSize = max(maxY, maxX) * 2.1 let maskView = UIView() maskView.frame.size = CGSize(width: maxSize, height: maxSize) maskView.center = endPoint maskView.backgroundColor = .black maskView.layer.cornerRadius = maxSize / 2.0 fromView.mask = maskView containerView.addSubview(fromView) UIView.animate( withDuration: durationTime, delay: 0, options: [.curveEaseOut], animations: { maskView.frame.size = CGSize(width: 1, height: 1) maskView.layer.cornerRadius = 0.5 maskView.center = self.endPoint }) { (flag) in transitionContext.completeTransition(!transitionContext.transitionWasCancelled) } } } |
萬事俱備,只欠東風
東西都建立好啦,接著就是如何使用了
我們在A畫面建立 CustomTransition,然後將B畫面的 transitioningDelegate 改為 CustomTransition,
並將 modalPresentationStyle 設置為 .custom
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
class PrepareToPushViewController: UIViewController { let button = UIButton(type: .system) let animation = CustomTransition() override func viewDidLoad() { super.viewDidLoad() // set view layout } } } // MARK: Setup UI methods extension PrepareToPushViewController { @objc func buttonPressed() { let vc = PresentSecondViewController() animation.destinationPoint = button.center vc.transitioningDelegate = animation vc.modalPresentationStyle = .custom animation.interation.wire(viewController: vc) present(vc, animated: true, completion: nil) } } |