Folder improvements

This commit is contained in:
Ali 2023-04-04 15:31:56 +04:00
parent 0d5468a567
commit 42f43bf767
10 changed files with 664 additions and 195 deletions

View File

@ -127,6 +127,28 @@ public class CheckNode: ASDisplayNode {
animation.timingFunction = CAMediaTimingFunction(name: selected ? CAMediaTimingFunctionName.easeOut : CAMediaTimingFunctionName.easeIn)
animation.duration = selected ? 0.21 : 0.15
self.pop_add(animation, forKey: "progress")
if selected {
self.layer.animateScale(from: 1.0, to: 0.9, duration: 0.08, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in
guard let self else {
return
}
self.layer.animateScale(from: 0.9, to: 1.1, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in
guard let self else {
return
}
self.layer.animateScale(from: 1.1, to: 1.0, duration: 0.1, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue)
})
})
} else {
self.layer.animateScale(from: 1.0, to: 0.9, duration: 0.08, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in
guard let self else {
return
}
self.layer.animateScale(from: 0.9, to: 1.0, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue)
})
}
} else {
self.pop_removeAllAnimations()
self.animatingOut = false
@ -152,108 +174,11 @@ public class CheckNode: ASDisplayNode {
}
if let parameters = parameters as? CheckNodeParameters {
let center = CGPoint(x: bounds.width / 2.0, y: bounds.width / 2.0)
var borderWidth: CGFloat = 1.0 + UIScreenPixel
if parameters.theme.hasInset {
borderWidth = 1.5
}
if let customBorderWidth = parameters.theme.borderWidth {
borderWidth = customBorderWidth
}
let checkWidth: CGFloat = 1.5
let inset: CGFloat = parameters.theme.hasInset ? 2.0 - UIScreenPixel : 0.0
let checkProgress = parameters.animatingOut ? 1.0 : parameters.animationProgress
let fillProgress = parameters.animatingOut ? 1.0 : min(1.0, parameters.animationProgress * 1.35)
context.setStrokeColor(parameters.theme.borderColor.cgColor)
if parameters.theme.isDottedBorder {
context.setLineDash(phase: 0.0, lengths: [4.0, 4.0])
}
context.setLineWidth(borderWidth)
let maybeScaleOut = {
let animate: Bool
if case .counter = parameters.content {
animate = true
} else if parameters.animatingOut {
animate = true
} else {
animate = false
}
if animate {
context.translateBy(x: bounds.width / 2.0, y: bounds.height / 2.0)
context.scaleBy(x: parameters.animationProgress, y: parameters.animationProgress)
context.translateBy(x: -bounds.width / 2.0, y: -bounds.height / 2.0)
context.setAlpha(parameters.animationProgress)
}
}
let borderInset = borderWidth / 2.0 + inset
let borderProgress: CGFloat = parameters.theme.filledBorder ? fillProgress : 1.0
let borderFrame = bounds.insetBy(dx: borderInset, dy: borderInset)
if parameters.theme.filledBorder {
maybeScaleOut()
}
context.saveGState()
if parameters.theme.hasShadow {
context.setShadow(offset: CGSize(), blur: 2.5, color: UIColor(rgb: 0x000000, alpha: 0.22).cgColor)
}
context.strokeEllipse(in: borderFrame.insetBy(dx: borderFrame.width * (1.0 - borderProgress), dy: borderFrame.height * (1.0 - borderProgress)))
context.restoreGState()
if !parameters.theme.filledBorder {
maybeScaleOut()
}
context.setFillColor(parameters.theme.backgroundColor.cgColor)
let fillInset = parameters.theme.overlayBorder ? borderWidth + inset : inset
let fillFrame = bounds.insetBy(dx: fillInset, dy: fillInset)
context.fillEllipse(in: fillFrame.insetBy(dx: fillFrame.width * (1.0 - fillProgress), dy: fillFrame.height * (1.0 - fillProgress)))
switch parameters.content {
case .check:
let scale = (bounds.width - inset) / 18.0
let firstSegment: CGFloat = max(0.0, min(1.0, checkProgress * 3.0))
let s = CGPoint(x: center.x - (4.0 - 0.3333) * scale, y: center.y + 0.5 * scale)
let p1 = CGPoint(x: 2.5 * scale, y: 3.0 * scale)
let p2 = CGPoint(x: 4.6667 * scale, y: -6.0 * scale)
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 = (checkProgress - 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.setStrokeColor(parameters.theme.strokeColor.cgColor)
if parameters.theme.strokeColor == .clear {
context.setBlendMode(.clear)
}
context.setLineWidth(checkWidth)
context.setLineCap(.round)
context.setLineJoin(.round)
context.setMiterLimit(10.0)
context.strokePath()
case let .counter(number):
let string = NSAttributedString(string: "\(number)", font: Font.with(size: 16.0, design: .round, weight: .semibold), textColor: parameters.theme.strokeColor)
let stringSize = string.boundingRect(with: bounds.size, options: .usesLineFragmentOrigin, context: nil).size
string.draw(at: CGPoint(x: floorToScreenPixels((bounds.width - stringSize.width) / 2.0), y: floorToScreenPixels((bounds.height - stringSize.height) / 2.0)))
}
CheckLayer.drawContents(
context: context,
size: bounds.size,
parameters: parameters
)
}
}
@ -388,6 +313,28 @@ public class CheckLayer: CALayer {
animation.timingFunction = CAMediaTimingFunction(name: selected ? CAMediaTimingFunctionName.easeOut : CAMediaTimingFunctionName.easeIn)
animation.duration = selected ? 0.21 : 0.15
self.pop_add(animation, forKey: "progress")
if selected {
self.animateScale(from: 1.0, to: 0.9, duration: 0.08, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in
guard let self else {
return
}
self.animateScale(from: 0.9, to: 1.1, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in
guard let self else {
return
}
self.animateScale(from: 1.1, to: 1.0, duration: 0.1, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue)
})
})
} else {
self.animateScale(from: 1.0, to: 0.9, duration: 0.08, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in
guard let self else {
return
}
self.animateScale(from: 0.9, to: 1.0, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue)
})
}
} else {
self.pop_removeAllAnimations()
self.animatingOut = false
@ -404,100 +351,125 @@ public class CheckLayer: CALayer {
return
}
self.contents = generateImage(self.bounds.size, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
CheckLayer.drawContents(
context: context,
size: size,
parameters: CheckNodeParameters(theme: self.theme, content: self.content, animationProgress: self.animationProgress, selected: self.selected, animatingOut: self.animatingOut)
)
})?.cgImage
}
fileprivate static func drawContents(context: CGContext, size: CGSize, parameters: CheckNodeParameters) {
context.clear(CGRect(origin: CGPoint(), size: size))
let parameters = CheckNodeParameters(theme: self.theme, content: self.content, animationProgress: self.animationProgress, selected: self.selected, animatingOut: self.animatingOut)
let center = CGPoint(x: size.width / 2.0, y: size.width / 2.0)
let center = CGPoint(x: bounds.width / 2.0, y: bounds.width / 2.0)
var borderWidth: CGFloat = 1.0 + UIScreenPixel
if parameters.theme.hasInset {
borderWidth = 1.5
}
if let customBorderWidth = parameters.theme.borderWidth {
borderWidth = customBorderWidth
}
var borderWidth: CGFloat = 1.0 + UIScreenPixel
if parameters.theme.hasInset {
borderWidth = 1.5
}
if let customBorderWidth = parameters.theme.borderWidth {
borderWidth = customBorderWidth
let checkWidth: CGFloat = 1.5
let inset: CGFloat = parameters.theme.hasInset ? 2.0 - UIScreenPixel : 0.0
let checkProgress: CGFloat
context.setStrokeColor(parameters.theme.borderColor.cgColor)
context.setLineWidth(borderWidth)
let maybeScaleOut = {
if parameters.animatingOut {
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: parameters.animationProgress, y: parameters.animationProgress)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
context.setAlpha(parameters.animationProgress)
}
}
let checkWidth: CGFloat = 1.5
let inset: CGFloat = parameters.theme.hasInset ? 2.0 - UIScreenPixel : 0.0
let checkProgress = parameters.animatingOut ? 1.0 : parameters.animationProgress
if !parameters.theme.filledBorder {
checkProgress = parameters.animationProgress
let fillProgress: CGFloat = parameters.animationProgress
context.setFillColor(parameters.theme.backgroundColor.mixedWith(parameters.theme.borderColor, alpha: 1.0 - fillProgress).cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
let innerDiameter: CGFloat = (fillProgress * 0.0) + (1.0 - fillProgress) * (size.width - borderWidth * 2.0)
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: (size.width - innerDiameter) * 0.5, y: (size.height - innerDiameter) * 0.5), size: CGSize(width: innerDiameter, height: innerDiameter)))
context.setBlendMode(.normal)
} else {
checkProgress = parameters.animatingOut ? 1.0 : parameters.animationProgress
let fillProgress = parameters.animatingOut ? 1.0 : min(1.0, parameters.animationProgress * 1.35)
context.setStrokeColor(parameters.theme.borderColor.cgColor)
context.setLineWidth(borderWidth)
let maybeScaleOut = {
if parameters.animatingOut {
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: parameters.animationProgress, y: parameters.animationProgress)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
context.setAlpha(parameters.animationProgress)
}
}
let borderInset = borderWidth / 2.0 + inset
let borderProgress: CGFloat = parameters.theme.filledBorder ? fillProgress : 1.0
let borderFrame = bounds.insetBy(dx: borderInset, dy: borderInset)
let borderFrame = CGRect(origin: CGPoint(), size: size).insetBy(dx: borderInset, dy: borderInset)
if parameters.theme.filledBorder {
maybeScaleOut()
}
context.saveGState()
if parameters.theme.hasShadow {
context.setShadow(offset: CGSize(), blur: 2.5, color: UIColor(rgb: 0x000000, alpha: 0.22).cgColor)
}
context.strokeEllipse(in: borderFrame.insetBy(dx: borderFrame.width * (1.0 - borderProgress), dy: borderFrame.height * (1.0 - borderProgress)))
context.restoreGState()
if !parameters.theme.filledBorder {
maybeScaleOut()
}
context.setFillColor(parameters.theme.backgroundColor.cgColor)
let fillInset = parameters.theme.overlayBorder ? borderWidth + inset : inset
let fillFrame = bounds.insetBy(dx: fillInset, dy: fillInset)
let fillFrame = CGRect(origin: CGPoint(), size: size).insetBy(dx: fillInset, dy: fillInset)
context.fillEllipse(in: fillFrame.insetBy(dx: fillFrame.width * (1.0 - fillProgress), dy: fillFrame.height * (1.0 - fillProgress)))
}
switch parameters.content {
case .check:
let scale = (bounds.width - inset) / 18.0
let firstSegment: CGFloat = max(0.0, min(1.0, checkProgress * 3.0))
let s = CGPoint(x: center.x - (4.0 - 0.3333) * scale, y: center.y + 0.5 * scale)
let p1 = CGPoint(x: 2.5 * scale, y: 3.0 * scale)
let p2 = CGPoint(x: 4.6667 * scale, y: -6.0 * scale)
switch parameters.content {
case .check:
let scale = (size.width - inset) / 18.0
let firstSegment: CGFloat = max(0.0, min(1.0, checkProgress * 3.0))
let s = CGPoint(x: center.x - (4.0 - 0.3333) * scale, y: center.y + 0.5 * scale)
let p1 = CGPoint(x: 2.5 * scale, y: 3.0 * scale)
let p2 = CGPoint(x: 4.6667 * scale, y: -6.0 * scale)
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 = (checkProgress - 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)
}
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 = (checkProgress - 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.setStrokeColor(parameters.theme.strokeColor.cgColor)
if parameters.theme.strokeColor == .clear {
context.setBlendMode(.clear)
}
context.setLineWidth(checkWidth)
context.setLineCap(.round)
context.setLineJoin(.round)
context.setMiterLimit(10.0)
context.setStrokeColor(parameters.theme.strokeColor.cgColor)
if parameters.theme.strokeColor == .clear {
context.setBlendMode(.clear)
}
context.setLineWidth(checkWidth)
context.setLineCap(.round)
context.setLineJoin(.round)
context.setMiterLimit(10.0)
context.strokePath()
case let .counter(number):
let text = NSAttributedString(string: "\(number)", font: Font.with(size: 16.0, design: .round, weight: .regular, traits: []), textColor: parameters.theme.strokeColor)
text.draw(at: CGPoint())
}
})?.cgImage
context.strokePath()
case let .counter(number):
let text = NSAttributedString(string: "\(number)", font: Font.with(size: 16.0, design: .round, weight: .regular, traits: []), textColor: parameters.theme.strokeColor)
text.draw(at: CGPoint())
}
}
}

