Swiftgram/submodules/TelegramUI/Sources/ChatOverscrollControl.swift
2021-08-01 18:12:53 +02:00

519 lines
17 KiB
Swift

import UIKit
import ComponentFlow
import Display
import TelegramCore
import Postbox
import AccountContext
import AvatarNode
final class BlurredRoundedRectangle: Component {
let color: UIColor
init(color: UIColor) {
self.color = color
}
static func ==(lhs: BlurredRoundedRectangle, rhs: BlurredRoundedRectangle) -> Bool {
if !lhs.color.isEqual(rhs.color) {
return false
}
return true
}
final class View: UIView {
private let background: NavigationBackgroundNode
init() {
self.background = NavigationBackgroundNode(color: .clear)
super.init(frame: CGRect())
self.addSubview(self.background.view)
}
required init?(coder aDecoder: NSCoder) {
preconditionFailure()
}
func update(component: BlurredRoundedRectangle, availableSize: CGSize, transition: Transition) -> CGSize {
transition.setFrame(view: self.background.view, frame: CGRect(origin: CGPoint(), size: availableSize))
self.background.updateColor(color: component.color, transition: .immediate)
self.background.update(size: availableSize, cornerRadius: min(availableSize.width, availableSize.height) / 2.0, transition: .immediate)
return availableSize
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
final class RadialProgressComponent: Component {
let color: UIColor
let lineWidth: CGFloat
let value: CGFloat
init(
color: UIColor,
lineWidth: CGFloat,
value: CGFloat
) {
self.color = color
self.lineWidth = lineWidth
self.value = value
}
static func ==(lhs: RadialProgressComponent, rhs: RadialProgressComponent) -> Bool {
if !lhs.color.isEqual(rhs.color) {
return false
}
if lhs.lineWidth != rhs.lineWidth {
return false
}
if lhs.value != rhs.value {
return false
}
return true
}
final class View: UIView {
init() {
super.init(frame: CGRect())
}
required init?(coder aDecoder: NSCoder) {
preconditionFailure()
}
func update(component: RadialProgressComponent, availableSize: CGSize, transition: Transition) -> CGSize {
func draw(context: CGContext) {
let diameter = availableSize.width
context.saveGState()
context.setBlendMode(.normal)
context.setFillColor(component.color.cgColor)
context.setStrokeColor(component.color.cgColor)
var progress: CGFloat
var startAngle: CGFloat
var endAngle: CGFloat
let value = component.value
progress = value
startAngle = -CGFloat.pi / 2.0
endAngle = CGFloat(progress) * 2.0 * CGFloat.pi + startAngle
if progress > 1.0 {
progress = 2.0 - progress
let tmp = startAngle
startAngle = endAngle
endAngle = tmp
}
progress = min(1.0, progress)
let lineWidth: CGFloat = component.lineWidth
let pathDiameter: CGFloat
pathDiameter = diameter - lineWidth
var angle: Double = 0.0
angle *= 4.0
context.translateBy(x: diameter / 2.0, y: diameter / 2.0)
context.rotate(by: CGFloat(angle.truncatingRemainder(dividingBy: Double.pi * 2.0)))
context.translateBy(x: -diameter / 2.0, y: -diameter / 2.0)
let path = UIBezierPath(arcCenter: CGPoint(x: diameter / 2.0, y: diameter / 2.0), radius: pathDiameter / 2.0, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.lineWidth = lineWidth
path.lineCapStyle = .round
path.stroke()
context.restoreGState()
}
if #available(iOS 10.0, *) {
let renderer = UIGraphicsImageRenderer(bounds: CGRect(origin: CGPoint(), size: availableSize))
let image = renderer.image { context in
UIGraphicsPushContext(context.cgContext)
draw(context: context.cgContext)
UIGraphicsPopContext()
}
self.layer.contents = image.cgImage
} else {
UIGraphicsBeginImageContextWithOptions(availableSize, false, 0.0)
draw(context: UIGraphicsGetCurrentContext()!)
self.layer.contents = UIGraphicsGetImageFromCurrentImageContext()?.cgImage
UIGraphicsEndImageContext()
}
return availableSize
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
final class CheckComponent: Component {
let color: UIColor
let lineWidth: CGFloat
let value: CGFloat
init(
color: UIColor,
lineWidth: CGFloat,
value: CGFloat
) {
self.color = color
self.lineWidth = lineWidth
self.value = value
}
static func ==(lhs: CheckComponent, rhs: CheckComponent) -> Bool {
if !lhs.color.isEqual(rhs.color) {
return false
}
if lhs.lineWidth != rhs.lineWidth {
return false
}
if lhs.value != rhs.value {
return false
}
return true
}
final class View: UIView {
init() {
super.init(frame: CGRect())
}
required init?(coder aDecoder: NSCoder) {
preconditionFailure()
}
func update(component: CheckComponent, availableSize: CGSize, transition: Transition) -> CGSize {
func draw(context: CGContext) {
let size = availableSize
let diameter = size.width
let factor = diameter / 50.0
context.saveGState()
context.setBlendMode(.normal)
context.setFillColor(component.color.cgColor)
context.setStrokeColor(component.color.cgColor)
let center = CGPoint(x: diameter / 2.0, y: diameter / 2.0)
let lineWidth = component.lineWidth
context.setLineWidth(max(1.7, lineWidth * factor))
context.setLineCap(.round)
context.setLineJoin(.round)
context.setMiterLimit(10.0)
let progress = component.value
let firstSegment: CGFloat = max(0.0, min(1.0, progress * 3.0))
var s = CGPoint(x: center.x - 10.0 * factor, y: center.y + 1.0 * factor)
var p1 = CGPoint(x: 7.0 * factor, y: 7.0 * factor)
var p2 = CGPoint(x: 13.0 * factor, y: -15.0 * factor)
if diameter < 36.0 {
s = CGPoint(x: center.x - 7.0 * factor, y: center.y + 1.0 * factor)
p1 = CGPoint(x: 4.5 * factor, y: 4.5 * factor)
p2 = CGPoint(x: 10.0 * factor, y: -11.0 * factor)
}
if !firstSegment.isZero {
if firstSegment < 1.0 {
context.move(to: CGPoint(x: s.x + p1.x * firstSegment, y: s.y + p1.y * firstSegment))
context.addLine(to: s)
} else {
let secondSegment = (progress - 0.33) * 1.5
context.move(to: CGPoint(x: s.x + p1.x + p2.x * secondSegment, y: s.y + p1.y + p2.y * secondSegment))
context.addLine(to: CGPoint(x: s.x + p1.x, y: s.y + p1.y))
context.addLine(to: s)
}
}
context.strokePath()
context.restoreGState()
}
if #available(iOS 10.0, *) {
let renderer = UIGraphicsImageRenderer(bounds: CGRect(origin: CGPoint(), size: availableSize))
let image = renderer.image { context in
UIGraphicsPushContext(context.cgContext)
draw(context: context.cgContext)
UIGraphicsPopContext()
}
self.layer.contents = image.cgImage
} else {
UIGraphicsBeginImageContextWithOptions(availableSize, false, 0.0)
draw(context: UIGraphicsGetCurrentContext()!)
self.layer.contents = UIGraphicsGetImageFromCurrentImageContext()?.cgImage
UIGraphicsEndImageContext()
}
return availableSize
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
final class AvatarComponent: Component {
let context: AccountContext
let peer: EnginePeer
init(context: AccountContext, peer: EnginePeer) {
self.context = context
self.peer = peer
}
static func ==(lhs: AvatarComponent, rhs: AvatarComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.peer != rhs.peer {
return false
}
return true
}
final class View: UIView {
private let avatarNode: AvatarNode
init() {
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0))
super.init(frame: CGRect())
self.addSubview(self.avatarNode.view)
}
required init?(coder aDecoder: NSCoder) {
preconditionFailure()
}
func update(component: AvatarComponent, availableSize: CGSize, transition: Transition) -> CGSize {
self.avatarNode.frame = CGRect(origin: CGPoint(), size: availableSize)
self.avatarNode.setPeer(context: component.context, theme: component.context.sharedContext.currentPresentationData.with({ $0 }).theme, peer: component.peer, synchronousLoad: true)
return availableSize
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
final class ChatOverscrollControl: CombinedComponent {
let text: String
let backgroundColor: UIColor
let foregroundColor: UIColor
let peer: EnginePeer?
let context: AccountContext
let expandProgress: CGFloat
init(
text: String,
backgroundColor: UIColor,
foregroundColor: UIColor,
peer: EnginePeer?,
context: AccountContext,
expandProgress: CGFloat
) {
self.text = text
self.backgroundColor = backgroundColor
self.foregroundColor = foregroundColor
self.peer = peer
self.context = context
self.expandProgress = expandProgress
}
static func ==(lhs: ChatOverscrollControl, rhs: ChatOverscrollControl) -> Bool {
if lhs.text != rhs.text {
return false
}
if !lhs.backgroundColor.isEqual(rhs.backgroundColor) {
return false
}
if !lhs.foregroundColor.isEqual(rhs.foregroundColor) {
return false
}
if lhs.peer != rhs.peer {
return false
}
if lhs.context !== rhs.context {
return false
}
if lhs.expandProgress != rhs.expandProgress {
return false
}
return true
}
static var body: Body {
let avatarBackground = Child(BlurredRoundedRectangle.self)
let avatarExpandProgress = Child(RadialProgressComponent.self)
let avatarCheck = Child(CheckComponent.self)
let avatar = Child(AvatarComponent.self)
let textBackground = Child(BlurredRoundedRectangle.self)
let text = Child(Text.self)
return { context in
let text = text.update(
component: Text(
text: context.component.text,
font: Font.regular(12.0),
color: context.component.foregroundColor
),
availableSize: CGSize(width: context.availableSize.width, height: 100.0),
transition: context.transition
)
let textHorizontalPadding: CGFloat = 6.0
let textVerticalPadding: CGFloat = 2.0
let avatarSize: CGFloat = 48.0
let avatarPadding: CGFloat = 8.0
let avatarTextSpacing: CGFloat = 8.0
let avatarProgressPadding: CGFloat = 2.5
let avatarBackgroundSize: CGFloat = context.component.peer != nil ? (avatarSize + avatarPadding * 2.0) : avatarSize
let avatarBackground = avatarBackground.update(
component: BlurredRoundedRectangle(
color: context.component.backgroundColor
),
availableSize: CGSize(width: avatarBackgroundSize, height: avatarBackgroundSize),
transition: context.transition
)
let avatarCheck = Condition(context.component.peer == nil, { () -> _UpdatedChildComponent in
let avatarCheckSize = avatarBackgroundSize + 2.0
return avatarCheck.update(
component: CheckComponent(
color: context.component.foregroundColor,
lineWidth: 2.5,
value: 1.0
),
availableSize: CGSize(width: avatarCheckSize, height: avatarCheckSize),
transition: context.transition
)
})
let avatarExpandProgress = avatarExpandProgress.update(
component: RadialProgressComponent(
color: context.component.foregroundColor,
lineWidth: 2.5,
value: context.component.peer == nil ? 0.0 : context.component.expandProgress
),
availableSize: CGSize(width: avatarBackground.size.width - avatarProgressPadding * 2.0, height: avatarBackground.size.height - avatarProgressPadding * 2.0),
transition: context.transition
)
let textBackground = textBackground.update(
component: BlurredRoundedRectangle(
color: context.component.backgroundColor
),
availableSize: CGSize(width: text.size.width + textHorizontalPadding * 2.0, height: text.size.height + textVerticalPadding * 2.0),
transition: context.transition
)
let size = CGSize(width: context.availableSize.width, height: avatarBackground.size.height + avatarTextSpacing + textBackground.size.height)
let avatarBackgroundFrame = avatarBackground.size.topCentered(in: CGRect(origin: CGPoint(), size: size))
let avatar = context.component.peer.flatMap { peer in
avatar.update(
component: AvatarComponent(
context: context.component.context,
peer: peer
),
availableSize: CGSize(width: avatarSize, height: avatarSize),
transition: context.transition
)
}
context.add(avatarBackground
.position(CGPoint(
x: avatarBackgroundFrame.midX,
y: avatarBackgroundFrame.midY
))
)
if let avatarCheck = avatarCheck {
context.add(avatarCheck
.position(CGPoint(
x: avatarBackgroundFrame.midX,
y: avatarBackgroundFrame.midY
))
)
}
context.add(avatarExpandProgress
.position(CGPoint(
x: avatarBackgroundFrame.midX,
y: avatarBackgroundFrame.midY
))
)
if let avatar = avatar {
context.add(avatar
.position(CGPoint(
x: avatarBackgroundFrame.midX,
y: avatarBackgroundFrame.midY
))
)
}
let textBackgroundFrame = textBackground.size.bottomCentered(in: CGRect(origin: CGPoint(), size: size))
context.add(textBackground
.position(CGPoint(
x: textBackgroundFrame.midX,
y: textBackgroundFrame.midY
))
)
let textFrame = text.size.centered(in: textBackgroundFrame)
context.add(text
.position(CGPoint(
x: textFrame.midX,
y: textFrame.midY
))
)
return size
}
}
}