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 {
...@@ -191,180 +214,162 @@ fileprivate extension MotionController { ...@@ -191,180 +214,162 @@ fileprivate extension MotionController {
} }
public extension MotionController { public extension MotionController {
// MARK: Interactive Transition
/** /**
Update the progress for the interactive transition. Updates the elapsed time for the interactive transition.
- Parameters: - Parameter elapsedTime t: the current progress, must be between -1...1.
- progress: the current progress, must be between -1...1
*/ */
public func update(progress: Double) { public func update(elapsedTime t: TimeInterval) {
guard isTransitioning else { return } guard isTransitioning else {
self.beginTime = nil return
self.elapsedTime = max(-1, min(1, progress)) }
beginTime = nil
elapsedTime = max(-1, min(1, t))
} }
/** /**
Finish the interactive transition. Finish 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 **end** state current state to the **end** state
- Parameter isAnimated: A boolean indicating if the completion is animated.
*/ */
public func end(animate: Bool = true) { public func end(isAnimated: Bool = true) {
guard isTransitioning else { return } guard isTransitioning else {
if !animate { return
self.complete(isFinished:true) }
guard isAnimated else {
complete(isFinished: true)
return return
} }
var maxTime: TimeInterval = 0
for animator in self.animators { var v: TimeInterval = 0
maxTime = max(maxTime, animator.resume(at: self.elapsedTime * self.totalDuration, isReversed: false)) for a in animators {
v = max(v, a.resume(at: elapsedTime * totalDuration, isReversed: false))
} }
self.complete(after: maxTime, isFinished: true)
complete(after: v, 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) { public func cancel(isAnimated: Bool = true) {
guard isTransitioning else { return } guard isTransitioning else {
if !animate {
self.complete(isFinished:false)
return return
} }
var maxTime: TimeInterval = 0
for animator in self.animators { guard isAnimated else {
var adjustedProgress = self.elapsedTime complete(isFinished:false)
if adjustedProgress < 0 { return
adjustedProgress = -adjustedProgress
} }
maxTime = max(maxTime, animator.resume(at: adjustedProgress * self.totalDuration, isReversed: true))
var v: TimeInterval = 0
for a in animators {
var t = elapsedTime
if t < 0 {
t = -t
} }
self.complete(after: maxTime, isFinished: false)
v = max(v, a.resume(at: t * totalDuration, isReversed: true))
}
complete(after: v, isFinished: false)
} }
/** /**
Override modifiers during an interactive animation. Override transition animations during an interactive animation.
For example: For example:
Motion.shared.apply([.position(x:50, y:50)], to:view) Motion.shared.apply([.position(x:50, y:50)], to: view)
will set the view's position to 50, 50 will set the view's position to 50, 50
- Parameters: - Parameter transitions: An Array of MotionTransitions.
- modifiers: the modifiers to override - Parameter to view: A UIView.
- view: the view to override to
*/ */
public func apply(transitions: [MotionTransition], to view: UIView) { public func apply(transitions: [MotionTransition], to view: UIView) {
guard isTransitioning else { return } guard isTransitioning else {
let targetState = MotionTargetState(transitions: transitions) return
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 {
// MARK: Observe Progress
/** let s = MotionTargetState(transitions: transitions)
Receive callbacks on each animation frame. let v = context.pairedView(for: view) ?? view
Observers will be cleaned when transition completes
- Parameters: for a in animators {
- observer: the observer a.apply(state: s, to: v)
*/
func observeForProgressUpdate(observer: MotionTransitionObserver) {
if transitionObservers == nil {
transitionObservers = []
} }
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()
]
animators = [
MotionDefaultAnimator<MotionCoreAnimationViewContext>()
]
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. prepareTransitionContainer()
for plugin in plugins { prepareContext()
processors.append(plugin) preparePreprocessors()
animators.append(plugin) prepareAnimators()
preparePlugins()
} }
transitionContainer.isUserInteractionEnabled = false /// Prepares the transition from-view & to-view pairs.
func prepareTransitionPairs() {
guard isTransitioning else {
fatalError()
}
// a view to hold all the animating views transitionPairs = [([UIView], [UIView])]()
container = UIView(frame: transitionContainer.bounds)
transitionContainer.addSubview(container)
context = MotionContext(container:container) for a in animators {
let fv = context.fromViews.filter { (view: UIView) -> Bool in
return a.canAnimate(view: view, isAppearing: false)
}
for processor in processors { let tv = context.toViews.filter { (view: UIView) -> Bool in
processor.context = context return a.canAnimate(view: view, isAppearing: true)
} }
for animator in animators {
animator.context = context transitionPairs.append((fv, tv))
} }
} }
}
internal extension MotionController {
func processContext() { func processContext() {
guard isTransitioning else { fatalError() } guard isTransitioning else {
for processor in processors { fatalError()
processor.process(fromViews: context.fromViews, toViews: context.toViews)
}
} }
func prepareForAnimation() { for v in preprocessors {
guard isTransitioning else { fatalError() } v.process(fromViews: context.fromViews, toViews: context.toViews)
transitionPairs = [([UIView], [UIView])]()
for animator in animators {
let currentFromViews = context.fromViews.filter { (view: UIView) -> Bool in
return animator.canAnimate(view: view, isAppearing: false)
}
let currentToViews = context.toViews.filter { (view: UIView) -> Bool in
return animator.canAnimate(view: view, isAppearing: true)
}
transitionPairs.append((currentFromViews, currentToViews))
} }
} }
/// Actually animate the views /// Actually animate the views
/// subclass should call `prepareForTransition` & `prepareForAnimation` before calling `animate` /// subclass should call `prepareTransition` & `prepareTransitionPairs` before calling `animate`
func animate() { func animate() {
guard isTransitioning else { fatalError() } guard isTransitioning else {
fatalError()
}
for (currentFromViews, currentToViews) in transitionPairs { for (currentFromViews, currentToViews) in transitionPairs {
// auto hide all animated views // auto hide all animated views
for view in currentFromViews { for view in currentFromViews {
context.hide(view: view) context.hide(view: view)
} }
for view in currentToViews { for view in currentToViews {
context.hide(view: view) context.hide(view: view)
} }
...@@ -372,9 +377,10 @@ internal extension MotionController { ...@@ -372,9 +377,10 @@ internal extension MotionController {
var totalDuration: TimeInterval = 0 var totalDuration: TimeInterval = 0
var animatorWantsInteractive = false var animatorWantsInteractive = false
for (i, animator) in animators.enumerated() { for (i, animator) in animators.enumerated() {
let duration = animator.animate(fromViews: transitionPairs[i].0, let duration = animator.animate(fromViews: transitionPairs[i].0, toViews: transitionPairs[i].1)
toViews: transitionPairs[i].1)
if duration == .infinity { if duration == .infinity {
animatorWantsInteractive = true animatorWantsInteractive = true
} else { } else {
...@@ -384,18 +390,22 @@ internal extension MotionController { ...@@ -384,18 +390,22 @@ internal extension MotionController {
self.totalDuration = totalDuration self.totalDuration = totalDuration
if animatorWantsInteractive { if animatorWantsInteractive {
update(progress: 0) update(elapsedTime: 0)
} else { } else {
complete(after: totalDuration, isFinished: true) complete(after: totalDuration, isFinished: true)
} }
} }
func complete(after: TimeInterval, isFinished: Bool) { func complete(after: TimeInterval, isFinished: Bool) {
guard isTransitioning else { fatalError() } guard isTransitioning else {
fatalError()
}
if after <= 0.001 { if after <= 0.001 {
complete(isFinished: isFinished) complete(isFinished: isFinished)
return return
} }
let v = (isFinished ? elapsedTime : 1 - elapsedTime) * totalDuration let v = (isFinished ? elapsedTime : 1 - elapsedTime) * totalDuration
self.isFinished = isFinished self.isFinished = isFinished
self.currentAnimationDuration = after + v self.currentAnimationDuration = after + v
...@@ -403,7 +413,10 @@ internal extension MotionController { ...@@ -403,7 +413,10 @@ internal extension MotionController {
} }
func complete(isFinished: Bool) { func complete(isFinished: Bool) {
guard isTransitioning else { fatalError() } guard isTransitioning else {
fatalError()
}
for animator in animators { for animator in animators {
animator.clean() animator.clean()
} }
...@@ -417,7 +430,7 @@ internal extension MotionController { ...@@ -417,7 +430,7 @@ internal extension MotionController {
transitionContainer = nil transitionContainer = nil
completionCallback = nil completionCallback = nil
container = nil container = nil
processors = nil preprocessors = nil
animators = nil animators = nil
plugins = nil plugins = nil
context = nil context = nil
...@@ -429,31 +442,101 @@ internal extension MotionController { ...@@ -429,31 +442,101 @@ internal extension MotionController {
} }
} }
// MARK: Plugin Support fileprivate extension MotionController {
/// Prepares the transition container.
func prepareTransitionContainer() {
transitionContainer.isUserInteractionEnabled = false
// a view to hold all the animating views
container = UIView(frame: transitionContainer.bounds)
transitionContainer.addSubview(container)
}
/// Prepares the context.
func prepareContext() {
context = MotionContext(container:container)
}
/// Prepares the preprocessors.
func preparePreprocessors() {
preprocessors = [
IgnoreSubviewModifiersPreprocessor(),
MatchPreprocessor(),
SourcePreprocessor(),
CascadePreprocessor(),
DurationPreprocessor()
]
for v in preprocessors {
v.context = context
}
}
/// Prepares the animators.
func prepareAnimators() {
animators = [
MotionDefaultAnimator<MotionCoreAnimationViewContext>()
]
if #available(iOS 10, tvOS 10, *) {
animators.append(MotionDefaultAnimator<MotionViewPropertyViewContext>())
}
for v in animators {
v.context = context
}
}
/// Prepares the plugins.
func preparePlugins() {
plugins = Motion.enabledPlugins.map({
return $0.init()
})
for plugin in plugins {
preprocessors.append(plugin)
animators.append(plugin)
}
}
}
internal extension MotionController { internal extension MotionController {
/**
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 isEnabled(plugin: MotionPlugin.Type) -> Bool { static func isEnabled(plugin: MotionPlugin.Type) -> Bool {
return enabledPlugins.index(where: { return $0 == plugin}) != nil return nil != enabledPlugins.index(where: { return $0 == plugin })
} }
/**
Enables a given plugin.
- Parameter plugin: A MotionPlugin.Type.
*/
static func enable(plugin: MotionPlugin.Type) { static func enable(plugin: MotionPlugin.Type) {
disable(plugin: plugin) disable(plugin: plugin)
enabledPlugins.append(plugin) enabledPlugins.append(plugin)
} }
/**
Disables a given plugin.
- Parameter plugin: A MotionPlugin.Type.
*/
static func disable(plugin: MotionPlugin.Type) { static func disable(plugin: MotionPlugin.Type) {
if let index = enabledPlugins.index(where: { return $0 == plugin}) { guard let index = enabledPlugins.index(where: { return $0 == plugin }) else {
enabledPlugins.remove(at: index) 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
} ?? processors.count
preprocessor.context = context preprocessor.context = context
processors.insert(preprocessor, at: processorIndex) preprocessors.insert(preprocessor, at: v)
} }
} }
...@@ -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