2023-10-20 02:18:33 +04:00

431 lines
17 KiB
Swift

import Foundation
import UIKit
import Display
import HierarchyTrackingLayer
import Postbox
import TelegramCore
import AnimationCache
import MultiAnimationRenderer
import SwiftSignalKit
import AccountContext
import EmojiTextAttachmentView
private let radius: CGFloat = 4.0
private let lineWidth: CGFloat = 3.0
private func addRoundedRectPath(context: CGContext, rect: CGRect, radius: CGFloat) {
context.saveGState()
context.translateBy(x: rect.minX, y: rect.minY)
context.scaleBy(x: radius, y: radius)
let fw = rect.width / radius
let fh = rect.height / radius
context.move(to: CGPoint(x: fw, y: fh / 2.0))
context.addArc(tangent1End: CGPoint(x: fw, y: fh), tangent2End: CGPoint(x: fw/2, y: fh), radius: 1.0)
context.addArc(tangent1End: CGPoint(x: 0, y: fh), tangent2End: CGPoint(x: 0, y: fh/2), radius: 1)
context.addArc(tangent1End: CGPoint(x: 0, y: 0), tangent2End: CGPoint(x: fw/2, y: 0), radius: 1)
context.addArc(tangent1End: CGPoint(x: fw, y: 0), tangent2End: CGPoint(x: fw, y: fh/2), radius: 1)
context.closePath()
context.restoreGState()
}
private func generateTemplateImage(isMonochrome: Bool) -> UIImage {
return generateImage(CGSize(width: radius * 2.0 + 4.0, height: radius * 2.0 + 8.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
addRoundedRectPath(context: context, rect: CGRect(origin: CGPoint(), size: size), radius: radius)
context.clip()
context.setFillColor(UIColor.white.withMultipliedAlpha(0.1).cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor.white.withAlphaComponent(isMonochrome ? 0.2 : 1.0).cgColor)
context.fill(CGRect(origin: CGPoint(), size: CGSize(width: lineWidth, height: size.height)))
})!.stretchableImage(withLeftCapWidth: Int(radius) + 2, topCapHeight: Int(radius) + 3).withRenderingMode(.alwaysTemplate)
}
private let plainTemplateImage: UIImage = {
return generateTemplateImage(isMonochrome: false)
}()
private let monochromePatternTemplateImage: UIImage = {
return generateTemplateImage(isMonochrome: true)
}()
private func generateDashBackgroundTemplateImage() -> UIImage {
return generateImage(CGSize(width: radius * 2.0 + 4.0, height: radius * 2.0 + 8.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: radius).cgPath)
context.clip()
context.setFillColor(UIColor.white.withMultipliedAlpha(0.1).cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor.white.withAlphaComponent(0.2).cgColor)
context.fill(CGRect(origin: CGPoint(), size: CGSize(width: lineWidth, height: size.height)))
})!.stretchableImage(withLeftCapWidth: Int(radius) + 2, topCapHeight: Int(radius) + 3).withRenderingMode(.alwaysTemplate)
}
private let dashBackgroundTemplateImage: UIImage = {
return generateDashBackgroundTemplateImage()
}()
private func generateDashTemplateImage(isMonochrome: Bool) -> UIImage {
return generateImage(CGSize(width: radius * 2.0, height: 18.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor.white.cgColor)
let dashOffset: CGFloat = isMonochrome ? -4.0 : 5.0
context.translateBy(x: 0.0, y: dashOffset)
for _ in 0 ..< 2 {
context.move(to: CGPoint(x: 0.0, y: 3.0))
context.addLine(to: CGPoint(x: lineWidth, y: 0.0))
context.addLine(to: CGPoint(x: lineWidth, y: 9.0))
context.addLine(to: CGPoint(x: 0.0, y: 9.0 + 3.0))
context.closePath()
context.fillPath()
context.translateBy(x: 0.0, y: 18.0)
}
context.clear(CGRect(origin: CGPoint(x: lineWidth, y: 0.0), size: CGSize(width: size.width - lineWidth, height: size.height)))
})!.resizableImage(withCapInsets: .zero, resizingMode: .tile).withRenderingMode(.alwaysTemplate)
}
private let dashOpaqueTemplateImage: UIImage = {
return generateDashTemplateImage(isMonochrome: false)
}()
private let dashMonochromeTemplateImage: UIImage = {
return generateDashTemplateImage(isMonochrome: true)
}()
private final class PatternContentsTarget: MultiAnimationRenderTarget {
private let imageUpdated: () -> Void
init(imageUpdated: @escaping () -> Void) {
self.imageUpdated = imageUpdated
super.init()
}
required init(coder: NSCoder) {
preconditionFailure()
}
override func transitionToContents(_ contents: AnyObject, didLoop: Bool) {
self.contents = contents
self.imageUpdated()
}
}
public final class MessageInlineBlockBackgroundView: UIView {
public final class Pattern: Equatable {
public let context: AccountContext
public let fileId: Int64
public let file: TelegramMediaFile?
public init(context: AccountContext, fileId: Int64, file: TelegramMediaFile?) {
self.context = context
self.fileId = fileId
self.file = file
}
public static func ==(lhs: Pattern, rhs: Pattern) -> Bool {
if lhs === rhs {
return true
}
if lhs.context !== rhs.context {
return false
}
if lhs.fileId != rhs.fileId {
return false
}
if lhs.file?.fileId != rhs.file?.fileId {
return false
}
return true
}
}
private struct Params: Equatable {
var size: CGSize
var primaryColor: UIColor
var secondaryColor: UIColor?
var pattern: Pattern?
var displayProgress: Bool
}
private var params: Params?
public var displayProgress: Bool = false {
didSet {
if self.displayProgress != oldValue {
if let params = self.params {
self.update(
size: params.size,
primaryColor: params.primaryColor,
secondaryColor: params.secondaryColor,
pattern: params.pattern,
animation: .System(duration: 0.2, transition: ControlledTransition(
duration: 0.2,
curve: .easeInOut,
interactive: false
))
)
}
}
}
}
private let backgroundView: UIImageView
private var dashView: UIImageView?
private var hierarchyTrackingLayer: HierarchyTrackingLayer?
private var patternContentsTarget: PatternContentsTarget?
private var patternContentLayers: [SimpleLayer] = []
private var patternFile: TelegramMediaFile?
private var patternFileDisposable: Disposable?
private var patternImage: UIImage?
private var patternImageDisposable: Disposable?
override public init(frame: CGRect) {
self.backgroundView = UIImageView()
super.init(frame: frame)
self.addSubview(self.backgroundView)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.patternFileDisposable?.dispose()
self.patternImageDisposable?.dispose()
}
private func updateAnimations() {
}
private func loadPatternFromFile() {
guard let pattern = self.params?.pattern else {
return
}
guard let patternContentsTarget = self.patternContentsTarget else {
return
}
guard let patternFile = self.patternFile else {
return
}
self.patternImageDisposable = pattern.context.animationRenderer.loadFirstFrame(
target: patternContentsTarget,
cache: pattern.context.animationCache, itemId: "reply-pattern-\(patternFile.fileId)",
size: CGSize(width: 64, height: 64),
fetch: animationCacheFetchFile(
postbox: pattern.context.account.postbox,
userLocation: .other,
userContentType: .sticker,
resource: .media(media: .standalone(media: patternFile), resource: patternFile.resource),
type: AnimationCacheAnimationType(file: patternFile),
keyframeOnly: false,
customColor: .white
),
completion: { [weak self] _, _ in
guard let self else {
return
}
self.updatePatternLayerImages()
}
)
}
private func updatePatternLayerImages() {
let image = self.patternContentsTarget?.contents
for patternContentLayer in self.patternContentLayers {
patternContentLayer.contents = image
}
}
public func update(
size: CGSize,
primaryColor: UIColor,
secondaryColor: UIColor?,
pattern: Pattern?,
animation: ListViewItemUpdateAnimation
) {
let params = Params(
size: size,
primaryColor: primaryColor,
secondaryColor: secondaryColor,
pattern: pattern,
displayProgress: self.displayProgress
)
if self.params == params {
return
}
let previousParams = self.params
self.params = params
if previousParams?.primaryColor != params.primaryColor || previousParams?.secondaryColor != params.secondaryColor {
for patternContentLayer in self.patternContentLayers {
patternContentLayer.layerTintColor = primaryColor.cgColor
}
if let secondaryColor = params.secondaryColor {
self.backgroundView.tintColor = params.primaryColor
if self.dashView == nil {
let dashView = UIImageView()
dashView.layer.cornerRadius = radius
if #available(iOS 13.0, *) {
dashView.layer.cornerCurve = .circular
}
self.dashView = dashView
self.addSubview(dashView)
}
if secondaryColor.alpha == 0.0 {
self.backgroundView.image = monochromePatternTemplateImage
self.dashView?.image = dashMonochromeTemplateImage
self.dashView?.tintColor = primaryColor
} else {
self.backgroundView.image = plainTemplateImage
self.dashView?.image = dashOpaqueTemplateImage
self.dashView?.tintColor = secondaryColor
}
} else {
self.backgroundView.image = plainTemplateImage
self.backgroundView.tintColor = params.primaryColor
if let dashView = self.dashView {
self.dashView = dashView
dashView.removeFromSuperview()
}
}
}
if previousParams?.pattern != params.pattern {
if let pattern = params.pattern {
self.layer.masksToBounds = true
self.layer.cornerRadius = radius
if #available(iOS 13.0, *) {
self.layer.cornerCurve = .circular
}
if self.patternContentsTarget == nil {
self.patternContentsTarget = PatternContentsTarget(imageUpdated: { [weak self] in
guard let self else {
return
}
self.updatePatternLayerImages()
})
}
if previousParams?.pattern?.fileId != pattern.fileId {
self.patternFile = nil
self.patternFileDisposable?.dispose()
self.patternFileDisposable = nil
self.patternImageDisposable?.dispose()
if let file = pattern.file {
self.patternFile = file
self.loadPatternFromFile()
} else {
let fileId = pattern.fileId
self.patternFileDisposable = (pattern.context.engine.stickers.resolveInlineStickers(fileIds: [pattern.fileId])
|> deliverOnMainQueue).startStrict(next: { [weak self] files in
guard let self else {
return
}
if let file = files[fileId] {
self.patternFile = file
self.loadPatternFromFile()
}
})
}
}
} else {
self.layer.masksToBounds = false
self.patternContentsTarget = nil
self.patternFileDisposable?.dispose()
self.patternFileDisposable = nil
self.patternFile = nil
}
}
self.dashView?.layer.masksToBounds = params.pattern == nil && params.secondaryColor != nil
animation.animator.updateFrame(layer: self.backgroundView.layer, frame: CGRect(origin: CGPoint(), size: size), completion: nil)
if let dashView = self.dashView {
animation.animator.updateFrame(layer: dashView.layer, frame: CGRect(origin: CGPoint(), size: CGSize(width: radius * 2.0, height: size.height)), completion: nil)
}
if params.pattern != nil {
var maxIndex = 0
struct Placement {
var position: CGPoint
var size: CGFloat
init(_ position: CGPoint, _ size: CGFloat) {
self.position = position
self.size = size
}
}
let placements: [Placement] = [
Placement(CGPoint(x: 176.0, y: 13.0), 38.0),
Placement(CGPoint(x: 51.0, y: 45.0), 58.0),
Placement(CGPoint(x: 349.0, y: 36.0), 58.0),
Placement(CGPoint(x: 132.0, y: 64.0), 46.0),
Placement(CGPoint(x: 241.0, y: 64.0), 54.0),
Placement(CGPoint(x: 68.0, y: 121.0), 44.0),
Placement(CGPoint(x: 178.0, y: 122.0), 47.0),
Placement(CGPoint(x: 315.0, y: 122.0), 47.0),
]
for placement in placements {
let patternContentLayer: SimpleLayer
if maxIndex < self.patternContentLayers.count {
patternContentLayer = self.patternContentLayers[maxIndex]
} else {
patternContentLayer = SimpleLayer()
patternContentLayer.layerTintColor = primaryColor.cgColor
self.layer.addSublayer(patternContentLayer)
self.patternContentLayers.append(patternContentLayer)
}
patternContentLayer.contents = self.patternContentsTarget?.contents
let itemSize = CGSize(width: placement.size / 3.0, height: placement.size / 3.0)
patternContentLayer.frame = CGRect(origin: CGPoint(x: size.width - placement.position.x / 3.0 - itemSize.width * 0.5, y: placement.position.y / 3.0 - itemSize.height * 0.5), size: itemSize)
var alphaFraction = abs(placement.position.x) / 400.0
alphaFraction = min(1.0, max(0.0, alphaFraction))
patternContentLayer.opacity = 0.3 * Float(1.0 - alphaFraction)
maxIndex += 1
}
if maxIndex < self.patternContentLayers.count {
for i in maxIndex ..< self.patternContentLayers.count {
self.patternContentLayers[i].removeFromSuperlayer()
}
self.patternContentLayers.removeSubrange(maxIndex ..< self.patternContentLayers.count)
}
} else {
for patternContentLayer in self.patternContentLayers {
patternContentLayer.removeFromSuperlayer()
}
self.patternContentLayers.removeAll()
}
self.updateAnimations()
}
}