Swift Rebuild AppStore Card Animation

thumbnail.png

大家好劳闹,我是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è)步驟:

  1. 我們觸發(fā)transition
  2. UIKit會(huì)向view controller要transitioning delegate污朽,如果沒(méi)有UIKit就會(huì)用內(nèi)建的默認(rèn)的散吵。
  3. 當(dāng)UIKit看到了transitioning delegate之后,會(huì)觸發(fā)這個(gè)protocol的第一個(gè)方法:animationController(forPresented:presenting:source:)蟆肆。如果沒(méi)有矾睦,就用默認(rèn)的。
  4. UIKit會(huì)構(gòu)建一個(gè)transitioning context颓芭。
  5. UIkit之后會(huì)詢(xún)問(wèn)controller里面的transitionDuration(using:)的方法
  6. UIKit接著call animateTransition(using:) function顷锰。
  7. 最后,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贫堰。

protocol.png

第一個(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歌懒。我給大家看一張圖就明白了。

transitioning.png

在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)
}

這下就好了未斑,效果呢還是很接近的了咕宿。希望大家喜歡。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末蜡秽,一起剝皮案震驚了整個(gè)濱河市府阀,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌芽突,老刑警劉巖试浙,帶你破解...
    沈念sama閱讀 222,590評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異寞蚌,居然都是意外死亡田巴,警方通過(guò)查閱死者的電腦和手機(jī)钠糊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,157評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)固额,“玉大人眠蚂,你說(shuō)我怎么就攤上這事《孵铮” “怎么了逝慧?”我有些...
    開(kāi)封第一講書(shū)人閱讀 169,301評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)啄糙。 經(jīng)常有香客問(wèn)我笛臣,道長(zhǎng),這世上最難降的妖魔是什么隧饼? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 60,078評(píng)論 1 300
  • 正文 為了忘掉前任沈堡,我火速辦了婚禮,結(jié)果婚禮上燕雁,老公的妹妹穿的比我還像新娘诞丽。我一直安慰自己,他們只是感情好拐格,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,082評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布僧免。 她就那樣靜靜地躺著,像睡著了一般捏浊。 火紅的嫁衣襯著肌膚如雪懂衩。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 52,682評(píng)論 1 312
  • 那天金踪,我揣著相機(jī)與錄音浊洞,去河邊找鬼。 笑死胡岔,一個(gè)胖子當(dāng)著我的面吹牛法希,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播姐军,決...
    沈念sama閱讀 41,155評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼铁材,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了奕锌?” 一聲冷哼從身側(cè)響起著觉,我...
    開(kāi)封第一講書(shū)人閱讀 40,098評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎惊暴,沒(méi)想到半個(gè)月后饼丘,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,638評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡辽话,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,701評(píng)論 3 342
  • 正文 我和宋清朗相戀三年肄鸽,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了卫病。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,852評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡典徘,死狀恐怖蟀苛,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情逮诲,我是刑警寧澤帜平,帶...
    沈念sama閱讀 36,520評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站梅鹦,受9級(jí)特大地震影響裆甩,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜齐唆,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,181評(píng)論 3 335
  • 文/蒙蒙 一嗤栓、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧箍邮,春花似錦茉帅、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,674評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至廷蓉,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間马昙,已是汗流浹背桃犬。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,788評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留行楞,地道東北人攒暇。 一個(gè)月前我還...
    沈念sama閱讀 49,279評(píng)論 3 379
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像子房,于是被迫代替她去往敵國(guó)和親形用。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,851評(píng)論 2 361

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