mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Improve rounded button shimmer
This commit is contained in:
parent
3d385c94bd
commit
4af2744424
@ -12,6 +12,7 @@ swift_library(
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/Components/HierarchyTrackingLayer:HierarchyTrackingLayer",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -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?
|
||||
|
@ -12,6 +12,7 @@ swift_library(
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/ShimmerEffect:ShimmerEffect",
|
||||
"//submodules/Components/HierarchyTrackingLayer:HierarchyTrackingLayer",
|
||||
],
|
||||
visibility = [
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user