一款非常火的过场动划开源库,github 2W+ Strar。

Hero是一个iOS界面切换库。它代替了UIKit本身的转场动画接口,使制作自定义的转场动画(Viespring漏洞w Contro后端开发是干什么的ller Transition)非常简单!

H缓存的视频在哪ero很像Keynote的“神奇移动”过渡(Magic Move)。在界面切换时,Hero会把开始界面的视图与结束界面的视图配对,假如他能找到一对儿有着一样的heroI后端是什么工作D的视图的话,Hero便会自动为此视图创建动画,从它一开始的状态移动到结束时的状态。

不仅如此,Hero还可以为没有配对的视图制作动画。每一个视图都可以轻易的用spring翻译heroModifiers来告诉Hero你想为这个视图所创造的动画。交互式动画(igit教程nteractive transition)也是支持的哟。

1.Hero调用

FromVC
redView.backgroundColor = UIColor.red
redView.heroID = "redView"
blueView.backgroundColor = UIColor.cyan
blueView.heroID = "blueView"
blueView.hero.modifiers = [.fade]
ToVC
redView.backgroundColor = UIColor.red
redView.heroID = "redView"
blueView.backgroundColor = UIColor.cyan
blueView.heroID = "blueView"
blueView.heroModifiers = [.fade]
collection.hero.modifiers = [.cascade(delta: 0.02, direction: .topToBottom, delayMatchedViews: true), .translate(y: 0), .useGlobalCoordinateSpace, .scale(0.5)]

Hero转场过程分析

2.转场代理实现类 HeroTransition


var interactiveTransitioning: UIViewControllerInteractiveTransitioning? {
    return forceNotInteractive ? nil : self
}
UINavigationControllerDelegate
optional func navigationController(_ navigationController: UINavigationController,
                                   animationControllerFor 
                                   operation:UINavigationController.Operation,
                                   from fromVC: UIViewController,
                                   to toVC: UIViewController) -> 
                                   UIViewControllerAnimatedTransitioning? {
    guard !isTransitioning else { return nil }
    self.state = .notified
    self.isPresenting = operation == .push
    self.fromViewController = fromViewController ?? fromVC
    self.toViewController = toViewController ?? toVC
    self.inNavigationController = true
    return self
}
optional func navigationController(_ navigationController: UINavigationController,
                                   interactionControllerFor 
                                   animationController:
                                   UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
  return interactiveTransitioning
}
UIViewControllerTransitioningDelegate
optional func animationController(forPresented presented: UIViewController,
                                  presenting: UIViewController,         
                                  source: UIViewController) -> 
                                  UIViewControllerAnimatedTransitioning? {
  guard !isTransitioning else { return nil }
  self.state = .notified
  self.isPresenting = true
  self.fromViewController = fromViewController ?? presenting
  self.toViewController = toViewController ?? presented
  return self
 }
optional func interactionControllerForPresentation(using animator: 
              UIViewControllerAnimatedTransitioning) ->
              UIViewControllerInteractiveTransitioning? {
  return interactiveTransitioning
 }
UITabBarControllerDelegate
optional func tabBarController(_ tabBarController: UITabBarController,
                               animationControllerForTransitionFrom 
                               fromVC: UIViewController,
                               to toVC: UIViewController) -> 
                               UIViewControllerAnimatedTransitioning? {
  guard !isTransitioning else { return nil }
  self.state = .notified
  let fromVCIndex = tabBarController.children.firstIndex(of: fromVC)!
  let toVCIndex = tabBarController.children.firstIndex(of: toVC)!
  self.isPresenting = toVCIndex > fromVCIndex
  self.fromViewController = fromViewController ?? fromVC
  self.toViewController = toViewController ?? toVC
  self.inTabBarController = true
  return self
}
optional func tabBarController(_ tabBarController: UITabBarController,
                               interactionControllerFor animationController: 
                               UIViewControllerAnimatedTransitioning) -> 
                               UIViewControllerInteractiveTransitioning? {
  return interactiveTransitioning
}

UIViewControllerAnimatedTransitioning:动画控制器协议,可以在遵守该协议的类中进行转场动画的设计,如果返回的对象为nil,则保持系统动画,不会使用自定义动画。

