Adding animated counter

This commit is contained in:
Ilya Yelagov
2022-11-27 14:12:07 +04:00
parent cbfe481a38
commit da7beffdd6
3 changed files with 541 additions and 30 deletions

View File

@@ -814,6 +814,7 @@ public final class _MediaStreamComponent: CombinedComponent {
var videoHiddenForPip = false
/// To update videoHiddenForPip
var onExpandedFromPictureInPicture: ((State) -> Void)?
private let infoThrottler = Throttler<Int?>.init(duration: 5, queue: .main)
init(call: PresentationGroupCallImpl) {
self.call = call
@@ -821,7 +822,7 @@ public final class _MediaStreamComponent: CombinedComponent {
if #available(iOSApplicationExtension 15.0, iOS 15.0, *), AVPictureInPictureController.isPictureInPictureSupported() {
self.isPictureInPictureSupported = true
} else {
self.isPictureInPictureSupported = false
self.isPictureInPictureSupported = true
}
super.init()
@@ -853,6 +854,22 @@ public final class _MediaStreamComponent: CombinedComponent {
}
var updated = false
// TODO: remove debug
Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { _ in
strongSelf.infoThrottler.publish(members.totalCount/*Int.random(in: 0..<10000000)*/) { [weak strongSelf] latestCount in
guard let strongSelf = strongSelf else { return }
var updated = false
let originInfo = OriginInfo(title: callPeer.debugDisplayTitle, memberCount: members.totalCount)
if strongSelf.originInfo != originInfo {
strongSelf.originInfo = originInfo
updated = true
}
//
if updated {
strongSelf.updated(transition: .immediate)
}
}
}.fire()
if state.canManageCall != strongSelf.canManageCall {
strongSelf.canManageCall = state.canManageCall
updated = true
@@ -873,12 +890,12 @@ public final class _MediaStreamComponent: CombinedComponent {
updated = true
}
let originInfo = OriginInfo(title: callPeer.debugDisplayTitle, memberCount: members.totalCount)
if strongSelf.originInfo != originInfo {
strongSelf.originInfo = originInfo
updated = true
}
// let originInfo = OriginInfo(title: callPeer.debugDisplayTitle, memberCount: members.totalCount)
// if strongSelf.originInfo != originInfo {
// strongSelf.originInfo = originInfo
// updated = true
// }
//
if updated {
strongSelf.updated(transition: .immediate)
}
@@ -998,8 +1015,9 @@ public final class _MediaStreamComponent: CombinedComponent {
}
var isFullscreen = state.isFullscreen
let isLandscape = context.availableSize.width > context.availableSize.height
if let videoSize = context.state.videoSize {
if videoSize.width > videoSize.height && isLandscape && !isFullscreen {
if let _ = context.state.videoSize {
// Always fullscreen in landscape
if /*videoSize.width > videoSize.height &&*/ isLandscape && !isFullscreen {
state.isFullscreen = true
isFullscreen = true
}
@@ -1500,7 +1518,9 @@ public final class _MediaStreamComponent: CombinedComponent {
topOffset: context.availableSize.height - sheetHeight + context.state.dismissOffset,
sheetHeight: max(sheetHeight - context.state.dismissOffset, sheetHeight),
backgroundColor: isFullscreen ? .clear : (isFullyDragged ? fullscreenBackgroundColor : panelBackgroundColor),
bottomPadding: bottomPadding
bottomPadding: bottomPadding,
participantsCount: // [0, 5, 15, 16, 95, 100, 16042, 942539].randomElement()!
context.state.originInfo?.memberCount ?? 0
),
availableSize: context.availableSize,
transition: context.transition
@@ -1582,7 +1602,8 @@ public final class _MediaStreamComponent: CombinedComponent {
topOffset: context.availableSize.height - sheetHeight + context.state.dismissOffset,
sheetHeight: max(sheetHeight - context.state.dismissOffset, sheetHeight),
backgroundColor: isFullscreen ? .clear : (isFullyDragged ? fullscreenBackgroundColor : panelBackgroundColor),
bottomPadding: 12
bottomPadding: 12,
participantsCount: -1 // context.state.originInfo?.memberCount ?? 0
),
availableSize: context.availableSize,
transition: context.transition
@@ -1866,3 +1887,67 @@ public final class _MediaStreamComponentController: ViewControllerComponentConta
}
public typealias MediaStreamComponentController = _MediaStreamComponentController
public final class Throttler<T: Hashable> {
public var duration: TimeInterval = 0.25
public var queue: DispatchQueue = .main
public var isEnabled: Bool { duration > 0 }
private var isThrottling: Bool = false
private var lastValue: T?
private var accumulator = Set<T>()
private var lastCompletedValue: T?
public init(duration: TimeInterval = 0.25, queue: DispatchQueue = .main) {
self.duration = duration
self.queue = queue
}
public func publish(_ value: T, includingLatest: Bool = false, using completion: ((T) -> Void)?) {
accumulator.insert(value)
if !isThrottling {
isThrottling = true
lastValue = nil
queue.async {
completion?(value)
self.lastCompletedValue = value
}
} else {
lastValue = value
}
if lastValue == nil {
queue.asyncAfter(deadline: .now() + duration) { [self] in
accumulator.removeAll()
isThrottling = false
guard
let lastValue = lastValue,
lastCompletedValue != lastValue || includingLatest
else { return }
accumulator.insert(lastValue)
self.lastValue = nil
completion?(lastValue)
lastCompletedValue = lastValue
}
}
}
public func cancelCurrent() {
lastValue = nil
isThrottling = false
accumulator.removeAll()
}
public func canEmit(_ value: T) -> Bool {
!accumulator.contains(value)
}
}
public extension Throttler where T == Bool {
func throttle(includingLatest: Bool = false, _ completion: ((T) -> Void)?) {
publish(true, includingLatest: includingLatest, using: completion)
}
}

View File

@@ -220,7 +220,7 @@ final class _MediaStreamVideoComponent: Component {
let delegate = PlaybackDelegateImpl()
delegate.onTransitionFinished = { [weak self] in
if self?.videoView?.alpha == 0 {
self?.videoView?.alpha = 1
// self?.videoView?.alpha = 1
}
}
return delegate

View File

@@ -17,8 +17,9 @@ final class StreamSheetComponent: CombinedComponent {
let sheetHeight: CGFloat
let topOffset: CGFloat
let backgroundColor: UIColor
let participantsCount: Int
let bottomPadding: CGFloat
init(
// color: UIColor,
topComponent: AnyComponent<Empty>,
@@ -26,7 +27,8 @@ final class StreamSheetComponent: CombinedComponent {
topOffset: CGFloat,
sheetHeight: CGFloat,
backgroundColor: UIColor,
bottomPadding: CGFloat
bottomPadding: CGFloat,
participantsCount: Int
) {
// self.leftItem = leftItem
self.topComponent = topComponent
@@ -36,6 +38,7 @@ final class StreamSheetComponent: CombinedComponent {
self.sheetHeight = sheetHeight
self.backgroundColor = backgroundColor
self.bottomPadding = bottomPadding
self.participantsCount = participantsCount
}
static func ==(lhs: StreamSheetComponent, rhs: StreamSheetComponent) -> Bool {
@@ -60,6 +63,9 @@ final class StreamSheetComponent: CombinedComponent {
if lhs.bottomPadding != rhs.bottomPadding {
return false
}
if lhs.participantsCount != rhs.participantsCount {
return false
}
return true
}
//
@@ -117,7 +123,7 @@ final class StreamSheetComponent: CombinedComponent {
let background = Child(SheetBackgroundComponent.self)
// let leftItem = Child(environment: Empty.self)
let topItem = Child(environment: Empty.self)
// let viewerCounter = Child(environment: Empty.self)
let viewerCounter = Child(ParticipantsComponent.self)
let bottomButtonsRow = Child(environment: Empty.self)
// let bottomButtons = Child(environment: Empty.self)
// let rightItems = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self)
@@ -140,13 +146,11 @@ final class StreamSheetComponent: CombinedComponent {
)
}
// let viewerCounter = context.component.viewerCounter.flatMap { viewerCounterComponent in
// return viewerCounter.update(
// component: viewerCounterComponent,
// availableSize: context.availableSize,
// transition: context.transition
// )
// }
let viewerCounter = viewerCounter.update(
component: ParticipantsComponent(count: context.component.participantsCount),
availableSize: CGSize(width: context.availableSize.width, height: 70),
transition: context.transition
)
let bottomButtonsRow = context.component.bottomButtonsRow.flatMap { bottomButtonsRowComponent in
return bottomButtonsRow.update(
@@ -164,6 +168,7 @@ final class StreamSheetComponent: CombinedComponent {
(context.view as? StreamSheetComponent.View)?.overlayComponentsFrames = []
context.view.backgroundColor = .clear
if let topItem = topItem {
context.add(topItem
.position(CGPoint(x: topItem.size.width / 2.0, y: topOffset + contentHeight / 2.0))
@@ -171,13 +176,13 @@ final class StreamSheetComponent: CombinedComponent {
(context.view as? StreamSheetComponent.View)?.overlayComponentsFrames.append(.init(x: 0, y: topOffset, width: topItem.size.width, height: topItem.size.height))
}
// if let viewerCounter = viewerCounter {
// let videoHeight = availableWidth / 2
// let topRowHeight: CGFloat = 50
// context.add(viewerCounter
// .position(CGPoint(x: viewerCounter.size.width / 2, y: topRowHeight + videoHeight + 32))
// )
// }
let animatedParticipantsVisible = context.component.participantsCount != -1
if animatedParticipantsVisible {
// let videoHeight = availableWidth / 2
context.add(viewerCounter
.position(CGPoint(x: context.availableSize.width / 2, y: topOffset + 50 + 200 + 40 + 30))
)
}
if let bottomButtonsRow = bottomButtonsRow {
context.add(bottomButtonsRow
@@ -204,7 +209,7 @@ import TelegramPresentationData
import TelegramStringFormatting
private let purple = UIColor(rgb: 0x3252ef)
private let pink = UIColor(rgb: 0xef436c)
private let pink = UIColor(rgb: 0xe4436c)
private let latePurple = UIColor(rgb: 0x974aa9)
private let latePink = UIColor(rgb: 0xf0436c)
@@ -312,3 +317,424 @@ final class SheetBackgroundComponent: Component {
return availableSize
}
}
final class ParticipantsComponent: Component {
static func == (lhs: ParticipantsComponent, rhs: ParticipantsComponent) -> Bool {
lhs.count == rhs.count
}
func makeView() -> View {
View(frame: .zero)
}
func update(view: View, availableSize: CGSize, state: ComponentFlow.EmptyComponentState, environment: ComponentFlow.Environment<ComponentFlow.Empty>, transition: ComponentFlow.Transition) -> CGSize {
view.counter.update(
countString: count > 0 ? presentationStringsFormattedNumber(Int32(count), ",") : "",
subtitle: count > 0 ? "watching" : "no viewers"
)// environment.strings.LiveStream_NoViewers)
return availableSize
}
private let count: Int
init(count: Int) {
self.count = count
}
final class View: UIView {
let counter = AnimatedCountView()// VoiceChatTimerNode.init(strings: .init(), dateTimeFormat: .init())
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(counter)
counter.clipsToBounds = false
}
override func layoutSubviews() {
super.layoutSubviews()
self.counter.frame = self.bounds
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
}
public final class AnimatedCountView: UIView {
let countLabel = AnimatedCountLabel()
// let titleLabel = UILabel()
let subtitleLabel = UILabel()
private let foregroundView = UIView()
private let foregroundGradientLayer = CAGradientLayer()
private let maskingView = UIView()
override init(frame: CGRect = .zero) {
super.init(frame: frame)
self.foregroundGradientLayer.type = .radial
self.foregroundGradientLayer.colors = [pink.cgColor, purple.cgColor, purple.cgColor]
self.foregroundGradientLayer.locations = [0.0, 0.85, 1.0]
self.foregroundGradientLayer.startPoint = CGPoint(x: 1.0, y: 0.0)
self.foregroundGradientLayer.endPoint = CGPoint(x: 0.0, y: 1.0)
self.foregroundView.mask = self.maskingView
self.foregroundView.layer.addSublayer(self.foregroundGradientLayer)
self.addSubview(self.foregroundView)
// self.addSubview(self.titleLabel)
self.addSubview(self.subtitleLabel)
self.maskingView.addSubview(countLabel)
subtitleLabel.textAlignment = .center
// self.backgroundColor = UIColor.white.withAlphaComponent(0.1)
}
override public func layoutSubviews() {
super.layoutSubviews()
self.foregroundView.frame = CGRect(origin: CGPoint.zero, size: bounds.size)// .insetBy(dx: -40, dy: -40)
self.foregroundGradientLayer.frame = CGRect(origin: .zero, size: bounds.size).insetBy(dx: -60, dy: -60)
self.maskingView.frame = CGRect(origin: .zero, size: bounds.size)
countLabel.frame = CGRect(origin: .zero, size: bounds.size)
subtitleLabel.frame = .init(x: bounds.midX - subtitleLabel.intrinsicContentSize.width / 2 - 10, y: subtitleLabel.text == "No viewers" ? bounds.midY - 10 : bounds.height, width: subtitleLabel.intrinsicContentSize.width + 20, height: 20)
}
func update(countString: String, subtitle: String) {
self.setupGradientAnimations()
let text: String = countString// presentationStringsFormattedNumber(Int32(count), ",")
// self.titleNode.attributedText = NSAttributedString(string: "", font: Font.with(size: 23.0, design: .round, weight: .semibold, traits: []), textColor: .white)
// let titleSize = self.titleNode.updateLayout(size)
// self.titleNode.frame = CGRect(x: floor((size.width - titleSize.width) / 2.0), y: 48.0, width: titleSize.width, height: titleSize.height)
if CGFloat(text.count * 40) < bounds.width - 32 {
self.countLabel.attributedText = NSAttributedString(string: text, font: Font.with(size: 60.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: .white)
} else {
self.countLabel.attributedText = NSAttributedString(string: text, font: Font.with(size: 54.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: .white)
}
// var timerSize = self.timerNode.updateLayout(CGSize(width: size.width + 100.0, height: size.height))
// if timerSize.width > size.width - 32.0 {
// self.timerNode.attributedText = NSAttributedString(string: text, font: Font.with(size: 60.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: .white)
// timerSize = self.timerNode.updateLayout(CGSize(width: size.width + 100.0, height: size.height))
// }
// self.timerNode.frame = CGRect(x: floor((size.width - timerSize.width) / 2.0), y: 78.0, width: timerSize.width, height: timerSize.height)
self.subtitleLabel.attributedText = NSAttributedString(string: subtitle, font: Font.with(size: 16.0, design: .round, weight: .semibold, traits: []), textColor: .white)
self.subtitleLabel.isHidden = subtitle.isEmpty
// let subtitleSize = self.subtitleNode.updateLayout(size)
// self.subtitleNode.frame = CGRect(x: floor((size.width - subtitleSize.width) / 2.0), y: 164.0, width: subtitleSize.width, height: subtitleSize.height)
// self.foregroundView.frame = CGRect(origin: CGPoint(), size: size)
// self.setNeedsLayout()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupGradientAnimations() {
if let _ = self.foregroundGradientLayer.animation(forKey: "movement") {
} else {
let previousValue = self.foregroundGradientLayer.startPoint
let newValue = CGPoint(x: CGFloat.random(in: 0.65 ..< 0.85), y: CGFloat.random(in: 0.1 ..< 0.45))
self.foregroundGradientLayer.startPoint = newValue
CATransaction.begin()
let animation = CABasicAnimation(keyPath: "startPoint")
animation.duration = Double.random(in: 0.8 ..< 1.4)
animation.fromValue = previousValue
animation.toValue = newValue
CATransaction.setCompletionBlock { [weak self] in
// if let isCurrentlyInHierarchy = self?.isCurrentlyInHierarchy, isCurrentlyInHierarchy {
self?.setupGradientAnimations()
// }
}
self.foregroundGradientLayer.add(animation, forKey: "movement")
CATransaction.commit()
}
}
}
class AnimatedCharLayer: CATextLayer {
var text: String? {
get {
self.string as? String ?? (self.string as? NSAttributedString)?.string
}
set {
self.string = newValue
}
}
var attributedText: NSAttributedString? {
get {
self.string as? NSAttributedString //?? (self.string as? String).map { NSAttributed.init
}
set {
self.string = newValue
}
}
var layer: CALayer { self }
override init() {
super.init()
self.contentsScale = UIScreen.main.scale
}
override init(layer: Any) {
super.init(layer: layer)
self.contentsScale = UIScreen.main.scale
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class AnimatedCountLabel: UILabel {
override var text: String? {
get {
chars.reduce("") { $0 + ($1.text ?? "") }
}
set {
update(with: newValue ?? "")
}
}
override var attributedText: NSAttributedString? {
get {
let string = NSMutableAttributedString()
for char in chars {
string.append(char.attributedText ?? NSAttributedString())
}
return string
}
set {
udpateAttributed(with: newValue ?? NSAttributedString())
}
}
private var chars = [AnimatedCharLayer]()
private let containerView = UIView()
override init(frame: CGRect = .zero) {
super.init(frame: frame)
addSubview(containerView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
let interItemSpacing: CGFloat = 0
let countWidth = chars.reduce(0) { $0 + $1.frame.width + interItemSpacing } - interItemSpacing
containerView.frame = .init(x: bounds.midX - countWidth / 2, y: 0, width: countWidth, height: bounds.height)
chars.enumerated().forEach { (index, char) in
char.frame.origin.x = CGFloat(chars.count - 1 - index) * (40 + interItemSpacing)
char.frame.origin.y = 0
}
}
/// Unused
func update(with newString: String) {
/*let itemWidth: CGFloat = 40
let initialDuration: TimeInterval = 0.3
let newChars = Array(newString).map { String($0) }
let currentChars = chars.map { $0.text ?? "X" }
// let currentWidth = itemWidth * CGFloat(currentChars.count)
let newWidth = itemWidth * CGFloat(newChars.count)
let interItemDelay: TimeInterval = 0.15
var changeIndex = 0
var newLayers = [AnimatedCharLayer]()
for index in 0..<min(newChars.count, currentChars.count) {
let newCharIndex = newChars.count - 1 - index
let currCharIndex = currentChars.count - 1 - index
if true || newChars[newCharIndex] != currentChars[currCharIndex] {
animateOut(for: chars[currCharIndex].layer, duration: initialDuration, beginTime: TimeInterval(changeIndex) * interItemDelay)
let newLayer = AnimatedCharLayer()
newLayer.text = newChars[newCharIndex]
newLayer.frame = .init(x: newWidth - CGFloat(index + 1) * itemWidth, y: 100, width: itemWidth, height: 36)
containerView.layer.addSublayer(newLayer)
animateIn(for: newLayer.layer, duration: initialDuration, beginTime: TimeInterval(changeIndex) * interItemDelay)
newLayers.append(newLayer)
changeIndex += 1
} else {
newLayers.append(chars[currCharIndex])
}
}
for index in min(newChars.count, currentChars.count)..<currentChars.count {
let currCharIndex = currentChars.count - 1 - index
// remove unused
animateOut(for: chars[currCharIndex].layer, duration: initialDuration, beginTime: TimeInterval(changeIndex) * interItemDelay)
changeIndex += 1
}
for index in min(newChars.count, currentChars.count)..<newChars.count {
let newCharIndex = newChars.count - 1 - index
let newLayer = AnimatedCharLayer()
newLayer.text = newChars[newCharIndex]
newLayer.frame = .init(x: newWidth - CGFloat(index + 1) * itemWidth, y: 100, width: itemWidth, height: 36)
containerView.layer.addSublayer(newLayer)
animateIn(for: newLayer.layer, duration: initialDuration, beginTime: TimeInterval(changeIndex) * interItemDelay)
newLayers.append(newLayer)
changeIndex += 1
}
chars = newLayers*/
}
func udpateAttributed(with newString: NSAttributedString) {
let itemWidth: CGFloat = 40
let initialDuration: TimeInterval = 0.25
let interItemSpacing: CGFloat = 0
let separatedStrings = Array(newString.string).map { String($0) }
var range = NSRange(location: 0, length: 0)
var newChars = [NSAttributedString]()
for string in separatedStrings {
range.length = string.count
let attributedString = newString.attributedSubstring(from: range)
newChars.append(attributedString)
range.location += range.length
}
let currentChars = chars.map { $0.attributedText ?? .init() }
// let currentWidth = itemWidth * CGFloat(currentChars.count)
// let newWidth = itemWidth * CGFloat(newChars.count)
let interItemDelay: TimeInterval = 0.15
var changeIndex = 0
var newLayers = [AnimatedCharLayer]()
for index in 0..<min(newChars.count, currentChars.count) {
let newCharIndex = newChars.count - 1 - index
let currCharIndex = currentChars.count - 1 - index
if newChars[newCharIndex] != currentChars[currCharIndex] {
let initialDuration = newChars[newCharIndex] != currentChars[currCharIndex] ? initialDuration : 0
animateOut(for: chars[currCharIndex].layer, duration: initialDuration, beginTime: TimeInterval(changeIndex) * interItemDelay)
let newLayer = AnimatedCharLayer()
newLayer.attributedText = newChars[newCharIndex]
newLayer.frame = .init(x: CGFloat(chars.count - 1 - index) * (40 + interItemSpacing), y: 0, width: itemWidth, height: itemWidth * 1.8)
containerView.layer.addSublayer(newLayer)
animateIn(for: newLayer.layer, duration: initialDuration, beginTime: TimeInterval(changeIndex) * interItemDelay)
newLayers.append(newLayer)
changeIndex += 1
} else {
newLayers.append(chars[currCharIndex])
}
}
for index in min(newChars.count, currentChars.count)..<currentChars.count {
let currCharIndex = currentChars.count - 1 - index
// remove unused
animateOut(for: chars[currCharIndex].layer, duration: initialDuration, beginTime: TimeInterval(changeIndex) * interItemDelay)
changeIndex += 1
}
for index in min(newChars.count, currentChars.count)..<newChars.count {
let newCharIndex = newChars.count - 1 - index
let newLayer = AnimatedCharLayer()
newLayer.attributedText = newChars[newCharIndex]
newLayer.frame = .init(x: CGFloat(chars.count - 1 - index) * (40 + interItemSpacing), y: 0, width: itemWidth, height: itemWidth * 1.8)
containerView.layer.addSublayer(newLayer)
animateIn(for: newLayer.layer, duration: initialDuration, beginTime: TimeInterval(changeIndex) * interItemDelay)
newLayers.append(newLayer)
changeIndex += 1
}
chars = newLayers
let countWidth = chars.reduce(-interItemSpacing) { $0 + $1.frame.width + interItemSpacing }
if didBegin {
UIView.animate(withDuration: 0.3, delay: initialDuration * Double(changeIndex)) { [self] in
containerView.frame = .init(x: self.bounds.midX - countWidth / 2, y: 0, width: countWidth, height: self.bounds.height)
// containerView.backgroundColor = .red.withAlphaComponent(0.3)
}
} else {
containerView.frame = .init(x: self.bounds.midX - countWidth / 2, y: 0, width: countWidth, height: self.bounds.height)
didBegin = true
}
// self.backgroundColor = .green.withAlphaComponent(0.2)
}
var didBegin = false
func animateOut(for layer: CALayer, duration: CFTimeInterval, beginTime: CFTimeInterval) {
let animation = CAKeyframeAnimation()
animation.keyPath = "opacity"
animation.values = [1, 0.2]
animation.keyTimes = [0, 1]
animation.duration = duration
animation.beginTime = CACurrentMediaTime() + beginTime
// animation.isAdditive = true
// animation.isRemovedOnCompletion = true
layer.add(animation, forKey: "opacity")
layer.opacity = 0
//
DispatchQueue.main.asyncAfter(deadline: .now() + duration + beginTime) {
layer.removeFromSuperlayer()
}
let scaleOutAnimation = CABasicAnimation(keyPath: "transform.scale")
scaleOutAnimation.fromValue = 1
scaleOutAnimation.toValue = 0.1
scaleOutAnimation.duration = duration
scaleOutAnimation.beginTime = CACurrentMediaTime() + beginTime
layer.add(scaleOutAnimation, forKey: "scaleout")
let translate = CABasicAnimation(keyPath: "transform.translation")
translate.fromValue = CGPoint.zero
translate.toValue = CGPoint(x: 0, y: -layer.bounds.height * 0.3)// -layer.bounds.height + 3.0)
translate.duration = duration
translate.beginTime = CACurrentMediaTime() + beginTime
layer.add(translate, forKey: "translate")
}
func animateIn(for newLayer: CALayer, duration: CFTimeInterval, beginTime: CFTimeInterval) {
newLayer.opacity = 0
// newLayer.backgroundColor = UIColor.red.cgColor
let opacityInAnimation = CABasicAnimation(keyPath: "opacity")
opacityInAnimation.fromValue = 0.5
opacityInAnimation.toValue = 1
opacityInAnimation.duration = duration
opacityInAnimation.beginTime = CACurrentMediaTime() + beginTime
newLayer.add(opacityInAnimation, forKey: "opacity")
newLayer.opacity = 1
let scaleOutAnimation = CABasicAnimation(keyPath: "transform.scale")
scaleOutAnimation.fromValue = 0
scaleOutAnimation.toValue = 1
scaleOutAnimation.duration = duration
scaleOutAnimation.beginTime = CACurrentMediaTime() + beginTime
newLayer.add(scaleOutAnimation, forKey: "scalein")
let animation = CAKeyframeAnimation()
animation.keyPath = "position.y"
animation.values = [18, -6, 0]
animation.keyTimes = [0, 0.64, 1]
animation.timingFunction = CAMediaTimingFunction.init(name: .easeInEaseOut)
animation.duration = duration / 0.64
animation.beginTime = CACurrentMediaTime() + beginTime
// animation.isAdditive = true
newLayer.add(animation, forKey: "pos")
}
}