Hero
HeroTransitions/Hero: Elegant transition library for iOS & tvOS
Hero是一個(gè)自定義轉(zhuǎn)場(chǎng)動(dòng)畫(huà)的框架。自定義跳轉(zhuǎn)自然是用了UIViewControllerInteractiveTransitioning嗅辣,Hero通過(guò)hero id對(duì)VC進(jìn)行解藕票编,同時(shí)也依賴(lài)hero id進(jìn)行UI變換,默認(rèn)實(shí)現(xiàn)是分扎,在路由時(shí)荆残,進(jìn)行截圖窝爪,然后拉伸
a在Hero里看見(jiàn)了Chameleon,懷念顾画,不過(guò)確實(shí)沒(méi)人維護(hù)了
因?yàn)槭菍?duì)截圖做變換,Hero的Transition主要是view的尺寸兆解、位置變化馆铁,而內(nèi)容的維護(hù)需要自己控制,比如我在A VC的hero id為"ironMan"的view內(nèi)增加了一個(gè)label锅睛,在轉(zhuǎn)場(chǎng)時(shí)埠巨,可以看到label被拉伸,并且现拒,如果在B VC中沒(méi)有添加這個(gè)label辣垒,那就會(huì)在視覺(jué)上丟失;同理在B VC印蔬,我把hero id為"ironMan"的view設(shè)置了另一個(gè)背景色勋桶,就會(huì)看到在路由結(jié)束之后,View突然從粉色變成灰色侥猬,沒(méi)有漸變
從代碼中例驹,看到有個(gè)Extension HeroContext,印證了之前的判斷:
extension HeroContext {
/**
- Returns: a snapshot view for animation
*/
public func snapshotView(for view: UIView) -> UIView {
// ...
}
}
當(dāng)然退唠,把"ironMan" 設(shè)置為不使用快照之后(redView.hero.modifiers = [.useNoSnapshot]
)再?lài)L試鹃锈,就沒(méi)有那種明顯的拉伸感了。
Hero有兩個(gè)關(guān)鍵入口函數(shù):start
和animate
Start函數(shù)比較長(zhǎng)瞧预,簡(jiǎn)單來(lái)說(shuō)包括:
- 提取一張全屏快照屎债,用來(lái)防閃爍
- 提取fromViews和toViews,標(biāo)準(zhǔn)是非hidden的view(會(huì)考慮容器和子view)并提取modifiers (
HeroContext.process(views:,idMap:)
)寫(xiě)入<UIView, HeroTargetState>字典targetStates
- processors處理fromViews和toViews垢油,這里有IgnoreSubviewModifiersPreprocessor盆驹、ConditionalPreprocessor、DefaultAnimationPreprocessor滩愁、MatchPreprocessor躯喇、SourcePreprocessor、CascadePreprocessor六個(gè)處理器硝枉,其中MatchPreprocessor以fromViews和toViews有id映射(
MatchPreprocessor.process(fromViews:toViews:)
)為標(biāo)準(zhǔn)寫(xiě)入<UIView, HeroTargetState>字典targetStates
玖瘸,SourcePreprocessor 提取剛才獲得的字典的view的參數(shù)(position、opacity檀咙、shadow等) - 提取animatingFromViews和animatingToViews雅倒,標(biāo)準(zhǔn)是字典
targetStates[view]
的HeroTargetState有值 - 調(diào)用animate()開(kāi)始動(dòng)畫(huà)
再看看HeroTransition的animate函數(shù):
extension HeroTransition {
open func animate() {
guard state == .starting else { return }
state = .animating
if let toView = toView {
context.unhide(view: toView)
}
// auto hide all animated views
for view in animatingFromViews {
context.hide(view: view)
}
for view in animatingToViews {
context.hide(view: view)
}
var totalDuration: TimeInterval = 0
var animatorWantsInteractive = false
if context.insertToViewFirst {
for v in animatingToViews { _ = context.snapshotView(for: v) }
for v in animatingFromViews { _ = context.snapshotView(for: v) }
} else {
for v in animatingFromViews { _ = context.snapshotView(for: v) }
for v in animatingToViews { _ = context.snapshotView(for: v) }
}
// UIKit appears to set fromView setNeedLayout to be true.
// We don't want fromView to layout after our animation starts.
// Therefore we kick off the layout beforehand
fromView?.layoutIfNeeded()
for animator in animators {
let duration = animator.animate(fromViews: animatingFromViews.filter({ animator.canAnimate(view: $0, appearing: false) }),
toViews: animatingToViews.filter({ animator.canAnimate(view: $0, appearing: true) }))
if duration == .infinity {
animatorWantsInteractive = true
} else {
totalDuration = max(totalDuration, duration)
}
}
self.totalDuration = totalDuration
if let forceFinishing = forceFinishing {
complete(finished: forceFinishing)
} else if let startingProgress = startingProgress {
update(startingProgress)
} else if animatorWantsInteractive {
update(0)
} else {
complete(after: totalDuration, finishing: true)
}
fullScreenSnapshot?.removeFromSuperview()
}
}
- hide隱藏了所有animatingFromViews和animatingToViews
- 拍攝快照。
- 調(diào)用animator實(shí)現(xiàn)動(dòng)畫(huà)
這個(gè)是簡(jiǎn)單例子弧可,app store的例子就要復(fù)雜很多和好看很多蔑匣。
在實(shí)現(xiàn)上多了很多細(xì)節(jié)劣欢,通過(guò)modifiers去實(shí)現(xiàn)細(xì)致的定制效果
首先還是通過(guò)hero id去做“復(fù)用”,也因此第二個(gè)cell不會(huì)在animatingFromViews裁良,要知道context.fromViews.count有24個(gè)凿将,但animatingFromViews只有兩個(gè),原因就在于价脾,MatchPreprocessor在處理的時(shí)候牧抵,從24個(gè)fromViews和15個(gè)toViews中,只匹配到了兩對(duì)id
同時(shí)侨把,不在from view里的detail label犀变,因?yàn)橛衜odifiers,在前面所述的提取fromViews和toViews步驟中秋柄,也被捕獲