UIViewControllerInteractiveTran瀑布流式布局sitioning:交互控制协议,该对象定义了转场动画的交互行为。

这里它做的事很简单就是设定缓存视频变成本地视频一些流程中的状态,然后将来源SpringVC与目标VC做保存的动作,以便日后取用。

extension HeroTransition: UIViewControllerAnimatedTransitioning {
   public func animateTransition(using context: UIViewControllerContextTransitioning) {
  transitionContext = context
  fromViewController = fromViewController ?? context.viewController(forKey: .from)
  toViewController = toViewController ?? context.viewController(forKey: .to)
  transitionContainer = context.containerView
  start()
  }
 public func transitionDuration(using transitionContext: 
                                 UIViewControllerContextTransitioning?) -> 
                                 TimeInterval{
  return 0.375 // doesn't matter, real duration will be calculated later
 }
 public func animationEnded(_ transitionCompleted: Bool) {
  self.state = .possible
 }
}
extension HeroTransition: UIViewControllerInteractiveTransitioning {
 public var wantsInteractiveStart: Bool {
  return true
 }
 public func startInteractiveTransition(_ transitionContext: 
                                         UIViewControllerContextTransitioning) {
  animateTransition(using: transitionContext)
 }
}

3.Starspringcloudt

start首先是做了一些比较简单的准备工作:

3.1 layout目标VC.View,获取到目标UI

3.后端2 设定自身状态并发送通知给代理即将开始缓存清理转场

3.3 截取起始屏幕UI贴在舞台github上防止一些中间态被展示出来

3瀑布流.4 Preprocessor和HeroAnimator


internal var processors: [HeroPreprocessor] = []
internal var animators: [HeroAnimator] = []
internal var plugins: [HeroPlugin] = []

HeroTransispringboot面试题tion 维护了成github员为遵守HeroPreprocessor和HeroAnimator协议的两个数组,用于描述和控制动画的过程

HeroPreprocessor只有一个process方法,在流程中起到一个瀑布流式布局《设计稿》的作用,会给两个VC内每一个sGitubView都设计他们在动画过程中的参数

HeroAnimator 暴露了交互接口,可用于控制动画的流程

HeroPlugin 是同时满足了He后端开发roPreprocessor, HeroAnim瀑布流式布局ator两个协议的类,用于提供给开发者进行自定义动画创作


public protocol HeroPreprocessor: class {
 var hero: HeroTransition! { get set }
 func process(fromViews: [UIView], toViews: [UIView])
}
public protocol HeroAnimator: class {
 var hero: HeroTransition! { get set }
 func canAnimate(view: UIView, appearing: Bool) -> Bool
 func animate(fromViews: [UIView], toViews: [UIView]) -> TimeInterval
 func clean()

 func seekTo(timePassed: TimeInterval)
 func resume(timePassed: TimeInterval, reverse: Bool) -> TimeInterval
 func apply(state: HeroTargetState, to view: UIView)
 func changeTarget(state: HeroTargetState, isDestination: Bool, to view: UIView)
}
open class HeroPlugin: NSObject, HeroPreprocessor, HeroAnimator

3.5 预设processor和Animator


plugins = HeroTransition.enabledPlugins.map({ return $0.init() })
processors = [
    IgnoreSubviewModifiersPreprocessor(),
    ConditionalPreprocessor(),
    DefaultAnimationPreprocessor(),
    MatchPreprocessor(),
    SourcePreprocessor(),
    CascadePreprocessor()
]
animators = [
    HeroDefaultAnimator<HeroCoreAnimationViewContext>()
]
// There is no covariant in Swift, so we need to add plugins one by one.
for plugin in plugins {
    processors.append(plugin)
    animators.append(plugin)
}

3.6springboot面试题 创建了一个新的View作为动画的表后端框架现层贴到了舞台上

3.7 HeroContext和HeroTargetState


public internal(set) var context: HeroContext!
context = HeroContext(container: container)
context.set(fromViews: fromView.flattenedViewHierarchy, toViews: toView.flattenedViewHierarchy)

