Commit 3e422773 by Daniel Dahan

second round reworking the MotionController

parent fcf3d822
...@@ -91,7 +91,7 @@ extension MotionDebugPlugin:MotionDebugViewDelegate { ...@@ -91,7 +91,7 @@ extension MotionDebugPlugin:MotionDebugViewDelegate {
public func onProcessSliderChanged(progress: Float) { public func onProcessSliderChanged(progress: Float) {
let seekValue = Motion.shared.isPresenting ? progress : 1.0 - progress let seekValue = Motion.shared.isPresenting ? progress : 1.0 - progress
Motion.shared.update(progress: Double(seekValue)) Motion.shared.update(elapsedTime: Double(seekValue))
} }
func onPerspectiveChanged(translation: CGPoint, rotation: CGFloat, scale: CGFloat) { func onPerspectiveChanged(translation: CGPoint, rotation: CGFloat, scale: CGFloat) {
......
...@@ -166,12 +166,12 @@ fileprivate extension Motion { ...@@ -166,12 +166,12 @@ fileprivate extension Motion {
prepareViewControllers() prepareViewControllers()
prepareSnapshotView() prepareSnapshotView()
prepareForTransition() prepareTransition()
prepareContext() prepareContext()
prepareToView() prepareToView()
prepareViewHierarchy() prepareViewHierarchy()
processContext() processContext()
prepareForAnimation() prepareTransitionPairs()
processForAnimation() processForAnimation()
} }
} }
...@@ -188,7 +188,7 @@ internal extension Motion { ...@@ -188,7 +188,7 @@ internal extension Motion {
fullScreenSnapshot?.removeFromSuperview() fullScreenSnapshot?.removeFromSuperview()
} }
override func complete(finished: Bool) { override func complete(isFinished finished: Bool) {
guard isTransitioning else { guard isTransitioning else {
return return
} }
...@@ -220,13 +220,13 @@ internal extension Motion { ...@@ -220,13 +220,13 @@ internal extension Motion {
} }
// move fromView & toView back from our container back to the one supplied by UIKit // move fromView & toView back from our container back to the one supplied by UIKit
if (toOverFullScreen && finished) || (fromOverFullScreen && !finished) { if (toOverFullScreen && isFinished) || (fromOverFullScreen && !finished) {
transitionContainer.addSubview(finished ? fromView : toView) transitionContainer.addSubview(isFinished ? fromView : toView)
} }
transitionContainer.addSubview(finished ? toView : fromView) transitionContainer.addSubview(isFinished ? toView : fromView)
if isPresenting != finished, !isContainerController { if isPresenting != isFinished, !isContainerController {
// only happens when present a .overFullScreen VC // only happens when present a .overFullScreen VC
// bug: http://openradar.appspot.com/radar?id=5320103646199808 // bug: http://openradar.appspot.com/radar?id=5320103646199808
UIApplication.shared.keyWindow!.addSubview(isPresenting ? fromView : toView) UIApplication.shared.keyWindow!.addSubview(isPresenting ? fromView : toView)
...@@ -249,7 +249,7 @@ internal extension Motion { ...@@ -249,7 +249,7 @@ internal extension Motion {
insertToViewFirst = false insertToViewFirst = false
defaultAnimation = .auto defaultAnimation = .auto
super.complete(finished: finished) super.complete(isFinished: isFinished)
if finished { if finished {
if let fvc = fvc, let tvc = tvc { if let fvc = fvc, let tvc = tvc {
...@@ -342,13 +342,13 @@ fileprivate extension Motion { ...@@ -342,13 +342,13 @@ fileprivate extension Motion {
} }
internal extension Motion { internal extension Motion {
override func prepareForTransition() { override func prepareTransition() {
super.prepareForTransition() super.prepareTransition()
insert(preprocessor: DefaultAnimationPreprocessor(motion: self), before: DurationPreprocessor.self) insert(preprocessor: DefaultAnimationPreprocessor(motion: self), before: DurationPreprocessor.self)
} }
override func prepareForAnimation() { override func prepareTransitionPairs() {
super.prepareForAnimation() super.prepareTransitionPairs()
context.hide(view: toView) context.hide(view: toView)
} }
} }
......
...@@ -107,7 +107,7 @@ public class MotionController: NSObject { ...@@ -107,7 +107,7 @@ public class MotionController: NSObject {
internal var isFinished = true internal var isFinished = true
/// An Array of MotionPreprocessors used during a transition. /// An Array of MotionPreprocessors used during a transition.
internal var processors: [MotionPreprocessor]! internal var preprocessors: [MotionPreprocessor]!
/// An Array of MotionAnimators used during a transition. /// An Array of MotionAnimators used during a transition.
internal var animators: [MotionAnimator]! internal var animators: [MotionAnimator]!
...@@ -125,6 +125,25 @@ public class MotionController: NSObject { ...@@ -125,6 +125,25 @@ public class MotionController: NSObject {
internal override init() {} internal override init() {}
} }
public extension MotionController {
/**
Receive callbacks on each animation frame.
Observers will be cleaned when a transition completes.
- Parameter observer: A MotionTransitionObserver.
*/
func addTransitionObserver(observer: MotionTransitionObserver) {
defer {
transitionObservers?.append(observer)
}
guard nil == transitionObservers else {
return
}
transitionObservers = []
}
}
fileprivate extension MotionController { fileprivate extension MotionController {
/// Updates the transition observers. /// Updates the transition observers.
func updateTransitionObservers() { func updateTransitionObservers() {
...@@ -155,6 +174,10 @@ fileprivate extension MotionController { ...@@ -155,6 +174,10 @@ fileprivate extension MotionController {
} }
fileprivate extension MotionController { fileprivate extension MotionController {
/**
Handler for the DisplayLink updates.
- Parameter _ link: CADisplayLink.
*/
@objc @objc
func handleDisplayLink(_ link: CADisplayLink) { func handleDisplayLink(_ link: CADisplayLink) {
guard isTransitioning else { guard isTransitioning else {
...@@ -180,7 +203,7 @@ fileprivate extension MotionController { ...@@ -180,7 +203,7 @@ fileprivate extension MotionController {
} else { } else {
var eTime = cTime / totalDuration var eTime = cTime / totalDuration
if !isFinished { if !isFinished {
eTime = 1 - eTime eTime = 1 - eTime
} }
...@@ -191,269 +214,329 @@ fileprivate extension MotionController { ...@@ -191,269 +214,329 @@ fileprivate extension MotionController {
} }
public extension MotionController { public extension MotionController {
// MARK: Interactive Transition /**
Updates the elapsed time for the interactive transition.
/** - Parameter elapsedTime t: the current progress, must be between -1...1.
Update the progress for the interactive transition. */
- Parameters: public func update(elapsedTime t: TimeInterval) {
- progress: the current progress, must be between -1...1 guard isTransitioning else {
*/ return
public func update(progress: Double) { }
guard isTransitioning else { return }
self.beginTime = nil beginTime = nil
self.elapsedTime = max(-1, min(1, progress)) elapsedTime = max(-1, min(1, t))
}
/**
Finish the interactive transition.
Will stop the interactive transition and animate from the
current state to the **end** state
*/
public func end(animate: Bool = true) {
guard isTransitioning else { return }
if !animate {
self.complete(isFinished:true)
return
} }
var maxTime: TimeInterval = 0
for animator in self.animators { /**
maxTime = max(maxTime, animator.resume(at: self.elapsedTime * self.totalDuration, isReversed: false)) Finish the interactive transition.
Will stop the interactive transition and animate from the
current state to the **end** state
- Parameter isAnimated: A boolean indicating if the completion is animated.
*/
public func end(isAnimated: Bool = true) {
guard isTransitioning else {
return
}
guard isAnimated else {
complete(isFinished: true)
return
}
var v: TimeInterval = 0
for a in animators {
v = max(v, a.resume(at: elapsedTime * totalDuration, isReversed: false))
}
complete(after: v, isFinished: true)
} }
self.complete(after: maxTime, isFinished: true)
}
/** /**
Cancel the interactive transition. Cancel the interactive transition.
Will stop the interactive transition and animate from the Will stop the interactive transition and animate from the
current state to the **begining** state current state to the **begining** state
*/ - Parameter isAnimated: A boolean indicating if the completion is animated.
public func cancel(animate: Bool = true) { */
guard isTransitioning else { return } public func cancel(isAnimated: Bool = true) {
if !animate { guard isTransitioning else {
self.complete(isFinished:false) return
return }
}
var maxTime: TimeInterval = 0 guard isAnimated else {
for animator in self.animators { complete(isFinished:false)
var adjustedProgress = self.elapsedTime return
if adjustedProgress < 0 { }
adjustedProgress = -adjustedProgress
} var v: TimeInterval = 0
maxTime = max(maxTime, animator.resume(at: adjustedProgress * self.totalDuration, isReversed: true)) for a in animators {
var t = elapsedTime
if t < 0 {
t = -t
}
v = max(v, a.resume(at: t * totalDuration, isReversed: true))
}
complete(after: v, isFinished: false)
} }
self.complete(after: maxTime, isFinished: false)
}
/**
Override modifiers during an interactive animation.
For example:
Motion.shared.apply([.position(x:50, y:50)], to:view) /**
Override transition animations during an interactive animation.
will set the view's position to 50, 50
- Parameters:
- modifiers: the modifiers to override
- view: the view to override to
*/
public func apply(transitions: [MotionTransition], to view: UIView) {
guard isTransitioning else { return }
let targetState = MotionTargetState(transitions: transitions)
if let otherView = self.context.pairedView(for: view) {
for animator in self.animators {
animator.apply(state: targetState, to: otherView)
}
}
for animator in self.animators {
animator.apply(state: targetState, to: view)
}
}
}
public extension MotionController { For example:
// MARK: Observe Progress
/** Motion.shared.apply([.position(x:50, y:50)], to: view)
Receive callbacks on each animation frame.
Observers will be cleaned when transition completes
- Parameters: will set the view's position to 50, 50
- observer: the observer - Parameter transitions: An Array of MotionTransitions.
*/ - Parameter to view: A UIView.
func observeForProgressUpdate(observer: MotionTransitionObserver) { */
if transitionObservers == nil { public func apply(transitions: [MotionTransition], to view: UIView) {
transitionObservers = [] guard isTransitioning else {
return
}
let s = MotionTargetState(transitions: transitions)
let v = context.pairedView(for: view) ?? view
for a in animators {
a.apply(state: s, to: v)
}
} }
transitionObservers!.append(observer)
}
} }
// internal methods for transition
internal extension MotionController { internal extension MotionController {
/// Load plugins, processors, animators, container, & context /**
/// must have transitionContainer set already Load plugins, processors, animators, container, & context
/// subclass should call context.set(fromViews:toViews) after inserting fromViews & toViews into the container The transitionContainer must already be set.
func prepareForTransition() { Subclasses should call context.set(fromViews: toViews) after
guard isTransitioning else { fatalError() } inserting fromViews & toViews into the container
plugins = Motion.enabledPlugins.map({ return $0.init() }) */
processors = [ func prepareTransition() {
IgnoreSubviewModifiersPreprocessor(), guard isTransitioning else {
MatchPreprocessor(), fatalError()
SourcePreprocessor(), }
CascadePreprocessor(),
DurationPreprocessor() prepareTransitionContainer()
] prepareContext()
animators = [ preparePreprocessors()
MotionDefaultAnimator<MotionCoreAnimationViewContext>() prepareAnimators()
] preparePlugins()
if #available(iOS 10, tvOS 10, *) {
animators.append(MotionDefaultAnimator<MotionViewPropertyViewContext>())
} }
// There is no covariant in Swift, so we need to add plugins one by one. /// Prepares the transition from-view & to-view pairs.
for plugin in plugins { func prepareTransitionPairs() {
processors.append(plugin) guard isTransitioning else {
animators.append(plugin) fatalError()
}
transitionPairs = [([UIView], [UIView])]()
for a in animators {
let fv = context.fromViews.filter { (view: UIView) -> Bool in
return a.canAnimate(view: view, isAppearing: false)
}
let tv = context.toViews.filter { (view: UIView) -> Bool in
return a.canAnimate(view: view, isAppearing: true)
}
transitionPairs.append((fv, tv))
}
} }
}
transitionContainer.isUserInteractionEnabled = false internal extension MotionController {
func processContext() {
// a view to hold all the animating views guard isTransitioning else {
container = UIView(frame: transitionContainer.bounds) fatalError()
transitionContainer.addSubview(container) }
context = MotionContext(container:container) for v in preprocessors {
v.process(fromViews: context.fromViews, toViews: context.toViews)
for processor in processors { }
processor.context = context
}
for animator in animators {
animator.context = context
} }
}
/// Actually animate the views
/// subclass should call `prepareTransition` & `prepareTransitionPairs` before calling `animate`
func animate() {
guard isTransitioning else {
fatalError()
}
for (currentFromViews, currentToViews) in transitionPairs {
// auto hide all animated views
for view in currentFromViews {
context.hide(view: view)
}
for view in currentToViews {
context.hide(view: view)
}
}
var totalDuration: TimeInterval = 0
var animatorWantsInteractive = false
for (i, animator) in animators.enumerated() {
let duration = animator.animate(fromViews: transitionPairs[i].0, toViews: transitionPairs[i].1)
if duration == .infinity {
animatorWantsInteractive = true
} else {
totalDuration = max(totalDuration, duration)
}
}
self.totalDuration = totalDuration
if animatorWantsInteractive {
update(elapsedTime: 0)
} else {
complete(after: totalDuration, isFinished: true)
}
}
func processContext() { func complete(after: TimeInterval, isFinished: Bool) {
guard isTransitioning else { fatalError() } guard isTransitioning else {
for processor in processors { fatalError()
processor.process(fromViews: context.fromViews, toViews: context.toViews) }
if after <= 0.001 {
complete(isFinished: isFinished)
return
}
let v = (isFinished ? elapsedTime : 1 - elapsedTime) * totalDuration
self.isFinished = isFinished
self.currentAnimationDuration = after + v
self.beginTime = CACurrentMediaTime() - v
} }
}
func complete(isFinished: Bool) {
func prepareForAnimation() { guard isTransitioning else {
guard isTransitioning else { fatalError() } fatalError()
transitionPairs = [([UIView], [UIView])]() }
for animator in animators {
let currentFromViews = context.fromViews.filter { (view: UIView) -> Bool in for animator in animators {
return animator.canAnimate(view: view, isAppearing: false) animator.clean()
} }
let currentToViews = context.toViews.filter { (view: UIView) -> Bool in
return animator.canAnimate(view: view, isAppearing: true) transitionContainer!.isUserInteractionEnabled = true
}
transitionPairs.append((currentFromViews, currentToViews)) let completion = completionCallback
transitionPairs = nil
transitionObservers = nil
transitionContainer = nil
completionCallback = nil
container = nil
preprocessors = nil
animators = nil
plugins = nil
context = nil
beginTime = nil
elapsedTime = 0
totalDuration = 0
completion?(isFinished)
} }
} }
/// Actually animate the views fileprivate extension MotionController {
/// subclass should call `prepareForTransition` & `prepareForAnimation` before calling `animate` /// Prepares the transition container.
func animate() { func prepareTransitionContainer() {
guard isTransitioning else { fatalError() } transitionContainer.isUserInteractionEnabled = false
for (currentFromViews, currentToViews) in transitionPairs {
// auto hide all animated views // a view to hold all the animating views
for view in currentFromViews { container = UIView(frame: transitionContainer.bounds)
context.hide(view: view) transitionContainer.addSubview(container)
}
for view in currentToViews {
context.hide(view: view)
}
} }
var totalDuration: TimeInterval = 0 /// Prepares the context.
var animatorWantsInteractive = false func prepareContext() {
for (i, animator) in animators.enumerated() { context = MotionContext(container:container)
let duration = animator.animate(fromViews: transitionPairs[i].0,
toViews: transitionPairs[i].1)
if duration == .infinity {
animatorWantsInteractive = true
} else {
totalDuration = max(totalDuration, duration)
}
} }
self.totalDuration = totalDuration /// Prepares the preprocessors.
if animatorWantsInteractive { func preparePreprocessors() {
update(progress: 0) preprocessors = [
} else { IgnoreSubviewModifiersPreprocessor(),
complete(after: totalDuration, isFinished: true) MatchPreprocessor(),
SourcePreprocessor(),
CascadePreprocessor(),
DurationPreprocessor()
]
for v in preprocessors {
v.context = context
}
} }
}
/// Prepares the animators.
func complete(after: TimeInterval, isFinished: Bool) { func prepareAnimators() {
guard isTransitioning else { fatalError() } animators = [
if after <= 0.001 { MotionDefaultAnimator<MotionCoreAnimationViewContext>()
complete(isFinished: isFinished) ]
return
if #available(iOS 10, tvOS 10, *) {
animators.append(MotionDefaultAnimator<MotionViewPropertyViewContext>())
}
for v in animators {
v.context = context
}
} }
let v = (isFinished ? elapsedTime : 1 - elapsedTime) * totalDuration
self.isFinished = isFinished /// Prepares the plugins.
self.currentAnimationDuration = after + v func preparePlugins() {
self.beginTime = CACurrentMediaTime() - v plugins = Motion.enabledPlugins.map({
} return $0.init()
})
func complete(isFinished: Bool) {
guard isTransitioning else { fatalError() } for plugin in plugins {
for animator in animators { preprocessors.append(plugin)
animator.clean() animators.append(plugin)
}
} }
transitionContainer!.isUserInteractionEnabled = true
let completion = completionCallback
transitionPairs = nil
transitionObservers = nil
transitionContainer = nil
completionCallback = nil
container = nil
processors = nil
animators = nil
plugins = nil
context = nil
beginTime = nil
elapsedTime = 0
totalDuration = 0
completion?(isFinished)
}
} }
// MARK: Plugin Support
internal extension MotionController { internal extension MotionController {
static func isEnabled(plugin: MotionPlugin.Type) -> Bool { /**
return enabledPlugins.index(where: { return $0 == plugin}) != nil Checks if a given plugin is enabled.
} - Parameter plugin: A MotionPlugin.Type.
- Returns: A boolean indicating if the plugin is enabled or not.
static func enable(plugin: MotionPlugin.Type) { */
disable(plugin: plugin) static func isEnabled(plugin: MotionPlugin.Type) -> Bool {
enabledPlugins.append(plugin) return nil != enabledPlugins.index(where: { return $0 == plugin })
} }
/**
Enables a given plugin.
- Parameter plugin: A MotionPlugin.Type.
*/
static func enable(plugin: MotionPlugin.Type) {
disable(plugin: plugin)
enabledPlugins.append(plugin)
}
static func disable(plugin: MotionPlugin.Type) { /**
if let index = enabledPlugins.index(where: { return $0 == plugin}) { Disables a given plugin.
enabledPlugins.remove(at: index) - Parameter plugin: A MotionPlugin.Type.
*/
static func disable(plugin: MotionPlugin.Type) {
guard let index = enabledPlugins.index(where: { return $0 == plugin }) else {
return
}
enabledPlugins.remove(at: index)
} }
}
} }
internal extension MotionController { internal extension MotionController {
// should call this after `prepareForTransition` & before `processContext` // should call this after `prepareTransitionPairs` & before `processContext`
func insert<T>(preprocessor: MotionPreprocessor, before: T.Type) { func insert<T>(preprocessor: MotionPreprocessor, before: T.Type) {
let processorIndex = processors.index { let v = preprocessors.index { $0 is T } ?? preprocessors.count
$0 is T preprocessor.context = context
} ?? processors.count preprocessors.insert(preprocessor, at: v)
preprocessor.context = context }
processors.insert(preprocessor, at: processorIndex)
}
} }
...@@ -37,11 +37,11 @@ public class MotionIndependentController: MotionController { ...@@ -37,11 +37,11 @@ public class MotionIndependentController: MotionController {
transitionContainer = rootView transitionContainer = rootView
completionCallback = completion completionCallback = completion
prepareForTransition() prepareTransition()
context.defaultCoordinateSpace = .sameParent context.defaultCoordinateSpace = .sameParent
context.set(fromViews: fromViews, toViews: toViews) context.set(fromViews: fromViews, toViews: toViews)
processContext() processContext()
prepareForAnimation() prepareTransitionPairs()
animate() animate()
} }
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment