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

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