HeroContext是Hero自身维护的一个上下文,里面保存了动画过程中需要用spring到的一切描述信息与映射关系,并且由构造方法可以看出,上下文持有了动画表现层


internal var heroIDToSourceView = [String: UIView]()
internal var heroIDToDestinationView = [String: UIView]()
internal var targetStates = [UIView: HeroTargetState]()
internal func set(fromViews: [UIView], toViews: [UIView]) {
    self.fromViews = fromViews
    self.toViews = toViews
    process(views: fromViews, idMap: &heroIDToSourceView)
    process(views: toViews, idMap: &heroIDToDestinationView)
}
internal func process(views: [UIView], idMap: inout [String: UIView]) {
    for view in views {
        view.layer.removeAllHeroAnimations()
        let targetState: HeroTargetState?
        if let modifiers = view.hero.modifiers {
            targetState = HeroTargetState(modifiers: modifiers)
        } else {
            targetState = nil
        }
        if targetState?.forceAnimate == true || container.convert(view.bounds, from: view).intersects(container.bounds) {
            if let heroID = view.hero.id {
                idMap[heroID] = view
            }
            targetStates[view] = targetState
        }
    }
 }

在set方法瀑布流中,会将拥有id映射关系的View存入各自缓存清理的字典中,并且为每一个View根后端是什么工作据其动画形式(modifiegitirs)创建一个可选的HeroTargetState,然后也存入状态保存字典

HeroTargetState
public struct HeroTargetState {
    public var beginState: [HeroModifier]?
    public var conditionalModifiers: [((HeroConditionalContext) -> Bool, [HeroModifier])]?
    public var position: CGPoint?
    public var size: CGSize?
    public var transform: CATransform3D?
    public var opacity: Float?
    public var cornerRadius: CGFloat?
    public var backgroundColor: CGColor?
    public var zPosition: CGFloat?
    public var contentsRect: CGRect?
    public var contentsScale: CGFloat?
    public var borderWidth: CGFloat?
    public var borderColor: CGColor?
    public var shadowColor: CGColor?
    public var shadowOpacity: Float?
    public var shadowOffset: CGSize?
    public var shadowRadius: CGFloat?
    public var shadowPath: CGPath?
    public var masksToBounds: Bool?
    public var displayShadow: Bool = true
    public var overlay: (color: CGColor, opacity: CGFloat)?
    public var spring: (CGFloat, CGFloat)?
    public var delay: TimeInterval = 0
    public var duration: TimeInterval?
    public var timingFunction: CAMediaTimingFunction?
    public var arc: CGFloat?
    public var source: String?
    public var cascade: (TimeInterval, CascadeDirection, Bool)?
    public var ignoreSubviewModifiers: Bool?
    public var coordinateSpace: HeroCoordinateSpace?
    public var useScaleBasedSizeChange: Bool?
    public var snapshotType: HeroSnapshotType?
    public var nonFade: Bool = false
    public var forceAnimate: Bool = false
    public var custom: [String: Any]?
}

HeroTarggitlabetState 是一个保存了每个视图起始和最终UI状态的结构体,但是在set方法中可github以看到,HeroTargetState只是被初始化存入了字典中,其中的各项参数都没有赋值

3.8 对每一个View写入State,筛选可以进行动画的视图


for processor in processors {
  processor.process(fromViews: context.fromViews, toViews: context.toViews)
}
animatingFromViews = context.fromViews.filter { (view: UIView) -> Bool in
                       for animator in animators {
                         if animator.canAnimate(view: view, appearing: false) {
                           return true
                         }
                       }
                       return false
                       }
animatingToViews = context.toViews.filter { (view: UIView) -> Bool in
                     for animator in animators {
                       if animator.canAnimate(view: view, appearing: true) {
                         return true
                       }
                     }
                     return false
}

Ex:CascadePreprocessor瀑布流式的动画则会给每一个视图设置github永久回家地址一个delagiticomfort是什么轮胎y

