Ilya Laktyushin 9cd5b06592 Various fixes
2025-09-28 03:34:50 +04:00

202 lines
8.4 KiB
Swift

import UIKit
import Display
import ComponentFlow
import TelegramPresentationData
private final class ShimmerEffectView: UIView {
private var currentBackgroundColor: UIColor?
private var currentForegroundColor: UIColor?
private let imageContainerView: UIView
private let imageView: UIImageView
private var absoluteLocation: (CGRect, CGSize)?
private var shouldBeAnimating = false
override init(frame: CGRect = .zero) {
self.imageContainerView = UIView()
self.imageContainerView.isUserInteractionEnabled = false
self.imageView = UIImageView()
self.imageView.isUserInteractionEnabled = false
self.imageView.contentMode = .scaleToFill
super.init(frame: frame)
self.isUserInteractionEnabled = false
self.clipsToBounds = true
self.imageContainerView.addSubview(self.imageView)
self.addSubview(self.imageContainerView)
}
required init?(coder: NSCoder) {
preconditionFailure()
}
override func didMoveToWindow() {
super.didMoveToWindow()
updateAnimation()
}
func update(backgroundColor: UIColor, foregroundColor: UIColor) {
if let currentBackgroundColor = self.currentBackgroundColor, currentBackgroundColor.argb == backgroundColor.argb,
let currentForegroundColor = self.currentForegroundColor, currentForegroundColor.argb == foregroundColor.argb {
return
}
self.currentBackgroundColor = backgroundColor
self.currentForegroundColor = foregroundColor
self.imageView.image = generateImage(CGSize(width: 4.0, height: 320.0), opaque: true, scale: 1.0, rotatedContext: { size, context in
context.setFillColor(backgroundColor.cgColor)
context.fill(CGRect(origin: .zero, size: size))
context.clip(to: CGRect(origin: .zero, 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: [])
})
}
func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
if let 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, shouldBeAnimating {
self.imageView.layer.removeAnimation(forKey: "shimmer")
self.addImageAnimation()
}
if frameUpdated {
self.imageContainerView.frame = CGRect(origin: CGPoint(x: -rect.minX, y: -rect.minY), size: containerSize)
}
self.updateAnimation()
}
private func updateAnimation() {
let inHierarchy = (self.window != nil)
let shouldAnimate = inHierarchy && (self.absoluteLocation != nil)
if shouldAnimate != self.shouldBeAnimating {
self.shouldBeAnimating = shouldAnimate
if shouldAnimate {
self.addImageAnimation()
} else {
self.imageView.layer.removeAnimation(forKey: "shimmer")
}
}
}
private func addImageAnimation() {
guard let containerSize = self.absoluteLocation?.1 else { return }
let gradientHeight: CGFloat = 250.0
self.imageView.frame = CGRect(origin: CGPoint(x: 0.0, y: -gradientHeight),
size: CGSize(width: containerSize.width, height: gradientHeight))
let anim = CABasicAnimation(keyPath: "position.y")
anim.fromValue = 0.0
anim.toValue = (containerSize.height + gradientHeight)
anim.duration = 1.3
anim.timingFunction = CAMediaTimingFunction(name: .easeOut)
anim.fillMode = .removed
anim.isRemovedOnCompletion = true
anim.repeatCount = .infinity
anim.beginTime = CACurrentMediaTime() + 1.0
anim.isAdditive = true
self.imageView.layer.add(anim, forKey: "shimmer")
}
}
public final class GiftLoadingShimmerView: UIView {
private let backgroundView = UIView()
private let effectView = ShimmerEffectView()
private let maskImageView = UIImageView()
private var currentParams: (size: CGSize, theme: PresentationTheme, showFilters: Bool)?
public override init(frame: CGRect = .zero) {
super.init(frame: frame)
self.isUserInteractionEnabled = false
self.backgroundColor = .clear
self.layer.allowsGroupOpacity = true
self.addSubview(self.backgroundView)
self.addSubview(self.effectView)
self.addSubview(self.maskImageView)
}
required init?(coder: NSCoder) { fatalError() }
public func update(size: CGSize, theme: PresentationTheme, showFilters: Bool = false, isPlain: Bool = false, transition: ContainedViewLayoutTransition) {
let backgroundColor = isPlain ? theme.list.itemBlocksBackgroundColor : theme.list.blocksBackgroundColor
let color = theme.list.itemSecondaryTextColor.mixedWith(theme.list.blocksBackgroundColor, alpha: 0.85)
if self.currentParams?.size != size || self.currentParams?.theme !== theme || self.currentParams?.showFilters != showFilters {
self.currentParams = (size, theme, showFilters)
self.backgroundView.backgroundColor = color
self.maskImageView.image = generateImage(size, rotatedContext: { size, context in
context.setFillColor(backgroundColor.cgColor)
context.fill(CGRect(origin: .zero, size: size))
let sideInset: CGFloat = 16.0
if showFilters {
let filterSpacing: CGFloat = 6.0
let filterWidth = (size.width - sideInset * 2.0 - filterSpacing * 3.0) / 4.0
for i in 0 ..< 4 {
let rect = CGRect(origin: CGPoint(x: sideInset + (filterWidth + filterSpacing) * CGFloat(i), y: 0.0),
size: CGSize(width: filterWidth, height: 28.0))
context.addPath(CGPath(roundedRect: rect, cornerWidth: 14.0, cornerHeight: 14.0, transform: nil))
}
}
var currentY: CGFloat = 39.0 + 7.0
var rowIndex: Int = 0
let optionSpacing: CGFloat = 10.0
let optionWidth = (size.width - sideInset * 2.0 - optionSpacing * 2.0) / 3.0
let itemSize = CGSize(width: optionWidth, height: 154.0)
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
while currentY < size.height {
for i in 0 ..< 3 {
let itemOrigin = CGPoint(
x: sideInset + CGFloat(i) * (itemSize.width + optionSpacing),
y: 39.0 + 9.0 + CGFloat(rowIndex) * (itemSize.height + optionSpacing)
)
context.addPath(CGPath(roundedRect: CGRect(origin: itemOrigin, size: itemSize),
cornerWidth: 10.0, cornerHeight: 10.0, transform: nil))
}
currentY += itemSize.height
rowIndex += 1
}
context.fillPath()
})
self.effectView.update(backgroundColor: color, foregroundColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4))
self.effectView.updateAbsoluteRect(CGRect(origin: .zero, size: size), within: size)
}
transition.updateFrame(view: self.backgroundView, frame: CGRect(origin: .zero, size: size))
transition.updateFrame(view: self.maskImageView, frame: CGRect(origin: .zero, size: size))
transition.updateFrame(view: self.effectView, frame: CGRect(origin: .zero, size: size))
}
}