330 lines
12 KiB
Swift
330 lines
12 KiB
Swift
//
|
|
// ContainableController.swift
|
|
// TGUIKit
|
|
//
|
|
// Created by keepcoder on 23/10/2016.
|
|
// Copyright © 2016 Telegram. All rights reserved.
|
|
//
|
|
|
|
import Cocoa
|
|
|
|
public enum ContainedViewLayoutTransitionCurve {
|
|
case linear
|
|
case easeInOut
|
|
case easeOut
|
|
case spring
|
|
case legacy
|
|
}
|
|
|
|
public let listViewAnimationCurveSystem: (CGFloat) -> CGFloat = { t in
|
|
return bezierPoint(0.23, 1.0, 0.32, 1.0, t)
|
|
}
|
|
public let listViewAnimationCurveEaseInOut: (CGFloat) -> CGFloat = { t in
|
|
return bezierPoint(0.42, 0.0, 0.58, 1.0, t)
|
|
}
|
|
|
|
public let listViewAnimationCurveLinear: (CGFloat) -> CGFloat = { t in
|
|
return t
|
|
}
|
|
|
|
public extension ContainedViewLayoutTransitionCurve {
|
|
var timingFunction: CAMediaTimingFunctionName {
|
|
switch self {
|
|
case .easeInOut:
|
|
return CAMediaTimingFunctionName.easeInEaseOut
|
|
case .spring:
|
|
return CAMediaTimingFunctionName.spring
|
|
case .legacy:
|
|
return CAMediaTimingFunctionName.easeInEaseOut
|
|
case .linear:
|
|
return CAMediaTimingFunctionName.linear
|
|
case .easeOut:
|
|
return CAMediaTimingFunctionName.easeOut
|
|
}
|
|
}
|
|
func solve(at offset: CGFloat) -> CGFloat {
|
|
switch self {
|
|
case .easeInOut:
|
|
return listViewAnimationCurveEaseInOut(offset)
|
|
case .spring:
|
|
return listViewAnimationCurveSystem(offset)
|
|
case .legacy:
|
|
return listViewAnimationCurveEaseInOut(offset)
|
|
case .easeOut:
|
|
return listViewAnimationCurveEaseInOut(offset)
|
|
case .linear:
|
|
return listViewAnimationCurveLinear(offset)
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public enum ContainedViewLayoutTransition {
|
|
case immediate
|
|
case animated(duration: Double, curve: ContainedViewLayoutTransitionCurve)
|
|
|
|
public var isAnimated: Bool {
|
|
switch self {
|
|
case .immediate:
|
|
return false
|
|
case .animated:
|
|
return true
|
|
}
|
|
}
|
|
public var duration: Double {
|
|
switch self {
|
|
case .immediate:
|
|
return 0
|
|
case let .animated(duration, _):
|
|
return duration
|
|
}
|
|
}
|
|
public var timingFunction:CAMediaTimingFunctionName {
|
|
switch self {
|
|
case .immediate:
|
|
return .linear
|
|
case let .animated(_, curve):
|
|
return curve.timingFunction
|
|
}
|
|
}
|
|
}
|
|
|
|
public extension ContainedViewLayoutTransition {
|
|
func updateFrame(view: NSView, frame: CGRect, completion: ((Bool) -> Void)? = nil) {
|
|
switch self {
|
|
case .immediate:
|
|
view.frame = frame
|
|
if let completion = completion {
|
|
completion(true)
|
|
}
|
|
case let .animated(duration, curve):
|
|
|
|
var curve = curve
|
|
|
|
if view is NSVisualEffectView {
|
|
curve = .legacy
|
|
}
|
|
switch curve {
|
|
case .legacy:
|
|
NSAnimationContext.runAnimationGroup({ ctx in
|
|
ctx.duration = duration
|
|
ctx.timingFunction = .init(name: curve.timingFunction)
|
|
view.animator().frame = frame
|
|
}, completionHandler: {
|
|
completion?(true)
|
|
})
|
|
default:
|
|
|
|
var ignoreSize: Bool = false
|
|
if let view = view as? TableView {
|
|
view.change(size: frame.size, animated: true, duration: duration, timingFunction: curve.timingFunction, completion: { completed in
|
|
completion?(completed)
|
|
})
|
|
}
|
|
|
|
var notifyOnOrigin: Bool = false
|
|
var notifyOnSize: Bool = false
|
|
if frame.size != view.frame.size {
|
|
notifyOnSize = true
|
|
} else if frame.origin != view.frame.origin {
|
|
notifyOnOrigin = true
|
|
} else {
|
|
completion?(true)
|
|
}
|
|
|
|
view._change(size: frame.size, animated: true, duration: duration, timingFunction: curve.timingFunction, completion: { completed in
|
|
if notifyOnSize {
|
|
completion?(completed)
|
|
}
|
|
})
|
|
view._change(pos: frame.origin, animated: true, duration: duration, timingFunction: curve.timingFunction, completion: { completed in
|
|
if notifyOnOrigin {
|
|
completion?(completed)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
func updateFrame(layer: CALayer, frame: CGRect, completion: ((Bool) -> Void)? = nil, save: Bool = true, updatePosition: Bool = false) {
|
|
switch self {
|
|
case .immediate:
|
|
layer.frame = frame
|
|
if let completion = completion {
|
|
completion(true)
|
|
}
|
|
case let .animated(duration, _):
|
|
|
|
|
|
func animateSize(_ layer: CALayer) -> Void {
|
|
var presentBounds:NSRect = layer.bounds
|
|
let presentation = layer.presentation()
|
|
if let presentation = presentation, layer.animation(forKey:"bounds") != nil {
|
|
presentBounds.size.width = NSWidth(presentation.bounds)
|
|
presentBounds.size.height = NSHeight(presentation.bounds)
|
|
}
|
|
layer.animateBounds(from: presentBounds, to: frame.size.bounds, duration: duration, timingFunction: timingFunction, completion: completion)
|
|
}
|
|
func animatePos(_ layer: CALayer) -> Void {
|
|
var presentRect:NSPoint = layer.position
|
|
let presentation = layer.presentation()
|
|
if let presentation = presentation, layer.animation(forKey:"position") != nil {
|
|
presentRect.x = presentation.position.x
|
|
presentRect.y = presentation.position.y
|
|
}
|
|
layer.animatePosition(from: presentRect, to: frame.origin, duration: duration, timingFunction: timingFunction, completion: completion)
|
|
}
|
|
if layer.frame.origin != frame.origin {
|
|
animatePos(layer)
|
|
}
|
|
if layer.frame.size != frame.size {
|
|
animateSize(layer)
|
|
}
|
|
if save {
|
|
if updatePosition {
|
|
layer.frame = frame.size.bounds
|
|
layer.position = frame.origin
|
|
} else {
|
|
layer.frame = frame
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
func updateTransformScale(layer: CALayer, scale: CGFloat, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
|
let t = layer.transform
|
|
let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13))
|
|
if currentScale.isEqual(to: scale) {
|
|
if let completion = completion {
|
|
completion(true)
|
|
}
|
|
return
|
|
}
|
|
|
|
switch self {
|
|
case .immediate:
|
|
layer.transform = CATransform3DMakeScale(scale, scale, 1.0)
|
|
if let completion = completion {
|
|
completion(true)
|
|
}
|
|
case let .animated(duration, curve):
|
|
let previousScale: CGFloat
|
|
if beginWithCurrentState, let presentation = layer.presentation() {
|
|
let t = presentation.transform
|
|
previousScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13))
|
|
} else {
|
|
previousScale = currentScale
|
|
}
|
|
layer.animateScaleSpring(from: previousScale, to: scale, duration: duration, bounce: false, completion: { result in
|
|
if let completion = completion {
|
|
completion(result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
func updateAlpha(view: NSView, alpha: CGFloat, completion: ((Bool) -> Void)? = nil) {
|
|
switch self {
|
|
case .immediate:
|
|
view.layer?.opacity = Float(alpha)
|
|
if let completion = completion {
|
|
completion(true)
|
|
}
|
|
case let .animated(duration, curve):
|
|
let previousAlpha = view.layer?.presentation()?.opacity ?? view.layer?.opacity ?? 1
|
|
view.layer?.opacity = Float(alpha)
|
|
view.layer?.animateAlpha(from: CGFloat(previousAlpha), to: alpha, duration: duration, timingFunction: curve.timingFunction, completion: { result in
|
|
if let completion = completion {
|
|
completion(result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func updateAlpha(layer: CALayer, alpha: CGFloat, completion: ((Bool) -> Void)? = nil) {
|
|
switch self {
|
|
case .immediate:
|
|
layer.opacity = Float(alpha)
|
|
if let completion = completion {
|
|
completion(true)
|
|
}
|
|
case let .animated(duration, curve):
|
|
let previousAlpha = layer.presentation()?.opacity ?? layer.opacity
|
|
layer.opacity = Float(alpha)
|
|
layer.animateAlpha(from: CGFloat(previousAlpha), to: alpha, duration: duration, timingFunction: curve.timingFunction, completion: { result in
|
|
if let completion = completion {
|
|
completion(result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// func updatePosition(layer: CALayer, position: NSPoint, completion: ((Bool) -> Void)? = nil) {
|
|
// switch self {
|
|
// case .immediate:
|
|
// layer.position = position
|
|
// if let completion = completion {
|
|
// completion(true)
|
|
// }
|
|
// case let .animated(duration, curve):
|
|
// let previousAlpha = layer.presentation()?.opacity ?? layer.opacity
|
|
// layer.position = position
|
|
// layer.animateAlpha(from: CGFloat(previousAlpha), to: alpha, duration: duration, timingFunction: curve.timingFunction, completion: { result in
|
|
// if let completion = completion {
|
|
// completion(result)
|
|
// }
|
|
// })
|
|
// }
|
|
// }
|
|
|
|
|
|
func animatePositionWithKeyframes(layer: CALayer, keyframes: [CGPoint], removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
|
switch self {
|
|
case .immediate:
|
|
completion?(true)
|
|
case let .animated(duration, curve):
|
|
layer.animateKeyframes(values: keyframes.map(NSValue.init(point:)), duration: duration, keyPath: "position", timingFunction: curve.timingFunction, removeOnCompletion: removeOnCompletion, completion: completion)
|
|
}
|
|
}
|
|
|
|
|
|
func setShapeLayerStrokeEnd(layer: CAShapeLayer, strokeEnd: CGFloat, completion: ((Bool) -> Void)? = nil) {
|
|
switch self {
|
|
case .immediate:
|
|
layer.strokeEnd = strokeEnd
|
|
completion?(true)
|
|
case let .animated(duration, curve):
|
|
let previousStrokeEnd = layer.strokeEnd
|
|
layer.strokeEnd = strokeEnd
|
|
|
|
layer.animate(
|
|
from: previousStrokeEnd as NSNumber,
|
|
to: strokeEnd as NSNumber,
|
|
keyPath: "strokeEnd",
|
|
timingFunction: curve.timingFunction,
|
|
duration: duration,
|
|
delay: 0.0,
|
|
removeOnCompletion: true,
|
|
additive: false,
|
|
completion: completion
|
|
)
|
|
}
|
|
}
|
|
|
|
|
|
}
|
|
|
|
public protocol ContainableController: class {
|
|
var view: View! { get }
|
|
|
|
func containerLayoutUpdated(transition: ContainedViewLayoutTransition)
|
|
}
|