CascadePreprocessor
class CascadePreprocessor: BasePreprocessor {
 override func process(fromViews: [UIView], toViews: [UIView]) {
  process(views: fromViews)
  process(views: toViews)
 }
 func process(views: [UIView]) {
  for view in views {
   guard let (deltaTime, direction, delayMatchedViews) = context[view]?.cascade else { continue }
   var parentView = view
   if view is UITableView, let wrapperView = view.subviews.get(0) {
    parentView = wrapperView
   }
   let sortedSubviews = parentView.subviews.sorted(by: direction.comparator)
   let initialDelay = context[view]!.delay
   let finalDelay = TimeInterval(sortedSubviews.count) * deltaTime + initialDelay
   for (i, subview) in sortedSubviews.enumerated() {
    let delay = TimeInterval(i) * deltaTime + initialDelay
    func applyDelay(view: UIView) {
     if context.pairedView(for: view) == nil {
      context[view]?.delay = delay
     } else if delayMatchedViews, let paired = context.pairedView(for: view) {
      context[view]?.delay = finalDelay
      context[paired]?.delay = finalDelay
     }
     for subview in view.subviews {
      applyDelay(view: subview)
     }
    }
    applyDelay(view: subview)
   }
  }
 }
}

以上全部都是参数的准备工作,完成后进入则开始动画


if inNavigationController {
    // When animating within navigationController, we have to dispatch later into the main queue.
    // otherwise snapshots will be pure white. Possibly a bug with UIKit
    DispatchQueue.main.async {    
        self.animate()    
    }    
} else {
    animate()
}

4.Animate

4.1 将需springboot要进行动画的View都存一张截图View来真正执行动画,目的是为了不修改原View的参数


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

4.2 执行动画的同时获取到每一个小动画的时长,取最大值即为整个动画的时长

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

 func animate(key: String, beginTime: TimeInterval, duration: TimeInterval, fromValue: Any?, toValue: Any?) -> TimeInterval {
  let anim = getAnimation(key: key, beginTime: beginTime, duration: duration, fromValue: fromValue, toValue: toValue)
  if let overlayKey = overlayKeyFor(key: key) {
   addAnimation(anim, for: overlayKey, to: getOverlayLayer())
  } else {
   switch key {
   case "cornerRadius", "contentsRect", "contentsScale":
    addAnimation(anim, for: key, to: snapshot.layer)
    if let contentLayer = contentLayer {
     // swiftlint:disable:next force_cast
     addAnimation(anim.copy() as! CAAnimation, for: key, to: contentLayer)
    }
    if let overlayLayer = overlayLayer {
     // swiftlint:disable:next force_cast
     addAnimation(anim.copy() as! CAAnimation, for: key, to: overlayLayer)
    }
   case "bounds.size":
    guard let fromSize = (fromValue as? NSValue)?.cgSizeValue, let toSize = (toValue as? NSValue)?.cgSizeValue else {
     addAnimation(anim, for: key, to: snapshot.layer)
     break
    }
    setSize(view: snapshot, newSize: fromSize)
    uiViewBasedAnimate(duration: anim.duration, delay: beginTime - currentTime) {
     self.setSize(view: self.snapshot, newSize: toSize)
    }
   default:
    addAnimation(anim, for: key, to: snapshot.layer)
   }
  }
  return anim.duration + anim.beginTime - beginTime
 }

5.HeroProgressR后端unner

HeroProg缓存视频变成本地视频ressRunner是一个状态与实际动画执行状态保持高度统一的监听类,用于在动画流程中监听各种事件,修改HeroTranslition的状态并且向代理发送动画生命周期通知,这里开启一个与动画时长相同的proggithubressRunner(与4.2并行)

self.totalDuration = totalDuration
complete(after: totalDuration, finishing: true)
var progressRunner: HeroProgressRunner
func complete(after: TimeInterval, finishing: Bool) {
    guard [HeroTransitionState.animating, .starting, .notified].contains(state) else { return }
    if after <= 1.0 / 120 {
        complete(finished: finishing)
        return
    }
    let totalTime: TimeInterval
    if finishing {
        totalTime = after / max((1 - progress), 0.01)
    } else {
        totalTime = after / max(progress, 0.01)
    }
    progressRunner.start(timePassed: progress * totalTime, totalTime: totalTime, reverse: !finishing)
}

