Improve rounded button shimmer

This commit is contained in:
Ilya Laktyushin 2022-04-26 04:52:16 +04:00
parent 3d385c94bd
commit 4af2744424
5 changed files with 243 additions and 15 deletions

View File

@ -12,6 +12,7 @@ swift_library(
deps = [
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/Components/HierarchyTrackingLayer:HierarchyTrackingLayer",
],
visibility = [
"//visibility:public",

View File

@ -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?

View File

@ -12,6 +12,7 @@ swift_library(
deps = [
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/ShimmerEffect:ShimmerEffect",
"//submodules/Components/HierarchyTrackingLayer:HierarchyTrackingLayer",
],
visibility = [

View File

@ -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 {

View File

@ -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 {