Hero
HeroTransitions/Hero: Elegant transition library for iOS & tvOS
Hero是一個自定義轉場動畫的框架。自定義跳轉自然是用了UIViewControllerInteractiveTransitioning,Hero通過hero id對VC進行解藕,同時也依賴hero id進行UI變換,默認實現(xiàn)是,在路由時,進行截圖,然后拉伸
a在Hero里看見了Chameleon,懷念,不過確實沒人維護了
因為是對截圖做變換,Hero的Transition主要是view的尺寸、位置變化,而內容的維護需要自己控制,比如我在A VC的hero id為"ironMan"的view內增加了一個label,在轉場時,可以看到label被拉伸,并且,如果在B VC中沒有添加這個label,那就會在視覺上丟失;同理在B VC,我把hero id為"ironMan"的view設置了另一個背景色,就會看到在路由結束之后,View突然從粉色變成灰色,沒有漸變
從代碼中,看到有個Extension HeroContext,印證了之前的判斷:
extension HeroContext {
/**
- Returns: a snapshot view for animation
*/
public func snapshotView(for view: UIView) -> UIView {
// ...
}
}
當然,把"ironMan" 設置為不使用快照之后(redView.hero.modifiers = [.useNoSnapshot]
)再嘗試,就沒有那種明顯的拉伸感了。
Hero有兩個關鍵入口函數(shù):start
和animate
Start函數(shù)比較長,簡單來說包括:
- 提取一張全屏快照,用來防閃爍
- 提取fromViews和toViews,標準是非hidden的view(會考慮容器和子view)并提取modifiers (
HeroContext.process(views:,idMap:)
)寫入<UIView, HeroTargetState>字典targetStates
- processors處理fromViews和toViews,這里有IgnoreSubviewModifiersPreprocessor、ConditionalPreprocessor、DefaultAnimationPreprocessor、MatchPreprocessor、SourcePreprocessor、CascadePreprocessor六個處理器,其中MatchPreprocessor以fromViews和toViews有id映射(
MatchPreprocessor.process(fromViews:toViews:)
)為標準寫入<UIView, HeroTargetState>字典targetStates
,SourcePreprocessor 提取剛才獲得的字典的view的參數(shù)(position、opacity、shadow等) - 提取animatingFromViews和animatingToViews,標準是字典
targetStates[view]
的HeroTargetState有值 - 調用animate()開始動畫
再看看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
- 拍攝快照。
- 調用animator實現(xiàn)動畫
這個是簡單例子,app store的例子就要復雜很多和好看很多。
在實現(xiàn)上多了很多細節(jié),通過modifiers去實現(xiàn)細致的定制效果
首先還是通過hero id去做“復用”,也因此第二個cell不會在animatingFromViews,要知道context.fromViews.count有24個,但animatingFromViews只有兩個,原因就在于,MatchPreprocessor在處理的時候,從24個fromViews和15個toViews中,只匹配到了兩對id
同時,不在from view里的detail label,因為有modifiers,在前面所述的提取fromViews和toViews步驟中,也被捕獲