Hero阅读

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突然从粉色变成灰色,没有渐变


Simulator Screen Recording - iPhone SE (3rd generation) - 2022-08-11 at 13.32.28.gif

从代码中,看到有个Extension HeroContext,印证了之前的判断:

extension HeroContext {
  /**
   - Returns: a snapshot view for animation
   */
  public func snapshotView(for view: UIView) -> UIView {
    // ...
  }
}

当然,把"ironMan" 设置为不使用快照之后(redView.hero.modifiers = [.useNoSnapshot])再尝试,就没有那种明显的拉伸感了。

Simulator Screen Recording - iPhone SE (3rd generation) - 2022-08-11 at 13.31.53.gif

Hero有两个关键入口函数:startanimate

Start函数比较长,简单来说包括:

  • 提取一张全屏快照,用来防闪烁
  • 提取fromViews和toViews,标准是非hidden的view(会考虑容器和子view)并提取modifiers (HeroContext.process(views:,idMap:))写入字典targetStates
  • processors处理fromViews和toViews,这里有IgnoreSubviewModifiersPreprocessor、ConditionalPreprocessor、DefaultAnimationPreprocessor、MatchPreprocessor、SourcePreprocessor、CascadePreprocessor六个处理器,其中MatchPreprocessor以fromViews和toViews有id映射(MatchPreprocessor.process(fromViews:toViews:))为标准写入字典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的例子就要复杂很多和好看很多。


Simulator Screen Recording - iPhone SE (3rd generation) - 2022-08-12 at 13.48.45.gif

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

你可能感兴趣的:(Hero阅读)