Hero
HeroTransitions/Hero: Elegant transition library for iOS & tvOS
Hero是一个自定义转场动画的框架。自定义跳转自然是用了UIViewControllerInteractiveTransitioning,Hero通过hero id对VC进行解藕,同时也依赖hero id进行UI变换,默认实现是,在路由时,进行截图,然后拉伸
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有两个关键入口函数:start
和animate
Start函数比较长,简单来说包括:
- 提取一张全屏快照,用来防闪烁
- 提取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的参数(position、opacity、shadow等) - 提取animatingFromViews和animatingToViews,标准是字典
targetStates[view]
的HeroTargetState有值 - 调用animate()开始动画
再看看HeroTransition的animate函数:
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实现动画
这个是简单例子,app store的例子就要复杂很多和好看很多。

在实现上多了很多细节,通过modifiers去实现细致的定制效果
首先还是通过hero id去做“复用”,也因此第二个cell不会在animatingFromViews,要知道context.fromViews.count有24个,但animatingFromViews只有两个,原因就在于,MatchPreprocessor在处理的时候,从24个fromViews和15个toViews中,只匹配到了两对id
同时,不在from view里的detail label,因为有modifiers,在前面所述的提取fromViews和toViews步骤中,也被捕获
网友评论