大家好劳闹,我是Tony。今天和大家分享一下我做的AppStore里面的其中兩個(gè)Animation洽瞬。就是當(dāng)大家點(diǎn)擊Card是和離開(kāi)Card時(shí)的Animation。
這里我先放一個(gè)我錄制的Gif給大家看下效果
這只是其中一個(gè)簡(jiǎn)單的效果业汰,當(dāng)然還有很多其他的效果值得我們?nèi)ネ诰蚧锴浴D敲矗@個(gè)簡(jiǎn)單的效果是怎么做成的呢样漆。首先我要給大家看一張圖
Transitioning Delegate
每一個(gè)controller都可以有自己的transitioningDelegate为障,也就是UIViewControllerTransitioningDelegate,每當(dāng)你顯示或者退出一個(gè)view controller的時(shí)候放祟, UIKit就會(huì)需要一個(gè)transitioning delegate鳍怨。當(dāng)然,每個(gè)方法都會(huì)有自己的default的animation跪妥,比如presentViewController就是從下往上顯示下一個(gè)ViewController鞋喇。
Animation Controller
在這里面它會(huì)返回一個(gè)object。并且這個(gè)Controller會(huì)繼承UIViewControllerAnimatedTransitioning眉撵,里面包括你自定義的animation侦香。
Transitioning Context
這里是官方的描述:
Declaration
protocol UIViewControllerContextTransitioning : NSObjectProtocol
Description
Description
A set of methods that provide contextual information for transition animations between view controllers.
Do not adopt this protocol in your own classes, nor should you directly create objects that adopt this protocol. During a transition, the animator objects involved in that transition receive a fully configured context object from UIKit. Custom animator objects—objects that adopt the UIViewControllerAnimatedTransitioning or UIViewControllerInteractiveTransitioning protocol—should simply retrieve the information they need from the provided object.
A context object encapsulates information about the views and view controllers involved in the transition. It also contains details about the how to execute the transition. For interactive transitions, the interactive animator object uses the methods of this protocol to report the animation’s progress. When the animation starts, the interactive animator object must save a pointer to the context object. Based on user interactions, the animator object then calls the updateInteractiveTransition(_:), finishInteractiveTransition(), or cancelInteractiveTransition() methods to report the progress toward completing the animation. Those methods send that information to UIKit so that it can drive the timing of the animations.
Important
When defining custom animator objects, always check the value returned by the isAnimated() method to determine whether you should create animations at all. And when you do create transition animations, always call the completeTransition(_:) method from an appropriate completion block to let UIKit know when all of your animations have finished.
大意就是說(shuō)transitioning context會(huì)用到UIViewControllerContextTransitioning,但是UIKit會(huì)幫你用到這個(gè)Protocol纽疟,正如圖面畫(huà)的那樣罐韩。并且在你結(jié)束的時(shí)候需要用到completeTransition。
The Transitioning Process
接下來(lái)是presentation transition的幾個(gè)步驟:
- 我們觸發(fā)transition
- UIKit會(huì)向view controller要transitioning delegate污朽,如果沒(méi)有UIKit就會(huì)用內(nèi)建的默認(rèn)的散吵。
- 當(dāng)UIKit看到了transitioning delegate之后,會(huì)觸發(fā)這個(gè)protocol的第一個(gè)方法:animationController(forPresented:presenting:source:)蟆肆。如果沒(méi)有矾睦,就用默認(rèn)的。
- UIKit會(huì)構(gòu)建一個(gè)transitioning context颓芭。
- UIkit之后會(huì)詢(xún)問(wèn)controller里面的transitionDuration(using:)的方法
- UIKit接著call animateTransition(using:) function顷锰。
- 最后,call completeTransition(_:)表示過(guò)程結(jié)束亡问。
說(shuō)了那么多官紫,不如動(dòng)手來(lái)做做肛宋。
首先給大家一個(gè)start project,在我的github里面可以下載
打開(kāi)之后大家可以先運(yùn)行程序束世,發(fā)現(xiàn)這里用的是默認(rèn)的
self.present(controller, animated: true, completion: nil)
首先第一步是, 按照我們剛才說(shuō)的那樣酝陈,創(chuàng)建一個(gè)自定義的animation controller。在Controllers里面創(chuàng)建一個(gè)新的文件名字為PresentSectionViewController
接著import uikit毁涉, 以及創(chuàng)建class:
import UIKit
class PresentSectionViewController: NSObject, UIViewControllerAnimatedTransitioning {
}
當(dāng)寫(xiě)完之后會(huì)出現(xiàn)報(bào)錯(cuò)沉帮,因?yàn)閁IViewControllerAnimatedTransitioning是一個(gè)protocol,所以必須滿(mǎn)足這個(gè)協(xié)議就要添加兩個(gè)function贫堰。
第一個(gè)function就很簡(jiǎn)單了穆壕,意思就是問(wèn)要多久,我們?cè)谶@里就填入0.6其屏。當(dāng)然喇勋,如果你想讓你的app更加靈活,就可以加入variable偎行,然后在Controller里面定也行川背。
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.6
}
接下來(lái)我們需要理解的就是,這個(gè)animation不是將原來(lái)的cell移動(dòng)成點(diǎn)進(jìn)去的那樣蛤袒,也就是說(shuō)我們沒(méi)有動(dòng)原來(lái)的cell熄云,而是將進(jìn)去之后的頁(yè)面變成其實(shí)的cell的大小以及位置,然后變大的妙真。所以這里我們需要cell的位置缴允,也就是cell的frame。我們用一個(gè)variable去儲(chǔ)存它隐孽。
var cellFrame: CGRect!
然后我們需要在animateTransition(using transitionContext: UIViewControllerContextTransitioning)加入東西了
// 1
let toVC = transitionContext.viewController(forKey: .to) as! AppDetailController
let containerView = transitionContext.containerView
let bounds = UIScreen.main.bounds
// 2
containerView.addSubview(toVC.view)
這里的1和2分別是什么意思呢癌椿?首先,第一個(gè)variable toVC的意思是我需要獲取這次animation到達(dá)的controller菱阵,也就是to踢俄。當(dāng)然,from對(duì)應(yīng)來(lái)的controller晴及。后面我們用as都办!是因?yàn)槲覀兛隙ù_定去的controller是AppDetialController,所以我們可以這么做虑稼,如果不知道琳钉,千萬(wàn)不可以這樣,不然會(huì)報(bào)錯(cuò)的蛛倦。
接著這個(gè)containerView是指我們的animation的最底層的這個(gè)View歌懒。我給大家看一張圖就明白了。
在UIWindow的上面溯壶,APPDetailController里面UIView的下面有這么一個(gè)UITransitioning View及皂。這個(gè)view就是我們這里的containerView甫男。所以我們之后會(huì)往上面加?xùn)|西。
這個(gè)bounds就是整個(gè)手機(jī)屏幕的frame验烧。
在第二步里我們就需要把toVC.view加到contrainerView里面板驳,也就是把AppDetailController的View加到上面。
// 3
toVC.image.layer.cornerRadius = 14
toVC.imageHeightConstraint.constant = 460
toVC.viewTopConstraint.constant = cellFrame.origin.y - 20
toVC.viewLeftConstraint.constant = cellFrame.origin.x
toVC.viewRightConstraint.constant = -(bounds.width - cellFrame.origin.x - cellFrame.width)
toVC.viewBottomConstraint.constant = -(bounds.height - cellFrame.origin.y - cellFrame.height)
toVC.view.layoutIfNeeded()
在第三步里面碍拆,有很多東西我們沒(méi)有在之前看到若治,就是這些constraint。這些constraint是auto constraint的variable感混。在AppDetailController里面端幼,我們用的是anchor的function,不妨打開(kāi)Extension里面的Anchor弧满,其實(shí)就是用的auto constraint静暂。如果不知道這是什么可以在評(píng)論里面問(wèn)我。但是由于我們用的這個(gè)constraint在這個(gè)PresentSectionViewControllerl里面不可以用到谱秽,所有我們需要再AppDetailController創(chuàng)建這些variable。
var viewTopConstraint: NSLayoutConstraint!
var viewLeftConstraint: NSLayoutConstraint!
var viewRightConstraint: NSLayoutConstraint!
var viewBottomConstraint: NSLayoutConstraint!
其次我們需要添加一個(gè)UIView摹迷, 名字為background疟赊,然后修改image。background是為了把image放在這個(gè)上面峡碉,并且加入它的constaint:
let background: UIView = {
let view = UIView()
view.backgroundColor = .white
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
let image: UIImageView = {
let image = UIImageView()
image.image = #imageLiteral(resourceName: "flBackground")
image.contentMode = .scaleAspectFill
image.clipsToBounds = true
image.isUserInteractionEnabled = true
image.translatesAutoresizingMaskIntoConstraints = false
return image
}()
fileprivate func setupViews() {
view.backgroundColor = .white
view.addSubview(background)
background.addSubview(image)
image.addSubview(cancelButton)
image.addSubview(icon)
image.addSubview(getButton)
viewTopConstraint = background.topAnchor.constraint(equalTo: view.topAnchor)
viewLeftConstraint = background.leftAnchor.constraint(equalTo: view.leftAnchor)
viewRightConstraint = background.rightAnchor.constraint(equalTo: view.rightAnchor)
viewBottomConstraint = background.bottomAnchor.constraint(equalTo: view.bottomAnchor)
NSLayoutConstraint.activate([
viewTopConstraint,
viewLeftConstraint,
viewRightConstraint,
viewBottomConstraint
])
image.anchor(top: background.topAnchor, left: background.leftAnchor, bottom: nil, right: view.rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 550)
cancelButton.anchor(top: image.topAnchor, left: nil, bottom: nil, right: image.rightAnchor, paddingTop: 14, paddingLeft: 0, paddingBottom: 0, paddingRight: 14, width: 28, height: 28)
cancelButton.addTarget(self, action: #selector(handleCancel), for: .touchUpInside)
icon.anchor(top: image.topAnchor, left: image.leftAnchor, bottom: nil, right: nil, paddingTop: 14, paddingLeft: 14, paddingBottom: 0, paddingRight: 0, width: 80, height: 80)
getButton.anchor(top: nil, left: nil, bottom: image.bottomAnchor, right: image.rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 20, paddingRight: 14, width: 80, height: 28)
}
接著我們需要加入imageHeightConstraint近哟。把image的anchor改為:
imageHeightConstraint = image.heightAnchor.constraint(equalToConstant: 550)
NSLayoutConstraint.activate([
image.topAnchor.constraint(equalTo: background.topAnchor),
image.leftAnchor.constraint(equalTo: background.leftAnchor),
image.rightAnchor.constraint(equalTo: background.rightAnchor),
imageHeightConstraint
])
對(duì)于下面的邏輯也是很簡(jiǎn)單的。
比如說(shuō)toVC.viewTopConstraint.constant需要等于cell的y軸減去20(status bar)的高度鲫寄,需要距離上面這么多也就正好是cell的頂部了吉执。其他同理。不明白可以評(píng)論問(wèn)我地来。
// 3
toVC.image.layer.cornerRadius = 14
toVC.imageHeightConstraint.constant = 460
toVC.viewTopConstraint.constant = cellFrame.origin.y - 20
toVC.viewLeftConstraint.constant = cellFrame.origin.x
toVC.viewRightConstraint.constant = -(bounds.width - cellFrame.origin.x - cellFrame.width)
toVC.viewBottomConstraint.constant = -(bounds.height - cellFrame.origin.y - cellFrame.height)
toVC.view.layoutIfNeeded()
再就是animation的部分了
// 4
let animator = UIViewPropertyAnimator(duration: 0.6, dampingRatio: 0.7) {
toVC.viewTopConstraint.constant = 0
toVC.viewLeftConstraint.constant = 0
toVC.viewRightConstraint.constant = 0
toVC.viewBottomConstraint.constant = 0
toVC.image.layer.cornerRadius = 0
toVC.imageHeightConstraint.constant = 550
toVC.view.layoutIfNeeded()
}
animator.startAnimation()
// 5
animator.addCompletion { (finished) in
transitionContext.completeTransition(true)
}
第四部分就是animation結(jié)束時(shí)需要的狀態(tài)戳玫,也就是距離上下左右都是0
第五部分就是告訴UIKit我們結(jié)束了
這里我們結(jié)束了AnimationController的code部分,接下來(lái)我們需要再ViewController里面用到這個(gè)協(xié)議:
extension ViewController: UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return presentSectionViewController
}
}
并且修改didselect的function
fileprivate var presentSectionViewController = PresentSectionViewController()
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let controller = AppDetailController()
controller.transitioningDelegate = self
let attributes = collectionView.layoutAttributesForItem(at: indexPath)!
let cellFrame = collectionView.convert(attributes.frame, to: view)
presentSectionViewController.cellFrame = cellFrame
self.present(controller, animated: true, completion: nil)
}
這下就好了未斑,效果呢還是很接近的了咕宿。希望大家喜歡。