mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-23 22:55:00 +00:00
Combo update
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import UIKit
|
||||
import ComponentFlow
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import TelegramCore
|
||||
import Postbox
|
||||
import AccountContext
|
||||
@@ -196,6 +197,9 @@ final class CheckComponent: Component {
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private var currentValue: CGFloat?
|
||||
private var animator: DisplayLinkAnimator?
|
||||
|
||||
init() {
|
||||
super.init(frame: CGRect())
|
||||
}
|
||||
@@ -204,10 +208,8 @@ final class CheckComponent: Component {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func update(component: CheckComponent, availableSize: CGSize, transition: Transition) -> CGSize {
|
||||
private func updateContent(size: CGSize, color: UIColor, lineWidth: CGFloat, value: CGFloat) {
|
||||
func draw(context: CGContext) {
|
||||
let size = availableSize
|
||||
|
||||
let diameter = size.width
|
||||
|
||||
let factor = diameter / 50.0
|
||||
@@ -215,19 +217,17 @@ final class CheckComponent: Component {
|
||||
context.saveGState()
|
||||
|
||||
context.setBlendMode(.normal)
|
||||
context.setFillColor(component.color.cgColor)
|
||||
context.setStrokeColor(component.color.cgColor)
|
||||
context.setFillColor(color.cgColor)
|
||||
context.setStrokeColor(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 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)
|
||||
@@ -257,7 +257,7 @@ final class CheckComponent: Component {
|
||||
}
|
||||
|
||||
if #available(iOS 10.0, *) {
|
||||
let renderer = UIGraphicsImageRenderer(bounds: CGRect(origin: CGPoint(), size: availableSize))
|
||||
let renderer = UIGraphicsImageRenderer(bounds: CGRect(origin: CGPoint(), size: size))
|
||||
let image = renderer.image { context in
|
||||
UIGraphicsPushContext(context.cgContext)
|
||||
draw(context: context.cgContext)
|
||||
@@ -265,11 +265,37 @@ final class CheckComponent: Component {
|
||||
}
|
||||
self.layer.contents = image.cgImage
|
||||
} else {
|
||||
UIGraphicsBeginImageContextWithOptions(availableSize, false, 0.0)
|
||||
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
|
||||
}
|
||||
@@ -284,13 +310,101 @@ final class CheckComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
final class BadgeComponent: CombinedComponent {
|
||||
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: 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
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
static var body: Body {
|
||||
let background = Child(BlurredRoundedRectangle.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: BlurredRoundedRectangle(color: context.component.backgroundColor),
|
||||
availableSize: backgroundSize,
|
||||
transition: .immediate
|
||||
)
|
||||
|
||||
context.add(background
|
||||
.position(CGPoint(x: backgroundSize.width / 2.0, y: backgroundSize.height / 2.0))
|
||||
)
|
||||
|
||||
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?
|
||||
|
||||
init(context: AccountContext, peer: EnginePeer) {
|
||||
init(context: AccountContext, peer: EnginePeer, badge: Badge?) {
|
||||
self.context = context
|
||||
self.peer = peer
|
||||
self.badge = badge
|
||||
}
|
||||
|
||||
static func ==(lhs: AvatarComponent, rhs: AvatarComponent) -> Bool {
|
||||
@@ -300,14 +414,20 @@ final class AvatarComponent: Component {
|
||||
if lhs.peer != rhs.peer {
|
||||
return false
|
||||
}
|
||||
if lhs.badge != rhs.badge {
|
||||
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())
|
||||
|
||||
@@ -322,6 +442,56 @@ final class AvatarComponent: Component {
|
||||
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)
|
||||
|
||||
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
|
||||
)),
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -335,32 +505,32 @@ final class AvatarComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
final class ChatOverscrollControl: CombinedComponent {
|
||||
let text: String
|
||||
final class OverscrollContentsComponent: Component {
|
||||
let context: AccountContext
|
||||
let backgroundColor: UIColor
|
||||
let foregroundColor: UIColor
|
||||
let peer: EnginePeer?
|
||||
let context: AccountContext
|
||||
let expandDistance: CGFloat
|
||||
let unreadCount: Int
|
||||
let expandOffset: CGFloat
|
||||
|
||||
init(
|
||||
text: String,
|
||||
context: AccountContext,
|
||||
backgroundColor: UIColor,
|
||||
foregroundColor: UIColor,
|
||||
peer: EnginePeer?,
|
||||
context: AccountContext,
|
||||
expandDistance: CGFloat
|
||||
unreadCount: Int,
|
||||
expandOffset: CGFloat
|
||||
) {
|
||||
self.text = text
|
||||
self.context = context
|
||||
self.backgroundColor = backgroundColor
|
||||
self.foregroundColor = foregroundColor
|
||||
self.peer = peer
|
||||
self.context = context
|
||||
self.expandDistance = expandDistance
|
||||
self.unreadCount = unreadCount
|
||||
self.expandOffset = expandOffset
|
||||
}
|
||||
|
||||
static func ==(lhs: ChatOverscrollControl, rhs: ChatOverscrollControl) -> Bool {
|
||||
if lhs.text != rhs.text {
|
||||
static func ==(lhs: OverscrollContentsComponent, rhs: OverscrollContentsComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if !lhs.backgroundColor.isEqual(rhs.backgroundColor) {
|
||||
@@ -372,6 +542,265 @@ final class ChatOverscrollControl: CombinedComponent {
|
||||
if lhs.peer != rhs.peer {
|
||||
return false
|
||||
}
|
||||
if lhs.unreadCount != rhs.unreadCount {
|
||||
return false
|
||||
}
|
||||
if lhs.expandOffset != rhs.expandOffset {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let backgroundScalingContainer: ASDisplayNode
|
||||
private let backgroundNode: NavigationBackgroundNode
|
||||
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: NavigationBackgroundNode
|
||||
private let titleNode: ImmediateTextNode
|
||||
|
||||
private var isFullyExpanded: Bool = false
|
||||
|
||||
private var validForegroundColor: UIColor?
|
||||
|
||||
init() {
|
||||
self.backgroundScalingContainer = ASDisplayNode()
|
||||
self.backgroundNode = NavigationBackgroundNode(color: .clear)
|
||||
self.backgroundNode.clipsToBounds = true
|
||||
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 = NavigationBackgroundNode(color: .clear)
|
||||
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 = 90.0
|
||||
let backgroundWidth: CGFloat = 50.0
|
||||
let minBackgroundHeight: CGFloat = backgroundWidth + 34.0
|
||||
let avatarInset: CGFloat = 6.0
|
||||
|
||||
let isFullyExpanded = component.expandOffset >= fullHeight
|
||||
|
||||
let backgroundHeight: CGFloat = max(minBackgroundHeight, min(fullHeight, component.expandOffset))
|
||||
|
||||
let backgroundFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - backgroundWidth) / 2.0), y: fullHeight - backgroundHeight), size: CGSize(width: backgroundWidth, height: backgroundHeight))
|
||||
|
||||
let expandProgress: CGFloat = max(0.1, min(1.0, component.expandOffset / minBackgroundHeight))
|
||||
let alphaProgress: CGFloat = max(0.0, min(1.0, component.expandOffset / 10.0))
|
||||
|
||||
let maxAvatarScale: CGFloat = 1.0
|
||||
let avatarExpandProgress: CGFloat = max(0.01, min(maxAvatarScale, component.expandOffset / fullHeight))
|
||||
|
||||
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.updateColor(color: component.backgroundColor, transition: .immediate)
|
||||
self.backgroundNode.update(size: backgroundFrame.size, cornerRadius: backgroundWidth / 2.0, transition: .immediate)
|
||||
|
||||
self.avatarView.frame = CGRect(origin: CGPoint(x: floor(-backgroundWidth / 2.0), y: floor(-backgroundWidth / 2.0)), size: CGSize(width: backgroundWidth, height: backgroundWidth))
|
||||
|
||||
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 - avatarExpandProgress) * 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 {
|
||||
//TODO:localize
|
||||
titleText = "You have no unread channels"
|
||||
}
|
||||
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.updateColor(color: component.backgroundColor, transition: .immediate)
|
||||
self.titleBackgroundNode.update(size: titleBackgroundFrame.size, cornerRadius: titleBackgroundFrame.size.height / 2.0, transition: .immediate)
|
||||
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 = backgroundWidth / 2.0
|
||||
self.backgroundNode.cornerRadius = backgroundWidth / 2.0
|
||||
|
||||
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 = 50.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
|
||||
)),
|
||||
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, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
final class ChatOverscrollControl: CombinedComponent {
|
||||
let backgroundColor: UIColor
|
||||
let foregroundColor: UIColor
|
||||
let peer: EnginePeer?
|
||||
let unreadCount: Int
|
||||
let context: AccountContext
|
||||
let expandDistance: CGFloat
|
||||
|
||||
init(
|
||||
backgroundColor: UIColor,
|
||||
foregroundColor: UIColor,
|
||||
peer: EnginePeer?,
|
||||
unreadCount: Int,
|
||||
context: AccountContext,
|
||||
expandDistance: CGFloat
|
||||
) {
|
||||
self.backgroundColor = backgroundColor
|
||||
self.foregroundColor = foregroundColor
|
||||
self.peer = peer
|
||||
self.unreadCount = unreadCount
|
||||
self.context = context
|
||||
self.expandDistance = expandDistance
|
||||
}
|
||||
|
||||
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.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
@@ -382,156 +811,51 @@ final class ChatOverscrollControl: CombinedComponent {
|
||||
}
|
||||
|
||||
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)
|
||||
let contents = Child(OverscrollContentsComponent.self)
|
||||
|
||||
return { context in
|
||||
let text = text.update(
|
||||
component: Text(
|
||||
text: context.component.text,
|
||||
font: Font.regular(12.0),
|
||||
color: context.component.foregroundColor
|
||||
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,
|
||||
expandOffset: context.component.expandDistance
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width, height: 100.0),
|
||||
availableSize: context.availableSize,
|
||||
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 size = CGSize(width: context.availableSize.width, height: contents.size.height)
|
||||
|
||||
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 progressSize = avatarBackground.size.width - avatarProgressPadding * 2.0
|
||||
|
||||
let halfDistance = progressSize
|
||||
let quarterDistance = halfDistance / 2.0
|
||||
|
||||
let clippedDistance = max(0.0, min(halfDistance * 2.0, context.component.expandDistance))
|
||||
|
||||
var mappedProgress: CGFloat
|
||||
if clippedDistance <= quarterDistance {
|
||||
mappedProgress = acos(1.0 - clippedDistance / quarterDistance) / (CGFloat.pi * 2.0)
|
||||
} else if clippedDistance <= halfDistance {
|
||||
let sectionDistance = halfDistance - clippedDistance
|
||||
mappedProgress = 0.25 + asin(1.0 - sectionDistance / quarterDistance) / (CGFloat.pi * 2.0)
|
||||
} else {
|
||||
let restDistance = clippedDistance - halfDistance
|
||||
mappedProgress = min(1.0, 0.5 + restDistance / 60.0)
|
||||
}
|
||||
mappedProgress = max(0.01, mappedProgress)
|
||||
|
||||
let avatarExpandProgress = avatarExpandProgress.update(
|
||||
component: RadialProgressComponent(
|
||||
color: context.component.foregroundColor,
|
||||
lineWidth: 2.5,
|
||||
value: context.component.peer == nil ? 0.0 : mappedProgress
|
||||
),
|
||||
availableSize: CGSize(width: progressSize, height: progressSize),
|
||||
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
|
||||
))
|
||||
context.add(contents
|
||||
.position(CGPoint(x: size.width / 2.0, y: size.height / 2.0))
|
||||
)
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class ChatInputPanelOverscrollNode: ASDisplayNode {
|
||||
let text: String
|
||||
let priority: Int
|
||||
private let titleNode: ImmediateTextNode
|
||||
|
||||
init(text: String, color: UIColor, priority: Int) {
|
||||
self.text = text
|
||||
self.priority = priority
|
||||
self.titleNode = ImmediateTextNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.titleNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: color)
|
||||
self.addSubnode(self.titleNode)
|
||||
}
|
||||
|
||||
func update(size: CGSize) {
|
||||
let titleSize = self.titleNode.updateLayout(size)
|
||||
self.titleNode.frame = titleSize.centered(in: CGRect(origin: CGPoint(), size: size))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user