import UIKit import AsyncDisplayKit import Display import HierarchyTrackingLayer public final class ShimmerEffectForegroundView: UIView { private var currentBackgroundColor: UIColor? private var currentForegroundColor: UIColor? private var currentHorizontal: Bool? private var currentGradientSize: CGFloat? private var currentDuration: Double? private let imageContainer: SimpleLayer private let image: SimpleLayer private var absoluteLocation: (CGRect, CGSize)? private var isCurrentlyInHierarchy = false private var shouldBeAnimating = false private var globalTimeOffset = true private let trackingLayer: HierarchyTrackingLayer public init() { self.imageContainer = SimpleLayer() self.image = SimpleLayer() self.image.contentsGravity = .resizeAspectFill self.trackingLayer = HierarchyTrackingLayer() super.init(frame: CGRect()) self.clipsToBounds = true self.isUserInteractionEnabled = false self.layer.addSublayer(self.imageContainer) self.imageContainer.addSublayer(self.image) self.layer.addSublayer(self.trackingLayer) self.trackingLayer.didEnterHierarchy = { [weak self] in guard let strongSelf = self else { return } strongSelf.isCurrentlyInHierarchy = true strongSelf.updateAnimation() } self.trackingLayer.didExitHierarchy = { [weak self] in guard let strongSelf = self else { return } strongSelf.isCurrentlyInHierarchy = false strongSelf.updateAnimation() } } required public init(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } public func update(backgroundColor: UIColor, foregroundColor: UIColor, gradientSize: CGFloat?, globalTimeOffset: Bool, duration: Double?, horizontal: Bool = false) { if let currentBackgroundColor = self.currentBackgroundColor, currentBackgroundColor.isEqual(backgroundColor), let currentForegroundColor = self.currentForegroundColor, currentForegroundColor.isEqual(foregroundColor), self.currentHorizontal == horizontal, self.currentGradientSize == gradientSize { return } self.currentBackgroundColor = backgroundColor self.currentForegroundColor = foregroundColor self.currentHorizontal = horizontal self.currentGradientSize = gradientSize self.globalTimeOffset = globalTimeOffset self.currentDuration = duration let image: UIImage? if horizontal { image = generateImage(CGSize(width: gradientSize ?? 320.0, height: 16.0), opaque: false, scale: 1.0, rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(backgroundColor.cgColor) context.fill(CGRect(origin: CGPoint(), size: size)) context.clip(to: CGRect(origin: CGPoint(), size: size)) let transparentColor = foregroundColor.withAlphaComponent(0.0).cgColor let peakColor = foregroundColor.cgColor var locations: [CGFloat] = [0.0, 0.5, 1.0] let colors: [CGColor] = [transparentColor, peakColor, transparentColor] let colorSpace = CGColorSpaceCreateDeviceRGB() let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions()) }) } else { image = generateImage(CGSize(width: 16.0, height: gradientSize ?? 250.0), opaque: false, scale: 1.0, rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(backgroundColor.cgColor) context.fill(CGRect(origin: CGPoint(), size: size)) context.clip(to: CGRect(origin: CGPoint(), size: size)) let transparentColor = foregroundColor.withAlphaComponent(0.0).cgColor let peakColor = foregroundColor.cgColor var locations: [CGFloat] = [0.0, 0.5, 1.0] let colors: [CGColor] = [transparentColor, peakColor, transparentColor] let colorSpace = CGColorSpaceCreateDeviceRGB() let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) }) } self.image.contents = image?.cgImage self.updateAnimation() } public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { if let absoluteLocation = self.absoluteLocation, absoluteLocation.0 == rect && absoluteLocation.1 == containerSize { return } let sizeUpdated = self.absoluteLocation?.1 != containerSize let frameUpdated = self.absoluteLocation?.0 != rect self.absoluteLocation = (rect, containerSize) if sizeUpdated { if self.shouldBeAnimating { self.image.removeAnimation(forKey: "shimmer") self.addImageAnimation() } else { self.updateAnimation() } } if frameUpdated { self.imageContainer.frame = CGRect(origin: CGPoint(x: -rect.minX, y: -rect.minY), size: containerSize) } } private func updateAnimation() { let shouldBeAnimating = self.isCurrentlyInHierarchy && self.absoluteLocation != nil && self.currentHorizontal != nil if shouldBeAnimating != self.shouldBeAnimating { self.shouldBeAnimating = shouldBeAnimating if shouldBeAnimating { self.addImageAnimation() } else { self.image.removeAnimation(forKey: "shimmer") } } } private func addImageAnimation() { guard let containerSize = self.absoluteLocation?.1, let horizontal = self.currentHorizontal else { return } if horizontal { let gradientSize = self.currentGradientSize ?? 320.0 self.image.frame = CGRect(origin: CGPoint(x: -gradientSize, y: 0.0), size: CGSize(width: gradientSize, height: containerSize.height)) let animation = self.image.makeAnimation(from: 0.0 as NSNumber, to: (containerSize.width + gradientSize) as NSNumber, keyPath: "position.x", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: self.currentDuration ?? 1.3, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true) animation.repeatCount = Float.infinity if self.globalTimeOffset { animation.beginTime = 1.0 } self.image.add(animation, forKey: "shimmer") } else { let gradientSize = self.currentGradientSize ?? 250.0 self.image.frame = CGRect(origin: CGPoint(x: 0.0, y: -gradientSize), size: CGSize(width: containerSize.width, height: gradientSize)) let animation = self.image.makeAnimation(from: 0.0 as NSNumber, to: (containerSize.height + gradientSize) as NSNumber, keyPath: "position.y", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: self.currentDuration ?? 1.3, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true) animation.repeatCount = Float.infinity if self.globalTimeOffset { animation.beginTime = 1.0 } self.image.add(animation, forKey: "shimmer") } } } private let shadowImage: UIImage? = { UIImage(named: "Stories/PanelGradient") }() public final class ShimmerEffectForegroundNode: ASDisplayNode { private var currentBackgroundColor: UIColor? private var currentForegroundColor: UIColor? private var currentHorizontal: Bool? private let imageNodeContainer: ASDisplayNode private let imageNode: ASImageNode private var absoluteLocation: (CGRect, CGSize)? private var isCurrentlyInHierarchy = false private var shouldBeAnimating = false private var globalTimeOffset = true private var duration: Double? public override init() { self.imageNodeContainer = ASDisplayNode() self.imageNodeContainer.isLayerBacked = true self.imageNode = ASImageNode() self.imageNode.isLayerBacked = true self.imageNode.displaysAsynchronously = false self.imageNode.displayWithoutProcessing = true self.imageNode.contentMode = .scaleToFill super.init() self.isLayerBacked = true self.clipsToBounds = true self.imageNodeContainer.addSubnode(self.imageNode) self.addSubnode(self.imageNodeContainer) } public override func didEnterHierarchy() { super.didEnterHierarchy() self.isCurrentlyInHierarchy = true self.updateAnimation() } public override func didExitHierarchy() { super.didExitHierarchy() self.isCurrentlyInHierarchy = false self.updateAnimation() } public func update(backgroundColor: UIColor, foregroundColor: UIColor, horizontal: Bool, effectSize: CGFloat?, globalTimeOffset: Bool, duration: Double?) { if let currentBackgroundColor = self.currentBackgroundColor, currentBackgroundColor.isEqual(backgroundColor), let currentForegroundColor = self.currentForegroundColor, currentForegroundColor.isEqual(foregroundColor), self.currentHorizontal == horizontal { return } self.currentBackgroundColor = backgroundColor self.currentForegroundColor = foregroundColor self.currentHorizontal = horizontal self.globalTimeOffset = globalTimeOffset self.duration = duration let image: UIImage? if horizontal { let baseAlpha: CGFloat = 0.1 image = generateImage(CGSize(width: effectSize ?? 200.0, height: 16.0), opaque: false, scale: 1.0, rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) let foregroundColor = UIColor(white: 1.0, alpha: min(1.0, baseAlpha * 4.0)) if let shadowImage { UIGraphicsPushContext(context) for i in 0 ..< 2 { let shadowFrame = CGRect(origin: CGPoint(x: CGFloat(i) * (size.width * 0.5), y: 0.0), size: CGSize(width: size.width * 0.5, height: size.height)) context.saveGState() context.translateBy(x: shadowFrame.midX, y: shadowFrame.midY) context.rotate(by: CGFloat(i == 0 ? 1.0 : -1.0) * CGFloat.pi * 0.5) let adjustedRect = CGRect(origin: CGPoint(x: -shadowFrame.height * 0.5, y: -shadowFrame.width * 0.5), size: CGSize(width: shadowFrame.height, height: shadowFrame.width)) context.clip(to: adjustedRect, mask: shadowImage.cgImage!) context.setFillColor(foregroundColor.cgColor) context.fill(adjustedRect) context.restoreGState() } UIGraphicsPopContext() } }) } else { image = generateImage(CGSize(width: 16.0, height: 320.0), opaque: false, scale: 1.0, rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(backgroundColor.cgColor) context.fill(CGRect(origin: CGPoint(), size: size)) context.clip(to: CGRect(origin: CGPoint(), size: size)) let transparentColor = foregroundColor.withAlphaComponent(0.0).cgColor let peakColor = foregroundColor.cgColor var locations: [CGFloat] = [0.0, 0.5, 1.0] let colors: [CGColor] = [transparentColor, peakColor, transparentColor] let colorSpace = CGColorSpaceCreateDeviceRGB() let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) }) } self.imageNode.image = image self.updateAnimation() } public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { if let absoluteLocation = self.absoluteLocation, absoluteLocation.0 == rect && absoluteLocation.1 == containerSize { return } let sizeUpdated = self.absoluteLocation?.1 != containerSize let frameUpdated = self.absoluteLocation?.0 != rect self.absoluteLocation = (rect, containerSize) if sizeUpdated { if self.shouldBeAnimating { self.imageNode.layer.removeAnimation(forKey: "shimmer") self.addImageAnimation() } else { self.updateAnimation() } } if frameUpdated { self.imageNodeContainer.frame = CGRect(origin: CGPoint(x: -rect.minX, y: -rect.minY), size: containerSize) } } private func updateAnimation() { let shouldBeAnimating = self.isCurrentlyInHierarchy && self.absoluteLocation != nil && self.currentHorizontal != nil if shouldBeAnimating != self.shouldBeAnimating { self.shouldBeAnimating = shouldBeAnimating if shouldBeAnimating { self.addImageAnimation() } else { self.imageNode.layer.removeAnimation(forKey: "shimmer") } } } private func addImageAnimation() { guard let containerSize = self.absoluteLocation?.1, let horizontal = self.currentHorizontal else { return } if horizontal { let gradientHeight: CGFloat = self.imageNode.image?.size.width ?? 320.0 self.imageNode.frame = CGRect(origin: CGPoint(x: -gradientHeight, y: 0.0), size: CGSize(width: gradientHeight, height: containerSize.height)) let animation = self.imageNode.layer.makeAnimation(from: 0.0 as NSNumber, to: (containerSize.width + gradientHeight) as NSNumber, keyPath: "position.x", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: duration ?? 1.3, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true) animation.repeatCount = Float.infinity if self.globalTimeOffset { animation.beginTime = 1.0 } self.imageNode.layer.add(animation, forKey: "shimmer") } else { let gradientHeight: CGFloat = 250.0 self.imageNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -gradientHeight), size: CGSize(width: containerSize.width, height: gradientHeight)) let animation = self.imageNode.layer.makeAnimation(from: 0.0 as NSNumber, to: (containerSize.height + gradientHeight) as NSNumber, keyPath: "position.y", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: duration ?? 1.3, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true) animation.repeatCount = Float.infinity if self.globalTimeOffset { animation.beginTime = 1.0 } self.imageNode.layer.add(animation, forKey: "shimmer") } } } public final class ShimmerEffectNode: ASDisplayNode { public enum Shape: Equatable { case circle(CGRect) case roundedRectLine(startPoint: CGPoint, width: CGFloat, diameter: CGFloat) case roundedRect(rect: CGRect, cornerRadius: CGFloat) case rect(rect: CGRect) case image(image: UIImage, rect: CGRect) } private let backgroundNode: ASDisplayNode private let effectNode: ShimmerEffectForegroundNode private let foregroundNode: ASImageNode private var currentShapes: [Shape] = [] private var currentBackgroundColor: UIColor? private var currentForegroundColor: UIColor? private var currentShimmeringColor: UIColor? private var currentHorizontal: Bool? private var currentEffectSize: CGFloat? private var currentSize = CGSize() override public init() { self.backgroundNode = ASDisplayNode() self.effectNode = ShimmerEffectForegroundNode() self.foregroundNode = ASImageNode() self.foregroundNode.displaysAsynchronously = false self.foregroundNode.displayWithoutProcessing = true super.init() self.addSubnode(self.backgroundNode) self.addSubnode(self.effectNode) self.addSubnode(self.foregroundNode) } public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { self.effectNode.updateAbsoluteRect(rect, within: containerSize) } public func update(backgroundColor: UIColor, foregroundColor: UIColor, shimmeringColor: UIColor, shapes: [Shape], horizontal: Bool = false, effectSize: CGFloat? = nil, globalTimeOffset: Bool = true, duration: Double? = nil, size: CGSize, mask: Bool = false) { if self.currentShapes == shapes, let currentBackgroundColor = self.currentBackgroundColor, currentBackgroundColor.isEqual(backgroundColor), let currentForegroundColor = self.currentForegroundColor, currentForegroundColor.isEqual(foregroundColor), let currentShimmeringColor = self.currentShimmeringColor, currentShimmeringColor.isEqual(shimmeringColor), horizontal == self.currentHorizontal, effectSize == self.currentEffectSize, self.currentSize == size { return } self.currentBackgroundColor = backgroundColor self.currentForegroundColor = foregroundColor self.currentShimmeringColor = shimmeringColor self.currentShapes = shapes self.currentHorizontal = horizontal self.currentSize = size self.backgroundNode.backgroundColor = foregroundColor self.effectNode.update(backgroundColor: foregroundColor, foregroundColor: shimmeringColor, horizontal: horizontal, effectSize: effectSize, globalTimeOffset: globalTimeOffset, duration: duration) self.foregroundNode.image = generateImage(size, rotatedContext: { size, context in if !mask { context.setFillColor(backgroundColor.cgColor) context.setBlendMode(.copy) context.fill(CGRect(origin: .zero, size: size)) context.setFillColor(UIColor.clear.cgColor) } else { context.clear(CGRect(origin: .zero, size: size)) context.setFillColor(UIColor.white.cgColor) } for shape in shapes { switch shape { case let .circle(frame): context.fillEllipse(in: frame) case let .roundedRectLine(startPoint, width, diameter): context.fillEllipse(in: CGRect(origin: startPoint, size: CGSize(width: diameter, height: diameter))) context.fillEllipse(in: CGRect(origin: CGPoint(x: startPoint.x + width - diameter, y: startPoint.y), size: CGSize(width: diameter, height: diameter))) context.fill(CGRect(origin: CGPoint(x: startPoint.x + diameter / 2.0, y: startPoint.y), size: CGSize(width: width - diameter, height: diameter))) case let .roundedRect(rect, radius): let path = UIBezierPath(roundedRect: rect, byRoundingCorners: [.topLeft, .topRight, .bottomLeft, .bottomRight], cornerRadii: CGSize(width: radius, height: radius)) UIGraphicsPushContext(context) path.fill() UIGraphicsPopContext() case let .rect(rect): context.fill(rect) case let .image(image, rect): if let image = image.cgImage { context.translateBy(x: size.width / 2.0, y: size.height / 2.0) context.scaleBy(x: 1.0, y: -1.0) context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) context.clip(to: rect, mask: image) context.fill(rect) } } } }) if mask { if self.view.mask == nil { self.foregroundNode.removeFromSupernode() self.view.mask = self.foregroundNode.view } } else { //if self.view.mask != nil { // self.view.mask = nil if self.foregroundNode.supernode == nil { self.addSubnode(self.foregroundNode) } //} } self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size) self.foregroundNode.frame = CGRect(origin: CGPoint(), size: size) self.effectNode.frame = CGRect(origin: CGPoint(), size: size) } } public final class StandaloneShimmerEffect { private var image: UIImage? private var background: UIColor? private var foreground: UIColor? public var layer: CALayer? { didSet { if self.layer !== oldValue { self.updateLayer() } } } public init() { } public func update(background: UIColor, foreground: UIColor) { if self.background == background && self.foreground == foreground { return } self.background = background self.foreground = foreground self.image = generateImage(CGSize(width: 1.0, height: 320.0), opaque: false, scale: 1.0, rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(background.cgColor) context.fill(CGRect(origin: CGPoint(), size: size)) context.clip(to: CGRect(origin: CGPoint(), size: size)) let transparentColor = foreground.withAlphaComponent(0.0).cgColor let peakColor = foreground.cgColor var locations: [CGFloat] = [0.0, 0.5, 1.0] let colors: [CGColor] = [transparentColor, peakColor, transparentColor] let colorSpace = CGColorSpaceCreateDeviceRGB() let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) }) self.updateLayer() } public func updateHorizontal(background: UIColor, foreground: UIColor) { if self.background == background && self.foreground == foreground { return } self.background = background self.foreground = foreground self.image = generateImage(CGSize(width: 320, height: 1), opaque: false, scale: 1.0, rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(background.cgColor) context.fill(CGRect(origin: CGPoint(), size: size)) context.clip(to: CGRect(origin: CGPoint(), size: size)) let transparentColor = foreground.withAlphaComponent(0.0).cgColor let peakColor = foreground.cgColor var locations: [CGFloat] = [0.0, 0.44, 0.55, 1.0] let colors: [CGColor] = [transparentColor, peakColor, peakColor, transparentColor] let colorSpace = CGColorSpaceCreateDeviceRGB() guard let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations) else { return } context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.2), end: CGPoint(x: size.width, y: 0.8), options: CGGradientDrawingOptions()) }) self.updateHorizontalLayer() } public func updateLayer() { guard let layer = self.layer, let image = self.image else { return } layer.contents = image.cgImage if layer.animation(forKey: "shimmer") == nil { let animation = CABasicAnimation(keyPath: "contentsRect.origin.y") animation.fromValue = 1.0 as NSNumber animation.toValue = -1.0 as NSNumber animation.isAdditive = true animation.repeatCount = .infinity animation.duration = 0.8 animation.beginTime = layer.convertTime(1.0, from: nil) layer.add(animation, forKey: "shimmer") } } private func updateHorizontalLayer() { guard let layer = self.layer, let image = self.image else { return } layer.contents = image.cgImage if layer.animation(forKey: "shimmer") == nil { var delay: TimeInterval { 1.6 } let animation = CABasicAnimation(keyPath: "contentsRect.origin.x") animation.fromValue = NSNumber(floatLiteral: delay) animation.toValue = NSNumber(floatLiteral: -delay) animation.isAdditive = true animation.repeatCount = .infinity animation.duration = 0.8 * delay animation.timingFunction = .init(name: .easeInEaseOut) layer.add(animation, forKey: "shimmer") } } }