在start方法中开启了一个displayLink,以屏幕刷新率的频率去调用displayUpdate(_ :))方法

 func start(timePassed: TimeInterval, totalTime: TimeInterval, reverse: Bool) {
  stop()
  self.timePassed = timePassed
  self.isReversed = reverse
  self.duration = totalTime
  displayLink = CADisplayLink(target: self, selector: #selector(displayUpdate(_:)))
  displayLink!.add(to: .main, forMode: RunLoop.Mode.common)
 }
 @objc func displayUpdate(_ link: CADisplayLink) {
  timePassed += isReversed ? -link.duration : link.duration
  if isReversed, timePassed <= 1.0 / 120 {
   delegate?.complete(finished: false)
   stop()
   return
  }
  if !isReversed, timePassed > duration - 1.0 / 120 {
   delegate?.complete(finished: true)
   stop()
   return
  }
  delegate?.updateProgress(progress: timePassed / duration)
 }

extension HeroTransition: HeroProgressRunnerDelegate {
 func updateProgress(progress: Double) {
  self.progress = progress
 }
}

displayUpdatespring框架方法通知到代理(HeroTransition)更新进度


 public internal(set) var progress: Double = 0 {
  didSet {
   if state == .animating {
    if let progressUpdateObservers = progressUpdateObservers {
     for observer in progressUpdateObservers {
      observer.heroDidUpdateProgress(progress: progress)
     }
    }
    let timePassed = progress * totalDuration
    if interactive {
     for animator in animators {
      animator.seekTo(timePassed: timePassed)
     }
    } else {
     for plugin in plugins where plugin.requirePerFrameCallback {
      plugin.seekTo(timePassed: timePassed)
     }
    }
    transitionContext?.updateInteractiveTransition(CGFloat(progress))
   }
   delegate?.heroTransition(self, didUpdate: progress)
  }
 }

HeroTransition则根据进度下发至每一个需要刷新进度的实例,包括开发者外部写入的giticomfort是什么轮胎plugin,系统的转场上下文,以及转场动画springmvc的工作原理代理

6. Complete

6.1 修改状态

6giticomfort是什么轮胎.2 根据动画形式缓存部分视图的透明度、截图视图等变量以便pop、dismiss操作可以重复gitee利用


context.storeViewAlpha(rootView: fromView)
fromViewController?.hero.storedSnapshot = container

6.3 置空其余所有中间流程中产生的临时变量,中间视图全部从父视图移除


func stop() {
  displayLink?.isPaused = true
  displayLink?.remove(from: RunLoop.main, forMode: RunLoop.Mode.common)
  displayLink = nil
}

6.4 将目标VC的截图贴到舞台上

6.5 将表现层从舞台移除

6.6 手动移除可能引起内存泄露的持有关系


public func clean() {
  for vc in viewContexts.values {
   vc.clean()
  }
  viewContexts.removeAll()
 }

6.7 向代理发送完成或取消转场的通知

7. 数据驱动

根据上面的全流程可以看出,Hero需要获取到目标VspringcloudC的初始化UI才可以进行动画转场,但是在实giti轮胎际应用的过程中,如果目标VC的主要UI框架为UITableView/UICospring面试题llect缓存视频合并ionView等以数据驱动UI的控件,其数spring漏洞据来源于后端API,view后端开发工程师Di后端开发是干什么的dLoad无法获取到API返回之后的UI样式,则Hero后端框架也无法为springcloud控件提缓存清理供转场动画,这个时候有两种方案来实现平滑的动画转场效果

Hero转场过程分析

7.1 骨架屏

使用骨架屏控件(SkeletonView)来为UITableView预设Cell的数量,使得目标VC瀑布流可以在viewDidLoad时就加载出UITableView的样式,这样就可以做相关的转场动画了

Hero转场过程分析

7.2 Loading动画

原理跟骨架屏是一样的,将FromVC的相关视图与ToVC的Loading瀑布流式布局动画视图做关联,转场完成后获取到数据再隐藏Loadin瀑布流g动画也可以达到平滑得转场效果,但是这缓存视频在手机哪里找种模式在Pop或者Dismiss的之前需要将loadingView的关联移除,原spring翻译理是一样的,动图就偷懒不贴了