You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
459 lines
16 KiB
459 lines
16 KiB
// The MIT License (MIT)
|
|
//
|
|
// Copyright (c) 2016 Luke Zhao <me@lkzhao.com>
|
|
//
|
|
// 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
|
|
|
|
extension CALayer {
|
|
internal static var heroAddedAnimations: [(CALayer, String, CAAnimation)]? = {
|
|
let swizzling: (AnyClass, Selector, Selector) -> Void = { forClass, originalSelector, swizzledSelector in
|
|
if let originalMethod = class_getInstanceMethod(forClass, originalSelector), let swizzledMethod = class_getInstanceMethod(forClass, swizzledSelector) {
|
|
method_exchangeImplementations(originalMethod, swizzledMethod)
|
|
}
|
|
}
|
|
let originalSelector = #selector(add(_:forKey:))
|
|
let swizzledSelector = #selector(hero_add(anim:forKey:))
|
|
swizzling(CALayer.self, originalSelector, swizzledSelector)
|
|
return nil
|
|
}()
|
|
|
|
@objc dynamic func hero_add(anim: CAAnimation, forKey: String?) {
|
|
if CALayer.heroAddedAnimations != nil {
|
|
let copiedAnim = anim.copy() as! CAAnimation
|
|
copiedAnim.delegate = nil // having delegate resulted some weird animation behavior
|
|
CALayer.heroAddedAnimations!.append((self, forKey!, copiedAnim))
|
|
hero_add(anim: anim, forKey: forKey)
|
|
} else {
|
|
hero_add(anim: anim, forKey: forKey)
|
|
}
|
|
}
|
|
}
|
|
|
|
internal class HeroCoreAnimationViewContext: HeroAnimatorViewContext {
|
|
|
|
var state = [String: (Any?, Any?)]()
|
|
var timingFunction: CAMediaTimingFunction = .standard
|
|
|
|
var animations: [(CALayer, String, CAAnimation)] = []
|
|
|
|
// computed
|
|
var contentLayer: CALayer? {
|
|
let firstLayer = snapshot.layer.sublayers?.get(0)
|
|
if firstLayer?.bounds == snapshot.bounds {
|
|
return firstLayer
|
|
}
|
|
return nil
|
|
}
|
|
var overlayLayer: CALayer?
|
|
|
|
override class func canAnimate(view: UIView, state: HeroTargetState, appearing: Bool) -> Bool {
|
|
return state.position != nil ||
|
|
state.size != nil ||
|
|
state.transform != nil ||
|
|
state.cornerRadius != nil ||
|
|
state.opacity != nil ||
|
|
state.overlay != nil ||
|
|
state.backgroundColor != nil ||
|
|
state.borderColor != nil ||
|
|
state.borderWidth != nil ||
|
|
state.shadowOpacity != nil ||
|
|
state.shadowRadius != nil ||
|
|
state.shadowOffset != nil ||
|
|
state.shadowColor != nil ||
|
|
state.shadowPath != nil ||
|
|
state.contentsRect != nil ||
|
|
state.forceAnimate
|
|
}
|
|
|
|
func getOverlayLayer() -> CALayer {
|
|
if overlayLayer == nil {
|
|
overlayLayer = CALayer()
|
|
overlayLayer!.frame = snapshot.bounds
|
|
overlayLayer!.opacity = 0
|
|
snapshot.layer.addSublayer(overlayLayer!)
|
|
}
|
|
return overlayLayer!
|
|
}
|
|
|
|
func overlayKeyFor(key: String) -> String? {
|
|
if key.hasPrefix("overlay.") {
|
|
var key = key
|
|
key.removeSubrange(key.startIndex..<key.index(key.startIndex, offsetBy: 8))
|
|
return key
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func currentValue(key: String) -> Any? {
|
|
if let key = overlayKeyFor(key: key) {
|
|
return (overlayLayer?.presentation() ?? overlayLayer)?.value(forKeyPath: key)
|
|
}
|
|
if snapshot.layer.animationKeys()?.isEmpty != false {
|
|
return snapshot.layer.value(forKeyPath: key)
|
|
}
|
|
return (snapshot.layer.presentation() ?? snapshot.layer).value(forKeyPath: key)
|
|
}
|
|
|
|
func getAnimation(key: String, beginTime: TimeInterval, duration: TimeInterval, fromValue: Any?, toValue: Any?, ignoreArc: Bool = false) -> CAPropertyAnimation {
|
|
let key = overlayKeyFor(key: key) ?? key
|
|
let anim: CAPropertyAnimation
|
|
|
|
if !ignoreArc, key == "position", let arcIntensity = targetState.arc,
|
|
let fromPos = (fromValue as? NSValue)?.cgPointValue,
|
|
let toPos = (toValue as? NSValue)?.cgPointValue,
|
|
abs(fromPos.x - toPos.x) >= 1, abs(fromPos.y - toPos.y) >= 1 {
|
|
let kanim = CAKeyframeAnimation(keyPath: key)
|
|
|
|
let path = CGMutablePath()
|
|
let maxControl = fromPos.y > toPos.y ? CGPoint(x: toPos.x, y: fromPos.y) : CGPoint(x: fromPos.x, y: toPos.y)
|
|
let minControl = (toPos - fromPos) / 2 + fromPos
|
|
|
|
path.move(to: fromPos)
|
|
path.addQuadCurve(to: toPos, control: minControl + (maxControl - minControl) * arcIntensity)
|
|
|
|
kanim.values = [fromValue!, toValue!]
|
|
kanim.path = path
|
|
kanim.duration = duration
|
|
kanim.timingFunctions = [timingFunction]
|
|
anim = kanim
|
|
} else if #available(iOS 9.0, *), key != "cornerRadius", let (stiffness, damping) = targetState.spring {
|
|
let sanim = CASpringAnimation(keyPath: key)
|
|
sanim.stiffness = stiffness
|
|
sanim.damping = damping
|
|
sanim.duration = sanim.settlingDuration
|
|
sanim.fromValue = fromValue
|
|
sanim.toValue = toValue
|
|
anim = sanim
|
|
} else {
|
|
let banim = CABasicAnimation(keyPath: key)
|
|
banim.duration = duration
|
|
banim.fromValue = fromValue
|
|
banim.toValue = toValue
|
|
banim.timingFunction = timingFunction
|
|
anim = banim
|
|
}
|
|
|
|
anim.fillMode = CAMediaTimingFillMode.both
|
|
anim.isRemovedOnCompletion = false
|
|
anim.beginTime = beginTime
|
|
return anim
|
|
}
|
|
|
|
func setSize(view: UIView, newSize: CGSize) {
|
|
let oldSize = view.bounds.size
|
|
if targetState.snapshotType != .noSnapshot {
|
|
if oldSize.width == 0 || oldSize.height == 0 || newSize.width == 0 || newSize.height == 0 {
|
|
for subview in view.subviews {
|
|
subview.center = newSize.center
|
|
subview.bounds.size = newSize
|
|
setSize(view: subview, newSize: newSize)
|
|
}
|
|
} else {
|
|
let sizeRatio = oldSize / newSize
|
|
for subview in view.subviews {
|
|
let center = subview.center
|
|
let size = subview.bounds.size
|
|
subview.center = center / sizeRatio
|
|
subview.bounds.size = size / sizeRatio
|
|
setSize(view: subview, newSize: size / sizeRatio)
|
|
}
|
|
}
|
|
view.bounds.size = newSize
|
|
} else {
|
|
view.bounds.size = newSize
|
|
view.layoutSubviews()
|
|
}
|
|
}
|
|
|
|
func uiViewBasedAnimate(duration: TimeInterval, delay: TimeInterval, _ animations: @escaping () -> Void) {
|
|
CALayer.heroAddedAnimations = []
|
|
|
|
if let (stiffness, damping) = targetState.spring {
|
|
UIView.animate(withDuration: duration, delay: delay, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: [], animations: animations, completion: nil)
|
|
|
|
let addedAnimations = CALayer.heroAddedAnimations!
|
|
CALayer.heroAddedAnimations = nil
|
|
|
|
for (layer, key, anim) in addedAnimations {
|
|
layer.removeAnimation(forKey: key)
|
|
if #available(iOS 9.0, *), let anim = anim as? CASpringAnimation {
|
|
anim.stiffness = stiffness
|
|
anim.damping = damping
|
|
self.addAnimation(anim, for: key, to: layer)
|
|
} else {
|
|
self.addAnimation(anim, for: key, to: layer)
|
|
}
|
|
}
|
|
} else {
|
|
CATransaction.begin()
|
|
CATransaction.setAnimationTimingFunction(timingFunction)
|
|
UIView.animate(withDuration: duration, delay: delay, options: [], animations: animations, completion: nil)
|
|
let addedAnimations = CALayer.heroAddedAnimations!
|
|
CALayer.heroAddedAnimations = nil
|
|
for (layer, key, anim) in addedAnimations {
|
|
layer.removeAnimation(forKey: key)
|
|
self.addAnimation(anim, for: key, to: layer)
|
|
}
|
|
CATransaction.commit()
|
|
}
|
|
}
|
|
|
|
func addAnimation(_ animation: CAAnimation, for key: String, to layer: CALayer) {
|
|
let heroAnimationKey = "hero.\(key)"
|
|
animations.append((layer, heroAnimationKey, animation))
|
|
layer.add(animation, forKey: heroAnimationKey)
|
|
}
|
|
|
|
// return the completion duration of the animation (duration + initial delay, not counting the beginTime)
|
|
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 {
|
|
addAnimation(anim.copy() as! CAAnimation, for: key, to: contentLayer)
|
|
}
|
|
if let overlayLayer = overlayLayer {
|
|
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
|
|
}
|
|
|
|
/**
|
|
- Returns: a CALayer [keyPath:value] map for animation
|
|
*/
|
|
func viewState(targetState: HeroTargetState) -> [String: Any?] {
|
|
var targetState = targetState
|
|
var rtn = [String: Any?]()
|
|
|
|
if let size = targetState.size {
|
|
if targetState.useScaleBasedSizeChange ?? self.targetState.useScaleBasedSizeChange ?? false {
|
|
let currentSize = snapshot.bounds.size
|
|
targetState.append(.scale(x:size.width / currentSize.width,
|
|
y:size.height / currentSize.height))
|
|
} else {
|
|
rtn["bounds.size"] = NSValue(cgSize: size)
|
|
}
|
|
}
|
|
if let position = targetState.position {
|
|
rtn["position"] = NSValue(cgPoint: position)
|
|
}
|
|
if let opacity = targetState.opacity, !(snapshot is UIVisualEffectView) {
|
|
rtn["opacity"] = NSNumber(value: opacity)
|
|
}
|
|
if let cornerRadius = targetState.cornerRadius {
|
|
rtn["cornerRadius"] = NSNumber(value: cornerRadius.native)
|
|
}
|
|
if let backgroundColor = targetState.backgroundColor {
|
|
rtn["backgroundColor"] = backgroundColor
|
|
}
|
|
if let zPosition = targetState.zPosition {
|
|
rtn["zPosition"] = NSNumber(value: zPosition.native)
|
|
}
|
|
|
|
if let borderWidth = targetState.borderWidth {
|
|
rtn["borderWidth"] = NSNumber(value: borderWidth.native)
|
|
}
|
|
if let borderColor = targetState.borderColor {
|
|
rtn["borderColor"] = borderColor
|
|
}
|
|
if let masksToBounds = targetState.masksToBounds {
|
|
rtn["masksToBounds"] = masksToBounds
|
|
}
|
|
|
|
if targetState.displayShadow {
|
|
if let shadowColor = targetState.shadowColor {
|
|
rtn["shadowColor"] = shadowColor
|
|
}
|
|
if let shadowRadius = targetState.shadowRadius {
|
|
rtn["shadowRadius"] = NSNumber(value: shadowRadius.native)
|
|
}
|
|
if let shadowOpacity = targetState.shadowOpacity {
|
|
rtn["shadowOpacity"] = NSNumber(value: shadowOpacity)
|
|
}
|
|
if let shadowPath = targetState.shadowPath {
|
|
rtn["shadowPath"] = shadowPath
|
|
}
|
|
if let shadowOffset = targetState.shadowOffset {
|
|
rtn["shadowOffset"] = NSValue(cgSize: shadowOffset)
|
|
}
|
|
}
|
|
|
|
if let contentsRect = targetState.contentsRect {
|
|
rtn["contentsRect"] = NSValue(cgRect: contentsRect)
|
|
}
|
|
|
|
if let contentsScale = targetState.contentsScale {
|
|
rtn["contentsScale"] = NSNumber(value: contentsScale.native)
|
|
}
|
|
|
|
if let transform = targetState.transform {
|
|
rtn["transform"] = NSValue(caTransform3D: transform)
|
|
}
|
|
|
|
if let (color, opacity) = targetState.overlay {
|
|
rtn["overlay.backgroundColor"] = color
|
|
rtn["overlay.opacity"] = NSNumber(value: opacity.native)
|
|
}
|
|
return rtn
|
|
}
|
|
|
|
override func apply(state: HeroTargetState) {
|
|
let targetState = viewState(targetState: state)
|
|
for (key, targetValue) in targetState {
|
|
if self.state[key] == nil {
|
|
let current = currentValue(key: key)
|
|
self.state[key] = (current, current)
|
|
}
|
|
let oldAnimations = animations
|
|
animations = []
|
|
_ = animate(key: key, beginTime: 0, duration: 100, fromValue: targetValue, toValue: targetValue)
|
|
animations = oldAnimations
|
|
}
|
|
}
|
|
|
|
override func changeTarget(state: HeroTargetState, isDestination: Bool) {
|
|
let targetState = viewState(targetState: state)
|
|
for (key, targetValue) in targetState {
|
|
let from: Any?, to: Any?
|
|
if let data = self.state[key] {
|
|
from = data.0
|
|
to = data.1
|
|
} else {
|
|
let data = currentValue(key: key)
|
|
from = data
|
|
to = data
|
|
}
|
|
if isDestination {
|
|
self.state[key] = (from, targetValue)
|
|
} else {
|
|
self.state[key] = (targetValue, to)
|
|
}
|
|
}
|
|
}
|
|
|
|
override func resume(timePassed: TimeInterval, reverse: Bool) -> TimeInterval {
|
|
for (key, (fromValue, toValue)) in state {
|
|
let realToValue = !reverse ? toValue : fromValue
|
|
let realFromValue = currentValue(key: key)
|
|
state[key] = (realFromValue, realToValue)
|
|
}
|
|
|
|
if reverse {
|
|
if timePassed > targetState.delay + duration {
|
|
let backDelay = timePassed - (targetState.delay + duration)
|
|
return animate(delay: backDelay, duration: duration)
|
|
} else if timePassed > targetState.delay {
|
|
return animate(delay: 0, duration: duration - (timePassed - targetState.delay))
|
|
} else {
|
|
return 0
|
|
}
|
|
} else {
|
|
if timePassed <= targetState.delay {
|
|
return animate(delay: targetState.delay - timePassed, duration: duration)
|
|
} else if timePassed <= targetState.delay + duration {
|
|
let timePassedDelay = timePassed - targetState.delay
|
|
return animate(delay: 0, duration: duration - timePassedDelay)
|
|
} else {
|
|
return 0
|
|
}
|
|
}
|
|
}
|
|
|
|
func animate(delay: TimeInterval, duration: TimeInterval) -> TimeInterval {
|
|
for (layer, key, _) in animations {
|
|
layer.removeAnimation(forKey: key)
|
|
}
|
|
|
|
if let tf = targetState.timingFunction {
|
|
timingFunction = tf
|
|
}
|
|
|
|
var timeUntilStop: TimeInterval = duration
|
|
|
|
animations = []
|
|
for (key, (fromValue, toValue)) in state {
|
|
let neededTime = animate(key: key, beginTime: currentTime + delay, duration: duration, fromValue: fromValue, toValue: toValue)
|
|
timeUntilStop = max(timeUntilStop, neededTime)
|
|
}
|
|
|
|
return timeUntilStop + delay
|
|
}
|
|
|
|
override func seek(timePassed: TimeInterval) {
|
|
let timeOffset = timePassed - targetState.delay
|
|
for (layer, key, anim) in animations {
|
|
anim.speed = 0
|
|
anim.timeOffset = timeOffset.clamp(0, anim.duration - 0.001)
|
|
layer.removeAnimation(forKey: key)
|
|
layer.add(anim, forKey: key)
|
|
}
|
|
}
|
|
|
|
override func clean() {
|
|
super.clean()
|
|
overlayLayer = nil
|
|
}
|
|
|
|
override func startAnimations() -> TimeInterval {
|
|
if let beginStateModifiers = targetState.beginState {
|
|
let beginState = HeroTargetState(modifiers: beginStateModifiers)
|
|
let appeared = viewState(targetState: beginState)
|
|
for (key, value) in appeared {
|
|
snapshot.layer.setValue(value, forKeyPath: key)
|
|
}
|
|
if let (color, opacity) = beginState.overlay {
|
|
let overlay = getOverlayLayer()
|
|
overlay.backgroundColor = color
|
|
overlay.opacity = Float(opacity)
|
|
}
|
|
}
|
|
|
|
let disappeared = viewState(targetState: targetState)
|
|
|
|
for (key, disappearedState) in disappeared {
|
|
let appearingState = currentValue(key: key)
|
|
let toValue = appearing ? appearingState : disappearedState
|
|
let fromValue = !appearing ? appearingState : disappearedState
|
|
state[key] = (fromValue, toValue)
|
|
}
|
|
return animate(delay: targetState.delay, duration: duration)
|
|
}
|
|
}
|