Commit 64b831e4 by Orkhan Alikhanov

Added MotionViewTransition to make transitions of views possible

parent 4bf570ca
......@@ -87,6 +87,8 @@
96E409AB1F24F7570015A2B5 /* MotionPreprocessor.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96E409621F24F7370015A2B5 /* MotionPreprocessor.swift */; settings = {ATTRIBUTES = (Public, ); }; };
96E409AC1F24F7570015A2B5 /* SourcePreprocessor.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96E409631F24F7370015A2B5 /* SourcePreprocessor.swift */; settings = {ATTRIBUTES = (Public, ); }; };
96E409AD1F24F7570015A2B5 /* TransitionPreprocessor.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96E409641F24F7370015A2B5 /* TransitionPreprocessor.swift */; settings = {ATTRIBUTES = (Public, ); }; };
9D72470D21557F7000C04B48 /* MotionViewTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D72470C21557F7000C04B48 /* MotionViewTransition.swift */; };
9D7247132158A15E00C04B48 /* MotionViewTransitionAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D7247122158A15E00C04B48 /* MotionViewTransitionAnimator.swift */; };
D4A2ECE521467454003162B4 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4A2ECE421467454003162B4 /* Application.swift */; };
/* End PBXBuildFile section */
......@@ -137,6 +139,8 @@
96E409641F24F7370015A2B5 /* TransitionPreprocessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransitionPreprocessor.swift; sourceTree = "<group>"; };
96E409BB1F24FC210015A2B5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
96E409BC1F24FC300015A2B5 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
9D72470C21557F7000C04B48 /* MotionViewTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MotionViewTransition.swift; sourceTree = "<group>"; };
9D7247122158A15E00C04B48 /* MotionViewTransitionAnimator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MotionViewTransitionAnimator.swift; sourceTree = "<group>"; };
D4A2ECE421467454003162B4 /* Application.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
......@@ -194,6 +198,7 @@
96E4095A1F24F7370015A2B5 /* MotionModifier.swift */,
96E4095C1F24F7370015A2B5 /* MotionTargetState.swift */,
96E4095B1F24F7370015A2B5 /* MotionTransitionObserver.swift */,
9D72470C21557F7000C04B48 /* MotionViewTransition.swift */,
96E4093C1F24F7370015A2B5 /* Animator */,
96E409431F24F7370015A2B5 /* Extensions */,
96E4095D1F24F7370015A2B5 /* Preprocessors */,
......@@ -210,6 +215,7 @@
96E4093E1F24F7370015A2B5 /* MotionAnimatorViewContext.swift */,
96E4093F1F24F7370015A2B5 /* MotionCoreAnimationViewContext.swift */,
96E409421F24F7370015A2B5 /* MotionViewPropertyViewContext.swift */,
9D7247122158A15E00C04B48 /* MotionViewTransitionAnimator.swift */,
);
path = Animator;
sourceTree = "<group>";
......@@ -360,6 +366,7 @@
965FE9931FE43DE10098BDD0 /* MotionTransition+UINavigationControllerDelegate.swift in Sources */,
96E409861F24F7370015A2B5 /* MotionPreprocessor.swift in Sources */,
96E409821F24F7370015A2B5 /* CascadePreprocessor.swift in Sources */,
9D7247132158A15E00C04B48 /* MotionViewTransitionAnimator.swift in Sources */,
96E4096C1F24F7370015A2B5 /* Motion+CALayer.swift in Sources */,
96E409781F24F7370015A2B5 /* MotionCAAnimation.swift in Sources */,
96E409811F24F7370015A2B5 /* MotionTargetState.swift in Sources */,
......@@ -378,6 +385,7 @@
965FE9691FDDA1F20098BDD0 /* MotionViewOrderStrategy.swift in Sources */,
965FE97A1FE1D83D0098BDD0 /* MotionTransition.swift in Sources */,
965FE98D1FE334E10098BDD0 /* MotionTransition+UIViewControllerTransitioningDelegate.swift in Sources */,
9D72470D21557F7000C04B48 /* MotionViewTransition.swift in Sources */,
96E409871F24F7370015A2B5 /* SourcePreprocessor.swift in Sources */,
965FE9A11FE43EF80098BDD0 /* MotionTransition+CustomTransition.swift in Sources */,
96E409701F24F7370015A2B5 /* Motion+UIKit.swift in Sources */,
......
......@@ -79,7 +79,7 @@ internal class MotionCoreAnimator<T: MotionAnimatorViewContext>: MotionAnimator
view is appearing.
*/
func canAnimate(view: UIView, isAppearing: Bool) -> Bool {
guard let state = context[view] else {
guard let state = targetState(for: view) else {
return false
}
......@@ -176,6 +176,24 @@ internal class MotionCoreAnimator<T: MotionAnimatorViewContext>: MotionAnimator
invalidateCurrentTime()
}
/**
Returns MotionTargetState for the given view.
- Parameter for view: A UIView.
- Returns: A MotionTargetState.
*/
func targetState(for view: UIView) -> MotionTargetState? {
return context[view]
}
/**
Returns snapshot view for the given view.
- Parameter for view: A UIView.
- Returns: A snapshot UIView.
*/
func snapshotView(for view: UIView) -> UIView {
return context.snapshotView(for: view)
}
}
fileprivate extension MotionCoreAnimator {
......@@ -186,7 +204,7 @@ fileprivate extension MotionCoreAnimator {
view is appearing.
*/
func createViewContext(view: UIView, isAppearing: Bool) {
viewToContexts[view] = T(animator: self, snapshot: context.snapshotView(for: view), targetState: context[view]!, isAppearing: isAppearing)
viewToContexts[view] = T(animator: self, snapshot: snapshotView(for: view), targetState: targetState(for: view)!, isAppearing: isAppearing)
}
}
......
/*
* The MIT License (MIT)
*
* Copyright (C) 2018, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import UIKit
internal class MotionViewTransitionAnimator: MotionCoreAnimator<MotionCoreAnimationViewContext> {
/**
Returns MotionTargetState for the given view.
- Parameter for view: A UIView.
- Returns: A MotionTargetState.
*/
override func targetState(for view: UIView) -> MotionTargetState? {
guard let modifiers = view.motionModifiers else {
return nil
}
return MotionTargetState(modifiers: modifiers)
}
/**
Returns snapshot view for the given view.
- Parameter for view: A UIView.
- Returns: A snapshot UIView.
*/
override func snapshotView(for view: UIView) -> UIView {
return view
}
}
/*
* The MIT License (MIT)
*
* Copyright (C) 2018, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import UIKit
private var MotionViewTransitionKey: UInt8 = 0
public extension UIView {
/// The MotionViewTransition instance associated with the view.
var motionViewTransition: MotionViewTransition {
get {
return AssociatedObject.get(base: self, key: &MotionViewTransitionKey) {
MotionViewTransition(self)
}
}
}
}
open class MotionViewTransition {
/// A MotionViewTransitionAnimator used during a transition.
private let animator = MotionViewTransitionAnimator()
/// A UIView whose subviews and itself will be animating during a transition.
private weak var container: UIView?
/// Maximum duration of the animations.
open private(set) var totalDuration: TimeInterval = 0
/// Progress of the current transition.
open private(set) var progress: CGFloat = 0
/// A Boolean to control if the models should be updated when animations end.
open var shouldUpdateModels: Bool = true
/// Current duration of the animations based on totalDuration and progress.
private var currentDuration: TimeInterval {
return totalDuration * Double(progress)
}
/**
An initializer that accepts a container transition view.
- Parameter container: A UIView.
*/
fileprivate init(_ container: UIView) {
self.container = container
}
/// Prepares the transition animations.
open func start() {
guard let v = container else {
return
}
totalDuration = animator.animate(fromViews: v.flattenedViewHierarchy, toViews: [])
update(0)
}
/**
Updates the elapsed time for the transition.
- Parameter progress: the current progress.
*/
open func update(_ progress: CGFloat) {
self.progress = progress.clamp(0, 1)
animator.seek(to: currentDuration)
}
/**
Cancels the interactive transition by animatin from the current state
to the **beginning** state.
- Parameter isAnimated: A boolean indicating if the completion is animated.
*/
open func cancel(isAnimated: Bool = true) {
end(isAnimated: isAnimated, isReversed: true)
}
/**
Finishes the interactive transition by animating from the current state
to the **end** state.
- Parameter isAnimated: A Boolean indicating if the completion is animated.
*/
open func finish(isAnimated: Bool = true) {
end(isAnimated: isAnimated, isReversed: false)
}
deinit {
clean()
}
}
private extension MotionViewTransition {
/**
Ends the interactive transition by animating from the current state
to the **beginning** or **end** state based on the value of isReversed.
- Parameter isAnimated: A Boolean indicating if the completion is animated.
- Parameter isReversed: A Boolean indicating the direction of completion.
*/
func end(isAnimated: Bool, isReversed: Bool) {
let duration = isAnimated ? currentDuration : isReversed ? 0 : totalDuration
/// 0.00001 is to make sure that animator adds the animations on resume.
let after = animator.resume(at: abs(duration - 0.00001), isReversed: isReversed)
updateModels()
if isAnimated {
Motion.delay(after, execute: complete)
} else {
complete()
}
}
/// Finalizes the transition.
func complete() {
clean()
removeAnimations()
}
/// Resets the transition.
func clean() {
animator.clean()
totalDuration = 0
progress = 0
}
}
private extension MotionViewTransition {
/// Updates the layers with final values of animations.
func updateModels() {
guard shouldUpdateModels else {
return
}
walkingThroughAnimations { layer, _, anim in
/// bounds.size somehow is directly set on the layer.
let toValue = anim.keyPath == "bounds.size" ? layer.bounds.size : anim.toValue
layer.setValue(toValue, forKeyPath: anim.keyPath!)
}
}
/// Removes added animations from layers.
func removeAnimations() {
guard shouldUpdateModels else {
return
}
walkingThroughAnimations { layer, key, _ in
layer.removeAnimation(forKey: key)
}
}
/**
Walks through each layer's animation and executes give closure with
CALayer, animation key String, and CABasicAnimation.
- Parameter execute: A closure accepting CALayer, String, and CABasicAnimation
which is called for each layer's animation.
*/
func walkingThroughAnimations(execute: (CALayer, String, CABasicAnimation) -> Void) {
animator.viewToContexts.keys.forEach { v in
v.layer.animations.forEach { key, animation in
if let anim = animation as? CABasicAnimation {
execute(v.layer, key, anim)
}
}
}
}
}
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