mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
519 lines
17 KiB
Swift
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
|
|
}
|
|
}
|
|
}
|