mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
1121 lines
44 KiB
Swift
1121 lines
44 KiB
Swift
import UIKit
|
|
import ComponentFlow
|
|
import Display
|
|
import AsyncDisplayKit
|
|
import TelegramCore
|
|
import Postbox
|
|
import AccountContext
|
|
import AvatarNode
|
|
import TextFormat
|
|
import Markdown
|
|
import WallpaperBackgroundNode
|
|
|
|
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, state: EmptyComponentState, environment: Environment<Empty>, 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, state: EmptyComponentState, environment: Environment<Empty>, 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 {
|
|
private var currentValue: CGFloat?
|
|
private var animator: DisplayLinkAnimator?
|
|
|
|
init() {
|
|
super.init(frame: CGRect())
|
|
}
|
|
|
|
required init?(coder aDecoder: NSCoder) {
|
|
preconditionFailure()
|
|
}
|
|
|
|
private func updateContent(size: CGSize, color: UIColor, lineWidth: CGFloat, value: CGFloat) {
|
|
func draw(context: CGContext) {
|
|
let diameter = size.width
|
|
|
|
let factor = diameter / 50.0
|
|
|
|
context.saveGState()
|
|
|
|
context.setBlendMode(.normal)
|
|
context.setFillColor(color.cgColor)
|
|
context.setStrokeColor(color.cgColor)
|
|
|
|
let center = CGPoint(x: diameter / 2.0, y: diameter / 2.0)
|
|
|
|
context.setLineWidth(max(1.7, lineWidth * factor))
|
|
context.setLineCap(.round)
|
|
context.setLineJoin(.round)
|
|
context.setMiterLimit(10.0)
|
|
|
|
let progress = 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: size))
|
|
let image = renderer.image { context in
|
|
UIGraphicsPushContext(context.cgContext)
|
|
draw(context: context.cgContext)
|
|
UIGraphicsPopContext()
|
|
}
|
|
self.layer.contents = image.cgImage
|
|
} else {
|
|
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
|
|
draw(context: UIGraphicsGetCurrentContext()!)
|
|
self.layer.contents = UIGraphicsGetImageFromCurrentImageContext()?.cgImage
|
|
UIGraphicsEndImageContext()
|
|
}
|
|
}
|
|
|
|
func update(component: CheckComponent, availableSize: CGSize, transition: Transition) -> CGSize {
|
|
if let currentValue = self.currentValue, currentValue != component.value, case .curve = transition.animation {
|
|
self.animator?.invalidate()
|
|
|
|
let animator = DisplayLinkAnimator(duration: 0.15, from: currentValue, to: component.value, update: { [weak self] value in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.updateContent(size: availableSize, color: component.color, lineWidth: component.lineWidth, value: value)
|
|
}, completion: { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.animator?.invalidate()
|
|
strongSelf.animator = nil
|
|
})
|
|
self.animator = animator
|
|
} else {
|
|
if self.animator == nil {
|
|
self.updateContent(size: availableSize, color: component.color, lineWidth: component.lineWidth, value: component.value)
|
|
}
|
|
}
|
|
|
|
self.currentValue = component.value
|
|
|
|
return availableSize
|
|
}
|
|
}
|
|
|
|
func makeView() -> View {
|
|
return View()
|
|
}
|
|
|
|
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
|
return view.update(component: self, availableSize: availableSize, transition: transition)
|
|
}
|
|
}
|
|
|
|
final class BadgeComponent: CombinedComponent {
|
|
let count: Int
|
|
let backgroundColor: UIColor
|
|
let foregroundColor: UIColor
|
|
let rect: CGRect
|
|
let withinSize: CGSize
|
|
let wallpaperNode: WallpaperBackgroundNode?
|
|
|
|
init(
|
|
count: Int,
|
|
backgroundColor: UIColor,
|
|
foregroundColor: UIColor,
|
|
rect: CGRect,
|
|
withinSize: CGSize,
|
|
wallpaperNode: WallpaperBackgroundNode?
|
|
) {
|
|
self.count = count
|
|
self.backgroundColor = backgroundColor
|
|
self.foregroundColor = foregroundColor
|
|
self.rect = rect
|
|
self.withinSize = withinSize
|
|
self.wallpaperNode = wallpaperNode
|
|
}
|
|
|
|
static func ==(lhs: BadgeComponent, rhs: BadgeComponent) -> Bool {
|
|
if lhs.count != rhs.count {
|
|
return false
|
|
}
|
|
if !lhs.backgroundColor.isEqual(rhs.backgroundColor) {
|
|
return false
|
|
}
|
|
if !lhs.foregroundColor.isEqual(rhs.foregroundColor) {
|
|
return false
|
|
}
|
|
if lhs.rect != rhs.rect {
|
|
return false
|
|
}
|
|
if lhs.withinSize != rhs.withinSize {
|
|
return false
|
|
}
|
|
if lhs.wallpaperNode !== rhs.wallpaperNode {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
static var body: Body {
|
|
let background = Child(WallpaperBlurComponent.self)
|
|
let text = Child(Text.self)
|
|
|
|
return { context in
|
|
let text = text.update(
|
|
component: Text(
|
|
text: "\(context.component.count)",
|
|
font: Font.regular(13.0),
|
|
color: context.component.foregroundColor
|
|
),
|
|
availableSize: CGSize(width: 100.0, height: 100.0),
|
|
transition: .immediate
|
|
)
|
|
|
|
let height = text.size.height + 4.0
|
|
let backgroundSize = CGSize(width: max(height, text.size.width + 8.0), height: height)
|
|
|
|
let background = background.update(
|
|
component: WallpaperBlurComponent(
|
|
rect: CGRect(origin: context.component.rect.origin, size: backgroundSize),
|
|
withinSize: context.component.withinSize,
|
|
color: context.component.backgroundColor,
|
|
wallpaperNode: context.component.wallpaperNode
|
|
),
|
|
availableSize: backgroundSize,
|
|
transition: .immediate
|
|
)
|
|
|
|
context.add(background
|
|
.position(CGPoint(x: backgroundSize.width / 2.0, y: backgroundSize.height / 2.0))
|
|
.cornerRadius(min(backgroundSize.width, backgroundSize.height) / 2.0)
|
|
.clipsToBounds(true)
|
|
)
|
|
|
|
context.add(text
|
|
.position(CGPoint(x: backgroundSize.width / 2.0, y: backgroundSize.height / 2.0))
|
|
)
|
|
|
|
return backgroundSize
|
|
}
|
|
}
|
|
}
|
|
|
|
final class AvatarComponent: Component {
|
|
final class Badge: Equatable {
|
|
let count: Int
|
|
let backgroundColor: UIColor
|
|
let foregroundColor: UIColor
|
|
|
|
init(count: Int, backgroundColor: UIColor, foregroundColor: UIColor) {
|
|
self.count = count
|
|
self.backgroundColor = backgroundColor
|
|
self.foregroundColor = foregroundColor
|
|
}
|
|
|
|
static func ==(lhs: Badge, rhs: Badge) -> Bool {
|
|
if lhs.count != rhs.count {
|
|
return false
|
|
}
|
|
if !lhs.backgroundColor.isEqual(rhs.backgroundColor) {
|
|
return false
|
|
}
|
|
if !lhs.foregroundColor.isEqual(rhs.foregroundColor) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
let context: AccountContext
|
|
let peer: EnginePeer
|
|
let badge: Badge?
|
|
let rect: CGRect
|
|
let withinSize: CGSize
|
|
let wallpaperNode: WallpaperBackgroundNode?
|
|
|
|
init(
|
|
context: AccountContext,
|
|
peer: EnginePeer,
|
|
badge: Badge?,
|
|
rect: CGRect,
|
|
withinSize: CGSize,
|
|
wallpaperNode: WallpaperBackgroundNode?
|
|
) {
|
|
self.context = context
|
|
self.peer = peer
|
|
self.badge = badge
|
|
self.rect = rect
|
|
self.withinSize = withinSize
|
|
self.wallpaperNode = wallpaperNode
|
|
}
|
|
|
|
static func ==(lhs: AvatarComponent, rhs: AvatarComponent) -> Bool {
|
|
if lhs.context !== rhs.context {
|
|
return false
|
|
}
|
|
if lhs.peer != rhs.peer {
|
|
return false
|
|
}
|
|
if lhs.badge != rhs.badge {
|
|
return false
|
|
}
|
|
if lhs.rect != rhs.rect {
|
|
return false
|
|
}
|
|
if lhs.withinSize != rhs.withinSize {
|
|
return false
|
|
}
|
|
if lhs.wallpaperNode !== rhs.wallpaperNode {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
final class View: UIView {
|
|
private let avatarNode: AvatarNode
|
|
private let avatarMask: CAShapeLayer
|
|
private var badgeView: ComponentHostView<Empty>?
|
|
|
|
init() {
|
|
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0))
|
|
self.avatarMask = CAShapeLayer()
|
|
|
|
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)
|
|
let theme = component.context.sharedContext.currentPresentationData.with({ $0 }).theme
|
|
self.avatarNode.setPeer(context: component.context, theme: theme, peer: component.peer, emptyColor: theme.list.mediaPlaceholderColor, synchronousLoad: true)
|
|
|
|
if let badge = component.badge {
|
|
let badgeView: ComponentHostView<Empty>
|
|
let animateIn = self.badgeView == nil
|
|
if let current = self.badgeView {
|
|
badgeView = current
|
|
} else {
|
|
badgeView = ComponentHostView<Empty>()
|
|
self.badgeView = badgeView
|
|
self.addSubview(badgeView)
|
|
}
|
|
|
|
let badgeSize = badgeView.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(BadgeComponent(
|
|
count: badge.count,
|
|
backgroundColor: badge.backgroundColor,
|
|
foregroundColor: badge.foregroundColor,
|
|
rect: CGRect(origin: component.rect.offsetBy(dx: 0.0, dy: 0.0).origin, size: CGSize()),
|
|
withinSize: component.withinSize,
|
|
wallpaperNode: component.wallpaperNode
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: 100.0, height: 100.0
|
|
))
|
|
let badgeDiameter = min(badgeSize.width, badgeSize.height)
|
|
let circlePoint = CGPoint(
|
|
x: availableSize.width / 2.0 + cos(CGFloat.pi / 4) * availableSize.width / 2.0,
|
|
y: availableSize.height / 2.0 - sin(CGFloat.pi / 4) * availableSize.width / 2.0
|
|
)
|
|
badgeView.frame = CGRect(origin: CGPoint(x: circlePoint.x - badgeDiameter / 2.0, y: circlePoint.y - badgeDiameter / 2.0), size: badgeSize)
|
|
|
|
self.avatarMask.frame = self.avatarNode.bounds
|
|
self.avatarMask.fillRule = .evenOdd
|
|
|
|
let path = UIBezierPath(rect: self.avatarMask.bounds)
|
|
path.append(UIBezierPath(roundedRect: badgeView.frame.insetBy(dx: -2.0, dy: -2.0), cornerRadius: badgeDiameter / 2.0))
|
|
self.avatarMask.path = path.cgPath
|
|
|
|
self.avatarNode.view.layer.mask = self.avatarMask
|
|
|
|
if animateIn {
|
|
badgeView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.14)
|
|
}
|
|
} else if let badgeView = self.badgeView {
|
|
self.badgeView = nil
|
|
|
|
badgeView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.14, removeOnCompletion: false, completion: { [weak badgeView] _ in
|
|
badgeView?.removeFromSuperview()
|
|
})
|
|
|
|
self.avatarNode.view.layer.mask = nil
|
|
}
|
|
|
|
return availableSize
|
|
}
|
|
}
|
|
|
|
func makeView() -> View {
|
|
return View()
|
|
}
|
|
|
|
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
|
return view.update(component: self, availableSize: availableSize, transition: transition)
|
|
}
|
|
}
|
|
|
|
private final class WallpaperBlurNode: ASDisplayNode {
|
|
private var backgroundNode: WallpaperBubbleBackgroundNode?
|
|
private let colorNode: ASDisplayNode
|
|
|
|
override init() {
|
|
self.colorNode = ASDisplayNode()
|
|
|
|
super.init()
|
|
|
|
//self.addSubnode(self.colorNode)
|
|
}
|
|
|
|
func update(rect: CGRect, within size: CGSize, color: UIColor, wallpaperNode: WallpaperBackgroundNode?, transition: ContainedViewLayoutTransition) {
|
|
var transition = transition
|
|
if self.backgroundNode == nil {
|
|
if let backgroundNode = wallpaperNode?.makeBubbleBackground(for: .free) {
|
|
self.backgroundNode = backgroundNode
|
|
self.insertSubnode(backgroundNode, at: 0)
|
|
transition = .immediate
|
|
}
|
|
}
|
|
|
|
self.colorNode.backgroundColor = color
|
|
transition.updateFrame(node: self.colorNode, frame: CGRect(origin: CGPoint(), size: rect.size))
|
|
|
|
if let backgroundNode = self.backgroundNode {
|
|
transition.updateFrame(node: backgroundNode, frame: CGRect(origin: CGPoint(), size: rect.size))
|
|
backgroundNode.update(rect: rect, within: size, transition: transition)
|
|
}
|
|
}
|
|
}
|
|
|
|
private final class WallpaperBlurComponent: Component {
|
|
let rect: CGRect
|
|
let withinSize: CGSize
|
|
let color: UIColor
|
|
let wallpaperNode: WallpaperBackgroundNode?
|
|
|
|
init(
|
|
rect: CGRect,
|
|
withinSize: CGSize,
|
|
color: UIColor,
|
|
wallpaperNode: WallpaperBackgroundNode?
|
|
) {
|
|
self.rect = rect
|
|
self.withinSize = withinSize
|
|
self.color = color
|
|
self.wallpaperNode = wallpaperNode
|
|
}
|
|
|
|
static func ==(lhs: WallpaperBlurComponent, rhs: WallpaperBlurComponent) -> Bool {
|
|
if lhs.rect != rhs.rect {
|
|
return false
|
|
}
|
|
if lhs.withinSize != rhs.withinSize {
|
|
return false
|
|
}
|
|
if !lhs.color.isEqual(rhs.color) {
|
|
return false
|
|
}
|
|
if lhs.wallpaperNode !== rhs.wallpaperNode {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
final class View: UIView {
|
|
private let background: WallpaperBlurNode
|
|
|
|
init() {
|
|
self.background = WallpaperBlurNode()
|
|
|
|
super.init(frame: CGRect())
|
|
|
|
self.addSubview(self.background.view)
|
|
}
|
|
|
|
required init?(coder aDecoder: NSCoder) {
|
|
preconditionFailure()
|
|
}
|
|
|
|
func update(component: WallpaperBlurComponent, availableSize: CGSize, transition: Transition) -> CGSize {
|
|
transition.setFrame(view: self.background.view, frame: CGRect(origin: CGPoint(), size: availableSize))
|
|
self.background.update(rect: component.rect, within: component.withinSize, color: component.color, wallpaperNode: component.wallpaperNode, transition: .immediate)
|
|
|
|
return availableSize
|
|
}
|
|
}
|
|
|
|
func makeView() -> View {
|
|
return View()
|
|
}
|
|
|
|
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
|
return view.update(component: self, availableSize: availableSize, transition: transition)
|
|
}
|
|
}
|
|
|
|
final class OverscrollContentsComponent: Component {
|
|
let context: AccountContext
|
|
let backgroundColor: UIColor
|
|
let foregroundColor: UIColor
|
|
let peer: EnginePeer?
|
|
let unreadCount: Int
|
|
let location: TelegramEngine.NextUnreadChannelLocation
|
|
let expandOffset: CGFloat
|
|
let freezeProgress: Bool
|
|
let absoluteRect: CGRect
|
|
let absoluteSize: CGSize
|
|
let wallpaperNode: WallpaperBackgroundNode?
|
|
|
|
init(
|
|
context: AccountContext,
|
|
backgroundColor: UIColor,
|
|
foregroundColor: UIColor,
|
|
peer: EnginePeer?,
|
|
unreadCount: Int,
|
|
location: TelegramEngine.NextUnreadChannelLocation,
|
|
expandOffset: CGFloat,
|
|
freezeProgress: Bool,
|
|
absoluteRect: CGRect,
|
|
absoluteSize: CGSize,
|
|
wallpaperNode: WallpaperBackgroundNode?
|
|
) {
|
|
self.context = context
|
|
self.backgroundColor = backgroundColor
|
|
self.foregroundColor = foregroundColor
|
|
self.peer = peer
|
|
self.unreadCount = unreadCount
|
|
self.location = location
|
|
self.expandOffset = expandOffset
|
|
self.freezeProgress = freezeProgress
|
|
self.absoluteRect = absoluteRect
|
|
self.absoluteSize = absoluteSize
|
|
self.wallpaperNode = wallpaperNode
|
|
}
|
|
|
|
static func ==(lhs: OverscrollContentsComponent, rhs: OverscrollContentsComponent) -> Bool {
|
|
if lhs.context !== rhs.context {
|
|
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.unreadCount != rhs.unreadCount {
|
|
return false
|
|
}
|
|
if lhs.location != rhs.location {
|
|
return false
|
|
}
|
|
if lhs.expandOffset != rhs.expandOffset {
|
|
return false
|
|
}
|
|
if lhs.freezeProgress != rhs.freezeProgress {
|
|
return false
|
|
}
|
|
if lhs.absoluteRect != rhs.absoluteRect {
|
|
return false
|
|
}
|
|
if lhs.absoluteSize != rhs.absoluteSize {
|
|
return false
|
|
}
|
|
if lhs.wallpaperNode !== rhs.wallpaperNode {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
final class View: UIView {
|
|
private let backgroundScalingContainer: ASDisplayNode
|
|
private let backgroundNode: WallpaperBlurNode
|
|
private let backgroundFolderMask: UIImageView
|
|
private let backgroundClippingNode: ASDisplayNode
|
|
private let avatarView = ComponentHostView<Empty>()
|
|
private let checkView = ComponentHostView<Empty>()
|
|
private let arrowNode: ASImageNode
|
|
private let avatarScalingContainer: ASDisplayNode
|
|
private let avatarExtraScalingContainer: ASDisplayNode
|
|
private let avatarOffsetContainer: ASDisplayNode
|
|
private let arrowOffsetContainer: ASDisplayNode
|
|
|
|
private let titleOffsetContainer: ASDisplayNode
|
|
private let titleBackgroundNode: WallpaperBlurNode
|
|
private let titleNode: ImmediateTextNode
|
|
|
|
private var isFullyExpanded: Bool = false
|
|
|
|
private var validForegroundColor: UIColor?
|
|
|
|
init() {
|
|
self.backgroundScalingContainer = ASDisplayNode()
|
|
self.backgroundNode = WallpaperBlurNode()
|
|
self.backgroundNode.clipsToBounds = true
|
|
|
|
self.backgroundFolderMask = UIImageView()
|
|
self.backgroundFolderMask.image = UIImage(bundleImageName: "Chat/OverscrollFolder")?.stretchableImage(withLeftCapWidth: 0, topCapHeight: 40)
|
|
|
|
self.backgroundClippingNode = ASDisplayNode()
|
|
self.backgroundClippingNode.clipsToBounds = true
|
|
self.arrowNode = ASImageNode()
|
|
self.avatarScalingContainer = ASDisplayNode()
|
|
self.avatarExtraScalingContainer = ASDisplayNode()
|
|
self.avatarOffsetContainer = ASDisplayNode()
|
|
self.arrowOffsetContainer = ASDisplayNode()
|
|
|
|
self.titleOffsetContainer = ASDisplayNode()
|
|
self.titleBackgroundNode = WallpaperBlurNode()
|
|
self.titleBackgroundNode.clipsToBounds = true
|
|
self.titleNode = ImmediateTextNode()
|
|
|
|
super.init(frame: CGRect())
|
|
|
|
self.addSubview(self.backgroundScalingContainer.view)
|
|
|
|
self.backgroundClippingNode.addSubnode(self.backgroundNode)
|
|
self.backgroundScalingContainer.addSubnode(self.backgroundClippingNode)
|
|
|
|
self.avatarScalingContainer.view.addSubview(self.avatarView)
|
|
self.avatarScalingContainer.view.addSubview(self.checkView)
|
|
self.avatarExtraScalingContainer.addSubnode(self.avatarScalingContainer)
|
|
self.avatarOffsetContainer.addSubnode(self.avatarExtraScalingContainer)
|
|
self.arrowOffsetContainer.addSubnode(self.arrowNode)
|
|
self.backgroundNode.addSubnode(self.arrowOffsetContainer)
|
|
self.addSubnode(self.avatarOffsetContainer)
|
|
|
|
self.titleOffsetContainer.addSubnode(self.titleBackgroundNode)
|
|
self.titleOffsetContainer.addSubnode(self.titleNode)
|
|
self.addSubnode(self.titleOffsetContainer)
|
|
}
|
|
|
|
required init?(coder aDecoder: NSCoder) {
|
|
preconditionFailure()
|
|
}
|
|
|
|
func update(component: OverscrollContentsComponent, availableSize: CGSize, transition: Transition) -> CGSize {
|
|
if let _ = component.peer {
|
|
self.avatarView.isHidden = false
|
|
self.checkView.isHidden = true
|
|
} else {
|
|
self.avatarView.isHidden = true
|
|
self.checkView.isHidden = false
|
|
}
|
|
|
|
let fullHeight: CGFloat = 94.0
|
|
let backgroundWidth: CGFloat = 56.0
|
|
let minBackgroundHeight: CGFloat = backgroundWidth + 5.0
|
|
let avatarInset: CGFloat = 6.0
|
|
|
|
let apparentExpandOffset: CGFloat
|
|
if component.freezeProgress {
|
|
apparentExpandOffset = fullHeight
|
|
} else {
|
|
apparentExpandOffset = component.expandOffset
|
|
}
|
|
|
|
let isFullyExpanded = apparentExpandOffset >= fullHeight
|
|
|
|
let isFolderMask: Bool
|
|
switch component.location {
|
|
case .archived, .folder:
|
|
isFolderMask = true
|
|
default:
|
|
isFolderMask = false
|
|
}
|
|
|
|
let expandProgress: CGFloat = max(0.1, min(1.0, apparentExpandOffset / fullHeight))
|
|
let trueExpandProgress: CGFloat = max(0.1, min(1.0, component.expandOffset / fullHeight))
|
|
|
|
func interpolate(from: CGFloat, to: CGFloat, value: CGFloat) -> CGFloat {
|
|
return (1.0 - value) * from + value * to
|
|
}
|
|
|
|
let backgroundHeight: CGFloat = interpolate(from: minBackgroundHeight, to: fullHeight, value: trueExpandProgress)
|
|
|
|
let backgroundFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - backgroundWidth) / 2.0), y: fullHeight - backgroundHeight), size: CGSize(width: backgroundWidth, height: backgroundHeight))
|
|
|
|
let alphaProgress: CGFloat = max(0.0, min(1.0, apparentExpandOffset / 10.0))
|
|
|
|
let maxAvatarScale: CGFloat = 1.0
|
|
var avatarExpandProgress: CGFloat = max(0.01, min(maxAvatarScale, apparentExpandOffset / fullHeight))
|
|
avatarExpandProgress *= expandProgress
|
|
|
|
let avatarOffsetProgress = interpolate(from: 0.1, to: 1.0, value: avatarExpandProgress)
|
|
|
|
transition.setAlpha(view: self.backgroundScalingContainer.view, alpha: alphaProgress)
|
|
transition.setFrame(view: self.backgroundScalingContainer.view, frame: CGRect(origin: CGPoint(x: floor(availableSize.width / 2.0), y: fullHeight), size: CGSize(width: 0.0, height: 0.0)))
|
|
transition.setSublayerTransform(view: self.backgroundScalingContainer.view, transform: CATransform3DMakeScale(expandProgress, expandProgress, 1.0))
|
|
|
|
transition.setFrame(view: self.backgroundNode.view, frame: CGRect(origin: CGPoint(x: 0.0, y: fullHeight - backgroundFrame.size.height), size: backgroundFrame.size))
|
|
self.backgroundNode.update(rect: backgroundFrame.offsetBy(dx: component.absoluteRect.minX, dy: component.absoluteRect.minY), within: component.absoluteSize, color: component.backgroundColor, wallpaperNode: component.wallpaperNode, transition: .immediate)
|
|
self.backgroundFolderMask.frame = CGRect(origin: CGPoint(), size: backgroundFrame.size)
|
|
|
|
let avatarFrame = CGRect(origin: CGPoint(x: floor(-backgroundWidth / 2.0), y: floor(-backgroundWidth / 2.0)), size: CGSize(width: backgroundWidth, height: backgroundWidth))
|
|
self.avatarView.frame = avatarFrame
|
|
|
|
transition.setFrame(view: self.avatarOffsetContainer.view, frame: CGRect())
|
|
transition.setFrame(view: self.avatarScalingContainer.view, frame: CGRect())
|
|
transition.setFrame(view: self.avatarExtraScalingContainer.view, frame: CGRect(origin: CGPoint(x: availableSize.width / 2.0, y: fullHeight - backgroundWidth / 2.0), size: CGSize()).offsetBy(dx: 0.0, dy: (1.0 - avatarOffsetProgress) * backgroundWidth * 0.5))
|
|
transition.setSublayerTransform(view: self.avatarScalingContainer.view, transform: CATransform3DMakeScale(avatarExpandProgress, avatarExpandProgress, 1.0))
|
|
|
|
let titleText: String
|
|
if let peer = component.peer {
|
|
titleText = peer.compactDisplayTitle
|
|
} else {
|
|
titleText = component.context.sharedContext.currentPresentationData.with({ $0 }).strings.Chat_NavigationNoChannels
|
|
}
|
|
self.titleNode.attributedText = NSAttributedString(string: titleText, font: Font.semibold(13.0), textColor: component.foregroundColor)
|
|
let titleSize = self.titleNode.updateLayout(CGSize(width: availableSize.width - 32.0, height: 100.0))
|
|
let titleBackgroundSize = CGSize(width: titleSize.width + 18.0, height: titleSize.height + 8.0)
|
|
let titleBackgroundFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleBackgroundSize.width) / 2.0), y: fullHeight - titleBackgroundSize.height - 8.0), size: titleBackgroundSize)
|
|
self.titleBackgroundNode.frame = titleBackgroundFrame
|
|
self.titleBackgroundNode.update(rect: titleBackgroundFrame.offsetBy(dx: component.absoluteRect.minX, dy: component.absoluteRect.minY), within: component.absoluteSize, color: component.backgroundColor, wallpaperNode: component.wallpaperNode, transition: .immediate)
|
|
self.titleBackgroundNode.cornerRadius = min(titleBackgroundFrame.width, titleBackgroundFrame.height) / 2.0
|
|
self.titleNode.frame = titleSize.centered(in: titleBackgroundFrame)
|
|
|
|
let backgroundClippingFrame = CGRect(origin: CGPoint(x: floor(-backgroundWidth / 2.0), y: -fullHeight), size: CGSize(width: backgroundWidth, height: isFullyExpanded ? backgroundWidth : fullHeight))
|
|
self.backgroundClippingNode.cornerRadius = isFolderMask ? 10.0 : backgroundWidth / 2.0
|
|
self.backgroundNode.cornerRadius = isFolderMask ? 0.0 : backgroundWidth / 2.0
|
|
self.backgroundNode.view.mask = isFolderMask ? self.backgroundFolderMask : nil
|
|
|
|
if !(self.validForegroundColor?.isEqual(component.foregroundColor) ?? false) {
|
|
self.validForegroundColor = component.foregroundColor
|
|
self.arrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/OverscrollArrow"), color: component.foregroundColor)
|
|
}
|
|
|
|
if let arrowImage = self.arrowNode.image {
|
|
self.arrowNode.frame = CGRect(origin: CGPoint(x: floor((backgroundWidth - arrowImage.size.width) / 2.0), y: floor((backgroundWidth - arrowImage.size.width) / 2.0)), size: arrowImage.size)
|
|
}
|
|
|
|
let transformTransition: ContainedViewLayoutTransition
|
|
if self.isFullyExpanded != isFullyExpanded {
|
|
self.isFullyExpanded = isFullyExpanded
|
|
transformTransition = .animated(duration: 0.12, curve: .easeInOut)
|
|
|
|
if isFullyExpanded {
|
|
func animateBounce(layer: CALayer) {
|
|
layer.animateScale(from: 1.0, to: 1.1, duration: 0.1, removeOnCompletion: false, completion: { [weak layer] _ in
|
|
layer?.animateScale(from: 1.1, to: 1.0, duration: 0.14, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue)
|
|
})
|
|
}
|
|
|
|
animateBounce(layer: self.backgroundClippingNode.layer)
|
|
animateBounce(layer: self.avatarExtraScalingContainer.layer)
|
|
|
|
func animateOffsetBounce(layer: CALayer) {
|
|
let firstAnimation = layer.makeAnimation(from: 0.0 as NSNumber, to: -5.0 as NSNumber, keyPath: "transform.translation.y", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.1, removeOnCompletion: false, additive: true, completion: { [weak layer] _ in
|
|
guard let layer = layer else {
|
|
return
|
|
}
|
|
let secondAnimation = layer.makeAnimation(from: -5.0 as NSNumber, to: 0.0 as NSNumber, keyPath: "transform.translation.y", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.14, removeOnCompletion: true, additive: true)
|
|
layer.add(secondAnimation, forKey: "bounceY")
|
|
})
|
|
layer.add(firstAnimation, forKey: "bounceY")
|
|
}
|
|
|
|
animateOffsetBounce(layer: self.layer)
|
|
}
|
|
} else {
|
|
transformTransition = .immediate
|
|
}
|
|
|
|
let checkSize: CGFloat = 56.0
|
|
self.checkView.frame = CGRect(origin: CGPoint(x: floor(-checkSize / 2.0), y: floor(-checkSize / 2.0)), size: CGSize(width: checkSize, height: checkSize))
|
|
let _ = self.checkView.update(
|
|
transition: Transition(animation: transformTransition.isAnimated ? .curve(duration: 0.2, curve: .easeInOut) : .none),
|
|
component: AnyComponent(CheckComponent(
|
|
color: component.foregroundColor,
|
|
lineWidth: 3.0,
|
|
value: isFullyExpanded ? 1.0 : 0.0
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: checkSize, height: checkSize)
|
|
)
|
|
|
|
if let peer = component.peer {
|
|
let _ = self.avatarView.update(
|
|
transition: Transition(animation: transformTransition.isAnimated ? .curve(duration: 0.2, curve: .easeInOut) : .none),
|
|
component: AnyComponent(AvatarComponent(
|
|
context: component.context,
|
|
peer: peer,
|
|
badge: isFullyExpanded ? AvatarComponent.Badge(count: component.unreadCount, backgroundColor: component.backgroundColor, foregroundColor: component.foregroundColor) : nil,
|
|
rect: avatarFrame.offsetBy(dx: self.avatarExtraScalingContainer.frame.midX + component.absoluteRect.minX, dy: self.avatarExtraScalingContainer.frame.midY + component.absoluteRect.minY),
|
|
withinSize: component.absoluteSize,
|
|
wallpaperNode: component.wallpaperNode
|
|
)),
|
|
environment: {},
|
|
containerSize: self.avatarView.bounds.size
|
|
)
|
|
}
|
|
|
|
transformTransition.updateAlpha(node: self.backgroundNode, alpha: (isFullyExpanded && component.peer != nil) ? 0.0 : 1.0)
|
|
transformTransition.updateAlpha(node: self.arrowNode, alpha: isFullyExpanded ? 0.0 : 1.0)
|
|
|
|
transformTransition.updateSublayerTransformOffset(layer: self.avatarOffsetContainer.layer, offset: CGPoint(x: 0.0, y: isFullyExpanded ? -(fullHeight - backgroundWidth) : 0.0))
|
|
transformTransition.updateSublayerTransformOffset(layer: self.arrowOffsetContainer.layer, offset: CGPoint(x: 0.0, y: isFullyExpanded ? -(fullHeight - backgroundWidth) : 0.0))
|
|
|
|
transformTransition.updateSublayerTransformOffset(layer: self.titleOffsetContainer.layer, offset: CGPoint(x: 0.0, y: isFullyExpanded ? 0.0 : (titleBackgroundSize.height + 50.0)))
|
|
|
|
transformTransition.updateSublayerTransformScale(node: self.avatarExtraScalingContainer, scale: isFullyExpanded ? 1.0 : ((backgroundWidth - avatarInset * 2.0) / backgroundWidth))
|
|
|
|
transformTransition.updateFrame(node: self.backgroundClippingNode, frame: backgroundClippingFrame)
|
|
|
|
return CGSize(width: availableSize.width, height: fullHeight)
|
|
}
|
|
}
|
|
|
|
func makeView() -> View {
|
|
return View()
|
|
}
|
|
|
|
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
|
return view.update(component: self, availableSize: availableSize, transition: transition)
|
|
}
|
|
}
|
|
|
|
public final class ChatOverscrollControl: CombinedComponent {
|
|
let backgroundColor: UIColor
|
|
let foregroundColor: UIColor
|
|
let peer: EnginePeer?
|
|
let unreadCount: Int
|
|
let location: TelegramEngine.NextUnreadChannelLocation
|
|
let context: AccountContext
|
|
let expandDistance: CGFloat
|
|
let freezeProgress: Bool
|
|
let absoluteRect: CGRect
|
|
let absoluteSize: CGSize
|
|
let wallpaperNode: WallpaperBackgroundNode?
|
|
|
|
public init(
|
|
backgroundColor: UIColor,
|
|
foregroundColor: UIColor,
|
|
peer: EnginePeer?,
|
|
unreadCount: Int,
|
|
location: TelegramEngine.NextUnreadChannelLocation,
|
|
context: AccountContext,
|
|
expandDistance: CGFloat,
|
|
freezeProgress: Bool,
|
|
absoluteRect: CGRect,
|
|
absoluteSize: CGSize,
|
|
wallpaperNode: WallpaperBackgroundNode?
|
|
) {
|
|
self.backgroundColor = backgroundColor
|
|
self.foregroundColor = foregroundColor
|
|
self.peer = peer
|
|
self.unreadCount = unreadCount
|
|
self.location = location
|
|
self.context = context
|
|
self.expandDistance = expandDistance
|
|
self.freezeProgress = freezeProgress
|
|
self.absoluteRect = absoluteRect
|
|
self.absoluteSize = absoluteSize
|
|
self.wallpaperNode = wallpaperNode
|
|
}
|
|
|
|
public static func ==(lhs: ChatOverscrollControl, rhs: ChatOverscrollControl) -> Bool {
|
|
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.unreadCount != rhs.unreadCount {
|
|
return false
|
|
}
|
|
if lhs.location != rhs.location {
|
|
return false
|
|
}
|
|
if lhs.context !== rhs.context {
|
|
return false
|
|
}
|
|
if lhs.expandDistance != rhs.expandDistance {
|
|
return false
|
|
}
|
|
if lhs.freezeProgress != rhs.freezeProgress {
|
|
return false
|
|
}
|
|
if lhs.absoluteRect != rhs.absoluteRect {
|
|
return false
|
|
}
|
|
if lhs.absoluteSize != rhs.absoluteSize {
|
|
return false
|
|
}
|
|
if lhs.wallpaperNode !== rhs.wallpaperNode {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
public static var body: Body {
|
|
let contents = Child(OverscrollContentsComponent.self)
|
|
|
|
return { context in
|
|
let contents = contents.update(
|
|
component: OverscrollContentsComponent(
|
|
context: context.component.context,
|
|
backgroundColor: context.component.backgroundColor,
|
|
foregroundColor: context.component.foregroundColor,
|
|
peer: context.component.peer,
|
|
unreadCount: context.component.unreadCount,
|
|
location: context.component.location,
|
|
expandOffset: context.component.expandDistance,
|
|
freezeProgress: context.component.freezeProgress,
|
|
absoluteRect: context.component.absoluteRect,
|
|
absoluteSize: context.component.absoluteSize,
|
|
wallpaperNode: context.component.wallpaperNode
|
|
),
|
|
availableSize: context.availableSize,
|
|
transition: context.transition
|
|
)
|
|
|
|
let size = CGSize(width: context.availableSize.width, height: contents.size.height)
|
|
|
|
context.add(contents
|
|
.position(CGPoint(x: size.width / 2.0, y: size.height / 2.0))
|
|
)
|
|
|
|
return size
|
|
}
|
|
}
|
|
}
|
|
|
|
public final class ChatInputPanelOverscrollNode: ASDisplayNode {
|
|
public let text: (String, [(Int, NSRange)])
|
|
public let priority: Int
|
|
private let titleNode: ImmediateTextNode
|
|
|
|
public init(text: (String, [(Int, NSRange)]), color: UIColor, priority: Int) {
|
|
self.text = text
|
|
self.priority = priority
|
|
self.titleNode = ImmediateTextNode()
|
|
|
|
super.init()
|
|
|
|
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: color)
|
|
let bold = MarkdownAttributeSet(font: Font.bold(14.0), textColor: color)
|
|
|
|
self.titleNode.attributedText = addAttributesToStringWithRanges(text, body: body, argumentAttributes: [0: bold])
|
|
|
|
self.addSubnode(self.titleNode)
|
|
}
|
|
|
|
public func update(size: CGSize) {
|
|
let titleSize = self.titleNode.updateLayout(size)
|
|
self.titleNode.frame = titleSize.centered(in: CGRect(origin: CGPoint(), size: size))
|
|
}
|
|
}
|