diff --git a/submodules/ShimmerEffect/BUILD b/submodules/ShimmerEffect/BUILD index 56cf4806fb..0943d7e93d 100644 --- a/submodules/ShimmerEffect/BUILD +++ b/submodules/ShimmerEffect/BUILD @@ -12,6 +12,7 @@ swift_library( deps = [ "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", + "//submodules/Components/HierarchyTrackingLayer:HierarchyTrackingLayer", ], visibility = [ "//visibility:public", diff --git a/submodules/ShimmerEffect/Sources/ShimmerEffect.swift b/submodules/ShimmerEffect/Sources/ShimmerEffect.swift index e7de2e4185..c4c35430ef 100644 --- a/submodules/ShimmerEffect/Sources/ShimmerEffect.swift +++ b/submodules/ShimmerEffect/Sources/ShimmerEffect.swift @@ -1,6 +1,172 @@ 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 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?, 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.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 + 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 + animation.beginTime = 1.0 + self.image.add(animation, forKey: "shimmer") + } + } +} final class ShimmerEffectForegroundNode: ASDisplayNode { private var currentBackgroundColor: UIColor? diff --git a/submodules/SolidRoundedButtonNode/BUILD b/submodules/SolidRoundedButtonNode/BUILD index 414ad4772d..fde9a24551 100644 --- a/submodules/SolidRoundedButtonNode/BUILD +++ b/submodules/SolidRoundedButtonNode/BUILD @@ -12,6 +12,7 @@ swift_library( deps = [ "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", + "//submodules/ShimmerEffect:ShimmerEffect", "//submodules/Components/HierarchyTrackingLayer:HierarchyTrackingLayer", ], visibility = [ diff --git a/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift b/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift index 4f01598070..e4f130e6b0 100644 --- a/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift +++ b/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift @@ -3,6 +3,7 @@ import UIKit import AsyncDisplayKit import Display import HierarchyTrackingLayer +import ShimmerEffect private func generateIndefiniteActivityIndicatorImage(color: UIColor, diameter: CGFloat = 22.0, lineWidth: CGFloat = 2.0) -> UIImage? { return generateImage(CGSize(width: diameter, height: diameter), rotatedContext: { size, context in @@ -52,7 +53,12 @@ public final class SolidRoundedButtonNode: ASDisplayNode { private var fontSize: CGFloat private let buttonBackgroundNode: ASDisplayNode - private let buttonGlossView: SolidRoundedButtonGlossView? + + private var shimmerView: ShimmerEffectForegroundView? + private var borderView: UIView? + private var borderMaskView: UIView? + private var borderShimmerView: ShimmerEffectForegroundView? + private let buttonNode: HighlightTrackingButtonNode private let titleNode: ImmediateTextNode private let subtitleNode: ImmediateTextNode @@ -95,6 +101,8 @@ public final class SolidRoundedButtonNode: ASDisplayNode { } } + private let gloss: Bool + public init(title: String? = nil, icon: UIImage? = nil, theme: SolidRoundedButtonTheme, font: SolidRoundedButtonFont = .bold, fontSize: CGFloat = 17.0, height: CGFloat = 48.0, cornerRadius: CGFloat = 24.0, gloss: Bool = false) { self.theme = theme self.font = font @@ -102,18 +110,13 @@ public final class SolidRoundedButtonNode: ASDisplayNode { self.buttonHeight = height self.buttonCornerRadius = cornerRadius self.title = title + self.gloss = gloss self.buttonBackgroundNode = ASDisplayNode() self.buttonBackgroundNode.clipsToBounds = true self.buttonBackgroundNode.backgroundColor = theme.backgroundColor self.buttonBackgroundNode.cornerRadius = cornerRadius - - if gloss { - self.buttonGlossView = SolidRoundedButtonGlossView(color: theme.foregroundColor, cornerRadius: cornerRadius) - } else { - self.buttonGlossView = nil - } - + self.buttonNode = HighlightTrackingButtonNode() self.titleNode = ImmediateTextNode() @@ -131,9 +134,6 @@ public final class SolidRoundedButtonNode: ASDisplayNode { super.init() self.addSubnode(self.buttonBackgroundNode) - if let buttonGlossView = self.buttonGlossView { - self.view.addSubview(buttonGlossView) - } self.addSubnode(self.buttonNode) self.addSubnode(self.titleNode) self.addSubnode(self.subtitleNode) @@ -217,6 +217,57 @@ public final class SolidRoundedButtonNode: ASDisplayNode { if #available(iOS 13.0, *) { self.buttonBackgroundNode.layer.cornerCurve = .continuous } + + if self.gloss { + let shimmerView = ShimmerEffectForegroundView() + self.shimmerView = shimmerView + + let borderView = UIView() + borderView.isUserInteractionEnabled = false + self.borderView = borderView + + let borderMaskView = UIView() + borderMaskView.layer.borderWidth = 1.0 + UIScreenPixel + borderMaskView.layer.borderColor = UIColor.white.cgColor + borderMaskView.layer.cornerRadius = self.buttonCornerRadius + borderView.mask = borderMaskView + self.borderMaskView = borderMaskView + + let borderShimmerView = ShimmerEffectForegroundView() + self.borderShimmerView = borderShimmerView + borderView.addSubview(borderShimmerView) + + self.view.insertSubview(shimmerView, belowSubview: self.buttonNode.view) + self.view.insertSubview(borderView, belowSubview: self.buttonNode.view) + + self.updateShimmerParameters() + } + } + + func updateShimmerParameters() { + guard let shimmerView = self.shimmerView, let borderShimmerView = self.borderShimmerView else { + return + } + + let color = self.theme.foregroundColor + let alpha: CGFloat + let borderAlpha: CGFloat + let compositingFilter: String? + if color.lightness > 0.5 { + alpha = 0.5 + borderAlpha = 0.75 + compositingFilter = "overlayBlendMode" + } else { + alpha = 0.2 + borderAlpha = 0.3 + compositingFilter = nil + } + + shimmerView.update(backgroundColor: .clear, foregroundColor: color.withAlphaComponent(alpha), gradientSize: 70.0, duration: 2.4, horizontal: true) + borderShimmerView.update(backgroundColor: .clear, foregroundColor: color.withAlphaComponent(borderAlpha), gradientSize: 70.0, duration: 2.4, horizontal: true) + + shimmerView.layer.compositingFilter = compositingFilter + borderShimmerView.layer.compositingFilter = compositingFilter } public func updateTheme(_ theme: SolidRoundedButtonTheme) { @@ -226,7 +277,6 @@ public final class SolidRoundedButtonNode: ASDisplayNode { self.theme = theme self.buttonBackgroundNode.backgroundColor = theme.backgroundColor - self.buttonGlossView?.color = theme.foregroundColor self.titleNode.attributedText = NSAttributedString(string: self.title ?? "", font: self.font == .bold ? Font.semibold(self.fontSize) : Font.regular(self.fontSize), textColor: theme.foregroundColor) self.subtitleNode.attributedText = NSAttributedString(string: self.subtitle ?? "", font: Font.regular(14.0), textColor: theme.foregroundColor) @@ -235,6 +285,8 @@ public final class SolidRoundedButtonNode: ASDisplayNode { if let width = self.validLayout { _ = self.updateLayout(width: width, transition: .immediate) } + + self.updateShimmerParameters() } public func sizeThatFits(_ constrainedSize: CGSize) -> CGSize { @@ -252,9 +304,17 @@ public final class SolidRoundedButtonNode: ASDisplayNode { let buttonSize = CGSize(width: width, height: self.buttonHeight) let buttonFrame = CGRect(origin: CGPoint(), size: buttonSize) transition.updateFrame(node: self.buttonBackgroundNode, frame: buttonFrame) - if let buttonGlossView = self.buttonGlossView { - transition.updateFrame(view: buttonGlossView, frame: buttonFrame) + + if let shimmerView = self.shimmerView, let borderView = self.borderView, let borderMaskView = self.borderMaskView, let borderShimmerView = self.borderShimmerView { + transition.updateFrame(view: shimmerView, frame: buttonFrame) + transition.updateFrame(view: borderView, frame: buttonFrame) + transition.updateFrame(view: borderMaskView, frame: buttonFrame) + transition.updateFrame(view: borderShimmerView, frame: buttonFrame) + + shimmerView.updateAbsoluteRect(CGRect(origin: CGPoint(x: width * 3.0, y: 0.0), size: buttonSize), within: CGSize(width: width * 7.0, height: buttonHeight)) + borderShimmerView.updateAbsoluteRect(CGRect(origin: CGPoint(x: width * 3.0, y: 0.0), size: buttonSize), within: CGSize(width: width * 7.0, height: buttonHeight)) } + transition.updateFrame(node: self.buttonNode, frame: buttonFrame) if self.title != self.titleNode.attributedText?.string { diff --git a/submodules/SparseItemGrid/Sources/SparseItemGridScrollingArea.swift b/submodules/SparseItemGrid/Sources/SparseItemGridScrollingArea.swift index ad13af832d..e5a77eaca6 100644 --- a/submodules/SparseItemGrid/Sources/SparseItemGridScrollingArea.swift +++ b/submodules/SparseItemGrid/Sources/SparseItemGridScrollingArea.swift @@ -1152,7 +1152,7 @@ public final class SparseItemGridScrollingArea: ASDisplayNode { ) let scrollIndicatorHeightFraction = min(1.0, max(0.0, (containerSize.height - containerInsets.top - containerInsets.bottom) / contentHeight)) - if scrollIndicatorHeightFraction >= 1.0 - .ulpOfOne { + if scrollIndicatorHeightFraction >= 0.55 - .ulpOfOne { self.dateIndicator.isHidden = true self.lineIndicator.isHidden = true } else {