View File

@ -365,6 +365,10 @@ public extension CALayer {
func animateScale(from: CGFloat, to: CGFloat, duration: Double, delay: Double = 0.0, timingFunction: String = CAMediaTimingFunctionName.easeInEaseOut.rawValue, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
self.animate(from: NSNumber(value: Float(from)), to: NSNumber(value: Float(to)), keyPath: "transform.scale", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion)
}
func animateSublayerScale(from: CGFloat, to: CGFloat, duration: Double, delay: Double = 0.0, timingFunction: String = CAMediaTimingFunctionName.easeInEaseOut.rawValue, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
self.animate(from: NSNumber(value: Float(from)), to: NSNumber(value: Float(to)), keyPath: "sublayerTransform.scale", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion)
}
func animateScaleX(from: CGFloat, to: CGFloat, duration: Double, delay: Double = 0.0, timingFunction: String = CAMediaTimingFunctionName.easeInEaseOut.rawValue, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) {
self.animate(from: NSNumber(value: Float(from)), to: NSNumber(value: Float(to)), keyPath: "transform.scale.x", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, completion: completion)

View File

@ -770,8 +770,12 @@ private final class LimitSheetContent: CombinedComponent {
string = component.count >= premiumLimit ? strings.Premium_MaxFoldersCountFinalText("\(premiumLimit)").string : strings.Premium_MaxFoldersCountText("\(limit)", "\(premiumLimit)").string
defaultValue = component.count > limit ? "\(limit)" : ""
premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)"
badgePosition = CGFloat(component.count) / CGFloat(premiumLimit)
badgeGraphPosition = badgePosition
if component.count >= premiumLimit {
badgeGraphPosition = max(0.15, CGFloat(limit) / CGFloat(premiumLimit))
} else {
badgeGraphPosition = max(0.15, CGFloat(component.count) / CGFloat(premiumLimit))
}
badgePosition = max(0.15, CGFloat(component.count) / CGFloat(premiumLimit))
if !state.isPremium && badgePosition > 0.5 {
string = strings.Premium_MaxFoldersCountText("\(limit)", "\(premiumLimit)").string
@ -811,11 +815,11 @@ private final class LimitSheetContent: CombinedComponent {
defaultValue = count > limit ? "\(limit)" : ""
premiumValue = count >= premiumLimit ? "" : "\(premiumLimit)"
if count >= premiumLimit {
badgeGraphPosition = max(0.1, CGFloat(limit) / CGFloat(premiumLimit))
badgeGraphPosition = max(0.15, CGFloat(limit) / CGFloat(premiumLimit))
} else {
badgeGraphPosition = max(0.1, CGFloat(count) / CGFloat(premiumLimit))
badgeGraphPosition = max(0.15, CGFloat(count) / CGFloat(premiumLimit))
}
badgePosition = max(0.1, CGFloat(count) / CGFloat(premiumLimit))
badgePosition = max(0.15, CGFloat(count) / CGFloat(premiumLimit))
if isPremiumDisabled {
badgeText = "\(limit)"
@ -829,8 +833,12 @@ private final class LimitSheetContent: CombinedComponent {
string = component.count >= premiumLimit ? strings.Premium_MaxSharedFolderMembershipFinalText("\(premiumLimit)").string : strings.Premium_MaxSharedFolderMembershipText("\(limit)", "\(premiumLimit)").string
defaultValue = component.count > limit ? "\(limit)" : ""
premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)"
badgePosition = CGFloat(component.count) / CGFloat(premiumLimit)
badgeGraphPosition = badgePosition
if component.count >= premiumLimit {
badgeGraphPosition = max(0.15, CGFloat(limit) / CGFloat(premiumLimit))
} else {
badgeGraphPosition = max(0.15, CGFloat(component.count) / CGFloat(premiumLimit))
}
badgePosition = max(0.15, CGFloat(component.count) / CGFloat(premiumLimit))
if isPremiumDisabled {
badgeText = "\(limit)"

View File

@ -641,10 +641,13 @@ private final class FetchImpl {
isComplete = true
let resultingSize = fetchRange.lowerBound + actualLength
if let currentKnownSize = self.knownSize {
Logger.shared.log("FetchV2", "\(self.loggingIdentifier): setting known size to min(\(currentKnownSize), \(resultingSize)) = \(min(currentKnownSize, resultingSize))")
self.knownSize = min(currentKnownSize, resultingSize)
} else {
Logger.shared.log("FetchV2", "\(self.loggingIdentifier): setting known size to \(resultingSize)")
self.knownSize = resultingSize
}
Logger.shared.log("FetchV2", "\(self.loggingIdentifier): reporting resource size \(fetchRange.lowerBound + actualLength)")
self.onNext(.resourceSizeUpdated(fetchRange.lowerBound + actualLength))
}
@ -662,15 +665,21 @@ private final class FetchImpl {
} else {
actualData = Data()
}
Logger.shared.log("FetchV2", "\(self.loggingIdentifier): extracting aligned part \(partRange) (\(fetchRange)): \(actualData.count)")
}
if !actualData.isEmpty {
Logger.shared.log("FetchV2", "\(self.loggingIdentifier): emitting data part \(partRange) (aligned as \(fetchRange)): \(actualData.count), isComplete: \(isComplete)")
self.onNext(.dataPart(
resourceOffset: partRange.lowerBound,
data: actualData,
range: 0 ..< Int64(actualData.count),
complete: isComplete
))
} else {
Logger.shared.log("FetchV2", "\(self.loggingIdentifier): not emitting data part \(partRange) (aligned as \(fetchRange))")
}
case let .cdnRedirect(cdnData):
self.state = .fetching(FetchImpl.FetchingState(

View File

@ -0,0 +1,19 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AnimatedCounterComponent",
module_name = "AnimatedCounterComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display",
"//submodules/ComponentFlow",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,279 @@
import Foundation
import UIKit
import Display
import ComponentFlow
final class AnimatedCounterItemComponent: Component {
public let font: UIFont
public let color: UIColor
public let text: String
public let numericValue: Int
public let alignment: CGFloat
public init(
font: UIFont,
color: UIColor,
text: String,
numericValue: Int,
alignment: CGFloat
) {
self.font = font
self.color = color
self.text = text
self.numericValue = numericValue
self.alignment = alignment
}
public static func ==(lhs: AnimatedCounterItemComponent, rhs: AnimatedCounterItemComponent) -> Bool {
if lhs.font != rhs.font {
return false
}
if lhs.color != rhs.color {
return false
}
if lhs.text != rhs.text {
return false
}
if lhs.numericValue != rhs.numericValue {
return false
}
if lhs.alignment != rhs.alignment {
return false
}
return true
}
public final class View: UIView {
private let contentView: UIImageView
private var component: AnimatedCounterItemComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.contentView = UIImageView()
super.init(frame: frame)
self.addSubview(self.contentView)
}
required init(coder: NSCoder) {
preconditionFailure()
}
func update(component: AnimatedCounterItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
let previousNumericValue = self.component?.numericValue
self.component = component
self.state = state
let text = NSAttributedString(string: component.text, font: component.font, textColor: component.color)
let textBounds = text.boundingRect(with: availableSize, options: [.usesLineFragmentOrigin], context: nil)
let size = CGSize(width: ceil(textBounds.width), height: ceil(textBounds.height))
let previousContentImage = self.contentView.image
let previousContentFrame = self.contentView.frame
self.contentView.image = generateImage(size, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
text.draw(at: textBounds.origin)
UIGraphicsPopContext()
})
self.contentView.frame = CGRect(origin: CGPoint(), size: size)
if !transition.animation.isImmediate, let previousContentImage, !previousContentFrame.isEmpty, let previousNumericValue, previousNumericValue != component.numericValue {
let previousContentView = UIImageView()
previousContentView.image = previousContentImage
previousContentView.frame = CGRect(origin: CGPoint(x: size.width * component.alignment - previousContentFrame.width * component.alignment, y: previousContentFrame.minY), size: previousContentFrame.size)
self.addSubview(previousContentView)
let offsetY: CGFloat = size.height * 0.6 * (previousNumericValue < component.numericValue ? -1.0 : 1.0)
let subTransition = Transition(animation: .curve(duration: 0.16, curve: .easeInOut))
subTransition.animatePosition(view: self.contentView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true)
subTransition.animateAlpha(view: self.contentView, from: 0.0, to: 1.0)
subTransition.setPosition(view: previousContentView, position: CGPoint(x: previousContentView.layer.position.x, y: previousContentView.layer.position.y - offsetY))
subTransition.setAlpha(view: previousContentView, alpha: 0.0, completion: { [weak previousContentView] _ in
previousContentView?.removeFromSuperview()
})
}
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public final class AnimatedCounterComponent: Component {
public enum Alignment {
case left
case right
}
public struct Item: Equatable {
public var id: AnyHashable
public var text: String
public var numericValue: Int
public init(id: AnyHashable, text: String, numericValue: Int) {
self.id = id
self.text = text
self.numericValue = numericValue
}
}
public let font: UIFont
public let color: UIColor
public let alignment: Alignment
public let items: [Item]
public init(
font: UIFont,
color: UIColor,
alignment: Alignment,
items: [Item]
) {
self.font = font
self.color = color
self.alignment = alignment
self.items = items
}
public static func ==(lhs: AnimatedCounterComponent, rhs: AnimatedCounterComponent) -> Bool {
if lhs.font != rhs.font {
return false
}
if lhs.color != rhs.color {
return false
}
if lhs.alignment != rhs.alignment {
return false
}
if lhs.items != rhs.items {
return false
}
return true
}
private final class ItemView {
let view = ComponentView<Empty>()
}
public final class View: UIView {
private var itemViews: [AnyHashable: ItemView] = [:]
private var component: AnimatedCounterComponent?
private weak var state: EmptyComponentState?
private var measuredSpaceWidth: CGFloat?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init(coder: NSCoder) {
preconditionFailure()
}
func update(component: AnimatedCounterComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
let spaceWidth: CGFloat
if let measuredSpaceWidth = self.measuredSpaceWidth, let previousComponent = self.component, previousComponent.font.pointSize == component.font.pointSize {
spaceWidth = measuredSpaceWidth
} else {
spaceWidth = ceil(NSAttributedString(string: " ", font: component.font, textColor: .black).boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil).width)
self.measuredSpaceWidth = spaceWidth
}
self.component = component
self.state = state
var size = CGSize()
var validIds: [AnyHashable] = []
for item in component.items {
if size.width != 0.0 {
size.width += spaceWidth
}
validIds.append(item.id)
let itemView: ItemView
var itemTransition = transition
if let current = self.itemViews[item.id] {
itemView = current
} else {
itemTransition = .immediate
itemView = ItemView()
self.itemViews[item.id] = itemView
}
let itemSize = itemView.view.update(
transition: itemTransition,
component: AnyComponent(AnimatedCounterItemComponent(
font: component.font,
color: component.color,
text: item.text,
numericValue: item.numericValue,
alignment: component.alignment == .left ? 0.0 : 1.0
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
if let itemComponentView = itemView.view.view {
if itemComponentView.superview == nil {
self.addSubview(itemComponentView)
}
let itemFrame = CGRect(origin: CGPoint(x: size.width, y: 0.0), size: itemSize)
switch component.alignment {
case .left:
itemComponentView.layer.anchorPoint = CGPoint(x: 0.0, y: 0.5)
itemTransition.setPosition(view: itemComponentView, position: CGPoint(x: itemFrame.minX, y: itemFrame.midY))
case .right:
itemComponentView.layer.anchorPoint = CGPoint(x: 1.0, y: 0.5)
itemTransition.setPosition(view: itemComponentView, position: CGPoint(x: itemFrame.maxX, y: itemFrame.midY))
}
itemComponentView.bounds = CGRect(origin: CGPoint(), size: itemFrame.size)
}
size.width += itemSize.width
size.height = max(size.height, itemSize.height)
}
var removeIds: [AnyHashable] = []
for (id, itemView) in self.itemViews {
if !validIds.contains(id) {
removeIds.append(id)
if let componentView = itemView.view.view {
transition.setAlpha(view: componentView, alpha: 0.0, completion: { [weak componentView] _ in
componentView?.removeFromSuperview()
})
}
}
}
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -26,6 +26,8 @@ swift_library(
"//submodules/PresentationDataUtils",
"//submodules/Components/SolidRoundedButtonComponent",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/TelegramUI/Components/AnimatedCounterComponent",
"//submodules/AvatarNode",
"//submodules/CheckNode",
"//submodules/Markdown",

View File

@ -20,6 +20,8 @@ import ButtonComponent
import ContextUI
import QrCodeUI
import InviteLinksUI
import PlainButtonComponent
import AnimatedCounterComponent
private final class ChatFolderLinkPreviewScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -154,7 +156,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
self.addSubview(self.navigationBarContainer)
self.scrollView.delaysContentTouches = true
self.scrollView.delaysContentTouches = false
self.scrollView.canCancelContentTouches = true
self.scrollView.clipsToBounds = false
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
@ -800,15 +802,21 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
listHeaderTitle = " "
}
let listHeaderActionTitle: String
//TODO:localize
let listHeaderActionItems: [AnimatedCounterComponent.Item]
if self.selectedItems.count == self.items.count {
listHeaderActionTitle = "DESELECT ALL"
listHeaderActionItems = [
AnimatedCounterComponent.Item(id: AnyHashable(0), text: "DESELECT", numericValue: 0),
AnimatedCounterComponent.Item(id: AnyHashable(1), text: "ALL", numericValue: 1)
]
} else {
listHeaderActionTitle = "SELECT ALL"
listHeaderActionItems = [
AnimatedCounterComponent.Item(id: AnyHashable(0), text: "SELECT", numericValue: 1),
AnimatedCounterComponent.Item(id: AnyHashable(1), text: "ALL", numericValue: 1)
]
}
let listHeaderBody = MarkdownAttributeSet(font: Font.with(size: 13.0, design: .regular, traits: [.monospacedNumbers]), textColor: environment.theme.list.freeTextColor)
let listHeaderActionBody = MarkdownAttributeSet(font: Font.with(size: 13.0, design: .regular, traits: [.monospacedNumbers]), textColor: environment.theme.list.itemAccentColor)
let listHeaderTextSize = self.listHeaderText.update(
transition: .immediate,
@ -838,19 +846,15 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
}
let listHeaderActionSize = self.listHeaderAction.update(
transition: .immediate,
component: AnyComponent(Button(
content: AnyComponent(MultilineTextComponent(
text: .markdown(
text: listHeaderActionTitle,
attributes: MarkdownAttributes(
body: listHeaderActionBody,
bold: listHeaderActionBody,
link: listHeaderActionBody,
linkAttribute: { _ in nil }
)
)
transition: transition,
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(AnimatedCounterComponent(
font: Font.regular(13.0),
color: environment.theme.list.itemAccentColor,
alignment: .right,
items: listHeaderActionItems
)),
effectAlignment: .right,
action: { [weak self] in
guard let self, let component = self.component, let linkContents = component.linkContents else {
return
@ -877,8 +881,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
self.scrollContentView.addSubview(listHeaderActionView)
}
let listHeaderActionFrame = CGRect(origin: CGPoint(x: availableSize.width - sideInset - 15.0 - listHeaderActionSize.width, y: contentHeight), size: listHeaderActionSize)
contentTransition.setPosition(view: listHeaderActionView, position: CGPoint(x: listHeaderActionFrame.maxX, y: listHeaderActionFrame.minY))
listHeaderActionView.bounds = CGRect(origin: CGPoint(), size: listHeaderActionFrame.size)
contentTransition.setFrame(view: listHeaderActionView, frame: listHeaderActionFrame)
if let linkContents = component.linkContents, !allChatsAdded, linkContents.peers.count > 1 {
listHeaderActionView.isHidden = false

View File

@ -0,0 +1,19 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "PlainButtonComponent",
module_name = "PlainButtonComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display",
"//submodules/ComponentFlow",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,154 @@
import Foundation
import UIKit
import Display
import ComponentFlow
public final class PlainButtonComponent: Component {
public enum EffectAlignment {
case left
case right
}
public let content: AnyComponent<Empty>
public let effectAlignment: EffectAlignment
public let action: () -> Void
public init(
content: AnyComponent<Empty>,
effectAlignment: EffectAlignment,
action: @escaping () -> Void
) {
self.content = content
self.effectAlignment = effectAlignment
self.action = action
}
public static func ==(lhs: PlainButtonComponent, rhs: PlainButtonComponent) -> Bool {
if lhs.content != rhs.content {
return false
}
if lhs.effectAlignment != rhs.effectAlignment {
return false
}
return true
}
public final class View: HighlightTrackingButton {
private var component: PlainButtonComponent?
private weak var componentState: EmptyComponentState?
private let contentContainer = UIView()
private let content = ComponentView<Empty>()
override init(frame: CGRect) {
super.init(frame: frame)
self.contentContainer.isUserInteractionEnabled = false
self.addSubview(self.contentContainer)
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
self.highligthedChanged = { [weak self] highlighted in
if let self, self.bounds.width > 0.0 {
let topScale: CGFloat = (self.bounds.width - 8.0) / self.bounds.width
let maxScale: CGFloat = (self.bounds.width + 2.0) / self.bounds.width
if highlighted {
self.contentContainer.layer.removeAnimation(forKey: "opacity")
self.contentContainer.layer.removeAnimation(forKey: "sublayerTransform")
self.contentContainer.alpha = 0.7
let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut))
transition.setScale(layer: self.contentContainer.layer, scale: topScale)
} else {
self.contentContainer.alpha = 1.0
self.contentContainer.layer.animateAlpha(from: 7, to: 1.0, duration: 0.2)
let transition = Transition(animation: .none)
transition.setScale(layer: self.contentContainer.layer, scale: 1.0)
self.contentContainer.layer.animateScale(from: topScale, to: maxScale, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in
guard let self else {
return
}
self.contentContainer.layer.animateScale(from: maxScale, to: 1.0, duration: 0.1, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue)
})
}
}
}
}
required init(coder: NSCoder) {
preconditionFailure()
}
@objc private func pressed() {
guard let component = self.component else {
return
}
component.action()
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event)
if result != nil {
return result
}
if !self.isEnabled {
return nil
}
if self.bounds.insetBy(dx: -8.0, dy: -8.0).contains(point) {
return self
}
return nil
}
func update(component: PlainButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.component = component
self.componentState = state
self.isEnabled = true
let contentAlpha: CGFloat = 1.0
let contentSize = self.content.update(
transition: transition,
component: component.content,
environment: {},
containerSize: availableSize
)
let size = contentSize
if let contentView = self.content.view {
var contentTransition = transition
if contentView.superview == nil {
contentTransition = .immediate
contentView.isUserInteractionEnabled = false
self.contentContainer.addSubview(contentView)
}
let contentFrame = CGRect(origin: CGPoint(x: floor((size.width - contentSize.width) * 0.5), y: floor((size.height - contentSize.height) * 0.5)), size: contentSize)
contentTransition.setFrame(view: contentView, frame: contentFrame)
contentTransition.setAlpha(view: contentView, alpha: contentAlpha)
}
self.contentContainer.layer.anchorPoint = CGPoint(x: component.effectAlignment == .left ? 0.0 : 1.0, y: 0.5)
transition.setBounds(view: self.contentContainer, bounds: CGRect(origin: CGPoint(), size: size))
transition.setPosition(view: self.contentContainer, position: CGPoint(x: component.effectAlignment == .left ? 0.0 : size.width, y: size.height * 0.5))
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}