mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-11-06 17:00:13 +00:00
Charts improvements
This commit is contained in:
parent
c5d39df2b3
commit
d6f0a02fc7
BIN
Telegram/Telegram-iOS/Resources/Charts.tgs
Normal file
BIN
Telegram/Telegram-iOS/Resources/Charts.tgs
Normal file
Binary file not shown.
@ -5368,6 +5368,7 @@ Any member of this group will be able to see messages in the channel.";
|
||||
"Stats.InteractionsTitle" = "INTERACTIONS";
|
||||
"Stats.InstantViewInteractionsTitle" = "INSTANT VIEW INTERACTIONS";
|
||||
"Stats.ViewsBySourceTitle" = "VIEWS BY SOURCE";
|
||||
"Stats.ViewsByHoursTitle" = "VIEWS BY HOURS";
|
||||
"Stats.FollowersBySourceTitle" = "FOLLOWERS BY SOURCE";
|
||||
"Stats.LanguagesTitle" = "LANGUAGES";
|
||||
"Stats.PostsTitle" = "RECENT POSTS";
|
||||
@ -5386,6 +5387,9 @@ Any member of this group will be able to see messages in the channel.";
|
||||
"Stats.MessageForwards_many" = "%@ forwards";
|
||||
"Stats.MessageForwards_any" = "%@ forwards";
|
||||
|
||||
"Stats.LoadingTitle" = "Preparing stats";
|
||||
"Stats.LoadingText" = "Please wait a few moments while\nwe generate your stats";
|
||||
|
||||
"InstantPage.Views_0" = "%@ views";
|
||||
"InstantPage.Views_1" = "%@ view";
|
||||
"InstantPage.Views_2" = "%@ views";
|
||||
|
||||
@ -38,7 +38,7 @@ open class OverlayMediaItemNode: ASDisplayNode {
|
||||
open func setShouldAcquireContext(_ value: Bool) {
|
||||
}
|
||||
|
||||
open func preferredSizeForOverlayDisplay() -> CGSize {
|
||||
open func preferredSizeForOverlayDisplay(boundingSize: CGSize) -> CGSize {
|
||||
return CGSize(width: 50.0, height: 50.0)
|
||||
}
|
||||
|
||||
|
||||
@ -1881,8 +1881,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
}
|
||||
|
||||
private func foldLineBreaks(_ text: String) -> String {
|
||||
var lines = text.split { $0.isNewline }
|
||||
var startedBothLines = false
|
||||
let lines = text.split { $0.isNewline }
|
||||
var result = ""
|
||||
for line in lines {
|
||||
if line.isEmpty {
|
||||
|
||||
@ -67,7 +67,15 @@ public extension ChartsCollection {
|
||||
}
|
||||
switch type {
|
||||
case .axix:
|
||||
axixValuesToSetup = try column.dropFirst().map { Date(timeIntervalSince1970: try Convert.doubleFrom($0) / 1000) }
|
||||
axixValuesToSetup = try column.dropFirst().map { value in
|
||||
let numberValue = try Convert.doubleFrom(value)
|
||||
if numberValue < 24.0 {
|
||||
return Date(timeIntervalSince1970: numberValue)
|
||||
} else {
|
||||
return Date(timeIntervalSince1970: numberValue / 1000)
|
||||
}
|
||||
|
||||
}
|
||||
case .chart, .bar, .area, .step:
|
||||
guard let colorString = colors[columnId],
|
||||
let color = GColor(hexString: colorString) else {
|
||||
|
||||
@ -245,8 +245,14 @@ class GeneralChartComponentController: ChartThemeContainer {
|
||||
var labels: [LinesChartLabel] = []
|
||||
for index in stride(from: chartsCollection.axisValues.count - 1, to: -1, by: -strideInterval).reversed() {
|
||||
let date = chartsCollection.axisValues[index]
|
||||
labels.append(LinesChartLabel(value: CGFloat(date.timeIntervalSince1970),
|
||||
text: scaleType.dateFormatter.string(from: date)))
|
||||
let timestamp = date.timeIntervalSince1970
|
||||
if timestamp <= 24 {
|
||||
labels.append(LinesChartLabel(value: CGFloat(timestamp),
|
||||
text: "\(Int(timestamp)):00"))
|
||||
} else {
|
||||
labels.append(LinesChartLabel(value: CGFloat(timestamp),
|
||||
text: scaleType.dateFormatter.string(from: date)))
|
||||
}
|
||||
}
|
||||
prevoiusHorizontalStrideInterval = strideInterval
|
||||
horizontalScalesRenderer.setup(labels: labels, animated: animated)
|
||||
@ -318,8 +324,9 @@ class GeneralChartComponentController: ChartThemeContainer {
|
||||
tapAction: { [weak self] in
|
||||
self?.zoomInOnDateClosure?(closestDate) },
|
||||
hideAction: { [weak self] in
|
||||
self?.setDetailsChartVisibleClosure?(false, true)
|
||||
|
||||
})
|
||||
})
|
||||
return viewModel
|
||||
}
|
||||
|
||||
|
||||
@ -157,8 +157,14 @@ public class BaseLinesChartController: BaseChartController {
|
||||
var labels: [LinesChartLabel] = []
|
||||
for index in stride(from: initialChartsCollection.axisValues.count - 1, to: -1, by: -strideInterval).reversed() {
|
||||
let date = initialChartsCollection.axisValues[index]
|
||||
labels.append(LinesChartLabel(value: CGFloat(date.timeIntervalSince1970),
|
||||
text: scaleType.dateFormatter.string(from: date)))
|
||||
let timestamp = date.timeIntervalSince1970
|
||||
if timestamp <= 24 {
|
||||
labels.append(LinesChartLabel(value: CGFloat(timestamp),
|
||||
text: "\(Int(timestamp)):00"))
|
||||
} else {
|
||||
labels.append(LinesChartLabel(value: CGFloat(timestamp),
|
||||
text: scaleType.dateFormatter.string(from: date)))
|
||||
}
|
||||
}
|
||||
return (strideInterval, labels)
|
||||
}
|
||||
@ -179,14 +185,14 @@ public class BaseLinesChartController: BaseChartController {
|
||||
}
|
||||
|
||||
func verticalLimitsLabels(verticalRange: ClosedRange<CGFloat>) -> (ClosedRange<CGFloat>, [LinesChartLabel]) {
|
||||
let ditance = verticalRange.distance
|
||||
let distance = verticalRange.distance
|
||||
let chartHeight = chartFrame().height
|
||||
|
||||
guard ditance > 0, chartHeight > 0 else { return (BaseConstants.defaultRange, []) }
|
||||
guard distance > 0, chartHeight > 0 else { return (BaseConstants.defaultRange, []) }
|
||||
|
||||
let approximateNumberOfChartValues = (chartHeight / BaseConstants.minimumAxisYLabelsDistance)
|
||||
|
||||
var numberOfOffsetsPerItem = ditance / approximateNumberOfChartValues
|
||||
var numberOfOffsetsPerItem = distance / approximateNumberOfChartValues
|
||||
var multiplier: CGFloat = 1.0
|
||||
while numberOfOffsetsPerItem > 10 {
|
||||
numberOfOffsetsPerItem /= 10
|
||||
|
||||
@ -237,7 +237,7 @@ public class TwoAxisLinesChartController: BaseLinesChartController {
|
||||
let chartHeight = chartFrame().height
|
||||
let approximateNumberOfChartValues = (chartHeight / BaseConstants.minimumAxisYLabelsDistance)
|
||||
|
||||
var dividorsAndMultiplers: [(startValue: CGFloat, base: CGFloat, count: Int, maximumNumberOfDecimals: Int)] = graphControllers.enumerated().map { arg in
|
||||
let dividorsAndMultiplers: [(startValue: CGFloat, base: CGFloat, count: Int, maximumNumberOfDecimals: Int)] = graphControllers.enumerated().map { arg in
|
||||
let (index, controller) = arg
|
||||
let verticalRange = LinesChartRenderer.LineData.verticalRange(lines: controller.chartLines,
|
||||
calculatingRange: horizontalRange,
|
||||
|
||||
@ -138,7 +138,7 @@ class PercentChartComponentController: GeneralChartComponentController {
|
||||
|
||||
let values: [ChartDetailsViewModel.Value] = chartsCollection.chartValues.enumerated().map { arg in
|
||||
let (index, component) = arg
|
||||
return ChartDetailsViewModel.Value(prefix: PercentConstants.percentValueFormatter.string(from: component.values[pointIndex] / total * 100),
|
||||
return ChartDetailsViewModel.Value(prefix: total > 0 ? PercentConstants.percentValueFormatter.string(from: component.values[pointIndex] / total * 100) : "0%",
|
||||
title: component.name,
|
||||
value: BaseConstants.detailsNumberFormatter.string(from: component.values[pointIndex]),
|
||||
color: component.color,
|
||||
@ -151,16 +151,16 @@ class PercentChartComponentController: GeneralChartComponentController {
|
||||
dateString = BaseConstants.headerMediumRangeFormatter.string(from: closestDate)
|
||||
}
|
||||
let viewModel = ChartDetailsViewModel(title: dateString,
|
||||
showArrow: self.isZoomable && !self.isZoomed,
|
||||
showArrow: total > 0 && self.isZoomable && !self.isZoomed,
|
||||
showPrefixes: true,
|
||||
values: values,
|
||||
totalValue: nil,
|
||||
tapAction: { [weak self] in
|
||||
self?.hideDetailsView(animated: true)
|
||||
self?.zoomInOnDateClosure?(closestDate) },
|
||||
hideAction: { [weak self] in
|
||||
self?.hideDetailsView(animated: true)
|
||||
})
|
||||
hideAction: { [weak self] in
|
||||
self?.hideDetailsView(animated: true)
|
||||
})
|
||||
return viewModel
|
||||
}
|
||||
|
||||
|
||||
@ -95,9 +95,11 @@ public class PercentPieChartController: BaseChartController {
|
||||
totalVerticalRange: BaseConstants.defaultRange)
|
||||
switchToChart(chartsCollection: percentController.chartsCollection, isZoomed: false, animated: false)
|
||||
|
||||
TimeInterval.animationDurationMultipler = 0.00001
|
||||
self.didTapZoomIn(date: Date(timeIntervalSinceReferenceDate: 603849600.0), animated: false)
|
||||
TimeInterval.animationDurationMultipler = 1
|
||||
if let lastDate = initialChartsCollection.axisValues.last {
|
||||
TimeInterval.animationDurationMultipler = 0.00001
|
||||
self.didTapZoomIn(date: lastDate, animated: false)
|
||||
TimeInterval.animationDurationMultipler = 1.0
|
||||
}
|
||||
}
|
||||
|
||||
func switchToChart(chartsCollection: ChartsCollection, isZoomed: Bool, animated: Bool) {
|
||||
|
||||
@ -17,25 +17,25 @@ class BarsComponentController: GeneralChartComponentController {
|
||||
let mainBarsRenderer: BarChartRenderer
|
||||
let horizontalScalesRenderer: HorizontalScalesRenderer
|
||||
let verticalScalesRenderer: VerticalScalesRenderer
|
||||
let secondVerticalScalesRenderer: VerticalScalesRenderer?
|
||||
|
||||
let previewBarsChartRenderer: BarChartRenderer
|
||||
private(set) var barsWidth: CGFloat = 1
|
||||
|
||||
private (set) var chartBars: BarChartRenderer.BarsData = .blank
|
||||
|
||||
private var step: Bool
|
||||
|
||||
init(isZoomed: Bool,
|
||||
mainBarsRenderer: BarChartRenderer,
|
||||
horizontalScalesRenderer: HorizontalScalesRenderer,
|
||||
verticalScalesRenderer: VerticalScalesRenderer,
|
||||
previewBarsChartRenderer: BarChartRenderer) {
|
||||
previewBarsChartRenderer: BarChartRenderer,
|
||||
step: Bool = false) {
|
||||
self.mainBarsRenderer = mainBarsRenderer
|
||||
self.horizontalScalesRenderer = horizontalScalesRenderer
|
||||
self.verticalScalesRenderer = verticalScalesRenderer
|
||||
self.previewBarsChartRenderer = previewBarsChartRenderer
|
||||
|
||||
self.secondVerticalScalesRenderer = VerticalScalesRenderer()
|
||||
self.secondVerticalScalesRenderer?.isRightAligned = true
|
||||
self.step = step
|
||||
|
||||
self.mainBarsRenderer.optimizationLevel = BaseConstants.barsChartOptimizationLevel
|
||||
self.previewBarsChartRenderer.optimizationLevel = BaseConstants.barsChartOptimizationLevel
|
||||
@ -44,7 +44,7 @@ class BarsComponentController: GeneralChartComponentController {
|
||||
}
|
||||
|
||||
override func initialize(chartsCollection: ChartsCollection, initialDate: Date, totalHorizontalRange _: ClosedRange<CGFloat>, totalVerticalRange _: ClosedRange<CGFloat>) {
|
||||
let (width, chartBars, totalHorizontalRange, totalVerticalRange) = BarChartRenderer.BarsData.initialComponents(chartsCollection: chartsCollection)
|
||||
let (width, chartBars, totalHorizontalRange, totalVerticalRange) = BarChartRenderer.BarsData.initialComponents(chartsCollection: chartsCollection, separate: self.step)
|
||||
self.chartBars = chartBars
|
||||
self.barsWidth = width
|
||||
|
||||
@ -76,10 +76,10 @@ class BarsComponentController: GeneralChartComponentController {
|
||||
mainBarsRenderer.bars = self.chartBars
|
||||
previewBarsChartRenderer.bars = self.chartBars
|
||||
|
||||
previewBarsChartRenderer.setup(verticalRange: 0...117278, animated: animated)
|
||||
previewBarsChartRenderer.setup(verticalRange: totalVerticalRange, animated: animated)
|
||||
previewBarsChartRenderer.setup(horizontalRange: totalHorizontalRange, animated: animated)
|
||||
|
||||
setupMainChart(verticalRange: 0...117278, animated: animated)
|
||||
setupMainChart(verticalRange: initialVerticalRange, animated: animated)
|
||||
setupMainChart(horizontalRange: initialHorizontalRange, animated: animated)
|
||||
|
||||
updateChartVerticalRanges(horizontalRange: initialHorizontalRange, animated: animated)
|
||||
@ -119,16 +119,12 @@ class BarsComponentController: GeneralChartComponentController {
|
||||
horizontalScalesRenderer.setVisible(visible, animated: animated)
|
||||
verticalScalesRenderer.setVisible(visible, animated: animated)
|
||||
previewBarsChartRenderer.setVisible(visible, animated: animated)
|
||||
|
||||
secondVerticalScalesRenderer?.setVisible(visible, animated: animated)
|
||||
}
|
||||
|
||||
func setupMainChart(horizontalRange: ClosedRange<CGFloat>, animated: Bool) {
|
||||
mainBarsRenderer.setup(horizontalRange: horizontalRange, animated: animated)
|
||||
horizontalScalesRenderer.setup(horizontalRange: horizontalRange, animated: animated)
|
||||
verticalScalesRenderer.setup(horizontalRange: horizontalRange, animated: animated)
|
||||
|
||||
secondVerticalScalesRenderer?.setup(horizontalRange: horizontalRange, animated: animated)
|
||||
}
|
||||
|
||||
var visibleBars: BarChartRenderer.BarsData {
|
||||
@ -142,6 +138,7 @@ class BarsComponentController: GeneralChartComponentController {
|
||||
|
||||
func updateChartVerticalRanges(horizontalRange: ClosedRange<CGFloat>, animated: Bool) {
|
||||
if let range = BarChartRenderer.BarsData.verticalRange(bars: visibleBars,
|
||||
separate: self.step,
|
||||
calculatingRange: horizontalRange,
|
||||
addBounds: true) {
|
||||
let (range, labels) = verticalLimitsLabels(verticalRange: range)
|
||||
@ -151,31 +148,19 @@ class BarsComponentController: GeneralChartComponentController {
|
||||
verticalScalesRenderer.setVisible(true, animated: animated)
|
||||
|
||||
setupMainChart(verticalRange: range, animated: animated)
|
||||
|
||||
let (secondRange, secondLabels) = verticalLimitsLabels(verticalRange: range)
|
||||
if secondVerticalScalesRenderer?.verticalRange.end != secondRange {
|
||||
secondVerticalScalesRenderer?.setup(verticalLimitsLabels: secondLabels, animated: animated)
|
||||
}
|
||||
secondVerticalScalesRenderer?.setVisible(true, animated: animated)
|
||||
} else {
|
||||
verticalScalesRenderer.setVisible(false, animated: animated)
|
||||
secondVerticalScalesRenderer?.setVisible(false, animated: animated)
|
||||
}
|
||||
|
||||
if let range = BarChartRenderer.BarsData.verticalRange(bars: visibleBars) {
|
||||
if let range = BarChartRenderer.BarsData.verticalRange(bars: visibleBars, separate: self.step) {
|
||||
previewBarsChartRenderer.setup(verticalRange: range, animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
func setupMainChart(verticalRange: ClosedRange<CGFloat>, animated: Bool) {
|
||||
var verticalRange = verticalRange
|
||||
if verticalRange.upperBound > 2000 && verticalRange.upperBound < 10000 {
|
||||
verticalRange = 0...117278
|
||||
}
|
||||
mainBarsRenderer.setup(verticalRange: verticalRange, animated: animated)
|
||||
horizontalScalesRenderer.setup(verticalRange: verticalRange, animated: animated)
|
||||
verticalScalesRenderer.setup(verticalRange: verticalRange, animated: animated)
|
||||
secondVerticalScalesRenderer?.setup(verticalRange: verticalRange, animated: animated)
|
||||
}
|
||||
|
||||
public override func updateChartsVisibility(visibility: [Bool], animated: Bool) {
|
||||
@ -198,12 +183,15 @@ class BarsComponentController: GeneralChartComponentController {
|
||||
var viewModel = super.chartDetailsViewModel(closestDate: closestDate, pointIndex: pointIndex)
|
||||
let visibleChartValues = self.visibleChartValues
|
||||
let totalSumm: CGFloat = visibleChartValues.map { CGFloat($0.values[pointIndex]) }.reduce(0, +)
|
||||
|
||||
viewModel.totalValue = ChartDetailsViewModel.Value(prefix: nil,
|
||||
title: "Total",
|
||||
value: BaseConstants.detailsNumberFormatter.string(from: totalSumm),
|
||||
color: .white,
|
||||
visible: visibleChartValues.count > 1)
|
||||
if !self.step {
|
||||
viewModel.totalValue = ChartDetailsViewModel.Value(prefix: nil,
|
||||
title: "Total",
|
||||
value: BaseConstants.detailsNumberFormatter.string(from: totalSumm),
|
||||
color: .white,
|
||||
visible: visibleChartValues.count > 1)
|
||||
} else {
|
||||
viewModel.title = ""
|
||||
}
|
||||
return viewModel
|
||||
}
|
||||
|
||||
@ -235,10 +223,6 @@ class BarsComponentController: GeneralChartComponentController {
|
||||
verticalScalesRenderer.horizontalLinesColor = theme.barChartStrongLinesColor
|
||||
mainBarsRenderer.update(backgroundColor: theme.chartBackgroundColor, animated: false)
|
||||
previewBarsChartRenderer.update(backgroundColor: theme.chartBackgroundColor, animated: false)
|
||||
|
||||
secondVerticalScalesRenderer?.labelsColor = theme.chartLabelsColor
|
||||
secondVerticalScalesRenderer?.axisXColor = theme.barChartStrongLinesColor
|
||||
secondVerticalScalesRenderer?.horizontalLinesColor = theme.barChartStrongLinesColor
|
||||
}
|
||||
|
||||
override func updateChartRangeTitle(animated: Bool) {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
//
|
||||
// DailyBarsChartController.swift
|
||||
// StackedBarsChartController.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 4/7/19.
|
||||
@ -17,6 +17,12 @@ public class StepBarsChartController: BaseChartController {
|
||||
let barsController: BarsComponentController
|
||||
let zoomedBarsController: BarsComponentController
|
||||
|
||||
override public var isZoomable: Bool {
|
||||
didSet {
|
||||
barsController.isZoomable = self.isZoomable
|
||||
}
|
||||
}
|
||||
|
||||
override public init(chartsCollection: ChartsCollection) {
|
||||
let horizontalScalesRenderer = HorizontalScalesRenderer()
|
||||
let verticalScalesRenderer = VerticalScalesRenderer()
|
||||
@ -24,13 +30,13 @@ public class StepBarsChartController: BaseChartController {
|
||||
mainBarsRenderer: BarChartRenderer(step: true),
|
||||
horizontalScalesRenderer: horizontalScalesRenderer,
|
||||
verticalScalesRenderer: verticalScalesRenderer,
|
||||
previewBarsChartRenderer: BarChartRenderer())
|
||||
previewBarsChartRenderer: BarChartRenderer(step: true), step: true)
|
||||
zoomedBarsController = BarsComponentController(isZoomed: true,
|
||||
mainBarsRenderer: BarChartRenderer(step: true),
|
||||
mainBarsRenderer: BarChartRenderer(),
|
||||
horizontalScalesRenderer: horizontalScalesRenderer,
|
||||
verticalScalesRenderer: verticalScalesRenderer,
|
||||
previewBarsChartRenderer: BarChartRenderer())
|
||||
|
||||
previewBarsChartRenderer: BarChartRenderer(), step: true)
|
||||
|
||||
super.init(chartsCollection: chartsCollection)
|
||||
|
||||
[barsController, zoomedBarsController].forEach { controller in
|
||||
@ -40,7 +46,7 @@ public class StepBarsChartController: BaseChartController {
|
||||
self.didTapZoomIn(date: date)
|
||||
}
|
||||
controller.setChartTitleClosure = { [unowned self] (title, animated) in
|
||||
self.setChartTitleClosure?(title, animated)
|
||||
self.setChartTitleClosure?("", animated)
|
||||
}
|
||||
controller.setDetailsViewPositionClosure = { [unowned self] (position) in
|
||||
self.setDetailsViewPositionClosure?(position)
|
||||
@ -60,12 +66,18 @@ public class StepBarsChartController: BaseChartController {
|
||||
}
|
||||
}
|
||||
|
||||
public var hourly: Bool = false
|
||||
|
||||
public convenience init(chartsCollection: ChartsCollection, hourly: Bool) {
|
||||
self.init(chartsCollection: chartsCollection)
|
||||
self.hourly = hourly
|
||||
}
|
||||
|
||||
public override var mainChartRenderers: [ChartViewRenderer] {
|
||||
return [barsController.mainBarsRenderer,
|
||||
zoomedBarsController.mainBarsRenderer,
|
||||
barsController.horizontalScalesRenderer,
|
||||
barsController.verticalScalesRenderer,
|
||||
barsController.secondVerticalScalesRenderer!
|
||||
// performanceRenderer
|
||||
]
|
||||
}
|
||||
@ -90,57 +102,65 @@ public class StepBarsChartController: BaseChartController {
|
||||
TimeInterval.setDefaultSuration(.osXDuration)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
super.isZoomed = isZoomed
|
||||
if isZoomed {
|
||||
let toHorizontalRange = zoomedBarsController.initialHorizontalRange
|
||||
let destinationHorizontalRange = (toHorizontalRange.lowerBound - barsController.barsWidth)...(toHorizontalRange.upperBound - barsController.barsWidth)
|
||||
// let initialChartVerticalRange = lineProportionAnimationRange()
|
||||
|
||||
// let visibleVerticalRange = BarChartRenderer.BarsData.verticalRange(bars: zoomedBarsController.visibleBars,
|
||||
// calculatingRange: zoomedBarsController.initialHorizontalRange) ?? BaseConstants.defaultRange
|
||||
zoomedBarsController.mainBarsRenderer.setup(verticalRange: 0...117278, animated: false)
|
||||
|
||||
zoomedBarsController.setupMainChart(horizontalRange: barsController.currentHorizontalMainChartRange, animated: false)
|
||||
let verticalVisibleRange = barsController.currentVerticalMainChartRange
|
||||
let initialVerticalRange = verticalVisibleRange.lowerBound...(verticalVisibleRange.upperBound + verticalVisibleRange.distance * 10)
|
||||
|
||||
zoomedBarsController.mainBarsRenderer.setup(horizontalRange: barsController.currentHorizontalMainChartRange, animated: false)
|
||||
zoomedBarsController.previewBarsChartRenderer.setup(horizontalRange: barsController.currentPreviewHorizontalRange, animated: false)
|
||||
// zoomedBarsController.mainLinesRenderer.setup(verticalRange: initialChartVerticalRange, animated: false)
|
||||
// zoomedBarsController.previewLinesChartRenderer.setup(verticalRange: initialChartVerticalRange, animated: false)
|
||||
zoomedBarsController.mainBarsRenderer.setVisible(false, animated: false)
|
||||
zoomedBarsController.previewBarsChartRenderer.setVisible(false, animated: false)
|
||||
|
||||
zoomedBarsController.mainBarsRenderer.setup(verticalRange: initialVerticalRange, animated: false)
|
||||
zoomedBarsController.previewBarsChartRenderer.setup(verticalRange: initialVerticalRange, animated: false)
|
||||
zoomedBarsController.mainBarsRenderer.setVisible(true, animated: false)
|
||||
zoomedBarsController.previewBarsChartRenderer.setVisible(true, animated: false)
|
||||
|
||||
barsController.setupMainChart(horizontalRange: destinationHorizontalRange, animated: animated)
|
||||
barsController.previewBarsChartRenderer.setup(horizontalRange: zoomedBarsController.totalHorizontalRange, animated: animated)
|
||||
barsController.mainBarsRenderer.setVisible(false, animated: animated)
|
||||
barsController.previewBarsChartRenderer.setVisible(false, animated: animated)
|
||||
|
||||
|
||||
zoomedBarsController.willAppear(animated: animated)
|
||||
barsController.willDisappear(animated: animated)
|
||||
|
||||
zoomedBarsController.updateChartsVisibility(visibility: zoomedBarsController.chartBars.components.map { _ in true }, animated: false)
|
||||
zoomedBarsController.updateChartsVisibility(visibility: barsController.chartVisibility, animated: false)
|
||||
zoomedBarsController.mainBarsRenderer.setup(verticalRange: zoomedBarsController.currentVerticalMainChartRange, animated: animated, timeFunction: .easeOut)
|
||||
zoomedBarsController.previewBarsChartRenderer.setup(verticalRange: zoomedBarsController.currentPreviewVerticalRange, animated: animated, timeFunction: .easeOut)
|
||||
} else {
|
||||
if !zoomedBarsController.chartsCollection.isBlank {
|
||||
barsController.hideDetailsView(animated: false)
|
||||
barsController.chartVisibility = zoomedBarsController.chartVisibility
|
||||
let visibleVerticalRange = BarChartRenderer.BarsData.verticalRange(bars: barsController.visibleBars,
|
||||
separate: true,
|
||||
calculatingRange: barsController.initialHorizontalRange) ?? BaseConstants.defaultRange
|
||||
barsController.mainBarsRenderer.setup(verticalRange: visibleVerticalRange, animated: false)
|
||||
|
||||
let toHorizontalRange = barsController.initialHorizontalRange
|
||||
// let destinationChartVerticalRange = lineProportionAnimationRange()
|
||||
|
||||
|
||||
let verticalVisibleRange = barsController.initialVerticalRange
|
||||
let targetVerticalRange = verticalVisibleRange.lowerBound...(verticalVisibleRange.upperBound + verticalVisibleRange.distance * 10)
|
||||
|
||||
zoomedBarsController.setupMainChart(horizontalRange: toHorizontalRange, animated: animated)
|
||||
// zoomedBarsController.mainLinesRenderer.setup(verticalRange: destinationChartVerticalRange, animated: animated)
|
||||
// zoomedBarsController.previewLinesChartRenderer.setup(verticalRange: destinationChartVerticalRange, animated: animated)
|
||||
zoomedBarsController.mainBarsRenderer.setup(verticalRange: targetVerticalRange, animated: animated, timeFunction: .easeIn)
|
||||
zoomedBarsController.previewBarsChartRenderer.setup(verticalRange: targetVerticalRange, animated: animated, timeFunction: .easeIn)
|
||||
zoomedBarsController.previewBarsChartRenderer.setup(horizontalRange: barsController.totalHorizontalRange, animated: animated)
|
||||
zoomedBarsController.mainBarsRenderer.setVisible(false, animated: animated)
|
||||
zoomedBarsController.previewBarsChartRenderer.setVisible(false, animated: animated)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .defaultDuration) { [weak self] in
|
||||
self?.zoomedBarsController.mainBarsRenderer.setVisible(false, animated: false)
|
||||
self?.zoomedBarsController.previewBarsChartRenderer.setVisible(false, animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
barsController.willAppear(animated: animated)
|
||||
zoomedBarsController.willDisappear(animated: animated)
|
||||
|
||||
if !zoomedBarsController.chartsCollection.isBlank {
|
||||
barsController.updateChartsVisibility(visibility: zoomedBarsController.chartVisibility, animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
self.setBackButtonVisibilityClosure?(isZoomed, animated)
|
||||
self.refreshChartToolsClosure?(animated)
|
||||
}
|
||||
|
||||
public override func updateChartsVisibility(visibility: [Bool], animated: Bool) {
|
||||
@ -166,13 +186,12 @@ public class StepBarsChartController: BaseChartController {
|
||||
|
||||
public override var actualChartsCollection: ChartsCollection {
|
||||
let collection = isZoomed ? zoomedBarsController.chartsCollection : barsController.chartsCollection
|
||||
|
||||
if collection.isBlank {
|
||||
return self.initialChartsCollection
|
||||
}
|
||||
return collection
|
||||
}
|
||||
|
||||
|
||||
public override func chartInteractionDidBegin(point: CGPoint) {
|
||||
if isZoomed {
|
||||
zoomedBarsController.chartInteractionDidBegin(point: point)
|
||||
@ -189,6 +208,10 @@ public class StepBarsChartController: BaseChartController {
|
||||
}
|
||||
}
|
||||
|
||||
public override var drawChartVisibity: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
public override var currentChartHorizontalRangeFraction: ClosedRange<CGFloat> {
|
||||
if isZoomed {
|
||||
return zoomedBarsController.currentChartHorizontalRangeFraction
|
||||
@ -221,15 +244,6 @@ public class StepBarsChartController: BaseChartController {
|
||||
})
|
||||
}
|
||||
|
||||
// func lineProportionAnimationRange() -> ClosedRange<CGFloat> {
|
||||
// let visibleLines = self.barsController.chartVisibility.enumerated().compactMap { $0.element ? self.zoomedBarsController.chartLines[$0.offset] : nil }
|
||||
// let linesRange = LinesChartRenderer.LineData.verticalRange(lines: visibleLines) ?? BaseConstants.defaultRange
|
||||
// let barsRange = BarChartRenderer.BarsData.verticalRange(bars: self.barsController.visibleBars,
|
||||
// calculatingRange: self.zoomedBarsController.totalHorizontalRange) ?? BaseConstants.defaultRange
|
||||
// let range = 0...(linesRange.upperBound / barsRange.distance * self.barsController.currentVerticalMainChartRange.distance)
|
||||
// return range
|
||||
// }
|
||||
|
||||
public override func didTapZoomOut() {
|
||||
cancelChartInteraction()
|
||||
switchToChart(chartsCollection: barsController.chartsCollection, isZoomed: false, animated: true)
|
||||
@ -243,17 +257,10 @@ public class StepBarsChartController: BaseChartController {
|
||||
}
|
||||
}
|
||||
|
||||
override public func apply(theme: ChartTheme, animated: Bool) {
|
||||
public override func apply(theme: ChartTheme, animated: Bool) {
|
||||
super.apply(theme: theme, animated: animated)
|
||||
|
||||
zoomedBarsController.apply(theme: theme, animated: animated)
|
||||
barsController.apply(theme: theme, animated: animated)
|
||||
}
|
||||
|
||||
public override var drawChartVisibity: Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: Убрать Performance полоски сверзу чартов (Не забыть)
|
||||
//TODO: Добавить ховеры на кнопки
|
||||
|
||||
@ -1,383 +0,0 @@
|
||||
import Foundation
|
||||
#if os(macOS)
|
||||
import Cocoa
|
||||
#else
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
public class StepBarsChartController2: BaseChartController {
|
||||
class GraphController {
|
||||
let mainBarsRenderer: BarChartRenderer
|
||||
let verticalScalesRenderer = VerticalScalesRenderer()
|
||||
let lineBulletsRenderer = LineBulletsRenderer()
|
||||
let previewBarsRenderer: BarChartRenderer
|
||||
|
||||
var chartBars: BarChartRenderer.BarsData = .blank
|
||||
var barsWidth: CGFloat = 1
|
||||
|
||||
var totalVerticalRange: ClosedRange<CGFloat> = BaseConstants.defaultRange
|
||||
|
||||
init(isZoomed: Bool,
|
||||
mainBarsRenderer: BarChartRenderer,
|
||||
previewBarsRenderer: BarChartRenderer) {
|
||||
self.mainBarsRenderer = mainBarsRenderer
|
||||
self.previewBarsRenderer = previewBarsRenderer
|
||||
|
||||
self.mainBarsRenderer.optimizationLevel = BaseConstants.barsChartOptimizationLevel
|
||||
self.previewBarsRenderer.optimizationLevel = BaseConstants.barsChartOptimizationLevel
|
||||
}
|
||||
}
|
||||
|
||||
private var graphControllers: [GraphController] = []
|
||||
private let horizontalScalesRenderer = HorizontalScalesRenderer()
|
||||
|
||||
private let verticalLineRenderer = VerticalLinesRenderer()
|
||||
|
||||
var chartVisibility: [Bool] = []
|
||||
var zoomChartVisibility: [Bool] = []
|
||||
|
||||
private let initialChartCollection: ChartsCollection
|
||||
var initialChartRange: ClosedRange<CGFloat> = BaseConstants.defaultRange
|
||||
var zoomedChartRange: ClosedRange<CGFloat> = BaseConstants.defaultRange
|
||||
var totalHorizontalRange: ClosedRange<CGFloat> = BaseConstants.defaultRange
|
||||
|
||||
var lastChartInteractionPoint: CGPoint = .zero
|
||||
var isChartInteractionBegun: Bool = false
|
||||
|
||||
override public init(chartsCollection: ChartsCollection) {
|
||||
self.initialChartCollection = chartsCollection
|
||||
|
||||
self.graphControllers = chartsCollection.chartValues.map { _ in GraphController(isZoomed: false, mainBarsRenderer: BarChartRenderer(step: true), previewBarsRenderer: BarChartRenderer(step: true))
|
||||
}
|
||||
|
||||
super.init(chartsCollection: chartsCollection)
|
||||
|
||||
self.chartVisibility = Array(repeating: true, count: chartsCollection.chartValues.count)
|
||||
self.zoomChartVisibility = self.chartVisibility
|
||||
|
||||
// self.graphControllers.map({ $0.barsController }).forEach { controller in
|
||||
// controller.chartFrame = { [unowned self] in self.chartFrame() }
|
||||
// controller.cartViewBounds = { [unowned self] in self.cartViewBounds() }
|
||||
// controller.zoomInOnDateClosure = { [unowned self] date in
|
||||
// self.didTapZoomIn(date: date)
|
||||
// }
|
||||
// controller.setChartTitleClosure = { [unowned self] (title, animated) in
|
||||
// self.setChartTitleClosure?(title, animated)
|
||||
// }
|
||||
// controller.setDetailsViewPositionClosure = { [unowned self] (position) in
|
||||
// self.setDetailsViewPositionClosure?(position)
|
||||
// }
|
||||
// controller.setDetailsChartVisibleClosure = { [unowned self] (visible, animated) in
|
||||
// self.setDetailsChartVisibleClosure?(visible, animated)
|
||||
// }
|
||||
// controller.setDetailsViewModel = { [unowned self] (viewModel, animated) in
|
||||
// self.setDetailsViewModel?(viewModel, animated)
|
||||
// }
|
||||
// controller.updatePreviewRangeClosure = { [unowned self] (fraction, animated) in
|
||||
// self.chartRangeUpdatedClosure?(fraction, animated)
|
||||
// }
|
||||
// controller.chartRangePagingClosure = { [unowned self] (isEnabled, pageSize) in
|
||||
// self.setChartRangePagingEnabled(isEnabled: isEnabled, minimumSelectionSize: pageSize)
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
public override var mainChartRenderers: [ChartViewRenderer] {
|
||||
var renderers: [ChartViewRenderer] = []
|
||||
self.graphControllers.forEach { controller in
|
||||
renderers.append(controller.mainBarsRenderer)
|
||||
}
|
||||
renderers.append(self.horizontalScalesRenderer)
|
||||
self.graphControllers.forEach { controller in
|
||||
renderers.append(controller.verticalScalesRenderer)
|
||||
renderers.append(controller.lineBulletsRenderer)
|
||||
}
|
||||
renderers.append(self.verticalLineRenderer)
|
||||
return renderers
|
||||
}
|
||||
|
||||
public override var navigationRenderers: [ChartViewRenderer] {
|
||||
return graphControllers.map { $0.previewBarsRenderer }
|
||||
}
|
||||
|
||||
public override func initializeChart() {
|
||||
if let first = initialChartCollection.axisValues.first?.timeIntervalSince1970,
|
||||
let last = initialChartCollection.axisValues.last?.timeIntervalSince1970 {
|
||||
initialChartRange = CGFloat(max(first, last - BaseConstants.defaultRangePresetLength))...CGFloat(last)
|
||||
}
|
||||
setupChartCollection(chartsCollection: initialChartCollection, animated: false, isZoomed: false)
|
||||
}
|
||||
|
||||
public override func updateChartsVisibility(visibility: [Bool], animated: Bool) {
|
||||
self.chartVisibility = visibility
|
||||
self.zoomChartVisibility = visibility
|
||||
let firstIndex = visibility.firstIndex(where: { $0 })
|
||||
for (index, isVisible) in visibility.enumerated() {
|
||||
let graph = graphControllers[index]
|
||||
for graphIndex in graph.chartBars.components.indices {
|
||||
graph.mainBarsRenderer.setComponentVisible(isVisible, at: graphIndex, animated: animated)
|
||||
graph.previewBarsRenderer.setComponentVisible(isVisible, at: graphIndex, animated: animated)
|
||||
graph.lineBulletsRenderer.setLineVisible(isVisible, at: graphIndex, animated: animated)
|
||||
}
|
||||
graph.verticalScalesRenderer.setVisible(isVisible, animated: animated)
|
||||
if let firstIndex = firstIndex {
|
||||
graph.verticalScalesRenderer.setHorizontalLinesVisible(index == firstIndex, animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
// updateVerticalLimitsAndRange(horizontalRange: currentHorizontalRange, animated: true)
|
||||
|
||||
if isChartInteractionBegun {
|
||||
chartInteractionDidBegin(point: lastChartInteractionPoint)
|
||||
}
|
||||
}
|
||||
|
||||
private func findClosestDateTo(dateToFind: Date) -> (Date, Int)? {
|
||||
guard self.initialChartCollection.axisValues.count > 0 else { return nil }
|
||||
var closestDate = self.initialChartCollection.axisValues[0]
|
||||
var minIndex = 0
|
||||
for (index, date) in self.initialChartCollection.axisValues.enumerated() {
|
||||
if abs(dateToFind.timeIntervalSince(date)) < abs(dateToFind.timeIntervalSince(closestDate)) {
|
||||
closestDate = date
|
||||
minIndex = index
|
||||
}
|
||||
}
|
||||
return (closestDate, minIndex)
|
||||
}
|
||||
|
||||
public override func chartInteractionDidBegin(point: CGPoint) {
|
||||
let horizontalRange = currentHorizontalRange
|
||||
let chartFrame = self.chartFrame()
|
||||
guard chartFrame.width > 0 else { return }
|
||||
|
||||
let dateToFind = Date(timeIntervalSince1970: TimeInterval(horizontalRange.distance * point.x + horizontalRange.lowerBound))
|
||||
guard let (closestDate, minIndex) = findClosestDateTo(dateToFind: dateToFind) else { return }
|
||||
|
||||
let chartInteractionWasBegin = isChartInteractionBegun
|
||||
super.chartInteractionDidBegin(point: point)
|
||||
|
||||
// for graphController in graphControllers {
|
||||
// graphController.lineBulletsRenderer.bullets = graphController.chartBars.components.map { component in
|
||||
// LineBulletsRenderer.Bullet(coordinate: component.values[minIndex], color: component.color)
|
||||
// }
|
||||
// graphController.lineBulletsRenderer.isEnabled = true
|
||||
// }
|
||||
|
||||
let chartValue: CGFloat = CGFloat(closestDate.timeIntervalSince1970)
|
||||
let detailsViewPosition = (chartValue - horizontalRange.lowerBound) / horizontalRange.distance * chartFrame.width + chartFrame.minX
|
||||
self.setDetailsViewModel?(chartDetailsViewModel(closestDate: closestDate, pointIndex: minIndex), chartInteractionWasBegin)
|
||||
self.setDetailsChartVisibleClosure?(true, true)
|
||||
self.setDetailsViewPositionClosure?(detailsViewPosition)
|
||||
self.verticalLineRenderer.values = [chartValue]
|
||||
}
|
||||
|
||||
// func chartDetailsViewModel(closestDate: Date, pointIndex: Int) -> ChartDetailsViewModel {
|
||||
// var viewModel = super.chartDetailsViewModel(closestDate: closestDate, pointIndex: pointIndex)
|
||||
// let visibleChartValues = self.visibleChartValues
|
||||
// let totalSumm: CGFloat = visibleChartValues.map { CGFloat($0.values[pointIndex]) }.reduce(0, +)
|
||||
//
|
||||
// viewModel.totalValue = ChartDetailsViewModel.Value(prefix: nil,
|
||||
// title: "Total",
|
||||
// value: BaseConstants.detailsNumberFormatter.string(from: totalSumm),
|
||||
// color: .white,
|
||||
// visible: visibleChartValues.count > 1)
|
||||
// return viewModel
|
||||
// }
|
||||
//
|
||||
func chartDetailsViewModel(closestDate: Date, pointIndex: Int) -> ChartDetailsViewModel {
|
||||
let values: [ChartDetailsViewModel.Value] = initialChartCollection.chartValues.enumerated().map { arg in
|
||||
let (index, component) = arg
|
||||
return ChartDetailsViewModel.Value(prefix: nil,
|
||||
title: component.name,
|
||||
value: BaseConstants.detailsNumberFormatter.string(from: NSNumber(value: component.values[pointIndex])) ?? "",
|
||||
color: component.color,
|
||||
visible: chartVisibility[index])
|
||||
}
|
||||
let dateString: String
|
||||
if isZoomed {
|
||||
dateString = BaseConstants.timeDateFormatter.string(from: closestDate)
|
||||
} else {
|
||||
dateString = BaseConstants.headerMediumRangeFormatter.string(from: closestDate)
|
||||
}
|
||||
let viewModel = ChartDetailsViewModel(title: dateString,
|
||||
showArrow: self.isZoomable && !self.isZoomed,
|
||||
showPrefixes: false,
|
||||
values: values,
|
||||
totalValue: nil,
|
||||
tapAction: { [weak self] in },
|
||||
hideAction: { [weak self] in
|
||||
self?.setDetailsChartVisibleClosure?(false, true)
|
||||
})
|
||||
return viewModel
|
||||
}
|
||||
|
||||
public override func chartInteractionDidEnd() {
|
||||
self.isChartInteractionBegun = false
|
||||
}
|
||||
|
||||
public override var currentHorizontalRange: ClosedRange<CGFloat> {
|
||||
return graphControllers.first?.mainBarsRenderer.horizontalRange.end ?? BaseConstants.defaultRange
|
||||
}
|
||||
|
||||
public override var currentChartHorizontalRangeFraction: ClosedRange<CGFloat> {
|
||||
let lowerPercent = (currentHorizontalRange.lowerBound - totalHorizontalRange.lowerBound) / totalHorizontalRange.distance
|
||||
let upperPercent = (currentHorizontalRange.upperBound - totalHorizontalRange.lowerBound) / totalHorizontalRange.distance
|
||||
return lowerPercent...upperPercent
|
||||
}
|
||||
|
||||
public override func cancelChartInteraction() {
|
||||
super.cancelChartInteraction()
|
||||
self.graphControllers.forEach { controller in
|
||||
controller.lineBulletsRenderer.isEnabled = false
|
||||
}
|
||||
|
||||
self.setDetailsChartVisibleClosure?(false, true)
|
||||
self.verticalLineRenderer.values = []
|
||||
}
|
||||
|
||||
func setupChartCollection(chartsCollection: ChartsCollection, animated: Bool, isZoomed: Bool) {
|
||||
for (index, controller) in self.graphControllers.enumerated() {
|
||||
let chart = chartsCollection.chartValues[index]
|
||||
let points = chart.values.enumerated().map({ (arg) -> CGPoint in
|
||||
return CGPoint(x: chartsCollection.axisValues[arg.offset].timeIntervalSince1970,
|
||||
y: arg.element)
|
||||
})
|
||||
|
||||
let (width, chartBars, totalHorizontalRange, totalVerticalRange) = BarChartRenderer.BarsData.initialComponents(chartsCollection: chartsCollection)
|
||||
controller.chartBars = chartBars
|
||||
controller.barsWidth = width
|
||||
|
||||
controller.verticalScalesRenderer.labelsColor = chart.color
|
||||
|
||||
controller.totalVerticalRange = totalVerticalRange
|
||||
self.totalHorizontalRange = totalHorizontalRange
|
||||
// controller.lineBulletsRenderer.bullets = chartBars.components.map { LineBulletsRenderer.Bullet(coordinate: $0.values.first ?? .zero,
|
||||
// color: $0.color) }
|
||||
controller.previewBarsRenderer.setup(horizontalRange: self.totalHorizontalRange, animated: animated)
|
||||
controller.previewBarsRenderer.setup(verticalRange: controller.totalVerticalRange, animated: animated)
|
||||
|
||||
controller.mainBarsRenderer.bars = chartBars
|
||||
controller.previewBarsRenderer.bars = chartBars
|
||||
|
||||
controller.verticalScalesRenderer.setHorizontalLinesVisible((index == 0), animated: animated)
|
||||
controller.verticalScalesRenderer.isRightAligned = (index != 0)
|
||||
}
|
||||
|
||||
let chartRange: ClosedRange<CGFloat>
|
||||
if isZoomed {
|
||||
chartRange = zoomedChartRange
|
||||
} else {
|
||||
chartRange = initialChartRange
|
||||
}
|
||||
|
||||
// updateHorizontalLimits(horizontalRange: chartRange, animated: animated)
|
||||
updateMainChartHorizontalRange(range: chartRange, animated: animated)
|
||||
updateMainChartVerticalRange(range: chartRange, animated: animated)
|
||||
// updateVerticalLimitsAndRange(horizontalRange: chartRange, animated: animated)
|
||||
|
||||
self.chartRangeUpdatedClosure?(currentChartHorizontalRangeFraction, animated)
|
||||
}
|
||||
|
||||
// func setupChartCollection(chartsCollection: ChartsCollection, animated: Bool, isZoomed: Bool) {
|
||||
// if animated {
|
||||
// TimeInterval.setDefaultSuration(.expandAnimationDuration)
|
||||
// DispatchQueue.main.asyncAfter(deadline: .now() + .expandAnimationDuration) {
|
||||
// TimeInterval.setDefaultSuration(.osXDuration)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// self.initialChartsCollection = chartsCollection
|
||||
// self.isZoomed = isZoomed
|
||||
//
|
||||
// self.setBackButtonVisibilityClosure?(isZoomed, animated)
|
||||
//
|
||||
// self.graphControllers.forEach { controller in
|
||||
// controller.barsController.willAppear(animated: animated)
|
||||
// }
|
||||
//
|
||||
// self.refreshChartToolsClosure?(animated)
|
||||
// }
|
||||
|
||||
public override func didTapZoomIn(date: Date) {
|
||||
guard isZoomed == false else { return }
|
||||
cancelChartInteraction()
|
||||
self.getDetailsData?(date, { updatedCollection in
|
||||
if let updatedCollection = updatedCollection {
|
||||
self.initialChartRange = self.currentHorizontalRange
|
||||
if let startDate = updatedCollection.axisValues.first,
|
||||
let endDate = updatedCollection.axisValues.last {
|
||||
self.zoomedChartRange = CGFloat(max(date.timeIntervalSince1970, startDate.timeIntervalSince1970))...CGFloat(min(date.timeIntervalSince1970 + .day - .hour, endDate.timeIntervalSince1970))
|
||||
} else {
|
||||
self.zoomedChartRange = CGFloat(date.timeIntervalSince1970)...CGFloat(date.timeIntervalSince1970 + .day - 1)
|
||||
}
|
||||
self.setupChartCollection(chartsCollection: updatedCollection, animated: true, isZoomed: true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public override func didTapZoomOut() {
|
||||
cancelChartInteraction()
|
||||
self.setupChartCollection(chartsCollection: self.initialChartCollection, animated: true, isZoomed: false)
|
||||
}
|
||||
|
||||
public override func updateChartRange(_ rangeFraction: ClosedRange<CGFloat>, animated: Bool) {
|
||||
cancelChartInteraction()
|
||||
|
||||
let horizontalRange = ClosedRange(uncheckedBounds:
|
||||
(lower: totalHorizontalRange.lowerBound + rangeFraction.lowerBound * totalHorizontalRange.distance,
|
||||
upper: totalHorizontalRange.lowerBound + rangeFraction.upperBound * totalHorizontalRange.distance))
|
||||
|
||||
zoomedChartRange = horizontalRange
|
||||
// updateChartRangeTitle(animated: true)
|
||||
|
||||
updateMainChartHorizontalRange(range: horizontalRange, animated: false)
|
||||
// updateHorizontalLimits(horizontalRange: horizontalRange, animated: true)
|
||||
// updateVerticalLimitsAndRange(horizontalRange: horizontalRange, animated: true)
|
||||
|
||||
// barsController.chartRangeFractionDidUpdated(rangeFraction)
|
||||
//
|
||||
// let totalHorizontalRange = barsController.totalHorizontalRange
|
||||
// let horizontalRange = ClosedRange(uncheckedBounds:
|
||||
// (lower: totalHorizontalRange.lowerBound + rangeFraction.lowerBound * totalHorizontalRange.distance,
|
||||
// upper: totalHorizontalRange.lowerBound + rangeFraction.upperBound * totalHorizontalRange.distance))
|
||||
//
|
||||
// updateMainChartHorizontalRange(range: horizontalRange, animated: false)
|
||||
}
|
||||
|
||||
func updateMainChartHorizontalRange(range: ClosedRange<CGFloat>, animated: Bool) {
|
||||
self.graphControllers.forEach { controller in
|
||||
controller.mainBarsRenderer.setup(horizontalRange: range, animated: animated)
|
||||
// controller.horizontalScalesRenderer.setup(horizontalRange: range, animated: animated)
|
||||
controller.verticalScalesRenderer.setup(horizontalRange: range, animated: animated)
|
||||
controller.lineBulletsRenderer.setup(horizontalRange: range, animated: animated)
|
||||
}
|
||||
self.horizontalScalesRenderer.setup(horizontalRange: range, animated: animated)
|
||||
self.verticalLineRenderer.setup(horizontalRange: range, animated: animated)
|
||||
}
|
||||
|
||||
func updateMainChartVerticalRange(range: ClosedRange<CGFloat>, animated: Bool) {
|
||||
self.verticalLineRenderer.setup(verticalRange: range, animated: animated)
|
||||
|
||||
self.graphControllers.forEach { controller in
|
||||
controller.lineBulletsRenderer.setup(verticalRange: range, animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
override public func apply(theme: ChartTheme, animated: Bool) {
|
||||
super.apply(theme: theme, animated: animated)
|
||||
|
||||
self.graphControllers.forEach { controller in
|
||||
controller.verticalScalesRenderer.horizontalLinesColor = theme.chartHelperLinesColor
|
||||
controller.lineBulletsRenderer.setInnerColor(theme.chartBackgroundColor, animated: animated)
|
||||
controller.verticalScalesRenderer.axisXColor = theme.chartStrongLinesColor
|
||||
}
|
||||
verticalLineRenderer.linesColor = theme.chartStrongLinesColor
|
||||
}
|
||||
|
||||
public override var drawChartVisibity: Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: Убрать Performance полоски сверзу чартов (Не забыть)
|
||||
//TODO: Добавить ховеры на кнопки
|
||||
@ -0,0 +1,309 @@
|
||||
import Foundation
|
||||
#if os(macOS)
|
||||
import Cocoa
|
||||
#else
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
private enum Constants {
|
||||
static let verticalBaseAnchors: [CGFloat] = [8, 5, 4, 2.5, 2, 1]
|
||||
}
|
||||
|
||||
public class TwoAxisStepBarsChartController: BaseLinesChartController {
|
||||
class GraphController {
|
||||
let mainBarsRenderer = BarChartRenderer(step: true)
|
||||
let verticalScalesRenderer = VerticalScalesRenderer()
|
||||
let lineBulletsRenderer = LineBulletsRenderer()
|
||||
let previewBarsRenderer = BarChartRenderer(step: true, lineWidth: 1.0)
|
||||
|
||||
var chartBars: BarChartRenderer.BarsData = .blank
|
||||
var barsWidth: CGFloat = 1
|
||||
|
||||
var totalVerticalRange: ClosedRange<CGFloat> = BaseConstants.defaultRange
|
||||
|
||||
init() {
|
||||
self.lineBulletsRenderer.isEnabled = false
|
||||
|
||||
self.mainBarsRenderer.optimizationLevel = BaseConstants.barsChartOptimizationLevel
|
||||
self.previewBarsRenderer.optimizationLevel = BaseConstants.barsChartOptimizationLevel
|
||||
}
|
||||
|
||||
func updateMainChartVerticalRange(range: ClosedRange<CGFloat>, animated: Bool) {
|
||||
mainBarsRenderer.setup(verticalRange: range, animated: animated)
|
||||
verticalScalesRenderer.setup(verticalRange: range, animated: animated)
|
||||
lineBulletsRenderer.setup(verticalRange: range, animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
private var graphControllers: [GraphController] = []
|
||||
private let verticalLineRenderer = VerticalLinesRenderer()
|
||||
private let horizontalScalesRenderer = HorizontalScalesRenderer()
|
||||
|
||||
var totalHorizontalRange: ClosedRange<CGFloat> = BaseConstants.defaultRange
|
||||
|
||||
private let initialChartCollection: ChartsCollection
|
||||
|
||||
private var prevoiusHorizontalStrideInterval: Int = 1
|
||||
|
||||
override public init(chartsCollection: ChartsCollection) {
|
||||
self.initialChartCollection = chartsCollection
|
||||
graphControllers = chartsCollection.chartValues.map { _ in GraphController() }
|
||||
|
||||
super.init(chartsCollection: chartsCollection)
|
||||
self.zoomChartVisibility = chartVisibility
|
||||
}
|
||||
|
||||
override func setupChartCollection(chartsCollection: ChartsCollection, animated: Bool, isZoomed: Bool) {
|
||||
super.setupChartCollection(chartsCollection: chartsCollection, animated: animated, isZoomed: isZoomed)
|
||||
|
||||
for (index, controller) in self.graphControllers.enumerated() {
|
||||
let chart = chartsCollection.chartValues[index]
|
||||
let initialComponents = [BarChartRenderer.BarsData.Component(color: chart.color,
|
||||
values: chart.values.map { CGFloat($0) })]
|
||||
let (width, chartBars, totalHorizontalRange, totalVerticalRange) = BarChartRenderer.BarsData.initialComponents(chartsCollection: chartsCollection, separate: true, initialComponents: initialComponents)
|
||||
controller.chartBars = chartBars
|
||||
controller.verticalScalesRenderer.labelsColor = chart.color
|
||||
controller.barsWidth = width
|
||||
controller.totalVerticalRange = totalVerticalRange
|
||||
self.totalHorizontalRange = totalHorizontalRange
|
||||
|
||||
var bullets: [LineBulletsRenderer.Bullet] = []
|
||||
if let component = chartBars.components.first {
|
||||
for i in 0 ..< chartBars.locations.count {
|
||||
let location = chartBars.locations[i]
|
||||
let value = component.values[i]
|
||||
bullets.append(LineBulletsRenderer.Bullet(coordinate: CGPoint(x: location, y: value), color: component.color))
|
||||
}
|
||||
}
|
||||
|
||||
controller.lineBulletsRenderer.bullets = bullets
|
||||
controller.previewBarsRenderer.setup(horizontalRange: self.totalHorizontalRange, animated: animated)
|
||||
controller.previewBarsRenderer.setup(verticalRange: controller.totalVerticalRange, animated: animated)
|
||||
controller.mainBarsRenderer.bars = chartBars
|
||||
controller.previewBarsRenderer.bars = chartBars
|
||||
|
||||
controller.verticalScalesRenderer.setHorizontalLinesVisible((index == 0), animated: animated)
|
||||
controller.verticalScalesRenderer.isRightAligned = (index != 0)
|
||||
}
|
||||
|
||||
self.prevoiusHorizontalStrideInterval = -1
|
||||
|
||||
let chartRange: ClosedRange<CGFloat>
|
||||
if isZoomed {
|
||||
chartRange = zoomedChartRange
|
||||
} else {
|
||||
chartRange = initialChartRange
|
||||
}
|
||||
|
||||
updateHorizontalLimits(horizontalRange: chartRange, animated: animated)
|
||||
updateMainChartHorizontalRange(range: chartRange, animated: animated)
|
||||
updateVerticalLimitsAndRange(horizontalRange: chartRange, animated: animated)
|
||||
|
||||
self.chartRangeUpdatedClosure?(currentChartHorizontalRangeFraction, animated)
|
||||
}
|
||||
|
||||
public override func initializeChart() {
|
||||
if let first = initialChartCollection.axisValues.first?.timeIntervalSince1970,
|
||||
let last = initialChartCollection.axisValues.last?.timeIntervalSince1970 {
|
||||
initialChartRange = CGFloat(max(first, last - BaseConstants.defaultRangePresetLength))...CGFloat(last)
|
||||
}
|
||||
setupChartCollection(chartsCollection: initialChartCollection, animated: false, isZoomed: false)
|
||||
}
|
||||
|
||||
public override var mainChartRenderers: [ChartViewRenderer] {
|
||||
return graphControllers.map { $0.mainBarsRenderer } +
|
||||
graphControllers.flatMap { [$0.verticalScalesRenderer, $0.lineBulletsRenderer] } +
|
||||
[horizontalScalesRenderer, verticalLineRenderer,
|
||||
// performanceRenderer
|
||||
]
|
||||
}
|
||||
|
||||
public override var navigationRenderers: [ChartViewRenderer] {
|
||||
return graphControllers.map { $0.previewBarsRenderer }
|
||||
}
|
||||
|
||||
public override func updateChartsVisibility(visibility: [Bool], animated: Bool) {
|
||||
chartVisibility = visibility
|
||||
zoomChartVisibility = visibility
|
||||
let firstIndex = visibility.firstIndex(where: { $0 })
|
||||
for (index, isVisible) in visibility.enumerated() {
|
||||
let graph = graphControllers[index]
|
||||
graph.mainBarsRenderer.setVisible(isVisible, animated: animated)
|
||||
graph.previewBarsRenderer.setVisible(isVisible, animated: animated)
|
||||
graph.lineBulletsRenderer.setLineVisible(isVisible, at: 0, animated: animated)
|
||||
graph.verticalScalesRenderer.setVisible(isVisible, animated: animated)
|
||||
if let firstIndex = firstIndex {
|
||||
graph.verticalScalesRenderer.setHorizontalLinesVisible(index == firstIndex, animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
updateVerticalLimitsAndRange(horizontalRange: currentHorizontalRange, animated: true)
|
||||
|
||||
if isChartInteractionBegun {
|
||||
chartInteractionDidBegin(point: lastChartInteractionPoint)
|
||||
}
|
||||
}
|
||||
|
||||
public override func chartInteractionDidBegin(point: CGPoint) {
|
||||
let horizontalRange = currentHorizontalRange
|
||||
let chartFrame = self.chartFrame()
|
||||
guard chartFrame.width > 0 else { return }
|
||||
|
||||
let dateToFind = Date(timeIntervalSince1970: TimeInterval(horizontalRange.distance * point.x + horizontalRange.lowerBound))
|
||||
guard let (closestDate, minIndex) = findClosestDateTo(dateToFind: dateToFind) else { return }
|
||||
|
||||
let chartInteractionWasBegin = isChartInteractionBegun
|
||||
super.chartInteractionDidBegin(point: point)
|
||||
|
||||
for graphController in graphControllers {
|
||||
|
||||
var bullets: [LineBulletsRenderer.Bullet] = []
|
||||
if let component = graphController.chartBars.components.first {
|
||||
let location = graphController.chartBars.locations[minIndex]
|
||||
let value = component.values[minIndex]
|
||||
bullets.append(LineBulletsRenderer.Bullet(coordinate: CGPoint(x: location, y: value), color: component.color))
|
||||
}
|
||||
graphController.lineBulletsRenderer.bullets = bullets
|
||||
graphController.lineBulletsRenderer.isEnabled = true
|
||||
}
|
||||
|
||||
let chartValue: CGFloat = CGFloat(closestDate.timeIntervalSince1970)
|
||||
let detailsViewPosition = (chartValue - horizontalRange.lowerBound) / horizontalRange.distance * chartFrame.width + chartFrame.minX
|
||||
self.setDetailsViewModel?(chartDetailsViewModel(closestDate: closestDate, pointIndex: minIndex), chartInteractionWasBegin)
|
||||
self.setDetailsChartVisibleClosure?(true, true)
|
||||
self.setDetailsViewPositionClosure?(detailsViewPosition)
|
||||
self.verticalLineRenderer.values = [chartValue]
|
||||
}
|
||||
|
||||
public override var currentChartHorizontalRangeFraction: ClosedRange<CGFloat> {
|
||||
let lowerPercent = (currentHorizontalRange.lowerBound - totalHorizontalRange.lowerBound) / totalHorizontalRange.distance
|
||||
let upperPercent = (currentHorizontalRange.upperBound - totalHorizontalRange.lowerBound) / totalHorizontalRange.distance
|
||||
return lowerPercent...upperPercent
|
||||
}
|
||||
|
||||
public override var currentHorizontalRange: ClosedRange<CGFloat> {
|
||||
return graphControllers.first?.mainBarsRenderer.horizontalRange.end ?? BaseConstants.defaultRange
|
||||
}
|
||||
|
||||
public override func cancelChartInteraction() {
|
||||
super.cancelChartInteraction()
|
||||
for graphController in graphControllers {
|
||||
graphController.lineBulletsRenderer.isEnabled = false
|
||||
}
|
||||
|
||||
self.setDetailsChartVisibleClosure?(false, true)
|
||||
self.verticalLineRenderer.values = []
|
||||
}
|
||||
|
||||
public override func didTapZoomOut() {
|
||||
cancelChartInteraction()
|
||||
self.setupChartCollection(chartsCollection: initialChartCollection, animated: true, isZoomed: false)
|
||||
}
|
||||
|
||||
public override func updateChartRange(_ rangeFraction: ClosedRange<CGFloat>, animated: Bool = true) {
|
||||
cancelChartInteraction()
|
||||
|
||||
let horizontalRange = ClosedRange(uncheckedBounds:
|
||||
(lower: totalHorizontalRange.lowerBound + rangeFraction.lowerBound * totalHorizontalRange.distance,
|
||||
upper: totalHorizontalRange.lowerBound + rangeFraction.upperBound * totalHorizontalRange.distance))
|
||||
|
||||
zoomedChartRange = horizontalRange
|
||||
updateChartRangeTitle(animated: true)
|
||||
|
||||
updateMainChartHorizontalRange(range: horizontalRange, animated: false)
|
||||
updateHorizontalLimits(horizontalRange: horizontalRange, animated: true)
|
||||
updateVerticalLimitsAndRange(horizontalRange: horizontalRange, animated: true)
|
||||
}
|
||||
|
||||
func updateMainChartHorizontalRange(range: ClosedRange<CGFloat>, animated: Bool) {
|
||||
for controller in graphControllers {
|
||||
controller.mainBarsRenderer.setup(horizontalRange: range, animated: animated)
|
||||
controller.verticalScalesRenderer.setup(horizontalRange: range, animated: animated)
|
||||
controller.lineBulletsRenderer.setup(horizontalRange: range, animated: animated)
|
||||
}
|
||||
horizontalScalesRenderer.setup(horizontalRange: range, animated: animated)
|
||||
verticalLineRenderer.setup(horizontalRange: range, animated: animated)
|
||||
}
|
||||
|
||||
func updateHorizontalLimits(horizontalRange: ClosedRange<CGFloat>, animated: Bool) {
|
||||
if let (stride, labels) = horizontalLimitsLabels(horizontalRange: horizontalRange,
|
||||
scaleType: isZoomed ? .minutes5 : .day,
|
||||
prevoiusHorizontalStrideInterval: prevoiusHorizontalStrideInterval) {
|
||||
self.horizontalScalesRenderer.setup(labels: labels, animated: animated)
|
||||
self.prevoiusHorizontalStrideInterval = stride
|
||||
}
|
||||
}
|
||||
|
||||
func updateVerticalLimitsAndRange(horizontalRange: ClosedRange<CGFloat>, animated: Bool) {
|
||||
let chartHeight = chartFrame().height
|
||||
let approximateNumberOfChartValues = (chartHeight / BaseConstants.minimumAxisYLabelsDistance)
|
||||
|
||||
let dividorsAndMultiplers: [(startValue: CGFloat, base: CGFloat, count: Int, maximumNumberOfDecimals: Int)] = graphControllers.enumerated().map { arg in
|
||||
let (index, controller) = arg
|
||||
let verticalRange = BarChartRenderer.BarsData.verticalRange(bars: controller.chartBars, separate: true, calculatingRange: horizontalRange, addBounds: true) ?? controller.totalVerticalRange
|
||||
|
||||
var numberOfOffsetsPerItem = verticalRange.distance / approximateNumberOfChartValues
|
||||
|
||||
var multiplier: CGFloat = 1.0
|
||||
while numberOfOffsetsPerItem > 10 {
|
||||
numberOfOffsetsPerItem /= 10
|
||||
multiplier *= 10
|
||||
}
|
||||
var dividor: CGFloat = 1.0
|
||||
var maximumNumberOfDecimals = 2
|
||||
while numberOfOffsetsPerItem < 1 {
|
||||
numberOfOffsetsPerItem *= 10
|
||||
dividor *= 10
|
||||
maximumNumberOfDecimals += 1
|
||||
}
|
||||
|
||||
let generalBase = Constants.verticalBaseAnchors.first { numberOfOffsetsPerItem > $0 } ?? BaseConstants.defaultVerticalBaseAnchor
|
||||
let base = generalBase * multiplier / dividor
|
||||
|
||||
var verticalValue = (verticalRange.lowerBound / base).rounded(.down) * base
|
||||
let startValue = verticalValue
|
||||
var count = 0
|
||||
if chartVisibility[index] {
|
||||
while verticalValue < verticalRange.upperBound {
|
||||
count += 1
|
||||
verticalValue += base
|
||||
}
|
||||
}
|
||||
return (startValue: startValue, base: base, count: count, maximumNumberOfDecimals: maximumNumberOfDecimals)
|
||||
}
|
||||
|
||||
let totalCount = dividorsAndMultiplers.map { $0.count }.max() ?? 0
|
||||
guard totalCount > 0 else { return }
|
||||
|
||||
let numberFormatter = BaseConstants.chartNumberFormatter
|
||||
for (index, controller) in graphControllers.enumerated() {
|
||||
let (startValue, base, _, maximumNumberOfDecimals) = dividorsAndMultiplers[index]
|
||||
|
||||
let updatedRange = startValue...(startValue + base * CGFloat(totalCount))
|
||||
if controller.verticalScalesRenderer.verticalRange.end != updatedRange {
|
||||
numberFormatter.maximumFractionDigits = maximumNumberOfDecimals
|
||||
|
||||
var verticalLabels: [LinesChartLabel] = []
|
||||
for multipler in 0...(totalCount - 1) {
|
||||
let verticalValue = startValue + base * CGFloat(multipler)
|
||||
let text: String = numberFormatter.string(from: NSNumber(value: Double(verticalValue))) ?? ""
|
||||
verticalLabels.append(LinesChartLabel(value: verticalValue, text: text))
|
||||
}
|
||||
|
||||
controller.verticalScalesRenderer.setup(verticalLimitsLabels: verticalLabels, animated: animated)
|
||||
controller.updateMainChartVerticalRange(range: updatedRange, animated: animated)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override func apply(theme: ChartTheme, animated: Bool) {
|
||||
horizontalScalesRenderer.labelsColor = theme.chartLabelsColor
|
||||
verticalLineRenderer.linesColor = theme.chartStrongLinesColor
|
||||
|
||||
for controller in graphControllers {
|
||||
controller.verticalScalesRenderer.horizontalLinesColor = theme.chartHelperLinesColor
|
||||
controller.lineBulletsRenderer.setInnerColor(theme.chartBackgroundColor, animated: animated)
|
||||
controller.verticalScalesRenderer.axisXColor = theme.chartStrongLinesColor
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -27,9 +27,11 @@ class BarChartRenderer: BaseChartRenderer {
|
||||
}
|
||||
|
||||
private var step = false
|
||||
private var lineWidth: CGFloat = 2.0
|
||||
|
||||
init(step: Bool = false) {
|
||||
init(step: Bool = false, lineWidth: CGFloat = 2.0) {
|
||||
self.step = step
|
||||
self.lineWidth = lineWidth
|
||||
|
||||
super.init()
|
||||
}
|
||||
@ -158,14 +160,10 @@ class BarChartRenderer: BaseChartRenderer {
|
||||
var leftX = transform(toChartCoordinateHorizontal: currentLocation - bars.barWidth, chartFrame: chartFrame)
|
||||
var rightX: CGFloat = 0
|
||||
|
||||
let startPoint = CGPoint(x: leftX,
|
||||
y: transform(toChartCoordinateVertical: verticalRange.current.lowerBound, chartFrame: chartFrame))
|
||||
|
||||
var backgourndPaths: [[CGPoint]] = bars.components.map { _ in Array() }
|
||||
var backgroundPaths: [[CGPoint]] = bars.components.map { _ in Array() }
|
||||
let itemsCount = ((bars.locations.count - barIndex) * 2) + 4
|
||||
for path in backgourndPaths.indices {
|
||||
backgourndPaths[path].reserveCapacity(itemsCount)
|
||||
backgourndPaths[path].append(startPoint)
|
||||
for path in backgroundPaths.indices {
|
||||
backgroundPaths[path].reserveCapacity(itemsCount)
|
||||
}
|
||||
|
||||
var maxValues: [CGFloat] = bars.components.map { _ in 0 }
|
||||
@ -173,18 +171,13 @@ class BarChartRenderer: BaseChartRenderer {
|
||||
currentLocation = bars.locations[barIndex]
|
||||
rightX = transform(toChartCoordinateHorizontal: currentLocation, chartFrame: chartFrame)
|
||||
|
||||
var stackedValue: CGFloat = 0
|
||||
var bottomY: CGFloat = transform(toChartCoordinateVertical: stackedValue, chartFrame: chartFrame)
|
||||
let bottomY: CGFloat = transform(toChartCoordinateVertical: 0.0, chartFrame: chartFrame)
|
||||
for (index, component) in bars.components.enumerated() {
|
||||
let visibilityPercent = componentsAnimators[index].current
|
||||
if visibilityPercent == 0 { continue }
|
||||
|
||||
var value = component.values[barIndex]
|
||||
if value < 5000 {
|
||||
value *= 40
|
||||
}
|
||||
let value = component.values[barIndex]
|
||||
let height = value * visibilityPercent
|
||||
// stackedValue += height
|
||||
let topY = transform(toChartCoordinateVertical: height, chartFrame: chartFrame)
|
||||
let componentHeight = (bottomY - topY)
|
||||
maxValues[index] = max(maxValues[index], componentHeight)
|
||||
@ -195,9 +188,8 @@ class BarChartRenderer: BaseChartRenderer {
|
||||
height: componentHeight)
|
||||
selectedPaths[index].append(rect)
|
||||
}
|
||||
backgourndPaths[index].append(CGPoint(x: leftX, y: topY))
|
||||
backgourndPaths[index].append(CGPoint(x: rightX, y: topY))
|
||||
// bottomY = topY
|
||||
backgroundPaths[index].append(CGPoint(x: leftX, y: topY))
|
||||
backgroundPaths[index].append(CGPoint(x: rightX, y: topY))
|
||||
}
|
||||
if currentLocation > range.upperBound {
|
||||
break
|
||||
@ -206,8 +198,6 @@ class BarChartRenderer: BaseChartRenderer {
|
||||
barIndex += 1
|
||||
}
|
||||
|
||||
let endPoint = CGPoint(x: transform(toChartCoordinateHorizontal: currentLocation, chartFrame: chartFrame).roundedUpToPixelGrid(),
|
||||
y: transform(toChartCoordinateVertical: verticalRange.current.lowerBound, chartFrame: chartFrame))
|
||||
let colorOffset = Double((1.0 - (1.0 - generalUnselectedAlpha) * selectedIndexAnimator.current) * chartsAlpha)
|
||||
|
||||
for (index, component) in bars.components.enumerated().reversed() {
|
||||
@ -216,12 +206,12 @@ class BarChartRenderer: BaseChartRenderer {
|
||||
}
|
||||
context.saveGState()
|
||||
|
||||
context.setLineWidth(2.0)
|
||||
context.setLineWidth(self.lineWidth)
|
||||
context.setStrokeColor(GColor.valueBetween(start: backgroundColorAnimator.current.color,
|
||||
end: component.color,
|
||||
offset: colorOffset).cgColor)
|
||||
context.beginPath()
|
||||
context.addLines(between: backgourndPaths[index])
|
||||
context.addLines(between: backgroundPaths[index])
|
||||
context.strokePath()
|
||||
context.restoreGState()
|
||||
}
|
||||
@ -236,11 +226,11 @@ class BarChartRenderer: BaseChartRenderer {
|
||||
let startPoint = CGPoint(x: leftX,
|
||||
y: transform(toChartCoordinateVertical: verticalRange.current.lowerBound, chartFrame: chartFrame))
|
||||
|
||||
var backgourndPaths: [[CGPoint]] = bars.components.map { _ in Array() }
|
||||
var backgroundPaths: [[CGPoint]] = bars.components.map { _ in Array() }
|
||||
let itemsCount = ((bars.locations.count - barIndex) * 2) + 4
|
||||
for path in backgourndPaths.indices {
|
||||
backgourndPaths[path].reserveCapacity(itemsCount)
|
||||
backgourndPaths[path].append(startPoint)
|
||||
for path in backgroundPaths.indices {
|
||||
backgroundPaths[path].reserveCapacity(itemsCount)
|
||||
backgroundPaths[path].append(startPoint)
|
||||
}
|
||||
var maxValues: [CGFloat] = bars.components.map { _ in 0 }
|
||||
while barIndex < bars.locations.count {
|
||||
@ -265,8 +255,8 @@ class BarChartRenderer: BaseChartRenderer {
|
||||
height: componentHeight)
|
||||
selectedPaths[index].append(rect)
|
||||
}
|
||||
backgourndPaths[index].append(CGPoint(x: leftX, y: topY))
|
||||
backgourndPaths[index].append(CGPoint(x: rightX, y: topY))
|
||||
backgroundPaths[index].append(CGPoint(x: leftX, y: topY))
|
||||
backgroundPaths[index].append(CGPoint(x: rightX, y: topY))
|
||||
bottomY = topY
|
||||
}
|
||||
if currentLocation > range.upperBound {
|
||||
@ -285,13 +275,13 @@ class BarChartRenderer: BaseChartRenderer {
|
||||
continue
|
||||
}
|
||||
context.saveGState()
|
||||
backgourndPaths[index].append(endPoint)
|
||||
backgroundPaths[index].append(endPoint)
|
||||
|
||||
context.setFillColor(GColor.valueBetween(start: backgroundColorAnimator.current.color,
|
||||
end: component.color,
|
||||
offset: colorOffset).cgColor)
|
||||
context.beginPath()
|
||||
context.addLines(between: backgourndPaths[index])
|
||||
context.addLines(between: backgroundPaths[index])
|
||||
context.closePath()
|
||||
context.fillPath()
|
||||
context.restoreGState()
|
||||
@ -308,7 +298,7 @@ class BarChartRenderer: BaseChartRenderer {
|
||||
}
|
||||
|
||||
extension BarChartRenderer.BarsData {
|
||||
static func initialComponents(chartsCollection: ChartsCollection) ->
|
||||
static func initialComponents(chartsCollection: ChartsCollection, separate: Bool = false, initialComponents: [BarChartRenderer.BarsData.Component]? = nil) ->
|
||||
(width: CGFloat,
|
||||
chartBars: BarChartRenderer.BarsData,
|
||||
totalHorizontalRange: ClosedRange<CGFloat>,
|
||||
@ -319,15 +309,13 @@ extension BarChartRenderer.BarsData {
|
||||
} else {
|
||||
width = 1
|
||||
}
|
||||
let components = chartsCollection.chartValues.map { BarChartRenderer.BarsData.Component(color: $0.color,
|
||||
let components = initialComponents ?? chartsCollection.chartValues.map { BarChartRenderer.BarsData.Component(color: $0.color,
|
||||
values: $0.values.map { CGFloat($0) }) }
|
||||
let chartBars = BarChartRenderer.BarsData(barWidth: width,
|
||||
locations: chartsCollection.axisValues.map { CGFloat($0.timeIntervalSince1970) },
|
||||
components: components)
|
||||
|
||||
|
||||
|
||||
let totalVerticalRange = BarChartRenderer.BarsData.verticalRange(bars: chartBars) ?? 0...1
|
||||
let totalVerticalRange = BarChartRenderer.BarsData.verticalRange(bars: chartBars, separate: separate) ?? 0...1
|
||||
let totalHorizontalRange = BarChartRenderer.BarsData.visibleHorizontalRange(bars: chartBars, width: width) ?? 0...1
|
||||
return (width: width, chartBars: chartBars, totalHorizontalRange: totalHorizontalRange, totalVerticalRange: totalVerticalRange)
|
||||
}
|
||||
@ -342,7 +330,7 @@ extension BarChartRenderer.BarsData {
|
||||
return (firstPoint - width)...lastPoint
|
||||
}
|
||||
|
||||
static func verticalRange(bars: BarChartRenderer.BarsData, calculatingRange: ClosedRange<CGFloat>? = nil, addBounds: Bool = false) -> ClosedRange<CGFloat>? {
|
||||
static func verticalRange(bars: BarChartRenderer.BarsData, separate: Bool = false, calculatingRange: ClosedRange<CGFloat>? = nil, addBounds: Bool = false) -> ClosedRange<CGFloat>? {
|
||||
guard bars.components.count > 0 else {
|
||||
return nil
|
||||
}
|
||||
@ -353,11 +341,17 @@ extension BarChartRenderer.BarsData {
|
||||
|
||||
var vMax: CGFloat = bars.components[0].values[index]
|
||||
while index < bars.locations.count {
|
||||
var summ: CGFloat = 0
|
||||
for component in bars.components {
|
||||
summ += component.values[index]
|
||||
if separate {
|
||||
for component in bars.components {
|
||||
vMax = max(vMax, component.values[index])
|
||||
}
|
||||
} else {
|
||||
var summ: CGFloat = 0
|
||||
for component in bars.components {
|
||||
summ += component.values[index]
|
||||
}
|
||||
vMax = max(vMax, summ)
|
||||
}
|
||||
vMax = max(vMax, summ)
|
||||
|
||||
if bars.locations[index] > calculatingRange.upperBound {
|
||||
break
|
||||
@ -370,11 +364,18 @@ extension BarChartRenderer.BarsData {
|
||||
|
||||
var vMax: CGFloat = bars.components[0].values[index]
|
||||
while index < bars.locations.count {
|
||||
var summ: CGFloat = 0
|
||||
for component in bars.components {
|
||||
summ += component.values[index]
|
||||
if separate {
|
||||
for component in bars.components {
|
||||
vMax = max(vMax, component.values[index])
|
||||
}
|
||||
} else {
|
||||
var summ: CGFloat = 0
|
||||
for component in bars.components {
|
||||
summ += component.values[index]
|
||||
}
|
||||
vMax = max(vMax, summ)
|
||||
}
|
||||
vMax = max(vMax, summ)
|
||||
|
||||
index += 1
|
||||
}
|
||||
return 0...vMax
|
||||
|
||||
@ -48,7 +48,7 @@ class LinesChartRenderer: BaseChartRenderer {
|
||||
linesShapeAnimator.set(current: 1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func setLineVisible(_ isVisible: Bool, at index: Int, animated: Bool) {
|
||||
linesAlphaAnimators[index].animate(to: isVisible ? 1 : 0, duration: animated ? .defaultDuration : 0)
|
||||
}
|
||||
@ -62,9 +62,10 @@ class LinesChartRenderer: BaseChartRenderer {
|
||||
for (index, toLine) in toLines.enumerated() {
|
||||
let alpha = linesAlphaAnimators[index].current * chartsAlpha
|
||||
if alpha == 0 { continue }
|
||||
context.setStrokeColor(toLine.color.withAlphaComponent(alpha).cgColor)
|
||||
context.setAlpha(alpha)
|
||||
context.setStrokeColor(toLine.color.cgColor)
|
||||
context.setLineWidth(lineWidth)
|
||||
|
||||
|
||||
if linesShapeAnimator.isAnimating {
|
||||
let animationOffset = linesShapeAnimator.current
|
||||
|
||||
@ -93,7 +94,7 @@ class LinesChartRenderer: BaseChartRenderer {
|
||||
var previousToPoint: CGPoint
|
||||
let startFromPoint: CGPoint?
|
||||
let startToPoint: CGPoint?
|
||||
|
||||
|
||||
if let validFrom = fromIndex {
|
||||
previousFromPoint = convertFromPoint(fromPoints[max(0, validFrom - 1)])
|
||||
startFromPoint = previousFromPoint
|
||||
@ -110,7 +111,7 @@ class LinesChartRenderer: BaseChartRenderer {
|
||||
}
|
||||
|
||||
var combinedPoints: [CGPoint] = []
|
||||
|
||||
|
||||
func add(pointToDraw: CGPoint) {
|
||||
if let startFromPoint = startFromPoint,
|
||||
pointToDraw.x < startFromPoint.x {
|
||||
@ -312,13 +313,7 @@ class LinesChartRenderer: BaseChartRenderer {
|
||||
|
||||
context.setLineCap(.round)
|
||||
context.strokeLineSegments(between: lines)
|
||||
|
||||
} else {
|
||||
let alpha = linesAlphaAnimators[index].current * chartsAlpha
|
||||
if alpha == 0 { continue }
|
||||
context.setStrokeColor(toLine.color.withAlphaComponent(alpha).cgColor)
|
||||
context.setLineWidth(lineWidth)
|
||||
|
||||
} else {
|
||||
if var index = toLine.points.firstIndex(where: { $0.x >= range.lowerBound }) {
|
||||
var lines: [CGPoint] = []
|
||||
index = max(0, index - 1)
|
||||
@ -436,33 +431,8 @@ class LinesChartRenderer: BaseChartRenderer {
|
||||
context.setLineCap(.round)
|
||||
context.strokeLineSegments(between: lines)
|
||||
}
|
||||
|
||||
// if var start = toLine.points.firstIndex(where: { $0.x > range.lowerBound }) {
|
||||
// let alpha = linesAlphaAnimators[index].current * chartsAlpha
|
||||
// if alpha == 0 { continue }
|
||||
// context.setStrokeColor(toLine.color.withAlphaComponent(alpha).cgColor)
|
||||
// context.setLineWidth(lineWidth)
|
||||
//
|
||||
// context.setLineCap(.round)
|
||||
// start = max(0, start - 1)
|
||||
// let startPoint = toLine.points[start]
|
||||
// var lines: [CGPoint] = []
|
||||
// var pointToDraw = CGPoint(x: transform(toChartCoordinateHorizontal: startPoint.x, chartFrame: chartFrame),
|
||||
// y: transform(toChartCoordinateVertical: startPoint.y, chartFrame: chartFrame))
|
||||
// for index in (start + 1)..<toLine.points.count {
|
||||
// lines.append(pointToDraw)
|
||||
// let point = toLine.points[index]
|
||||
// pointToDraw = CGPoint(x: transform(toChartCoordinateHorizontal: point.x, chartFrame: chartFrame),
|
||||
// y: transform(toChartCoordinateVertical: point.y, chartFrame: chartFrame))
|
||||
// lines.append(pointToDraw)
|
||||
// if point.x > range.upperBound {
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// context.strokeLineSegments(between: lines)
|
||||
// }
|
||||
}
|
||||
context.setAlpha(1.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,6 +54,21 @@ extension GColor {
|
||||
case "lightblue":
|
||||
self.init(hexString: "#5ac8fa")
|
||||
return
|
||||
case "seablue":
|
||||
self.init(hexString: "#35afdc")
|
||||
return
|
||||
case "orange":
|
||||
self.init(hexString: "#fd7a32")
|
||||
return
|
||||
case "violet":
|
||||
self.init(hexString: "#9968f7")
|
||||
return
|
||||
case "emerald":
|
||||
self.init(hexString: "#37cca3")
|
||||
return
|
||||
case "pink":
|
||||
self.init(hexString: "#ff4f79")
|
||||
return
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@ private let cornerRadius: CGFloat = 5
|
||||
private let verticalMargins: CGFloat = 8
|
||||
private var labelHeight: CGFloat = 18
|
||||
private var margin: CGFloat = 10
|
||||
private var prefixLabelWidth: CGFloat = 27
|
||||
private var prefixLabelWidth: CGFloat = 29
|
||||
private var textLabelWidth: CGFloat = 96
|
||||
private var valueLabelWidth: CGFloat = 65
|
||||
|
||||
@ -56,9 +56,10 @@ class ChartDetailsView: UIControl {
|
||||
func setup(viewModel: ChartDetailsViewModel, animated: Bool) {
|
||||
self.viewModel = viewModel
|
||||
|
||||
titleLabel.setText(viewModel.title, animated: animated)
|
||||
titleLabel.setText(viewModel.title, animated: false)
|
||||
titleLabel.setVisible(!viewModel.title.isEmpty, animated: animated)
|
||||
arrowView.setVisible(viewModel.showArrow, animated: animated)
|
||||
arrowButton.isUserInteractionEnabled = viewModel.showArrow
|
||||
|
||||
let width: CGFloat = margin * 2 + (viewModel.showPrefixes ? (prefixLabelWidth + margin) : 0) + textLabelWidth + valueLabelWidth
|
||||
var y: CGFloat = verticalMargins
|
||||
|
||||
@ -13,6 +13,8 @@ public enum ChartType {
|
||||
case pie
|
||||
case bars
|
||||
case step
|
||||
case twoAxisStep
|
||||
case hourlyStep
|
||||
}
|
||||
|
||||
public extension ChartTheme {
|
||||
@ -24,6 +26,54 @@ public extension ChartTheme {
|
||||
}
|
||||
}
|
||||
|
||||
public func createChartController(_ data: String, type: ChartType, getDetailsData: @escaping (Date, @escaping (String?) -> Void) -> Void) -> BaseChartController? {
|
||||
var resultController: BaseChartController?
|
||||
if let data = data.data(using: .utf8) {
|
||||
ChartsDataManager.readChart(data: data, extraCopiesCount: 0, sync: true, success: { collection in
|
||||
let controller: BaseChartController
|
||||
switch type {
|
||||
case .lines:
|
||||
controller = GeneralLinesChartController(chartsCollection: collection)
|
||||
controller.isZoomable = false
|
||||
case .twoAxis:
|
||||
controller = TwoAxisLinesChartController(chartsCollection: collection)
|
||||
controller.isZoomable = false
|
||||
case .pie:
|
||||
controller = PercentPieChartController(chartsCollection: collection)
|
||||
case .bars:
|
||||
controller = StackedBarsChartController(chartsCollection: collection)
|
||||
controller.isZoomable = false
|
||||
case .step:
|
||||
controller = StepBarsChartController(chartsCollection: collection)
|
||||
case .twoAxisStep:
|
||||
controller = TwoAxisStepBarsChartController(chartsCollection: collection)
|
||||
case .hourlyStep:
|
||||
controller = StepBarsChartController(chartsCollection: collection, hourly: true)
|
||||
controller.isZoomable = false
|
||||
}
|
||||
controller.getDetailsData = { date, completion in
|
||||
getDetailsData(date, { detailsData in
|
||||
if let detailsData = detailsData, let data = detailsData.data(using: .utf8) {
|
||||
ChartsDataManager.readChart(data: data, extraCopiesCount: 0, sync: true, success: { collection in
|
||||
Queue.mainQueue().async {
|
||||
completion(collection)
|
||||
}
|
||||
}) { error in
|
||||
completion(nil)
|
||||
}
|
||||
} else {
|
||||
completion(nil)
|
||||
}
|
||||
})
|
||||
}
|
||||
resultController = controller
|
||||
}) { error in
|
||||
|
||||
}
|
||||
}
|
||||
return resultController
|
||||
}
|
||||
|
||||
public final class ChartNode: ASDisplayNode {
|
||||
private var chartView: ChartStackSection {
|
||||
return self.view as! ChartStackSection
|
||||
@ -36,64 +86,21 @@ public final class ChartNode: ASDisplayNode {
|
||||
return ChartStackSection()
|
||||
})
|
||||
}
|
||||
|
||||
public override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.view.disablesInteractiveTransitionGestureRecognizer = true
|
||||
}
|
||||
|
||||
|
||||
public func setupTheme(_ theme: ChartTheme) {
|
||||
self.chartView.apply(theme: ChartTheme.defaultDayTheme, animated: false)
|
||||
}
|
||||
|
||||
public override func layout() {
|
||||
super.layout()
|
||||
|
||||
self.chartView.setNeedsDisplay()
|
||||
public func setup(controller: BaseChartController) {
|
||||
var displayRange = true
|
||||
if let controller = controller as? StepBarsChartController {
|
||||
displayRange = !controller.hourly
|
||||
}
|
||||
self.chartView.setup(controller: controller, displayRange: displayRange)
|
||||
}
|
||||
|
||||
public func setup(_ data: String, type: ChartType, getDetailsData: @escaping (Date, @escaping (String?) -> Void) -> Void) {
|
||||
if let data = data.data(using: .utf8) {
|
||||
ChartsDataManager.readChart(data: data, extraCopiesCount: 0, sync: true, success: { [weak self] collection in
|
||||
let controller: BaseChartController
|
||||
switch type {
|
||||
case .lines:
|
||||
controller = GeneralLinesChartController(chartsCollection: collection)
|
||||
controller.isZoomable = false
|
||||
case .twoAxis:
|
||||
controller = TwoAxisLinesChartController(chartsCollection: collection)
|
||||
controller.isZoomable = false
|
||||
case .pie:
|
||||
controller = PercentPieChartController(chartsCollection: collection)
|
||||
case .bars:
|
||||
controller = StackedBarsChartController(chartsCollection: collection)
|
||||
controller.isZoomable = false
|
||||
case .step:
|
||||
controller = StepBarsChartController(chartsCollection: collection)
|
||||
}
|
||||
controller.getDetailsData = { date, completion in
|
||||
getDetailsData(date, { detailsData in
|
||||
if let detailsData = detailsData, let data = detailsData.data(using: .utf8) {
|
||||
ChartsDataManager.readChart(data: data, extraCopiesCount: 0, sync: true, success: { collection in
|
||||
Queue.mainQueue().async {
|
||||
completion(collection)
|
||||
}
|
||||
}) { error in
|
||||
completion(nil)
|
||||
}
|
||||
} else {
|
||||
completion(nil)
|
||||
}
|
||||
})
|
||||
}
|
||||
if let strongSelf = self {
|
||||
strongSelf.chartView.setup(controller: controller, title: "")
|
||||
}
|
||||
}) { error in
|
||||
|
||||
}
|
||||
}
|
||||
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
|
||||
@ -35,6 +35,8 @@ class ChartStackSection: UIView, ChartThemeContainer {
|
||||
var controller: BaseChartController?
|
||||
var theme: ChartTheme?
|
||||
|
||||
var displayRange: Bool = true
|
||||
|
||||
init() {
|
||||
sectionContainerView = UIView()
|
||||
chartView = ChartView()
|
||||
@ -157,13 +159,20 @@ class ChartStackSection: UIView, ChartThemeContainer {
|
||||
self.titleLabel.frame = CGRect(origin: CGPoint(x: backButton.alpha > 0.0 ? 36.0 : 0.0, y: 5.0), size: CGSize(width: bounds.width, height: 28.0))
|
||||
self.sectionContainerView.frame = CGRect(origin: CGPoint(), size: CGSize(width: bounds.width, height: 750.0))
|
||||
self.chartView.frame = CGRect(origin: CGPoint(), size: CGSize(width: bounds.width, height: 250.0))
|
||||
|
||||
self.rangeView.isHidden = !self.displayRange
|
||||
|
||||
self.rangeView.frame = CGRect(origin: CGPoint(x: 0.0, y: 250.0), size: CGSize(width: bounds.width, height: 42.0))
|
||||
self.visibilityView.frame = CGRect(origin: CGPoint(x: 0.0, y: 308.0), size: CGSize(width: bounds.width, height: 350.0))
|
||||
self.visibilityView.frame = CGRect(origin: CGPoint(x: 0.0, y: self.displayRange ? 308.0 : 266.0), size: CGSize(width: bounds.width, height: 350.0))
|
||||
self.backButton.frame = CGRect(x: 0.0, y: 0.0, width: 96.0, height: 38.0)
|
||||
|
||||
self.chartView.setNeedsDisplay()
|
||||
}
|
||||
|
||||
func setup(controller: BaseChartController, title: String) {
|
||||
func setup(controller: BaseChartController, displayRange: Bool = true) {
|
||||
self.controller = controller
|
||||
self.displayRange = displayRange
|
||||
|
||||
if let theme = self.theme {
|
||||
controller.apply(theme: theme, animated: false)
|
||||
}
|
||||
@ -222,7 +231,10 @@ class ChartStackSection: UIView, ChartThemeContainer {
|
||||
controller.initializeChart()
|
||||
updateToolViews(animated: false)
|
||||
|
||||
rangeView.setRange(0.8...1.0, animated: false)
|
||||
controller.updateChartRange(0.8...1.0, animated: false)
|
||||
let range: ClosedRange<CGFloat> = displayRange ? 0.8 ... 1.0 : 0.0 ... 1.0
|
||||
rangeView.setRange(range, animated: false)
|
||||
controller.updateChartRange(range, animated: false)
|
||||
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,7 +75,9 @@ class ChartView: UIControl {
|
||||
}
|
||||
|
||||
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
if let point = touches.first?.location(in: self) {
|
||||
if var point = touches.first?.location(in: self) {
|
||||
point.x = max(0.0, min(self.frame.width, point.x))
|
||||
point.y = max(0.0, min(self.frame.height, point.y))
|
||||
let fractionPoint = CGPoint(x: (point.x - chartFrame.origin.x) / chartFrame.width,
|
||||
y: (point.y - chartFrame.origin.y) / chartFrame.height)
|
||||
userDidSelectCoordinateClosure?(fractionPoint)
|
||||
|
||||
@ -17,9 +17,42 @@ private enum Constants {
|
||||
static let insets = UIEdgeInsets(top: 0, left: 16, bottom: 16, right: 16)
|
||||
}
|
||||
|
||||
struct ChartVisibilityItem {
|
||||
public struct ChartVisibilityItem {
|
||||
var title: String
|
||||
var color: UIColor
|
||||
|
||||
public init(title: String, color: UIColor) {
|
||||
self.title = title
|
||||
self.color = color
|
||||
}
|
||||
}
|
||||
|
||||
public func calculateVisiblityHeight(width: CGFloat, items: [ChartVisibilityItem]) -> CGFloat {
|
||||
let frames = generateItemsFrames(frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: CGFloat.greatestFiniteMagnitude)), items: items)
|
||||
guard let lastFrame = frames.last else { return .zero }
|
||||
return lastFrame.maxY + Constants.insets.bottom
|
||||
}
|
||||
|
||||
private func generateItemsFrames(frame: CGRect, items: [ChartVisibilityItem]) -> [CGRect] {
|
||||
var previousPoint = CGPoint(x: Constants.insets.left, y: Constants.insets.top)
|
||||
var frames: [CGRect] = []
|
||||
|
||||
for item in items {
|
||||
let labelSize = (item.title as NSString).size(withAttributes: [.font: ChartVisibilityItemView.textFont])
|
||||
let width = (labelSize.width + Constants.labelTextApproxInsets).rounded(.up)
|
||||
if previousPoint.x + width < (frame.width - Constants.insets.left - Constants.insets.right) {
|
||||
frames.append(CGRect(origin: previousPoint, size: CGSize(width: width, height: Constants.itemHeight)))
|
||||
} else if previousPoint.x <= Constants.insets.left {
|
||||
frames.append(CGRect(origin: previousPoint, size: CGSize(width: width, height: Constants.itemHeight)))
|
||||
} else {
|
||||
previousPoint.y += Constants.itemHeight + Constants.itemSpacing
|
||||
previousPoint.x = Constants.insets.left
|
||||
frames.append(CGRect(origin: previousPoint, size: CGSize(width: width, height: Constants.itemHeight)))
|
||||
}
|
||||
previousPoint.x += width + Constants.itemSpacing
|
||||
}
|
||||
|
||||
return frames
|
||||
}
|
||||
|
||||
class ChartVisibilityView: UIView {
|
||||
@ -80,29 +113,7 @@ class ChartVisibilityView: UIView {
|
||||
}
|
||||
|
||||
private var selectionViews: [ChartVisibilityItemView] = []
|
||||
|
||||
private func generateItemsFrames(frame: CGRect) -> [CGRect] {
|
||||
var previousPoint = CGPoint(x: Constants.insets.left, y: Constants.insets.top)
|
||||
var frames: [CGRect] = []
|
||||
|
||||
for item in items {
|
||||
let labelSize = (item.title as NSString).size(withAttributes: [.font: ChartVisibilityItemView.textFont])
|
||||
let width = (labelSize.width + Constants.labelTextApproxInsets).rounded(.up)
|
||||
if previousPoint.x + width < (frame.width - Constants.insets.left - Constants.insets.right) {
|
||||
frames.append(CGRect(origin: previousPoint, size: CGSize(width: width, height: Constants.itemHeight)))
|
||||
} else if previousPoint.x <= Constants.insets.left {
|
||||
frames.append(CGRect(origin: previousPoint, size: CGSize(width: width, height: Constants.itemHeight)))
|
||||
} else {
|
||||
previousPoint.y += Constants.itemHeight + Constants.itemSpacing
|
||||
previousPoint.x = Constants.insets.left
|
||||
frames.append(CGRect(origin: previousPoint, size: CGSize(width: width, height: Constants.itemHeight)))
|
||||
}
|
||||
previousPoint.x += width + Constants.itemSpacing
|
||||
}
|
||||
|
||||
return frames
|
||||
}
|
||||
|
||||
var selectionCallbackClosure: (([Bool]) -> Void)?
|
||||
|
||||
func setItemSelected(_ selected: Bool, at index: Int, animated: Bool) {
|
||||
@ -129,7 +140,7 @@ class ChartVisibilityView: UIView {
|
||||
}
|
||||
|
||||
private func updateFrames() {
|
||||
for (index, frame) in generateItemsFrames(frame: bounds).enumerated() {
|
||||
for (index, frame) in generateItemsFrames(frame: bounds, items: self.items).enumerated() {
|
||||
selectionViews[index].frame = frame
|
||||
}
|
||||
}
|
||||
@ -140,7 +151,7 @@ class ChartVisibilityView: UIView {
|
||||
size.height = 0
|
||||
return size
|
||||
}
|
||||
let frames = generateItemsFrames(frame: UIScreen.main.bounds)
|
||||
let frames = generateItemsFrames(frame: UIScreen.main.bounds, items: self.items)
|
||||
guard let lastFrame = frames.last else { return .zero }
|
||||
let size = CGSize(width: frame.width, height: lastFrame.maxY + Constants.insets.bottom)
|
||||
return size
|
||||
|
||||
@ -293,7 +293,11 @@ typedef enum
|
||||
[_wrapperView addSubview:_fadeView];
|
||||
}
|
||||
|
||||
CGFloat circleWrapperViewLength = 216.0f + 38.0f;
|
||||
CGFloat minSide = MIN(_wrapperView.frame.size.width, _wrapperView.frame.size.height);
|
||||
CGFloat diameter = minSide > 320.0f ? 240.0f : 216.0f;
|
||||
CGFloat shadowSize = minSide > 320.0f ? 21.0f : 19.0f;
|
||||
|
||||
CGFloat circleWrapperViewLength = diameter + shadowSize * 2.0;
|
||||
_circleWrapperView = [[UIView alloc] initWithFrame:(CGRect){
|
||||
.origin.x = (_wrapperView.bounds.size.width - circleWrapperViewLength) / 2.0f,
|
||||
.origin.y = _wrapperView.bounds.size.height + circleWrapperViewLength * 0.3f,
|
||||
@ -309,7 +313,7 @@ typedef enum
|
||||
_shadowView.frame = _circleWrapperView.bounds;
|
||||
[_circleWrapperView addSubview:_shadowView];
|
||||
|
||||
_circleView = [[UIView alloc] initWithFrame:CGRectInset(_circleWrapperView.bounds, 19.0f, 19.0f)];
|
||||
_circleView = [[UIView alloc] initWithFrame:CGRectInset(_circleWrapperView.bounds, shadowSize, shadowSize)];
|
||||
_circleView.clipsToBounds = true;
|
||||
_circleView.layer.cornerRadius = _circleView.frame.size.width / 2.0f;
|
||||
[_circleWrapperView addSubview:_circleView];
|
||||
@ -325,7 +329,7 @@ typedef enum
|
||||
_placeholderView.accessibilityIgnoresInvertColors = true;
|
||||
}
|
||||
|
||||
CGFloat ringViewLength = 234.0f;
|
||||
CGFloat ringViewLength = minSide > 320.0f ? 260.0f : 234.0f;
|
||||
_ringView = [[TGVideoMessageRingView alloc] initWithFrame:(CGRect){
|
||||
.origin.x = (_circleWrapperView.bounds.size.width - ringViewLength) / 2.0f,
|
||||
.origin.y = (_circleWrapperView.bounds.size.height - ringViewLength) / 2.0f,
|
||||
|
||||
18
submodules/ManagedAnimationNode/BUCK
Normal file
18
submodules/ManagedAnimationNode/BUCK
Normal file
@ -0,0 +1,18 @@
|
||||
load("//Config:buck_rule_macros.bzl", "static_library")
|
||||
|
||||
static_library(
|
||||
name = "ManagedAnimationNode",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
deps = [
|
||||
"//submodules/Display:Display#shared",
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit#shared",
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit#shared",
|
||||
"//submodules/rlottie:RLottieBinding",
|
||||
],
|
||||
frameworks = [
|
||||
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
|
||||
"$SDKROOT/System/Library/Frameworks/UIKit.framework",
|
||||
],
|
||||
)
|
||||
21
submodules/ManagedAnimationNode/BUILD
Normal file
21
submodules/ManagedAnimationNode/BUILD
Normal file
@ -0,0 +1,21 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "ManagedAnimationNode",
|
||||
module_name = "ManagedAnimationNode",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
deps = [
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||
"//submodules/Postbox:Postbox",
|
||||
"//submodules/GZip:GZip",
|
||||
"//submodules/rlottie:RLottieBinding",
|
||||
"//submodules/AppBundle:AppBundle",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
22
submodules/ManagedAnimationNode/Info.plist
Normal file
22
submodules/ManagedAnimationNode/Info.plist
Normal file
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@ -0,0 +1,11 @@
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
//! Project version number for ManagedAnimationNode.
|
||||
FOUNDATION_EXPORT double ManagedAnimationNodeVersionNumber;
|
||||
|
||||
//! Project version string for ManagedAnimationNode.
|
||||
FOUNDATION_EXPORT const unsigned char ManagedAnimationNodeVersionString[];
|
||||
|
||||
// In this header, you should import all the public headers of your framework using statements like #import <ManagedAnimationNode/PublicHeader.h>
|
||||
|
||||
|
||||
@ -0,0 +1,231 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import Postbox
|
||||
import RLottieBinding
|
||||
import AppBundle
|
||||
import GZip
|
||||
import SwiftSignalKit
|
||||
|
||||
public final class ManagedAnimationState {
|
||||
public let item: ManagedAnimationItem
|
||||
|
||||
private let instance: LottieInstance
|
||||
|
||||
let frameCount: Int
|
||||
let fps: Double
|
||||
|
||||
var relativeTime: Double = 0.0
|
||||
public var frameIndex: Int?
|
||||
|
||||
private let renderContext: DrawingContext
|
||||
|
||||
public init?(displaySize: CGSize, item: ManagedAnimationItem, current: ManagedAnimationState?) {
|
||||
let resolvedInstance: LottieInstance
|
||||
let renderContext: DrawingContext
|
||||
|
||||
if let current = current {
|
||||
resolvedInstance = current.instance
|
||||
renderContext = current.renderContext
|
||||
} else {
|
||||
guard let path = item.source.path else {
|
||||
return nil
|
||||
}
|
||||
guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else {
|
||||
return nil
|
||||
}
|
||||
guard let unpackedData = TGGUnzipData(data, 5 * 1024 * 1024) else {
|
||||
return nil
|
||||
}
|
||||
guard let instance = LottieInstance(data: unpackedData, cacheKey: item.source.cacheKey) else {
|
||||
return nil
|
||||
}
|
||||
resolvedInstance = instance
|
||||
renderContext = DrawingContext(size: displaySize, scale: UIScreenScale, premultiplied: true, clear: true)
|
||||
}
|
||||
|
||||
self.item = item
|
||||
self.instance = resolvedInstance
|
||||
self.renderContext = renderContext
|
||||
|
||||
self.frameCount = Int(self.instance.frameCount)
|
||||
self.fps = Double(self.instance.frameRate)
|
||||
}
|
||||
|
||||
func draw() -> UIImage? {
|
||||
self.instance.renderFrame(with: Int32(self.frameIndex ?? 0), into: self.renderContext.bytes.assumingMemoryBound(to: UInt8.self), width: Int32(self.renderContext.size.width * self.renderContext.scale), height: Int32(self.renderContext.size.height * self.renderContext.scale), bytesPerRow: Int32(self.renderContext.bytesPerRow))
|
||||
return self.renderContext.generateImage()
|
||||
}
|
||||
}
|
||||
|
||||
public struct ManagedAnimationFrameRange: Equatable {
|
||||
var startFrame: Int
|
||||
var endFrame: Int
|
||||
|
||||
public init(startFrame: Int, endFrame: Int) {
|
||||
self.startFrame = startFrame
|
||||
self.endFrame = endFrame
|
||||
}
|
||||
}
|
||||
|
||||
public enum ManagedAnimationSource: Equatable {
|
||||
case local(String)
|
||||
case resource(MediaBox, MediaResource)
|
||||
|
||||
var cacheKey: String {
|
||||
switch self {
|
||||
case let .local(name):
|
||||
return name
|
||||
case let .resource(_, resource):
|
||||
return resource.id.uniqueId
|
||||
}
|
||||
}
|
||||
|
||||
var path: String? {
|
||||
switch self {
|
||||
case let .local(name):
|
||||
return getAppBundle().path(forResource: name, ofType: "tgs")
|
||||
case let .resource(mediaBox, resource):
|
||||
return mediaBox.completedResourcePath(resource)
|
||||
}
|
||||
}
|
||||
|
||||
public static func == (lhs: ManagedAnimationSource, rhs: ManagedAnimationSource) -> Bool {
|
||||
switch lhs {
|
||||
case let .local(lhsPath):
|
||||
if case let .local(rhsPath) = rhs, lhsPath == rhsPath {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .resource(lhsMediaBox, lhsResource):
|
||||
if case let .resource(rhsMediaBox, rhsResource) = rhs, lhsMediaBox === rhsMediaBox, lhsResource.isEqual(to: rhsResource) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct ManagedAnimationItem: Equatable {
|
||||
public let source: ManagedAnimationSource
|
||||
var frames: ManagedAnimationFrameRange
|
||||
var duration: Double
|
||||
|
||||
public init(source: ManagedAnimationSource, frames: ManagedAnimationFrameRange, duration: Double) {
|
||||
self.source = source
|
||||
self.frames = frames
|
||||
self.duration = duration
|
||||
}
|
||||
}
|
||||
|
||||
open class ManagedAnimationNode: ASDisplayNode {
|
||||
public let intrinsicSize: CGSize
|
||||
|
||||
private let imageNode: ASImageNode
|
||||
private let displayLink: CADisplayLink
|
||||
|
||||
public var state: ManagedAnimationState?
|
||||
public var trackStack: [ManagedAnimationItem] = []
|
||||
public var didTryAdvancingState = false
|
||||
|
||||
public init(size: CGSize) {
|
||||
self.intrinsicSize = size
|
||||
|
||||
self.imageNode = ASImageNode()
|
||||
self.imageNode.displayWithoutProcessing = true
|
||||
self.imageNode.displaysAsynchronously = false
|
||||
self.imageNode.frame = CGRect(origin: CGPoint(), size: self.intrinsicSize)
|
||||
|
||||
final class DisplayLinkTarget: NSObject {
|
||||
private let f: () -> Void
|
||||
|
||||
init(_ f: @escaping () -> Void) {
|
||||
self.f = f
|
||||
}
|
||||
|
||||
@objc func event() {
|
||||
self.f()
|
||||
}
|
||||
}
|
||||
var displayLinkUpdate: (() -> Void)?
|
||||
self.displayLink = CADisplayLink(target: DisplayLinkTarget {
|
||||
displayLinkUpdate?()
|
||||
}, selector: #selector(DisplayLinkTarget.event))
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.imageNode)
|
||||
|
||||
self.displayLink.add(to: RunLoop.main, forMode: .common)
|
||||
|
||||
displayLinkUpdate = { [weak self] in
|
||||
self?.updateAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
open func advanceState() {
|
||||
guard !self.trackStack.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
let item = self.trackStack.removeFirst()
|
||||
|
||||
if let state = self.state, state.item.source == item.source {
|
||||
self.state = ManagedAnimationState(displaySize: self.intrinsicSize, item: item, current: state)
|
||||
} else {
|
||||
self.state = ManagedAnimationState(displaySize: self.intrinsicSize, item: item, current: nil)
|
||||
}
|
||||
|
||||
self.didTryAdvancingState = false
|
||||
}
|
||||
|
||||
public func updateAnimation() {
|
||||
if self.state == nil {
|
||||
self.advanceState()
|
||||
}
|
||||
|
||||
guard let state = self.state else {
|
||||
return
|
||||
}
|
||||
let timestamp = CACurrentMediaTime()
|
||||
|
||||
let fps = state.fps
|
||||
let frameRange = state.item.frames
|
||||
|
||||
let duration: Double = state.item.duration
|
||||
var t = state.relativeTime / duration
|
||||
t = max(0.0, t)
|
||||
t = min(1.0, t)
|
||||
//print("\(t) \(state.item.name)")
|
||||
let frameOffset = Int(Double(frameRange.startFrame) * (1.0 - t) + Double(frameRange.endFrame) * t)
|
||||
let lowerBound: Int = 0
|
||||
let upperBound = state.frameCount - 1
|
||||
let frameIndex = max(lowerBound, min(upperBound, frameOffset))
|
||||
|
||||
if state.frameIndex != frameIndex {
|
||||
state.frameIndex = frameIndex
|
||||
if let image = state.draw() {
|
||||
self.imageNode.image = image
|
||||
}
|
||||
}
|
||||
|
||||
var animationAdvancement: Double = 1.0 / 60.0
|
||||
animationAdvancement *= Double(min(2, self.trackStack.count + 1))
|
||||
|
||||
state.relativeTime += animationAdvancement
|
||||
|
||||
if state.relativeTime >= duration && !self.didTryAdvancingState {
|
||||
self.didTryAdvancingState = true
|
||||
self.advanceState()
|
||||
}
|
||||
}
|
||||
|
||||
public func trackTo(item: ManagedAnimationItem) {
|
||||
self.trackStack.append(item)
|
||||
self.didTryAdvancingState = false
|
||||
self.updateAnimation()
|
||||
}
|
||||
}
|
||||
@ -40,6 +40,7 @@ static_library(
|
||||
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
|
||||
"//submodules/OverlayStatusController:OverlayStatusController",
|
||||
"//submodules/rlottie:RLottieBinding",
|
||||
"//submodules/ManagedAnimationNode:ManagedAnimationNode",
|
||||
],
|
||||
frameworks = [
|
||||
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
|
||||
|
||||
@ -39,6 +39,7 @@ swift_library(
|
||||
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
|
||||
"//submodules/OverlayStatusController:OverlayStatusController",
|
||||
"//submodules/rlottie:RLottieBinding",
|
||||
"//submodules/ManagedAnimationNode:ManagedAnimationNode",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@ -1,340 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import RLottieBinding
|
||||
import AppBundle
|
||||
import GZip
|
||||
import SwiftSignalKit
|
||||
|
||||
private final class ManagedAnimationState {
|
||||
let item: ManagedAnimationItem
|
||||
|
||||
private let instance: LottieInstance
|
||||
|
||||
let frameCount: Int
|
||||
let fps: Double
|
||||
|
||||
var relativeTime: Double = 0.0
|
||||
var frameIndex: Int?
|
||||
|
||||
private let renderContext: DrawingContext
|
||||
|
||||
init?(displaySize: CGSize, item: ManagedAnimationItem, current: ManagedAnimationState?) {
|
||||
let resolvedInstance: LottieInstance
|
||||
let renderContext: DrawingContext
|
||||
|
||||
if let current = current {
|
||||
resolvedInstance = current.instance
|
||||
renderContext = current.renderContext
|
||||
} else {
|
||||
guard let path = getAppBundle().path(forResource: item.name, ofType: "tgs") else {
|
||||
return nil
|
||||
}
|
||||
guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else {
|
||||
return nil
|
||||
}
|
||||
guard let unpackedData = TGGUnzipData(data, 5 * 1024 * 1024) else {
|
||||
return nil
|
||||
}
|
||||
guard let instance = LottieInstance(data: unpackedData, cacheKey: item.name) else {
|
||||
return nil
|
||||
}
|
||||
resolvedInstance = instance
|
||||
renderContext = DrawingContext(size: displaySize, scale: UIScreenScale, premultiplied: true, clear: true)
|
||||
}
|
||||
|
||||
self.item = item
|
||||
self.instance = resolvedInstance
|
||||
self.renderContext = renderContext
|
||||
|
||||
self.frameCount = Int(self.instance.frameCount)
|
||||
self.fps = Double(self.instance.frameRate)
|
||||
}
|
||||
|
||||
func draw() -> UIImage? {
|
||||
self.instance.renderFrame(with: Int32(self.frameIndex ?? 0), into: self.renderContext.bytes.assumingMemoryBound(to: UInt8.self), width: Int32(self.renderContext.size.width * self.renderContext.scale), height: Int32(self.renderContext.size.height * self.renderContext.scale), bytesPerRow: Int32(self.renderContext.bytesPerRow))
|
||||
return self.renderContext.generateImage()
|
||||
}
|
||||
}
|
||||
|
||||
struct ManagedAnimationFrameRange: Equatable {
|
||||
var startFrame: Int
|
||||
var endFrame: Int
|
||||
}
|
||||
|
||||
struct ManagedAnimationItem: Equatable {
|
||||
let name: String
|
||||
var frames: ManagedAnimationFrameRange
|
||||
var duration: Double
|
||||
}
|
||||
|
||||
class ManagedAnimationNode: ASDisplayNode {
|
||||
let intrinsicSize: CGSize
|
||||
|
||||
private let imageNode: ASImageNode
|
||||
private let displayLink: CADisplayLink
|
||||
|
||||
fileprivate var state: ManagedAnimationState?
|
||||
fileprivate var trackStack: [ManagedAnimationItem] = []
|
||||
fileprivate var didTryAdvancingState = false
|
||||
|
||||
init(size: CGSize) {
|
||||
self.intrinsicSize = size
|
||||
|
||||
self.imageNode = ASImageNode()
|
||||
self.imageNode.displayWithoutProcessing = true
|
||||
self.imageNode.displaysAsynchronously = false
|
||||
self.imageNode.frame = CGRect(origin: CGPoint(), size: self.intrinsicSize)
|
||||
|
||||
final class DisplayLinkTarget: NSObject {
|
||||
private let f: () -> Void
|
||||
|
||||
init(_ f: @escaping () -> Void) {
|
||||
self.f = f
|
||||
}
|
||||
|
||||
@objc func event() {
|
||||
self.f()
|
||||
}
|
||||
}
|
||||
var displayLinkUpdate: (() -> Void)?
|
||||
self.displayLink = CADisplayLink(target: DisplayLinkTarget {
|
||||
displayLinkUpdate?()
|
||||
}, selector: #selector(DisplayLinkTarget.event))
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.imageNode)
|
||||
|
||||
self.displayLink.add(to: RunLoop.main, forMode: .common)
|
||||
|
||||
displayLinkUpdate = { [weak self] in
|
||||
self?.updateAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
func advanceState() {
|
||||
guard !self.trackStack.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
let item = self.trackStack.removeFirst()
|
||||
|
||||
if let state = self.state, state.item.name == item.name {
|
||||
self.state = ManagedAnimationState(displaySize: self.intrinsicSize, item: item, current: state)
|
||||
} else {
|
||||
self.state = ManagedAnimationState(displaySize: self.intrinsicSize, item: item, current: nil)
|
||||
}
|
||||
|
||||
self.didTryAdvancingState = false
|
||||
}
|
||||
|
||||
fileprivate func updateAnimation() {
|
||||
if self.state == nil {
|
||||
self.advanceState()
|
||||
}
|
||||
|
||||
guard let state = self.state else {
|
||||
return
|
||||
}
|
||||
let timestamp = CACurrentMediaTime()
|
||||
|
||||
let fps = state.fps
|
||||
let frameRange = state.item.frames
|
||||
|
||||
let duration: Double = state.item.duration
|
||||
var t = state.relativeTime / duration
|
||||
t = max(0.0, t)
|
||||
t = min(1.0, t)
|
||||
//print("\(t) \(state.item.name)")
|
||||
let frameOffset = Int(Double(frameRange.startFrame) * (1.0 - t) + Double(frameRange.endFrame) * t)
|
||||
let lowerBound: Int = 0
|
||||
let upperBound = state.frameCount - 1
|
||||
let frameIndex = max(lowerBound, min(upperBound, frameOffset))
|
||||
|
||||
if state.frameIndex != frameIndex {
|
||||
state.frameIndex = frameIndex
|
||||
if let image = state.draw() {
|
||||
self.imageNode.image = image
|
||||
}
|
||||
}
|
||||
|
||||
var animationAdvancement: Double = 1.0 / 60.0
|
||||
animationAdvancement *= Double(min(2, self.trackStack.count + 1))
|
||||
|
||||
state.relativeTime += animationAdvancement
|
||||
|
||||
if state.relativeTime >= duration && !self.didTryAdvancingState {
|
||||
self.didTryAdvancingState = true
|
||||
self.advanceState()
|
||||
}
|
||||
}
|
||||
|
||||
func trackTo(item: ManagedAnimationItem) {
|
||||
self.trackStack.append(item)
|
||||
self.didTryAdvancingState = false
|
||||
self.updateAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
enum ManagedMonkeyAnimationIdle: CaseIterable {
|
||||
case blink
|
||||
case ear
|
||||
case still
|
||||
}
|
||||
|
||||
enum ManagedMonkeyAnimationState: Equatable {
|
||||
case idle(ManagedMonkeyAnimationIdle)
|
||||
case eyesClosed
|
||||
case peeking
|
||||
case tracking(CGFloat)
|
||||
}
|
||||
|
||||
final class ManagedMonkeyAnimationNode: ManagedAnimationNode {
|
||||
private var monkeyState: ManagedMonkeyAnimationState = .idle(.blink)
|
||||
private var timer: SwiftSignalKit.Timer?
|
||||
|
||||
init() {
|
||||
super.init(size: CGSize(width: 136.0, height: 136.0))
|
||||
|
||||
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyIdle", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0), duration: 0.3))
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.timer?.invalidate()
|
||||
}
|
||||
|
||||
private func startIdleTimer() {
|
||||
self.timer?.invalidate()
|
||||
let timer = SwiftSignalKit.Timer(timeout: Double.random(in: 1.0 ..< 1.5), repeat: false, completion: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
switch strongSelf.monkeyState {
|
||||
case .idle:
|
||||
if let idle = ManagedMonkeyAnimationIdle.allCases.randomElement() {
|
||||
strongSelf.setState(.idle(idle))
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}, queue: .mainQueue())
|
||||
self.timer = timer
|
||||
timer.start()
|
||||
}
|
||||
|
||||
override func advanceState() {
|
||||
super.advanceState()
|
||||
|
||||
self.timer?.invalidate()
|
||||
self.timer = nil
|
||||
|
||||
if self.trackStack.isEmpty, case .idle = self.monkeyState {
|
||||
self.startIdleTimer()
|
||||
}
|
||||
}
|
||||
|
||||
private func enqueueIdle(_ idle: ManagedMonkeyAnimationIdle) {
|
||||
switch idle {
|
||||
case .still:
|
||||
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyIdle", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0), duration: 0.3))
|
||||
case .blink:
|
||||
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyIdle1", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 30), duration: 0.3))
|
||||
case .ear:
|
||||
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyIdle2", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 30), duration: 0.3))
|
||||
//self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyIdle", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 179), duration: 3.0))
|
||||
}
|
||||
}
|
||||
|
||||
func setState(_ monkeyState: ManagedMonkeyAnimationState) {
|
||||
let previousState = self.monkeyState
|
||||
self.monkeyState = monkeyState
|
||||
|
||||
self.timer?.invalidate()
|
||||
self.timer = nil
|
||||
|
||||
func enqueueTracking(_ value: CGFloat) {
|
||||
let lowerBound = 18
|
||||
let upperBound = 160
|
||||
let frameIndex = lowerBound + Int(value * CGFloat(upperBound - lowerBound))
|
||||
if let state = self.state, state.item.name == "TwoFactorSetupMonkeyTracking" {
|
||||
let item = ManagedAnimationItem(name: "TwoFactorSetupMonkeyTracking", frames: ManagedAnimationFrameRange(startFrame: state.frameIndex ?? 0, endFrame: frameIndex), duration: 0.3)
|
||||
self.state = ManagedAnimationState(displaySize: self.intrinsicSize, item: item, current: state)
|
||||
self.didTryAdvancingState = false
|
||||
self.updateAnimation()
|
||||
} else {
|
||||
self.trackStack = self.trackStack.filter {
|
||||
$0.name != "TwoFactorSetupMonkeyTracking"
|
||||
}
|
||||
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyTracking", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: frameIndex), duration: 0.3))
|
||||
}
|
||||
}
|
||||
|
||||
func enqueueClearTracking() {
|
||||
if let state = self.state, state.item.name == "TwoFactorSetupMonkeyTracking" {
|
||||
let item = ManagedAnimationItem(name: "TwoFactorSetupMonkeyTracking", frames: ManagedAnimationFrameRange(startFrame: state.frameIndex ?? 0, endFrame: 0), duration: 0.3)
|
||||
self.state = ManagedAnimationState(displaySize: self.intrinsicSize, item: item, current: state)
|
||||
self.didTryAdvancingState = false
|
||||
self.updateAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
switch previousState {
|
||||
case let .idle(previousIdle):
|
||||
switch monkeyState {
|
||||
case let .idle(idle):
|
||||
self.enqueueIdle(idle)
|
||||
case .eyesClosed:
|
||||
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyClose", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 41), duration: 0.3))
|
||||
case .peeking:
|
||||
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyCloseAndPeek", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 41), duration: 0.3))
|
||||
case let .tracking(value):
|
||||
enqueueTracking(value)
|
||||
}
|
||||
case .eyesClosed:
|
||||
switch monkeyState {
|
||||
case let .idle(idle):
|
||||
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyClose", frames: ManagedAnimationFrameRange(startFrame: 41, endFrame: 0), duration: 0.3))
|
||||
self.enqueueIdle(idle)
|
||||
case .eyesClosed:
|
||||
break
|
||||
case .peeking:
|
||||
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyPeek", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 14), duration: 0.3))
|
||||
case let .tracking(value):
|
||||
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyClose", frames: ManagedAnimationFrameRange(startFrame: 41, endFrame: 0), duration: 0.3))
|
||||
enqueueTracking(value)
|
||||
}
|
||||
case .peeking:
|
||||
switch monkeyState {
|
||||
case let .idle(idle):
|
||||
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyCloseAndPeek", frames: ManagedAnimationFrameRange(startFrame: 41, endFrame: 0), duration: 0.3))
|
||||
self.enqueueIdle(idle)
|
||||
case .eyesClosed:
|
||||
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyPeek", frames: ManagedAnimationFrameRange(startFrame: 14, endFrame: 0), duration: 0.3))
|
||||
case .peeking:
|
||||
break
|
||||
case let .tracking(value):
|
||||
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyCloseAndPeek", frames: ManagedAnimationFrameRange(startFrame: 41, endFrame: 0), duration: 0.3))
|
||||
enqueueTracking(value)
|
||||
}
|
||||
case let .tracking(currentValue):
|
||||
switch monkeyState {
|
||||
case let .idle(idle):
|
||||
enqueueClearTracking()
|
||||
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyIdle", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0), duration: 0.3))
|
||||
self.enqueueIdle(idle)
|
||||
case .eyesClosed:
|
||||
enqueueClearTracking()
|
||||
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyClose", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 41), duration: 0.3))
|
||||
case .peeking:
|
||||
enqueueClearTracking()
|
||||
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyCloseAndPeek", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 41), duration: 0.3))
|
||||
case let .tracking(value):
|
||||
if abs(currentValue - value) > CGFloat.ulpOfOne {
|
||||
enqueueTracking(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,170 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import RLottieBinding
|
||||
import AppBundle
|
||||
import GZip
|
||||
import SwiftSignalKit
|
||||
import ManagedAnimationNode
|
||||
|
||||
enum ManagedMonkeyAnimationIdle: CaseIterable {
|
||||
case blink
|
||||
case ear
|
||||
case still
|
||||
}
|
||||
|
||||
enum ManagedMonkeyAnimationState: Equatable {
|
||||
case idle(ManagedMonkeyAnimationIdle)
|
||||
case eyesClosed
|
||||
case peeking
|
||||
case tracking(CGFloat)
|
||||
}
|
||||
|
||||
final class ManagedMonkeyAnimationNode: ManagedAnimationNode {
|
||||
private var monkeyState: ManagedMonkeyAnimationState = .idle(.blink)
|
||||
private var timer: SwiftSignalKit.Timer?
|
||||
|
||||
init() {
|
||||
super.init(size: CGSize(width: 136.0, height: 136.0))
|
||||
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyIdle"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0), duration: 0.3))
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.timer?.invalidate()
|
||||
}
|
||||
|
||||
private func startIdleTimer() {
|
||||
self.timer?.invalidate()
|
||||
let timer = SwiftSignalKit.Timer(timeout: Double.random(in: 1.0 ..< 1.5), repeat: false, completion: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
switch strongSelf.monkeyState {
|
||||
case .idle:
|
||||
if let idle = ManagedMonkeyAnimationIdle.allCases.randomElement() {
|
||||
strongSelf.setState(.idle(idle))
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}, queue: .mainQueue())
|
||||
self.timer = timer
|
||||
timer.start()
|
||||
}
|
||||
|
||||
override func advanceState() {
|
||||
super.advanceState()
|
||||
|
||||
self.timer?.invalidate()
|
||||
self.timer = nil
|
||||
|
||||
if self.trackStack.isEmpty, case .idle = self.monkeyState {
|
||||
self.startIdleTimer()
|
||||
}
|
||||
}
|
||||
|
||||
private func enqueueIdle(_ idle: ManagedMonkeyAnimationIdle) {
|
||||
switch idle {
|
||||
case .still:
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyIdle"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0), duration: 0.3))
|
||||
case .blink:
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyIdle1"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 30), duration: 0.3))
|
||||
case .ear:
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyIdle2"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 30), duration: 0.3))
|
||||
//self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyIdle"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 179), duration: 3.0))
|
||||
}
|
||||
}
|
||||
|
||||
func setState(_ monkeyState: ManagedMonkeyAnimationState) {
|
||||
let previousState = self.monkeyState
|
||||
self.monkeyState = monkeyState
|
||||
|
||||
self.timer?.invalidate()
|
||||
self.timer = nil
|
||||
|
||||
func enqueueTracking(_ value: CGFloat) {
|
||||
let lowerBound = 18
|
||||
let upperBound = 160
|
||||
let frameIndex = lowerBound + Int(value * CGFloat(upperBound - lowerBound))
|
||||
if let state = self.state, state.item.source == .local("TwoFactorSetupMonkeyTracking") {
|
||||
let item = ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyTracking"), frames: ManagedAnimationFrameRange(startFrame: state.frameIndex ?? 0, endFrame: frameIndex), duration: 0.3)
|
||||
self.state = ManagedAnimationState(displaySize: self.intrinsicSize, item: item, current: state)
|
||||
self.didTryAdvancingState = false
|
||||
self.updateAnimation()
|
||||
} else {
|
||||
self.trackStack = self.trackStack.filter {
|
||||
$0.source != .local("TwoFactorSetupMonkeyTracking")
|
||||
}
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyTracking"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: frameIndex), duration: 0.3))
|
||||
}
|
||||
}
|
||||
|
||||
func enqueueClearTracking() {
|
||||
if let state = self.state, state.item.source == .local("TwoFactorSetupMonkeyTracking") {
|
||||
let item = ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyTracking"), frames: ManagedAnimationFrameRange(startFrame: state.frameIndex ?? 0, endFrame: 0), duration: 0.3)
|
||||
self.state = ManagedAnimationState(displaySize: self.intrinsicSize, item: item, current: state)
|
||||
self.didTryAdvancingState = false
|
||||
self.updateAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
switch previousState {
|
||||
case let .idle(previousIdle):
|
||||
switch monkeyState {
|
||||
case let .idle(idle):
|
||||
self.enqueueIdle(idle)
|
||||
case .eyesClosed:
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyClose"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 41), duration: 0.3))
|
||||
case .peeking:
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyCloseAndPeek"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 41), duration: 0.3))
|
||||
case let .tracking(value):
|
||||
enqueueTracking(value)
|
||||
}
|
||||
case .eyesClosed:
|
||||
switch monkeyState {
|
||||
case let .idle(idle):
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyClose"), frames: ManagedAnimationFrameRange(startFrame: 41, endFrame: 0), duration: 0.3))
|
||||
self.enqueueIdle(idle)
|
||||
case .eyesClosed:
|
||||
break
|
||||
case .peeking:
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyPeek"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 14), duration: 0.3))
|
||||
case let .tracking(value):
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyClose"), frames: ManagedAnimationFrameRange(startFrame: 41, endFrame: 0), duration: 0.3))
|
||||
enqueueTracking(value)
|
||||
}
|
||||
case .peeking:
|
||||
switch monkeyState {
|
||||
case let .idle(idle):
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyCloseAndPeek"), frames: ManagedAnimationFrameRange(startFrame: 41, endFrame: 0), duration: 0.3))
|
||||
self.enqueueIdle(idle)
|
||||
case .eyesClosed:
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyPeek"), frames: ManagedAnimationFrameRange(startFrame: 14, endFrame: 0), duration: 0.3))
|
||||
case .peeking:
|
||||
break
|
||||
case let .tracking(value):
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyCloseAndPeek"), frames: ManagedAnimationFrameRange(startFrame: 41, endFrame: 0), duration: 0.3))
|
||||
enqueueTracking(value)
|
||||
}
|
||||
case let .tracking(currentValue):
|
||||
switch monkeyState {
|
||||
case let .idle(idle):
|
||||
enqueueClearTracking()
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyIdle"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0), duration: 0.3))
|
||||
self.enqueueIdle(idle)
|
||||
case .eyesClosed:
|
||||
enqueueClearTracking()
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyClose"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 41), duration: 0.3))
|
||||
case .peeking:
|
||||
enqueueClearTracking()
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyCloseAndPeek"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 41), duration: 0.3))
|
||||
case let .tracking(value):
|
||||
if abs(currentValue - value) > CGFloat.ulpOfOne {
|
||||
enqueueTracking(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -24,6 +24,7 @@ static_library(
|
||||
"//submodules/PhotoResources:PhotoResources",
|
||||
"//submodules/GraphCore:GraphCore",
|
||||
"//submodules/GraphUI:GraphUI",
|
||||
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
|
||||
],
|
||||
frameworks = [
|
||||
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
|
||||
|
||||
@ -25,6 +25,7 @@ swift_library(
|
||||
"//submodules/PhotoResources:PhotoResources",
|
||||
"//submodules/GraphCore:GraphCore",
|
||||
"//submodules/GraphUI:GraphUI",
|
||||
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@ -296,36 +296,36 @@ private enum StatsEntry: ItemListNodeEntry {
|
||||
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
|
||||
let arguments = arguments as! StatsControllerArguments
|
||||
switch self {
|
||||
case let .overviewHeader(theme, text, dates):
|
||||
case let .overviewHeader(_, text, dates):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, accessoryText: ItemListSectionHeaderAccessoryText(value: dates, color: .generic), sectionId: self.section)
|
||||
case let .growthTitle(theme, text),
|
||||
let .followersTitle(theme, text),
|
||||
let .notificationsTitle(theme, text),
|
||||
let .viewsByHourTitle(theme, text),
|
||||
let .viewsBySourceTitle(theme, text),
|
||||
let .followersBySourceTitle(theme, text),
|
||||
let .languagesTitle(theme, text),
|
||||
let .postInteractionsTitle(theme, text),
|
||||
let .postsTitle(theme, text),
|
||||
let .instantPageInteractionsTitle(theme, text):
|
||||
case let .growthTitle(_, text),
|
||||
let .followersTitle(_, text),
|
||||
let .notificationsTitle(_, text),
|
||||
let .viewsByHourTitle(_, text),
|
||||
let .viewsBySourceTitle(_, text),
|
||||
let .followersBySourceTitle(_, text),
|
||||
let .languagesTitle(_, text),
|
||||
let .postInteractionsTitle(_, text),
|
||||
let .postsTitle(_, text),
|
||||
let .instantPageInteractionsTitle(_, text):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||
case let .overview(theme, stats):
|
||||
case let .overview(_, stats):
|
||||
return StatsOverviewItem(presentationData: presentationData, stats: stats, sectionId: self.section, style: .blocks)
|
||||
case let .growthGraph(theme, strings, dateTimeFormat, graph, type):
|
||||
case let .growthGraph(_, _, _, graph, type):
|
||||
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, sectionId: self.section, style: .blocks)
|
||||
case let .followersGraph(theme, strings, dateTimeFormat, graph, type):
|
||||
case let .followersGraph(_, _, _, graph, type):
|
||||
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, sectionId: self.section, style: .blocks)
|
||||
case let .notificationsGraph(theme, strings, dateTimeFormat, graph, type):
|
||||
case let .notificationsGraph(_, _, _, graph, type):
|
||||
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, sectionId: self.section, style: .blocks)
|
||||
case let .viewsByHourGraph(theme, strings, dateTimeFormat, graph, type):
|
||||
case let .viewsByHourGraph(_, _, _, graph, type):
|
||||
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, sectionId: self.section, style: .blocks)
|
||||
case let .viewsBySourceGraph(theme, strings, dateTimeFormat, graph, type):
|
||||
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, height: 160.0, sectionId: self.section, style: .blocks)
|
||||
case let .followersBySourceGraph(theme, strings, dateTimeFormat, graph, type):
|
||||
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, height: 160.0, sectionId: self.section, style: .blocks)
|
||||
case let .languagesGraph(theme, strings, dateTimeFormat, graph, type):
|
||||
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, height: 100.0, sectionId: self.section, style: .blocks)
|
||||
case let .postInteractionsGraph(theme, strings, dateTimeFormat, graph, type):
|
||||
case let .viewsBySourceGraph(_, _, _, graph, type):
|
||||
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, sectionId: self.section, style: .blocks)
|
||||
case let .followersBySourceGraph(_, _, _, graph, type):
|
||||
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, sectionId: self.section, style: .blocks)
|
||||
case let .languagesGraph(_, _, _, graph, type):
|
||||
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, sectionId: self.section, style: .blocks)
|
||||
case let .postInteractionsGraph(_, _, _, graph, type):
|
||||
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, getDetailsData: { date, completion in
|
||||
let _ = arguments.loadDetailedGraph(graph, Int64(date.timeIntervalSince1970) * 1000).start(next: { graph in
|
||||
if let graph = graph, case let .Loaded(_, data) = graph {
|
||||
@ -333,18 +333,18 @@ private enum StatsEntry: ItemListNodeEntry {
|
||||
}
|
||||
})
|
||||
}, sectionId: self.section, style: .blocks)
|
||||
case let .post(index, theme, strings, dateTimeFormat, message, interactions):
|
||||
case let .instantPageInteractionsGraph(_, _, _, graph, type):
|
||||
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, getDetailsData: { date, completion in
|
||||
let _ = arguments.loadDetailedGraph(graph, Int64(date.timeIntervalSince1970) * 1000).start(next: { graph in
|
||||
if let graph = graph, case let .Loaded(_, data) = graph {
|
||||
completion(data)
|
||||
}
|
||||
})
|
||||
}, sectionId: self.section, style: .blocks)
|
||||
case let .post(_, _, _, _, message, interactions):
|
||||
return StatsMessageItem(context: arguments.context, presentationData: presentationData, message: message, views: interactions.views, forwards: interactions.forwards, sectionId: self.section, style: .blocks, action: {
|
||||
arguments.openMessage(message.id)
|
||||
})
|
||||
case let .instantPageInteractionsGraph(theme, strings, dateTimeFormat, graph, type):
|
||||
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, getDetailsData: { date, completion in
|
||||
let _ = arguments.loadDetailedGraph(graph, Int64(date.timeIntervalSince1970) * 1000).start(next: { graph in
|
||||
if let graph = graph, case let .Loaded(_, data) = graph {
|
||||
completion(data)
|
||||
}
|
||||
})
|
||||
}, sectionId: self.section, style: .blocks)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -368,32 +368,37 @@ private func statsControllerEntries(data: ChannelStats?, messages: [Message]?, i
|
||||
entries.append(.followersTitle(presentationData.theme, presentationData.strings.Stats_FollowersTitle))
|
||||
entries.append(.followersGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.followersGraph, .lines))
|
||||
}
|
||||
|
||||
|
||||
if !data.muteGraph.isEmpty {
|
||||
entries.append(.notificationsTitle(presentationData.theme, presentationData.strings.Stats_NotificationsTitle))
|
||||
entries.append(.notificationsGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.muteGraph, .lines))
|
||||
}
|
||||
|
||||
if !data.topHoursGraph.isEmpty {
|
||||
entries.append(.viewsByHourTitle(presentationData.theme, presentationData.strings.Stats_ViewsByHoursTitle))
|
||||
entries.append(.viewsByHourGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.topHoursGraph, .hourlyStep))
|
||||
}
|
||||
|
||||
if !data.viewsBySourceGraph.isEmpty {
|
||||
entries.append(.viewsBySourceTitle(presentationData.theme, presentationData.strings.Stats_ViewsBySourceTitle))
|
||||
entries.append(.viewsBySourceGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.viewsBySourceGraph, .bars))
|
||||
}
|
||||
|
||||
|
||||
if !data.newFollowersBySourceGraph.isEmpty {
|
||||
entries.append(.followersBySourceTitle(presentationData.theme, presentationData.strings.Stats_FollowersBySourceTitle))
|
||||
entries.append(.followersBySourceGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.newFollowersBySourceGraph, .bars))
|
||||
}
|
||||
|
||||
|
||||
if !data.languagesGraph.isEmpty {
|
||||
entries.append(.languagesTitle(presentationData.theme, presentationData.strings.Stats_LanguagesTitle))
|
||||
entries.append(.languagesGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.languagesGraph, .pie))
|
||||
}
|
||||
|
||||
|
||||
if !data.interactionsGraph.isEmpty {
|
||||
entries.append(.postInteractionsTitle(presentationData.theme, presentationData.strings.Stats_InteractionsTitle))
|
||||
entries.append(.postInteractionsGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.interactionsGraph, .step))
|
||||
entries.append(.postInteractionsGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.interactionsGraph, .twoAxisStep))
|
||||
}
|
||||
|
||||
|
||||
if let messages = messages, !messages.isEmpty, let interactions = interactions, !interactions.isEmpty {
|
||||
entries.append(.postsTitle(presentationData.theme, presentationData.strings.Stats_PostsTitle))
|
||||
var index: Int32 = 0
|
||||
@ -404,7 +409,7 @@ private func statsControllerEntries(data: ChannelStats?, messages: [Message]?, i
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if !data.instantPageInteractionsGraph.isEmpty {
|
||||
entries.append(.instantPageInteractionsTitle(presentationData.theme, presentationData.strings.Stats_InstantViewInteractionsTitle))
|
||||
entries.append(.instantPageInteractionsGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.instantPageInteractionsGraph, .step))
|
||||
@ -439,6 +444,7 @@ public func channelStatsController(context: AccountContext, peerId: PeerId, cach
|
||||
if let statsContext = statsContext, let stats = stats {
|
||||
if case .OnDemand = stats.interactionsGraph {
|
||||
statsContext.loadInteractionsGraph()
|
||||
statsContext.loadTopHoursGraph()
|
||||
statsContext.loadNewFollowersBySourceGraph()
|
||||
statsContext.loadViewsBySourceGraph()
|
||||
statsContext.loadLanguagesGraph()
|
||||
@ -460,12 +466,21 @@ public func channelStatsController(context: AccountContext, peerId: PeerId, cach
|
||||
}
|
||||
messagesPromise.set(.single(nil) |> then(messageView))
|
||||
|
||||
let signal = combineLatest(context.sharedContext.presentationData, dataPromise.get(), messagesPromise.get())
|
||||
let longLoadingSignal: Signal<Bool, NoError> = .single(false) |> then(.single(true) |> delay(1.5, queue: Queue.mainQueue()))
|
||||
|
||||
let previousData = Atomic<ChannelStats?>(value: nil)
|
||||
|
||||
let signal = combineLatest(context.sharedContext.presentationData, dataPromise.get(), messagesPromise.get(), longLoadingSignal)
|
||||
|> deliverOnMainQueue
|
||||
|> map { presentationData, data, messageView -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
||||
|> map { presentationData, data, messageView, longLoading -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
||||
let previous = previousData.swap(data)
|
||||
var emptyStateItem: ItemListControllerEmptyStateItem?
|
||||
if data == nil {
|
||||
emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme)
|
||||
if longLoading {
|
||||
emptyStateItem = StatsEmptyStateItem(theme: presentationData.theme, strings: presentationData.strings)
|
||||
} else {
|
||||
emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme)
|
||||
}
|
||||
}
|
||||
|
||||
let messages = messageView?.entries.map { $0.message }.sorted(by: { (lhsMessage, rhsMessage) -> Bool in
|
||||
@ -478,7 +493,7 @@ public func channelStatsController(context: AccountContext, peerId: PeerId, cach
|
||||
}
|
||||
|
||||
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.ChannelInfo_Stats), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
|
||||
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: statsControllerEntries(data: data, messages: messages, interactions: interactions, presentationData: presentationData), style: .blocks, emptyStateItem: emptyStateItem, crossfadeState: false, animateChanges: false)
|
||||
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: statsControllerEntries(data: data, messages: messages, interactions: interactions, presentationData: presentationData), style: .blocks, emptyStateItem: emptyStateItem, crossfadeState: previous == nil, animateChanges: false)
|
||||
|
||||
return (controllerState, (listState, arguments))
|
||||
}
|
||||
|
||||
107
submodules/StatisticsUI/Sources/StatsEmptyItem.swift
Normal file
107
submodules/StatisticsUI/Sources/StatsEmptyItem.swift
Normal file
@ -0,0 +1,107 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import TelegramPresentationData
|
||||
import ItemListUI
|
||||
import PresentationDataUtils
|
||||
import AnimatedStickerNode
|
||||
import AppBundle
|
||||
|
||||
final class StatsEmptyStateItem: ItemListControllerEmptyStateItem {
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
|
||||
init(theme: PresentationTheme, strings: PresentationStrings) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
}
|
||||
|
||||
func isEqual(to: ItemListControllerEmptyStateItem) -> Bool {
|
||||
if let item = to as? StatsEmptyStateItem {
|
||||
return self.theme === item.theme && self.strings === item.strings
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func node(current: ItemListControllerEmptyStateItemNode?) -> ItemListControllerEmptyStateItemNode {
|
||||
if let current = current as? StatsEmptyStateItemNode {
|
||||
current.item = self
|
||||
return current
|
||||
} else {
|
||||
return StatsEmptyStateItemNode(item: self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class StatsEmptyStateItemNode: ItemListControllerEmptyStateItemNode {
|
||||
private var animationNode: AnimatedStickerNode
|
||||
private let titleNode: ASTextNode
|
||||
private let textNode: ASTextNode
|
||||
private var validLayout: (ContainerViewLayout, CGFloat)?
|
||||
|
||||
var item: StatsEmptyStateItem {
|
||||
didSet {
|
||||
self.updateThemeAndStrings(theme: self.item.theme, strings: self.item.strings)
|
||||
if let (layout, navigationHeight) = self.validLayout {
|
||||
self.updateLayout(layout: layout, navigationBarHeight: navigationHeight, transition: .immediate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(item: StatsEmptyStateItem) {
|
||||
self.item = item
|
||||
|
||||
self.animationNode = AnimatedStickerNode()
|
||||
if let path = getAppBundle().path(forResource: "Charts", ofType: "tgs") {
|
||||
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 192, height: 192, playbackMode: .once, mode: .direct)
|
||||
self.animationNode.visibility = true
|
||||
}
|
||||
|
||||
self.titleNode = ASTextNode()
|
||||
self.titleNode.isUserInteractionEnabled = false
|
||||
|
||||
self.textNode = ASTextNode()
|
||||
self.textNode.isUserInteractionEnabled = false
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.animationNode)
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.textNode)
|
||||
|
||||
self.updateThemeAndStrings(theme: self.item.theme, strings: self.item.strings)
|
||||
}
|
||||
|
||||
private func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
|
||||
self.titleNode.attributedText = NSAttributedString(string: strings.Stats_LoadingTitle, font: Font.bold(17.0), textColor: theme.list.freeTextColor, paragraphAlignment: .center)
|
||||
self.textNode.attributedText = NSAttributedString(string: strings.Stats_LoadingText, font: Font.regular(14.0), textColor: theme.list.freeTextColor, paragraphAlignment: .center)
|
||||
}
|
||||
|
||||
override func updateLayout(layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
self.validLayout = (layout, navigationBarHeight)
|
||||
var insets = layout.insets(options: [])
|
||||
insets.top += navigationBarHeight
|
||||
|
||||
let imageSpacing: CGFloat = 20.0
|
||||
let textSpacing: CGFloat = 8.0
|
||||
|
||||
let imageSize = CGSize(width: 96.0, height: 96.0)
|
||||
let imageHeight = layout.size.width < layout.size.height ? imageSize.height + imageSpacing : 0.0
|
||||
|
||||
self.animationNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - imageSize.width) / 2.0), y: -10.0), size: imageSize)
|
||||
self.animationNode.updateLayout(size: imageSize)
|
||||
|
||||
let titleSize = self.titleNode.measure(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - layout.intrinsicInsets.left - layout.intrinsicInsets.right - 50.0, height: max(1.0, layout.size.height - insets.top - insets.bottom)))
|
||||
let textSize = self.textNode.measure(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - layout.intrinsicInsets.left - layout.intrinsicInsets.right - 50.0, height: max(1.0, layout.size.height - insets.top - insets.bottom)))
|
||||
|
||||
let totalHeight = imageHeight + titleSize.height + textSpacing + textSize.height
|
||||
let topOffset = insets.top + floor((layout.size.height - insets.top - insets.bottom - totalHeight) / 2.0)
|
||||
|
||||
transition.updateAlpha(node: self.animationNode, alpha: imageHeight > 0.0 ? 1.0 : 0.0)
|
||||
transition.updateFrame(node: self.animationNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - imageSize.width) / 2.0), y: topOffset), size: imageSize))
|
||||
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - titleSize.width - layout.safeInsets.left - layout.safeInsets.right - layout.intrinsicInsets.left - layout.intrinsicInsets.right) / 2.0), y: topOffset + imageHeight), size: titleSize))
|
||||
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - textSize.width - layout.safeInsets.left - layout.safeInsets.right - layout.intrinsicInsets.left - layout.intrinsicInsets.right) / 2.0), y: self.titleNode.frame.maxY + textSpacing), size: textSize))
|
||||
}
|
||||
}
|
||||
@ -16,16 +16,14 @@ class StatsGraphItem: ListViewItem, ItemListItem {
|
||||
let presentationData: ItemListPresentationData
|
||||
let graph: ChannelStatsGraph
|
||||
let type: ChartType
|
||||
let height: CGFloat
|
||||
let getDetailsData: ((Date, @escaping (String?) -> Void) -> Void)?
|
||||
let sectionId: ItemListSectionId
|
||||
let style: ItemListStyle
|
||||
|
||||
init(presentationData: ItemListPresentationData, graph: ChannelStatsGraph, type: ChartType, height: CGFloat = 0.0, getDetailsData: ((Date, @escaping (String?) -> Void) -> Void)? = nil, sectionId: ItemListSectionId, style: ItemListStyle) {
|
||||
init(presentationData: ItemListPresentationData, graph: ChannelStatsGraph, type: ChartType, getDetailsData: ((Date, @escaping (String?) -> Void) -> Void)? = nil, sectionId: ItemListSectionId, style: ItemListStyle) {
|
||||
self.presentationData = presentationData
|
||||
self.graph = graph
|
||||
self.type = type
|
||||
self.height = height
|
||||
self.getDetailsData = getDetailsData
|
||||
self.sectionId = sectionId
|
||||
self.style = style
|
||||
@ -72,11 +70,13 @@ class StatsGraphItemNode: ListViewItemNode {
|
||||
private let topStripeNode: ASDisplayNode
|
||||
private let bottomStripeNode: ASDisplayNode
|
||||
private let maskNode: ASImageNode
|
||||
private let chartContainerNode: ASDisplayNode
|
||||
|
||||
let chartNode: ChartNode
|
||||
private let activityIndicator: ActivityIndicator
|
||||
|
||||
private var item: StatsGraphItem?
|
||||
private var visibilityHeight: CGFloat?
|
||||
|
||||
init() {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
@ -84,41 +84,46 @@ class StatsGraphItemNode: ListViewItemNode {
|
||||
self.backgroundNode.backgroundColor = .white
|
||||
|
||||
self.maskNode = ASImageNode()
|
||||
self.maskNode.isUserInteractionEnabled = false
|
||||
|
||||
self.topStripeNode = ASDisplayNode()
|
||||
self.topStripeNode.isLayerBacked = true
|
||||
|
||||
self.bottomStripeNode = ASDisplayNode()
|
||||
self.bottomStripeNode.isLayerBacked = true
|
||||
|
||||
|
||||
self.chartContainerNode = ASDisplayNode()
|
||||
self.chartContainerNode.clipsToBounds = true
|
||||
self.chartContainerNode.isUserInteractionEnabled = true
|
||||
|
||||
self.chartNode = ChartNode()
|
||||
self.activityIndicator = ActivityIndicator(type: ActivityIndicatorType.custom(.black, 16.0, 2.0, false))
|
||||
self.activityIndicator.isHidden = true
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
self.clipsToBounds = true
|
||||
|
||||
self.addSubnode(self.chartNode)
|
||||
self.addSubnode(self.activityIndicator)
|
||||
self.chartContainerNode.addSubnode(self.chartNode)
|
||||
self.chartContainerNode.addSubnode(self.activityIndicator)
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.chartNode.view.interactiveTransitionGestureRecognizerTest = { point -> Bool in
|
||||
return point.x > 30.0
|
||||
self.view.interactiveTransitionGestureRecognizerTest = { point -> Bool in
|
||||
return point.x > 30.0 || (point.y > 250.0 && point.y < 295.0)
|
||||
}
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: StatsGraphItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
let currentItem = self.item
|
||||
let currentVisibilityHeight = self.visibilityHeight
|
||||
|
||||
return { item, params, neighbors in
|
||||
let leftInset = params.leftInset
|
||||
let rightInset: CGFloat = params.rightInset
|
||||
var updatedTheme: PresentationTheme?
|
||||
var updatedGraph: ChannelStatsGraph?
|
||||
var updatedController: BaseChartController?
|
||||
|
||||
if currentItem?.presentationData.theme !== item.presentationData.theme {
|
||||
updatedTheme = item.presentationData.theme
|
||||
@ -126,9 +131,16 @@ class StatsGraphItemNode: ListViewItemNode {
|
||||
|
||||
if currentItem?.graph != item.graph {
|
||||
updatedGraph = item.graph
|
||||
if case let .Loaded(_, data) = updatedGraph {
|
||||
updatedController = createChartController(data, type: item.type, getDetailsData: { [weak self] date, completion in
|
||||
if let strongSelf = self, let item = strongSelf.item {
|
||||
item.getDetailsData?(date, completion)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let contentSize: CGSize
|
||||
var contentSize: CGSize
|
||||
let insets: UIEdgeInsets
|
||||
let separatorHeight = UIScreenPixel
|
||||
let itemBackgroundColor: UIColor
|
||||
@ -138,20 +150,39 @@ class StatsGraphItemNode: ListViewItemNode {
|
||||
case .plain:
|
||||
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
|
||||
itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor
|
||||
contentSize = CGSize(width: params.width, height: 350.0 + item.height)
|
||||
contentSize = CGSize(width: params.width, height: 301.0)
|
||||
insets = itemListNeighborsPlainInsets(neighbors)
|
||||
case .blocks:
|
||||
itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
|
||||
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
|
||||
contentSize = CGSize(width: params.width, height: 350.0 + item.height)
|
||||
contentSize = CGSize(width: params.width, height: 301.0)
|
||||
insets = itemListNeighborsGroupedInsets(neighbors)
|
||||
}
|
||||
|
||||
var visibilityHeight = currentVisibilityHeight
|
||||
if let updatedController = updatedController {
|
||||
var height: CGFloat = 0.0
|
||||
var items: [ChartVisibilityItem] = []
|
||||
for item in updatedController.actualChartsCollection.chartValues {
|
||||
items.append(ChartVisibilityItem(title: item.name, color: .black))
|
||||
}
|
||||
if items.count > 1 {
|
||||
height = calculateVisiblityHeight(width: params.width - params.leftInset - params.rightInset, items: items)
|
||||
}
|
||||
if item.type == .hourlyStep {
|
||||
height -= 42.0
|
||||
}
|
||||
visibilityHeight = height
|
||||
}
|
||||
if let visibilityHeight = visibilityHeight {
|
||||
contentSize.height += visibilityHeight
|
||||
}
|
||||
|
||||
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
|
||||
|
||||
return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
strongSelf.visibilityHeight = visibilityHeight
|
||||
|
||||
if let _ = updatedTheme {
|
||||
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
|
||||
@ -178,14 +209,17 @@ class StatsGraphItemNode: ListViewItemNode {
|
||||
if strongSelf.backgroundNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
|
||||
}
|
||||
if strongSelf.chartContainerNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.chartContainerNode, at: 1)
|
||||
}
|
||||
if strongSelf.topStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
|
||||
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 2)
|
||||
}
|
||||
if strongSelf.bottomStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
|
||||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 3)
|
||||
}
|
||||
if strongSelf.maskNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
|
||||
strongSelf.insertSubnode(strongSelf.maskNode, at: 4)
|
||||
}
|
||||
let hasCorners = itemListHasRoundedBlockLayout(params)
|
||||
var hasTopCorners = false
|
||||
@ -207,7 +241,8 @@ class StatsGraphItemNode: ListViewItemNode {
|
||||
strongSelf.bottomStripeNode.isHidden = hasCorners
|
||||
}
|
||||
|
||||
strongSelf.chartNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: layout.size.width - leftInset - rightInset, height: 750.0))
|
||||
strongSelf.chartContainerNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: layout.size.width - leftInset - rightInset, height: contentSize.height))
|
||||
strongSelf.chartNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width - leftInset - rightInset, height: 750.0))
|
||||
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
|
||||
|
||||
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
|
||||
@ -220,13 +255,13 @@ class StatsGraphItemNode: ListViewItemNode {
|
||||
|
||||
strongSelf.activityIndicator.type = .custom(item.presentationData.theme.list.itemSecondaryTextColor, 16.0, 2.0, false)
|
||||
|
||||
if let updatedTheme = updatedTheme {
|
||||
strongSelf.chartNode.setupTheme(ChartTheme(presentationTheme: updatedTheme))
|
||||
}
|
||||
|
||||
if let updatedGraph = updatedGraph {
|
||||
if case let .Loaded(_, data) = updatedGraph {
|
||||
strongSelf.chartNode.setup(data, type: item.type, getDetailsData: { [weak self] date, completion in
|
||||
if let strongSelf = self, let item = strongSelf.item {
|
||||
item.getDetailsData?(date, completion)
|
||||
}
|
||||
})
|
||||
if case let .Loaded(_, data) = updatedGraph, let updatedController = updatedController {
|
||||
strongSelf.chartNode.setup(controller: updatedController)
|
||||
strongSelf.activityIndicator.isHidden = true
|
||||
strongSelf.chartNode.isHidden = false
|
||||
} else if case .OnDemand = updatedGraph {
|
||||
@ -234,10 +269,6 @@ class StatsGraphItemNode: ListViewItemNode {
|
||||
strongSelf.chartNode.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
if let updatedTheme = updatedTheme {
|
||||
strongSelf.chartNode.setupTheme(ChartTheme(presentationTheme: updatedTheme))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -170,15 +170,16 @@ public class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
|
||||
let itemBackgroundColor: UIColor
|
||||
let itemSeparatorColor: UIColor
|
||||
|
||||
var leftInset = 16.0 + params.leftInset
|
||||
var rightInset = 16.0 + params.rightInset
|
||||
let leftInset = 16.0 + params.leftInset
|
||||
let rightInset = 16.0 + params.rightInset
|
||||
var totalLeftInset = leftInset
|
||||
var additionalRightInset: CGFloat = 93.0
|
||||
let additionalRightInset: CGFloat = 93.0
|
||||
|
||||
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
|
||||
|
||||
let contentKind = messageContentKind(contentSettings: item.context.currentContentSettings.with { $0 }, message: item.message, strings: item.presentationData.strings, nameDisplayOrder: .firstLast, accountPeerId: item.context.account.peerId)
|
||||
let text = stringForMediaKind(contentKind, strings: item.presentationData.strings).0
|
||||
var text = stringForMediaKind(contentKind, strings: item.presentationData.strings).0
|
||||
text = foldLineBreaks(text)
|
||||
|
||||
var contentImageMedia: Media?
|
||||
for media in item.message.media {
|
||||
|
||||
@ -156,26 +156,26 @@ class StatsOverviewItemNode: ListViewItemNode {
|
||||
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseHeaderFontSize)
|
||||
let deltaFont = Font.regular(item.presentationData.fontSize.itemListBaseHeaderFontSize)
|
||||
|
||||
let (followersValueLabelLayout, followersValueLabelApply) = makeFollowersValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: compactNumericCountString(Int(item.stats.followers.current)), font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 140.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
let (followersValueLabelLayout, followersValueLabelApply) = makeFollowersValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: compactNumericCountString(Int(item.stats.followers.current)), font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let (viewsPerPostValueLabelLayout, viewsPerPostValueLabelApply) = makeViewsPerPostValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: compactNumericCountString(Int(item.stats.viewsPerPost.current)), font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 140.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
let (viewsPerPostValueLabelLayout, viewsPerPostValueLabelApply) = makeViewsPerPostValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.stats.viewsPerPost.current > 0 ? compactNumericCountString(Int(item.stats.viewsPerPost.current)) : "", font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let (sharesPerPostValueLabelLayout, sharesPerPostValueLabelApply) = makeSharesPerPostValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: compactNumericCountString(Int(item.stats.sharesPerPost.current)), font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
let (sharesPerPostValueLabelLayout, sharesPerPostValueLabelApply) = makeSharesPerPostValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.stats.sharesPerPost.current > 0 ? compactNumericCountString(Int(item.stats.sharesPerPost.current)) : "", font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
var enabledNotifications: Double = 0.0
|
||||
if item.stats.enabledNotifications.total > 0 {
|
||||
enabledNotifications = item.stats.enabledNotifications.value / item.stats.enabledNotifications.total
|
||||
}
|
||||
|
||||
let (enabledNotificationsValueLabelLayout, enabledNotificationsValueLabelApply) = makeEnabledNotificationsValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: String(format: "%.02f%%", enabledNotifications * 100.0), font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
let (enabledNotificationsValueLabelLayout, enabledNotificationsValueLabelApply) = makeEnabledNotificationsValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: String(format: "%.02f%%", enabledNotifications * 100.0), font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let (followersTitleLabelLayout, followersTitleLabelApply) = makeFollowersTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_Followers, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 140.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
let (followersTitleLabelLayout, followersTitleLabelApply) = makeFollowersTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_Followers, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let (viewsPerPostTitleLabelLayout, viewsPerPostTitleLabelApply) = makeViewsPerPostTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_ViewsPerPost, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 140.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
let (viewsPerPostTitleLabelLayout, viewsPerPostTitleLabelApply) = makeViewsPerPostTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.stats.viewsPerPost.current > 0 ? item.presentationData.strings.Stats_ViewsPerPost : "", font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let (sharesPerPostTitleLabelLayout, sharesPerPostTitleLabelApply) = makeSharesPerPostTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_SharesPerPost, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 140.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
let (sharesPerPostTitleLabelLayout, sharesPerPostTitleLabelApply) = makeSharesPerPostTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.stats.sharesPerPost.current > 0 ? item.presentationData.strings.Stats_SharesPerPost : "", font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let (enabledNotificationsTitleLabelLayout, enabledNotificationsTitleLabelApply) = makeEnabledNotificationsTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_EnabledNotifications, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 140.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
let (enabledNotificationsTitleLabelLayout, enabledNotificationsTitleLabelApply) = makeEnabledNotificationsTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_EnabledNotifications, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let followersDeltaValue = item.stats.followers.current - item.stats.followers.previous
|
||||
let followersDeltaCompact = compactNumericCountString(abs(Int(followersDeltaValue)))
|
||||
@ -185,7 +185,9 @@ class StatsOverviewItemNode: ListViewItemNode {
|
||||
followersDeltaPercentage = abs(followersDeltaValue / item.stats.followers.previous)
|
||||
}
|
||||
|
||||
let (followersDeltaLabelLayout, followersDeltaLabelApply) = makeFollowersDeltaLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: String(format: "%@ (%.02f%%)", followersDelta, followersDeltaPercentage * 100.0), font: deltaFont, textColor: followersDeltaValue > 0.0 ? item.presentationData.theme.list.freeTextSuccessColor : item.presentationData.theme.list.freeTextErrorColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 140.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
let followersDeltaText = abs(followersDeltaPercentage) > 0.0 ? String(format: "%@ (%.02f%%)", followersDelta, followersDeltaPercentage * 100.0) : ""
|
||||
|
||||
let (followersDeltaLabelLayout, followersDeltaLabelApply) = makeFollowersDeltaLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: followersDeltaText, font: deltaFont, textColor: followersDeltaValue > 0.0 ? item.presentationData.theme.list.freeTextSuccessColor : item.presentationData.theme.list.freeTextErrorColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let viewsPerPostDeltaValue = item.stats.viewsPerPost.current - item.stats.viewsPerPost.previous
|
||||
let viewsPerPostDeltaCompact = compactNumericCountString(abs(Int(viewsPerPostDeltaValue)))
|
||||
@ -195,7 +197,9 @@ class StatsOverviewItemNode: ListViewItemNode {
|
||||
viewsPerPostDeltaPercentage = abs(viewsPerPostDeltaValue / item.stats.viewsPerPost.previous)
|
||||
}
|
||||
|
||||
let (viewsPerPostDeltaLabelLayout, viewsPerPostDeltaLabelApply) = makeViewsPerPostDeltaLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: String(format: "%@ (%.02f%%)", viewsPerPostDelta, viewsPerPostDeltaPercentage * 100.0), font: deltaFont, textColor: viewsPerPostDeltaValue > 0.0 ? item.presentationData.theme.list.freeTextSuccessColor : item.presentationData.theme.list.freeTextErrorColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 140.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
let viewsPerPostDeltaText = abs(viewsPerPostDeltaPercentage) > 0.0 ? String(format: "%@ (%.02f%%)", viewsPerPostDelta, viewsPerPostDeltaPercentage * 100.0) : ""
|
||||
|
||||
let (viewsPerPostDeltaLabelLayout, viewsPerPostDeltaLabelApply) = makeViewsPerPostDeltaLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: viewsPerPostDeltaText, font: deltaFont, textColor: viewsPerPostDeltaValue > 0.0 ? item.presentationData.theme.list.freeTextSuccessColor : item.presentationData.theme.list.freeTextErrorColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let sharesPerPostDeltaValue = item.stats.sharesPerPost.current - item.stats.sharesPerPost.previous
|
||||
let sharesPerPostDeltaCompact = compactNumericCountString(abs(Int(sharesPerPostDeltaValue)))
|
||||
@ -205,7 +209,9 @@ class StatsOverviewItemNode: ListViewItemNode {
|
||||
sharesPerPostDeltaPercentage = abs(sharesPerPostDeltaValue / item.stats.sharesPerPost.previous)
|
||||
}
|
||||
|
||||
let (sharesPerPostDeltaLabelLayout, sharesPerPostDeltaLabelApply) = makeSharesPerPostDeltaLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: String(format: "%@ (%.02f%%)", sharesPerPostDelta, sharesPerPostDeltaPercentage * 100.0), font: deltaFont, textColor: sharesPerPostDeltaValue > 0.0 ? item.presentationData.theme.list.freeTextSuccessColor : item.presentationData.theme.list.freeTextErrorColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 140.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
let sharesPerPostDeltaText = abs(sharesPerPostDeltaPercentage) > 0.0 ? String(format: "%@ (%.02f%%)", sharesPerPostDelta, sharesPerPostDeltaPercentage * 100.0) : ""
|
||||
|
||||
let (sharesPerPostDeltaLabelLayout, sharesPerPostDeltaLabelApply) = makeSharesPerPostDeltaLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: sharesPerPostDeltaText, font: deltaFont, textColor: sharesPerPostDeltaValue > 0.0 ? item.presentationData.theme.list.freeTextSuccessColor : item.presentationData.theme.list.freeTextErrorColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let contentSize: CGSize
|
||||
let insets: UIEdgeInsets
|
||||
@ -213,11 +219,35 @@ class StatsOverviewItemNode: ListViewItemNode {
|
||||
let itemBackgroundColor: UIColor
|
||||
let itemSeparatorColor: UIColor
|
||||
|
||||
let height: CGFloat
|
||||
if item.stats.viewsPerPost.current.isZero && item.stats.sharesPerPost.current.isZero {
|
||||
height = 64.0
|
||||
let horizontalSpacing: CGFloat = 4.0
|
||||
let verticalSpacing: CGFloat = 18.0
|
||||
let topInset: CGFloat = 14.0
|
||||
let sideInset: CGFloat = 16.0
|
||||
|
||||
var height: CGFloat = topInset * 2.0
|
||||
height += enabledNotificationsValueLabelLayout.size.height + enabledNotificationsTitleLabelLayout.size.height
|
||||
|
||||
var twoColumnLayout = true
|
||||
if max(followersValueLabelLayout.size.width + followersDeltaLabelLayout.size.width + horizontalSpacing + enabledNotificationsValueLabelLayout.size.width, viewsPerPostValueLabelLayout.size.width + viewsPerPostDeltaLabelLayout.size.width + horizontalSpacing + sharesPerPostValueLabelLayout.size.width + sharesPerPostDeltaLabelLayout.size.width) > params.width - leftInset - rightInset {
|
||||
twoColumnLayout = false
|
||||
}
|
||||
|
||||
if twoColumnLayout {
|
||||
if !item.stats.viewsPerPost.current.isZero || !item.stats.sharesPerPost.current.isZero {
|
||||
height += verticalSpacing
|
||||
height += sharesPerPostValueLabelLayout.size.height + sharesPerPostTitleLabelLayout.size.height
|
||||
}
|
||||
} else {
|
||||
height = 120.0
|
||||
height += verticalSpacing
|
||||
height += enabledNotificationsValueLabelLayout.size.height + enabledNotificationsTitleLabelLayout.size.height
|
||||
if !item.stats.viewsPerPost.current.isZero {
|
||||
height += verticalSpacing
|
||||
height += viewsPerPostValueLabelLayout.size.height + viewsPerPostTitleLabelLayout.size.height
|
||||
}
|
||||
if !item.stats.sharesPerPost.current.isZero {
|
||||
height += verticalSpacing
|
||||
height += sharesPerPostValueLabelLayout.size.height + sharesPerPostTitleLabelLayout.size.height
|
||||
}
|
||||
}
|
||||
|
||||
switch item.style {
|
||||
@ -316,28 +346,25 @@ class StatsOverviewItemNode: ListViewItemNode {
|
||||
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight))
|
||||
}
|
||||
|
||||
let horizontalSpacing: CGFloat = 4.0
|
||||
let verticalSpacing: CGFloat = 70.0
|
||||
let topInset: CGFloat = 14.0
|
||||
let sideInset: CGFloat = 16.0
|
||||
|
||||
strongSelf.followersValueLabel.frame = CGRect(origin: CGPoint(x: sideInset + leftInset, y: topInset), size: followersValueLabelLayout.size)
|
||||
strongSelf.followersTitleLabel.frame = CGRect(origin: CGPoint(x: sideInset + leftInset, y: strongSelf.followersValueLabel.frame.maxY), size: followersTitleLabelLayout.size)
|
||||
strongSelf.followersDeltaLabel.frame = CGRect(origin: CGPoint(x: strongSelf.followersValueLabel.frame.maxX + horizontalSpacing, y: strongSelf.followersValueLabel.frame.minY + 4.0), size: followersDeltaLabelLayout.size)
|
||||
strongSelf.followersDeltaLabel.frame = CGRect(origin: CGPoint(x: strongSelf.followersValueLabel.frame.maxX + horizontalSpacing, y: strongSelf.followersValueLabel.frame.maxY - followersDeltaLabelLayout.size.height - 2.0), size: followersDeltaLabelLayout.size)
|
||||
|
||||
strongSelf.viewsPerPostValueLabel.frame = CGRect(origin: CGPoint(x: sideInset + leftInset, y: verticalSpacing), size: viewsPerPostValueLabelLayout.size)
|
||||
let secondColumnX = twoColumnLayout ? max(layout.size.width / 2.0, sideInset + leftInset + max(followersValueLabelLayout.size.width + followersDeltaLabelLayout.size.width, viewsPerPostValueLabelLayout.size.width + viewsPerPostDeltaLabelLayout.size.width) + horizontalSpacing) : sideInset + leftInset
|
||||
|
||||
let enabledNotificationsY = twoColumnLayout ? topInset : strongSelf.followersTitleLabel.frame.maxY + verticalSpacing
|
||||
strongSelf.enabledNotificationsValueLabel.frame = CGRect(origin: CGPoint(x: secondColumnX, y: enabledNotificationsY), size: enabledNotificationsValueLabelLayout.size)
|
||||
strongSelf.enabledNotificationsTitleLabel.frame = CGRect(origin: CGPoint(x: secondColumnX, y: strongSelf.enabledNotificationsValueLabel.frame.maxY), size: enabledNotificationsTitleLabelLayout.size)
|
||||
|
||||
let viewsPerPostY = twoColumnLayout ? strongSelf.followersTitleLabel.frame.maxY + verticalSpacing : strongSelf.enabledNotificationsTitleLabel.frame.maxY + verticalSpacing
|
||||
strongSelf.viewsPerPostValueLabel.frame = CGRect(origin: CGPoint(x: sideInset + leftInset, y: viewsPerPostY), size: viewsPerPostValueLabelLayout.size)
|
||||
strongSelf.viewsPerPostTitleLabel.frame = CGRect(origin: CGPoint(x: sideInset + leftInset, y: strongSelf.viewsPerPostValueLabel.frame.maxY), size: viewsPerPostTitleLabelLayout.size)
|
||||
strongSelf.viewsPerPostDeltaLabel.frame = CGRect(origin: CGPoint(x: strongSelf.viewsPerPostValueLabel.frame.maxX + horizontalSpacing, y: strongSelf.viewsPerPostValueLabel.frame.minY + 4.0), size: viewsPerPostDeltaLabelLayout.size)
|
||||
|
||||
let rightColumnX = max(layout.size.width / 2.0, max(strongSelf.followersDeltaLabel.frame.maxX, strongSelf.viewsPerPostDeltaLabel.frame.maxX) + horizontalSpacing)
|
||||
|
||||
strongSelf.sharesPerPostValueLabel.frame = CGRect(origin: CGPoint(x: rightColumnX, y: verticalSpacing), size: sharesPerPostValueLabelLayout.size)
|
||||
strongSelf.enabledNotificationsValueLabel.frame = CGRect(origin: CGPoint(x: rightColumnX, y: topInset), size: enabledNotificationsValueLabelLayout.size)
|
||||
|
||||
strongSelf.sharesPerPostTitleLabel.frame = CGRect(origin: CGPoint(x: rightColumnX, y: strongSelf.sharesPerPostValueLabel.frame.maxY), size: sharesPerPostTitleLabelLayout.size)
|
||||
strongSelf.enabledNotificationsTitleLabel.frame = CGRect(origin: CGPoint(x: rightColumnX, y: strongSelf.enabledNotificationsValueLabel.frame.maxY), size: enabledNotificationsTitleLabelLayout.size)
|
||||
|
||||
strongSelf.sharesPerPostDeltaLabel.frame = CGRect(origin: CGPoint(x: strongSelf.sharesPerPostValueLabel.frame.maxX + horizontalSpacing, y: strongSelf.sharesPerPostValueLabel.frame.minY + 4.0), size: sharesPerPostDeltaLabelLayout.size)
|
||||
strongSelf.viewsPerPostDeltaLabel.frame = CGRect(origin: CGPoint(x: strongSelf.viewsPerPostValueLabel.frame.maxX + horizontalSpacing, y: strongSelf.viewsPerPostValueLabel.frame.maxY - viewsPerPostDeltaLabelLayout.size.height - 2.0), size: viewsPerPostDeltaLabelLayout.size)
|
||||
|
||||
let sharesPerPostY = twoColumnLayout ? strongSelf.enabledNotificationsTitleLabel.frame.maxY + verticalSpacing : strongSelf.viewsPerPostTitleLabel.frame.maxY + verticalSpacing
|
||||
strongSelf.sharesPerPostValueLabel.frame = CGRect(origin: CGPoint(x: secondColumnX, y: sharesPerPostY), size: sharesPerPostValueLabelLayout.size)
|
||||
strongSelf.sharesPerPostTitleLabel.frame = CGRect(origin: CGPoint(x: secondColumnX, y: strongSelf.sharesPerPostValueLabel.frame.maxY), size: sharesPerPostTitleLabelLayout.size)
|
||||
strongSelf.sharesPerPostDeltaLabel.frame = CGRect(origin: CGPoint(x: strongSelf.sharesPerPostValueLabel.frame.maxX + horizontalSpacing, y: strongSelf.sharesPerPostValueLabel.frame.maxY - sharesPerPostDeltaLabelLayout.size.height - 2.0), size: sharesPerPostDeltaLabelLayout.size)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -31,7 +31,7 @@ public enum ChannelStatsGraph: Equatable {
|
||||
case .Empty:
|
||||
return true
|
||||
case let .Failed(error):
|
||||
return true
|
||||
return error.lowercased().contains("not enough data")
|
||||
default:
|
||||
return false
|
||||
}
|
||||
@ -598,8 +598,11 @@ extension ChannelStatsMessageInteractions {
|
||||
extension ChannelStats {
|
||||
convenience init(apiBroadcastStats: Api.stats.BroadcastStats, peerId: PeerId) {
|
||||
switch apiBroadcastStats {
|
||||
case let .broadcastStats(period, followers, viewsPerPost, sharesPerPost, enabledNotifications, growthGraph, followersGraph, muteGraph, topHoursGraph, interactionsGraph, instantViewInteractionsGraph, viewsBySourceGraph, newFollowersBySourceGraph, languagesGraph, recentMessageInteractions):
|
||||
self.init(period: ChannelStatsDateRange(apiStatsDateRangeDays: period), followers: ChannelStatsValue(apiStatsAbsValueAndPrev: followers), viewsPerPost: ChannelStatsValue(apiStatsAbsValueAndPrev: viewsPerPost), sharesPerPost: ChannelStatsValue(apiStatsAbsValueAndPrev: sharesPerPost), enabledNotifications: ChannelStatsPercentValue(apiPercentValue: enabledNotifications), growthGraph: ChannelStatsGraph(apiStatsGraph: growthGraph), followersGraph: ChannelStatsGraph(apiStatsGraph: followersGraph), muteGraph: ChannelStatsGraph(apiStatsGraph: muteGraph), topHoursGraph: ChannelStatsGraph(apiStatsGraph: topHoursGraph), interactionsGraph: ChannelStatsGraph(apiStatsGraph: interactionsGraph), instantPageInteractionsGraph: ChannelStatsGraph(apiStatsGraph: instantViewInteractionsGraph), viewsBySourceGraph: ChannelStatsGraph(apiStatsGraph: viewsBySourceGraph), newFollowersBySourceGraph: ChannelStatsGraph(apiStatsGraph: newFollowersBySourceGraph), languagesGraph: ChannelStatsGraph(apiStatsGraph: languagesGraph), messageInteractions: recentMessageInteractions.map { ChannelStatsMessageInteractions(apiMessageInteractionCounters: $0, peerId: peerId) })
|
||||
case let .broadcastStats(period, followers, viewsPerPost, sharesPerPost, enabledNotifications, apiGrowthGraph, apiFollowersGraph, apiMuteGraph, apiTopHoursGraph, apiInteractionsGraph, apiInstantViewInteractionsGraph, apiViewsBySourceGraph, apiNewFollowersBySourceGraph, apiLanguagesGraph, recentMessageInteractions):
|
||||
let growthGraph = ChannelStatsGraph(apiStatsGraph: apiGrowthGraph)
|
||||
let isEmpty = growthGraph.isEmpty
|
||||
|
||||
self.init(period: ChannelStatsDateRange(apiStatsDateRangeDays: period), followers: ChannelStatsValue(apiStatsAbsValueAndPrev: followers), viewsPerPost: ChannelStatsValue(apiStatsAbsValueAndPrev: viewsPerPost), sharesPerPost: ChannelStatsValue(apiStatsAbsValueAndPrev: sharesPerPost), enabledNotifications: ChannelStatsPercentValue(apiPercentValue: enabledNotifications), growthGraph: growthGraph, followersGraph: ChannelStatsGraph(apiStatsGraph: apiFollowersGraph), muteGraph: ChannelStatsGraph(apiStatsGraph: apiMuteGraph), topHoursGraph: ChannelStatsGraph(apiStatsGraph: apiTopHoursGraph), interactionsGraph: isEmpty ? .Empty : ChannelStatsGraph(apiStatsGraph: apiInteractionsGraph), instantPageInteractionsGraph: isEmpty ? .Empty : ChannelStatsGraph(apiStatsGraph: apiInstantViewInteractionsGraph), viewsBySourceGraph: isEmpty ? .Empty : ChannelStatsGraph(apiStatsGraph: apiViewsBySourceGraph), newFollowersBySourceGraph: isEmpty ? .Empty : ChannelStatsGraph(apiStatsGraph: apiNewFollowersBySourceGraph), languagesGraph: isEmpty ? .Empty : ChannelStatsGraph(apiStatsGraph: apiLanguagesGraph), messageInteractions: recentMessageInteractions.map { ChannelStatsMessageInteractions(apiMessageInteractionCounters: $0, peerId: peerId) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -221,3 +221,19 @@ public func descriptionStringForMessage(contentSettings: ContentSettings, messag
|
||||
}
|
||||
return stringForMediaKind(messageContentKind(contentSettings: contentSettings, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, accountPeerId: accountPeerId), strings: strings)
|
||||
}
|
||||
|
||||
public func foldLineBreaks(_ text: String) -> String {
|
||||
let lines = text.split { $0.isNewline }
|
||||
var result = ""
|
||||
for line in lines {
|
||||
if line.isEmpty {
|
||||
continue
|
||||
}
|
||||
if result.isEmpty {
|
||||
result += line
|
||||
} else {
|
||||
result += " " + line
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@ -203,6 +203,7 @@ framework(
|
||||
"//submodules/AccountUtils:AccountUtils",
|
||||
"//submodules/Svg:Svg",
|
||||
"//submodules/StatisticsUI:StatisticsUI",
|
||||
"//submodules/ManagedAnimationNode:ManagedAnimationNode",
|
||||
],
|
||||
frameworks = [
|
||||
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
|
||||
|
||||
@ -201,6 +201,7 @@ swift_library(
|
||||
"//submodules/SemanticStatusNode:SemanticStatusNode",
|
||||
"//submodules/AccountUtils:AccountUtils",
|
||||
"//submodules/Svg:Svg",
|
||||
"//submodules/ManagedAnimationNode:ManagedAnimationNode",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
Binary file not shown.
@ -120,7 +120,6 @@ public final class ChatControllerInteraction {
|
||||
var stickerSettings: ChatInterfaceStickerSettings
|
||||
var searchTextHighightState: (String, [MessageIndex])?
|
||||
var seenOneTimeAnimatedMedia = Set<MessageId>()
|
||||
var seenDicePointsValue = [MessageId: Int]()
|
||||
|
||||
init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, TapLongTapOrDoubleTapGestureRecognizer?) -> Void, openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, tapMessage: ((Message) -> Void)?, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendCurrentMessage: @escaping (Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool, ASDisplayNode, CGRect) -> Bool, sendGif: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, requestMessageActionUrlAuth: @escaping (String, MessageId, Int32) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?, Message?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @escaping (Message) -> Void, openTheme: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, chatControllerNode: @escaping () -> ASDisplayNode?, reactionContainerNode: @escaping () -> ReactionSelectionParentNode?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction, Message?) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, navigateToFirstDateMessage: @escaping(Int32) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, rateCall: @escaping (Message, CallId) -> Void, requestSelectMessagePollOptions: @escaping (MessageId, [Data]) -> Void, requestOpenMessagePollResults: @escaping (MessageId, MediaId) -> Void, openAppStorePage: @escaping () -> Void, displayMessageTooltip: @escaping (MessageId, String, ASDisplayNode?, CGRect?) -> Void, seekToTimecode: @escaping (Message, Double, Bool) -> Void, scheduleCurrentMessage: @escaping () -> Void, sendScheduledMessagesNow: @escaping ([MessageId]) -> Void, editScheduledMessagesTime: @escaping ([MessageId]) -> Void, performTextSelectionAction: @escaping (UInt32, String, TextSelectionAction) -> Void, updateMessageReaction: @escaping (MessageId, String?) -> Void, openMessageReactions: @escaping (MessageId) -> Void, displaySwipeToReplyHint: @escaping () -> Void, dismissReplyMarkupMessage: @escaping (Message) -> Void, openMessagePollResults: @escaping (MessageId, Data) -> Void, openPollCreation: @escaping (Bool?) -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings) {
|
||||
self.openMessage = openMessage
|
||||
|
||||
@ -2240,35 +2240,40 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
|
||||
var messages: [EnqueueMessage] = []
|
||||
|
||||
let inputText = convertMarkdownToAttributes(effectivePresentationInterfaceState.interfaceState.composeInputState.inputText)
|
||||
|
||||
for text in breakChatInputText(trimChatInputText(inputText)) {
|
||||
if text.length != 0 {
|
||||
var attributes: [MessageAttribute] = []
|
||||
let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text))
|
||||
if !entities.isEmpty {
|
||||
attributes.append(TextEntitiesMessageAttribute(entities: entities))
|
||||
let effectiveInputText = effectivePresentationInterfaceState.interfaceState.composeInputState.inputText
|
||||
if effectiveInputText.string.trimmingCharacters(in: .whitespacesAndNewlines) == "🎲" {
|
||||
messages.append(.message(text: "", attributes: [], mediaReference: AnyMediaReference.standalone(media: TelegramMediaDice()), replyToMessageId: self.chatPresentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil))
|
||||
} else {
|
||||
let inputText = convertMarkdownToAttributes(effectiveInputText)
|
||||
|
||||
for text in breakChatInputText(trimChatInputText(inputText)) {
|
||||
if text.length != 0 {
|
||||
var attributes: [MessageAttribute] = []
|
||||
let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text))
|
||||
if !entities.isEmpty {
|
||||
attributes.append(TextEntitiesMessageAttribute(entities: entities))
|
||||
}
|
||||
var webpage: TelegramMediaWebpage?
|
||||
if self.chatPresentationInterfaceState.interfaceState.composeDisableUrlPreview != nil {
|
||||
attributes.append(OutgoingContentInfoMessageAttribute(flags: [.disableLinkPreviews]))
|
||||
} else {
|
||||
webpage = self.chatPresentationInterfaceState.urlPreview?.1
|
||||
}
|
||||
messages.append(.message(text: text.string, attributes: attributes, mediaReference: webpage.flatMap(AnyMediaReference.standalone), replyToMessageId: self.chatPresentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil))
|
||||
}
|
||||
var webpage: TelegramMediaWebpage?
|
||||
if self.chatPresentationInterfaceState.interfaceState.composeDisableUrlPreview != nil {
|
||||
attributes.append(OutgoingContentInfoMessageAttribute(flags: [.disableLinkPreviews]))
|
||||
} else {
|
||||
webpage = self.chatPresentationInterfaceState.urlPreview?.1
|
||||
}
|
||||
messages.append(.message(text: text.string, attributes: attributes, mediaReference: webpage.flatMap(AnyMediaReference.standalone), replyToMessageId: self.chatPresentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil))
|
||||
}
|
||||
}
|
||||
|
||||
var forwardingToSameChat = false
|
||||
if case let .peer(id) = self.chatPresentationInterfaceState.chatLocation, id.namespace == Namespaces.Peer.CloudUser, id != self.context.account.peerId, let forwardMessageIds = self.chatPresentationInterfaceState.interfaceState.forwardMessageIds {
|
||||
for messageId in forwardMessageIds {
|
||||
if messageId.peerId == id {
|
||||
forwardingToSameChat = true
|
||||
var forwardingToSameChat = false
|
||||
if case let .peer(id) = self.chatPresentationInterfaceState.chatLocation, id.namespace == Namespaces.Peer.CloudUser, id != self.context.account.peerId, let forwardMessageIds = self.chatPresentationInterfaceState.interfaceState.forwardMessageIds {
|
||||
for messageId in forwardMessageIds {
|
||||
if messageId.peerId == id {
|
||||
forwardingToSameChat = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !messages.isEmpty && forwardingToSameChat {
|
||||
self.controllerInteraction.displaySwipeToReplyHint()
|
||||
if !messages.isEmpty && forwardingToSameChat {
|
||||
self.controllerInteraction.displaySwipeToReplyHint()
|
||||
}
|
||||
}
|
||||
|
||||
if !messages.isEmpty || self.chatPresentationInterfaceState.interfaceState.forwardMessageIds != nil {
|
||||
|
||||
@ -28,7 +28,12 @@ func chatHistoryEntriesForView(location: ChatLocation, view: MessageHistoryView,
|
||||
|
||||
var groupBucket: [(Message, Bool, ChatHistoryMessageSelection, ChatMessageEntryAttributes)] = []
|
||||
loop: for entry in view.entries {
|
||||
var contentTypeHint: ChatMessageEntryContentType = .generic
|
||||
|
||||
for media in entry.message.media {
|
||||
if media is TelegramMediaDice {
|
||||
contentTypeHint = .animatedEmoji
|
||||
}
|
||||
if let action = media as? TelegramMediaAction {
|
||||
switch action.action {
|
||||
case .channelMigratedFromGroup, .groupMigratedToChannel, .historyCleared:
|
||||
@ -44,7 +49,7 @@ func chatHistoryEntriesForView(location: ChatLocation, view: MessageHistoryView,
|
||||
adminRank = adminRanks[author.id]
|
||||
}
|
||||
|
||||
var contentTypeHint: ChatMessageEntryContentType = .generic
|
||||
|
||||
if presentationData.largeEmoji, entry.message.media.isEmpty {
|
||||
if stickersEnabled && entry.message.text.count == 1, let _ = associatedData.animatedEmojiStickers[entry.message.text.basicEmoji.0] {
|
||||
contentTypeHint = .animatedEmoji
|
||||
|
||||
@ -18,375 +18,24 @@ import AnimatedStickerNode
|
||||
import TelegramAnimatedStickerNode
|
||||
import Emoji
|
||||
import Markdown
|
||||
|
||||
import RLottieBinding
|
||||
import AppBundle
|
||||
import GZip
|
||||
import ManagedAnimationNode
|
||||
|
||||
private let nameFont = Font.medium(14.0)
|
||||
private let inlineBotPrefixFont = Font.regular(14.0)
|
||||
private let inlineBotNameFont = nameFont
|
||||
|
||||
private final class ManagedAnimationState {
|
||||
let item: ManagedAnimationItem
|
||||
protocol GenericAnimatedStickerNode: ASDisplayNode {
|
||||
|
||||
private let instance: LottieInstance
|
||||
|
||||
let frameCount: Int
|
||||
let fps: Double
|
||||
|
||||
var relativeTime: Double = 0.0
|
||||
var frameIndex: Int?
|
||||
|
||||
private let renderContext: DrawingContext
|
||||
|
||||
init?(displaySize: CGSize, item: ManagedAnimationItem, current: ManagedAnimationState?) {
|
||||
let resolvedInstance: LottieInstance
|
||||
let renderContext: DrawingContext
|
||||
|
||||
if let current = current {
|
||||
resolvedInstance = current.instance
|
||||
renderContext = current.renderContext
|
||||
} else {
|
||||
guard let path = item.source.path else {
|
||||
return nil
|
||||
}
|
||||
guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else {
|
||||
return nil
|
||||
}
|
||||
guard let unpackedData = TGGUnzipData(data, 5 * 1024 * 1024) else {
|
||||
return nil
|
||||
}
|
||||
guard let instance = LottieInstance(data: unpackedData, cacheKey: item.source.cacheKey) else {
|
||||
return nil
|
||||
}
|
||||
resolvedInstance = instance
|
||||
renderContext = DrawingContext(size: displaySize, scale: UIScreenScale, premultiplied: true, clear: true)
|
||||
}
|
||||
|
||||
self.item = item
|
||||
self.instance = resolvedInstance
|
||||
self.renderContext = renderContext
|
||||
|
||||
self.frameCount = Int(self.instance.frameCount)
|
||||
self.fps = Double(self.instance.frameRate)
|
||||
}
|
||||
|
||||
func draw() -> UIImage? {
|
||||
self.instance.renderFrame(with: Int32(self.frameIndex ?? 0), into: self.renderContext.bytes.assumingMemoryBound(to: UInt8.self), width: Int32(self.renderContext.size.width * self.renderContext.scale), height: Int32(self.renderContext.size.height * self.renderContext.scale), bytesPerRow: Int32(self.renderContext.bytesPerRow))
|
||||
return self.renderContext.generateImage()
|
||||
}
|
||||
}
|
||||
|
||||
struct ManagedAnimationFrameRange: Equatable {
|
||||
var startFrame: Int
|
||||
var endFrame: Int
|
||||
}
|
||||
|
||||
enum ManagedAnimationSource: Equatable {
|
||||
case local(String)
|
||||
case resource(MediaBox, MediaResource)
|
||||
extension AnimatedStickerNode: GenericAnimatedStickerNode {
|
||||
|
||||
var cacheKey: String {
|
||||
switch self {
|
||||
case let .local(name):
|
||||
return name
|
||||
case let .resource(mediaBox, resource):
|
||||
return resource.id.uniqueId
|
||||
}
|
||||
}
|
||||
|
||||
var path: String? {
|
||||
switch self {
|
||||
case let .local(name):
|
||||
return getAppBundle().path(forResource: name, ofType: "tgs")
|
||||
case let .resource(mediaBox, resource):
|
||||
return mediaBox.completedResourcePath(resource)
|
||||
}
|
||||
}
|
||||
|
||||
static func == (lhs: ManagedAnimationSource, rhs: ManagedAnimationSource) -> Bool {
|
||||
switch lhs {
|
||||
case let .local(lhsPath):
|
||||
if case let .local(rhsPath) = rhs, lhsPath == rhsPath {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .resource(lhsMediaBox, lhsResource):
|
||||
if case let .resource(rhsMediaBox, rhsResource) = rhs, lhsMediaBox === rhsMediaBox, lhsResource.isEqual(to: rhsResource) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ManagedAnimationItem: Equatable {
|
||||
let source: ManagedAnimationSource
|
||||
var frames: ManagedAnimationFrameRange
|
||||
var duration: Double
|
||||
}
|
||||
|
||||
class ManagedAnimationNode: ASDisplayNode {
|
||||
let intrinsicSize: CGSize
|
||||
|
||||
private let imageNode: ASImageNode
|
||||
private let displayLink: CADisplayLink
|
||||
|
||||
fileprivate var state: ManagedAnimationState?
|
||||
fileprivate var trackStack: [ManagedAnimationItem] = []
|
||||
fileprivate var didTryAdvancingState = false
|
||||
|
||||
init(size: CGSize) {
|
||||
self.intrinsicSize = size
|
||||
|
||||
self.imageNode = ASImageNode()
|
||||
self.imageNode.displayWithoutProcessing = true
|
||||
self.imageNode.displaysAsynchronously = false
|
||||
self.imageNode.frame = CGRect(origin: CGPoint(), size: self.intrinsicSize)
|
||||
|
||||
final class DisplayLinkTarget: NSObject {
|
||||
private let f: () -> Void
|
||||
|
||||
init(_ f: @escaping () -> Void) {
|
||||
self.f = f
|
||||
}
|
||||
|
||||
@objc func event() {
|
||||
self.f()
|
||||
}
|
||||
}
|
||||
var displayLinkUpdate: (() -> Void)?
|
||||
self.displayLink = CADisplayLink(target: DisplayLinkTarget {
|
||||
displayLinkUpdate?()
|
||||
}, selector: #selector(DisplayLinkTarget.event))
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.imageNode)
|
||||
|
||||
self.displayLink.add(to: RunLoop.main, forMode: .common)
|
||||
|
||||
displayLinkUpdate = { [weak self] in
|
||||
self?.updateAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
func advanceState() {
|
||||
guard !self.trackStack.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
let item = self.trackStack.removeFirst()
|
||||
|
||||
if let state = self.state, state.item.source == item.source {
|
||||
self.state = ManagedAnimationState(displaySize: self.intrinsicSize, item: item, current: state)
|
||||
} else {
|
||||
self.state = ManagedAnimationState(displaySize: self.intrinsicSize, item: item, current: nil)
|
||||
}
|
||||
|
||||
self.didTryAdvancingState = false
|
||||
}
|
||||
|
||||
fileprivate func updateAnimation() {
|
||||
if self.state == nil {
|
||||
self.advanceState()
|
||||
}
|
||||
|
||||
guard let state = self.state else {
|
||||
return
|
||||
}
|
||||
let timestamp = CACurrentMediaTime()
|
||||
|
||||
let fps = state.fps
|
||||
let frameRange = state.item.frames
|
||||
|
||||
let duration: Double = state.item.duration
|
||||
var t = state.relativeTime / duration
|
||||
t = max(0.0, t)
|
||||
t = min(1.0, t)
|
||||
|
||||
let frameOffset = Int(Double(frameRange.startFrame) * (1.0 - t) + Double(frameRange.endFrame) * t)
|
||||
let lowerBound: Int = 0
|
||||
let upperBound = state.frameCount - 1
|
||||
let frameIndex = max(lowerBound, min(upperBound, frameOffset))
|
||||
|
||||
if state.frameIndex != frameIndex {
|
||||
state.frameIndex = frameIndex
|
||||
if let image = state.draw() {
|
||||
self.imageNode.image = image
|
||||
}
|
||||
}
|
||||
|
||||
var animationAdvancement: Double = 1.0 / 60.0
|
||||
animationAdvancement *= Double(min(2, self.trackStack.count + 1))
|
||||
|
||||
state.relativeTime += animationAdvancement
|
||||
|
||||
if state.relativeTime >= duration && !self.didTryAdvancingState {
|
||||
self.didTryAdvancingState = true
|
||||
self.advanceState()
|
||||
}
|
||||
}
|
||||
|
||||
func trackTo(item: ManagedAnimationItem) {
|
||||
self.trackStack.append(item)
|
||||
self.didTryAdvancingState = false
|
||||
self.updateAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
enum ManagedDiceAnimationState: Equatable {
|
||||
case rolling
|
||||
case value(Int)
|
||||
}
|
||||
|
||||
final class ManagedDiceAnimationNode: ManagedAnimationNode {
|
||||
private let context: AccountContext
|
||||
private let emojis: [TelegramMediaFile]
|
||||
|
||||
private var diceState: ManagedDiceAnimationState = .rolling
|
||||
private let disposable = MetaDisposable()
|
||||
|
||||
init(context: AccountContext, emojis: [TelegramMediaFile]) {
|
||||
self.context = context
|
||||
self.emojis = emojis
|
||||
|
||||
super.init(size: CGSize(width: 136.0, height: 136.0))
|
||||
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("DiceRolling"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0), duration: 0.3))
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disposable.dispose()
|
||||
}
|
||||
|
||||
func setState(_ diceState: ManagedDiceAnimationState) {
|
||||
let previousState = self.diceState
|
||||
self.diceState = diceState
|
||||
|
||||
switch previousState {
|
||||
case .rolling:
|
||||
switch diceState {
|
||||
case let .value(value):
|
||||
// self.disposable.set(freeMediaFileInteractiveFetched(account: item.context.account, fileReference: .standalone(media: emojiFile)).start())
|
||||
//
|
||||
// return chatMessageAnimationData(postbox: self.account.postbox, resource: self.resource, fitzModifier: self.fitzModifier, width: width, height: height, synchronousLoad: false)
|
||||
// |> filter { data in
|
||||
// return data.size != 0
|
||||
// }
|
||||
// |> map { data -> (String, Bool) in
|
||||
// return (data.path, data.complete)
|
||||
// }
|
||||
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("DiceRolling"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0), duration: 0.3))
|
||||
case .rolling:
|
||||
break
|
||||
}
|
||||
case let .value(currentValue):
|
||||
switch diceState {
|
||||
case .rolling:
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("DiceRolling"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0), duration: 0.3))
|
||||
case let .value(value):
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private class ChatMessageHeartbeatHaptic {
|
||||
private var hapticFeedback = HapticFeedback()
|
||||
private var timer: SwiftSignalKit.Timer?
|
||||
private var time: Double = 0.0
|
||||
var enabled = false {
|
||||
didSet {
|
||||
if !self.enabled {
|
||||
self.reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var active: Bool {
|
||||
return self.timer != nil
|
||||
}
|
||||
|
||||
private func reset() {
|
||||
if let timer = self.timer {
|
||||
self.time = 0.0
|
||||
timer.invalidate()
|
||||
self.timer = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func beat(time: Double) {
|
||||
let epsilon = 0.1
|
||||
if fabs(0.0 - time) < epsilon || fabs(1.0 - time) < epsilon || fabs(2.0 - time) < epsilon {
|
||||
self.hapticFeedback.impact(.medium)
|
||||
} else if fabs(0.2 - time) < epsilon || fabs(1.2 - time) < epsilon || fabs(2.2 - time) < epsilon {
|
||||
self.hapticFeedback.impact(.light)
|
||||
}
|
||||
}
|
||||
|
||||
func start(time: Double) {
|
||||
self.hapticFeedback.prepareImpact()
|
||||
|
||||
if time > 2.0 {
|
||||
return
|
||||
}
|
||||
|
||||
var startTime: Double = 0.0
|
||||
var delay: Double = 0.0
|
||||
|
||||
if time > 0.0 {
|
||||
if time <= 1.0 {
|
||||
startTime = 1.0
|
||||
} else if time <= 2.0 {
|
||||
startTime = 2.0
|
||||
}
|
||||
}
|
||||
|
||||
delay = max(0.0, startTime - time)
|
||||
|
||||
let block = { [weak self] in
|
||||
guard let strongSelf = self, strongSelf.enabled else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.time = startTime
|
||||
strongSelf.beat(time: startTime)
|
||||
strongSelf.timer = SwiftSignalKit.Timer(timeout: 0.2, repeat: true, completion: { [weak self] in
|
||||
guard let strongSelf = self, strongSelf.enabled else {
|
||||
return
|
||||
}
|
||||
strongSelf.time += 0.2
|
||||
strongSelf.beat(time: strongSelf.time)
|
||||
|
||||
if strongSelf.time > 2.2 {
|
||||
strongSelf.reset()
|
||||
strongSelf.time = 0.0
|
||||
strongSelf.timer?.invalidate()
|
||||
strongSelf.timer = nil
|
||||
}
|
||||
|
||||
}, queue: Queue.mainQueue())
|
||||
strongSelf.timer?.start()
|
||||
}
|
||||
|
||||
if delay > 0.0 {
|
||||
Queue.mainQueue().after(delay, block)
|
||||
} else {
|
||||
block()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
private let contextSourceNode: ContextExtractedContentContainingNode
|
||||
let imageNode: TransformImageNode
|
||||
private let animationNode: AnimatedStickerNode
|
||||
private var animationNode: GenericAnimatedStickerNode?
|
||||
private var didSetUpAnimationNode = false
|
||||
private var isPlaying = false
|
||||
|
||||
@ -399,6 +48,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
|
||||
var telegramFile: TelegramMediaFile?
|
||||
var emojiFile: TelegramMediaFile?
|
||||
var telegramDice: TelegramMediaDice?
|
||||
private let disposable = MetaDisposable()
|
||||
|
||||
private var viaBotNode: TextNode?
|
||||
@ -410,34 +60,20 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
|
||||
private var highlightedState: Bool = false
|
||||
|
||||
private var heartbeatHaptic: ChatMessageHeartbeatHaptic?
|
||||
private var heartbeatHaptic: HeartbeatHaptic?
|
||||
|
||||
private var currentSwipeToReplyTranslation: CGFloat = 0.0
|
||||
|
||||
required init() {
|
||||
self.contextSourceNode = ContextExtractedContentContainingNode()
|
||||
self.imageNode = TransformImageNode()
|
||||
self.animationNode = AnimatedStickerNode()
|
||||
self.dateAndStatusNode = ChatMessageDateAndStatusNode()
|
||||
|
||||
super.init(layerBacked: false)
|
||||
|
||||
self.animationNode.started = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.imageNode.alpha = 0.0
|
||||
|
||||
if let item = strongSelf.item {
|
||||
if let _ = strongSelf.emojiFile {
|
||||
item.controllerInteraction.seenOneTimeAnimatedMedia.insert(item.message.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.imageNode.displaysAsynchronously = false
|
||||
self.addSubnode(self.contextSourceNode)
|
||||
self.contextSourceNode.contentNode.addSubnode(self.imageNode)
|
||||
self.contextSourceNode.contentNode.addSubnode(self.animationNode)
|
||||
self.contextSourceNode.contentNode.addSubnode(self.dateAndStatusNode)
|
||||
}
|
||||
|
||||
@ -460,7 +96,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
}
|
||||
|
||||
if strongSelf.telegramFile == nil {
|
||||
if strongSelf.animationNode.frame.contains(point) {
|
||||
if let animationNode = strongSelf.animationNode, animationNode.frame.contains(point) {
|
||||
return .waitForDoubleTap
|
||||
}
|
||||
}
|
||||
@ -511,9 +147,36 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
}
|
||||
}
|
||||
|
||||
private func setupNode(item: ChatMessageItem) {
|
||||
var isDice = false
|
||||
|
||||
if let telegramDice = self.telegramDice, let diceEmojis = item.associatedData.animatedEmojiStickers["🎲"] {
|
||||
let animationNode = ManagedDiceAnimationNode(context: item.context, emojis: diceEmojis.map { $0.file })
|
||||
self.animationNode = animationNode
|
||||
} else {
|
||||
let animationNode = AnimatedStickerNode()
|
||||
animationNode.started = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.imageNode.alpha = 0.0
|
||||
|
||||
if let item = strongSelf.item {
|
||||
if let _ = strongSelf.emojiFile {
|
||||
item.controllerInteraction.seenOneTimeAnimatedMedia.insert(item.message.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.animationNode = animationNode
|
||||
}
|
||||
|
||||
if let animationNode = self.animationNode {
|
||||
self.contextSourceNode.contentNode.insertSubnode(animationNode, aboveSubnode: self.imageNode)
|
||||
}
|
||||
}
|
||||
|
||||
override func setupItem(_ item: ChatMessageItem) {
|
||||
super.setupItem(item)
|
||||
|
||||
|
||||
for media in item.message.media {
|
||||
if let telegramFile = media as? TelegramMediaFile {
|
||||
if self.telegramFile?.id != telegramFile.id {
|
||||
@ -524,29 +187,26 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
self.disposable.set(freeMediaFileInteractiveFetched(account: item.context.account, fileReference: .message(message: MessageReference(item.message), media: telegramFile)).start())
|
||||
}
|
||||
break
|
||||
} else if let telegramDice = media as? TelegramMediaDice {
|
||||
self.telegramDice = telegramDice
|
||||
}
|
||||
}
|
||||
|
||||
self.setupNode(item: item)
|
||||
|
||||
let (emoji, fitz) = item.message.text.basicEmoji
|
||||
if self.telegramFile == nil {
|
||||
if let telegramDice = self.telegramDice, let diceNode = self.animationNode as? ManagedDiceAnimationNode {
|
||||
if let value = telegramDice.value {
|
||||
diceNode.setState(value == 0 ? .rolling : .value(value))
|
||||
} else {
|
||||
diceNode.setState(.rolling)
|
||||
}
|
||||
} else if self.telegramFile == nil {
|
||||
let (emoji, fitz) = item.message.text.basicEmoji
|
||||
var emojiFile: TelegramMediaFile?
|
||||
|
||||
if false && emoji == "🎲" {
|
||||
var pointsValue: Int
|
||||
if let value = item.controllerInteraction.seenDicePointsValue[item.message.id] {
|
||||
pointsValue = value
|
||||
} else {
|
||||
pointsValue = Int(arc4random_uniform(6))
|
||||
item.controllerInteraction.seenDicePointsValue[item.message.id] = pointsValue
|
||||
}
|
||||
if let diceEmojis = item.associatedData.animatedEmojiStickers[emoji] {
|
||||
emojiFile = diceEmojis[pointsValue].file
|
||||
}
|
||||
} else {
|
||||
emojiFile = item.associatedData.animatedEmojiStickers[emoji]?.first?.file
|
||||
if emojiFile == nil {
|
||||
emojiFile = item.associatedData.animatedEmojiStickers[emoji.strippedEmoji]?.first?.file
|
||||
}
|
||||
emojiFile = item.associatedData.animatedEmojiStickers[emoji]?.first?.file
|
||||
if emojiFile == nil {
|
||||
emojiFile = item.associatedData.animatedEmojiStickers[emoji.strippedEmoji]?.first?.file
|
||||
}
|
||||
|
||||
if self.emojiFile?.id != emojiFile?.id {
|
||||
@ -570,65 +230,69 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
return
|
||||
}
|
||||
|
||||
let isPlaying = self.visibilityStatus
|
||||
if self.isPlaying != isPlaying {
|
||||
self.isPlaying = isPlaying
|
||||
|
||||
var alreadySeen = false
|
||||
if isPlaying, let _ = self.emojiFile {
|
||||
if item.controllerInteraction.seenOneTimeAnimatedMedia.contains(item.message.id) {
|
||||
alreadySeen = true
|
||||
}
|
||||
}
|
||||
|
||||
self.animationNode.visibility = isPlaying && !alreadySeen
|
||||
|
||||
if self.didSetUpAnimationNode && alreadySeen {
|
||||
if let emojiFile = self.emojiFile, emojiFile.resource is LocalFileReferenceMediaResource {
|
||||
} else {
|
||||
self.animationNode.seekTo(.start)
|
||||
}
|
||||
}
|
||||
|
||||
if self.isPlaying && !self.didSetUpAnimationNode {
|
||||
self.didSetUpAnimationNode = true
|
||||
if let animationNode = self.animationNode as? AnimatedStickerNode {
|
||||
let isPlaying = self.visibilityStatus
|
||||
if self.isPlaying != isPlaying {
|
||||
self.isPlaying = isPlaying
|
||||
|
||||
var file: TelegramMediaFile?
|
||||
var playbackMode: AnimatedStickerPlaybackMode = .loop
|
||||
var isEmoji = false
|
||||
var fitzModifier: EmojiFitzModifier?
|
||||
|
||||
if let telegramFile = self.telegramFile {
|
||||
file = telegramFile
|
||||
if !item.controllerInteraction.stickerSettings.loopAnimatedStickers {
|
||||
playbackMode = .once
|
||||
}
|
||||
} else if let emojiFile = self.emojiFile {
|
||||
isEmoji = true
|
||||
file = emojiFile
|
||||
if alreadySeen && emojiFile.resource is LocalFileReferenceMediaResource {
|
||||
playbackMode = .still(.end)
|
||||
} else {
|
||||
playbackMode = .once
|
||||
}
|
||||
let (_, fitz) = item.message.text.basicEmoji
|
||||
if let fitz = fitz {
|
||||
fitzModifier = EmojiFitzModifier(emoji: fitz)
|
||||
var alreadySeen = false
|
||||
if isPlaying, let _ = self.emojiFile {
|
||||
if item.controllerInteraction.seenOneTimeAnimatedMedia.contains(item.message.id) {
|
||||
alreadySeen = true
|
||||
}
|
||||
}
|
||||
|
||||
if let file = file {
|
||||
let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||
let fittedSize = isEmoji ? dimensions.cgSize.aspectFilled(CGSize(width: 384.0, height: 384.0)) : dimensions.cgSize.aspectFitted(CGSize(width: 384.0, height: 384.0))
|
||||
let mode: AnimatedStickerMode
|
||||
if file.resource is LocalFileReferenceMediaResource {
|
||||
mode = .direct
|
||||
animationNode.visibility = isPlaying && !alreadySeen
|
||||
|
||||
if self.didSetUpAnimationNode && alreadySeen {
|
||||
if let emojiFile = self.emojiFile, emojiFile.resource is LocalFileReferenceMediaResource {
|
||||
} else {
|
||||
mode = .cached
|
||||
animationNode.seekTo(.start)
|
||||
}
|
||||
}
|
||||
|
||||
if self.isPlaying && !self.didSetUpAnimationNode {
|
||||
self.didSetUpAnimationNode = true
|
||||
|
||||
var file: TelegramMediaFile?
|
||||
var playbackMode: AnimatedStickerPlaybackMode = .loop
|
||||
var isEmoji = false
|
||||
var fitzModifier: EmojiFitzModifier?
|
||||
|
||||
if let telegramFile = self.telegramFile {
|
||||
file = telegramFile
|
||||
if !item.controllerInteraction.stickerSettings.loopAnimatedStickers {
|
||||
playbackMode = .once
|
||||
}
|
||||
} else if let emojiFile = self.emojiFile {
|
||||
isEmoji = true
|
||||
file = emojiFile
|
||||
if alreadySeen && emojiFile.resource is LocalFileReferenceMediaResource {
|
||||
playbackMode = .still(.end)
|
||||
} else {
|
||||
playbackMode = .once
|
||||
}
|
||||
let (_, fitz) = item.message.text.basicEmoji
|
||||
if let fitz = fitz {
|
||||
fitzModifier = EmojiFitzModifier(emoji: fitz)
|
||||
}
|
||||
}
|
||||
|
||||
if let file = file {
|
||||
let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||
let fittedSize = isEmoji ? dimensions.cgSize.aspectFilled(CGSize(width: 384.0, height: 384.0)) : dimensions.cgSize.aspectFitted(CGSize(width: 384.0, height: 384.0))
|
||||
let mode: AnimatedStickerMode
|
||||
if file.resource is LocalFileReferenceMediaResource {
|
||||
mode = .direct
|
||||
} else {
|
||||
mode = .cached
|
||||
}
|
||||
animationNode.setup(source: AnimatedStickerResourceSource(account: item.context.account, resource: file.resource, fitzModifier: fitzModifier), width: Int(fittedSize.width), height: Int(fittedSize.height), playbackMode: playbackMode, mode: mode)
|
||||
}
|
||||
self.animationNode.setup(source: AnimatedStickerResourceSource(account: item.context.account, resource: file.resource, fitzModifier: fitzModifier), width: Int(fittedSize.width), height: Int(fittedSize.height), playbackMode: playbackMode, mode: mode)
|
||||
}
|
||||
}
|
||||
} else if let animationNode = self.animationNode as? ManagedDiceAnimationNode {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -931,8 +595,10 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
}
|
||||
|
||||
strongSelf.imageNode.frame = updatedContentFrame
|
||||
strongSelf.animationNode.frame = updatedContentFrame.insetBy(dx: imageInset, dy: imageInset)
|
||||
strongSelf.animationNode.updateLayout(size: updatedContentFrame.insetBy(dx: imageInset, dy: imageInset).size)
|
||||
strongSelf.animationNode?.frame = updatedContentFrame.insetBy(dx: imageInset, dy: imageInset)
|
||||
if let animationNode = strongSelf.animationNode as? AnimatedStickerNode {
|
||||
animationNode.updateLayout(size: updatedContentFrame.insetBy(dx: imageInset, dy: imageInset).size)
|
||||
}
|
||||
imageApply()
|
||||
|
||||
strongSelf.contextSourceNode.contentRect = strongSelf.imageNode.frame
|
||||
@ -1175,13 +841,13 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
} else if let _ = self.emojiFile {
|
||||
let (emoji, fitz) = item.message.text.basicEmoji
|
||||
if emoji == "🎲" {
|
||||
|
||||
} else {
|
||||
|
||||
} else if let animationNode = self.animationNode as? AnimatedStickerNode {
|
||||
var startTime: Signal<Double, NoError>
|
||||
if self.animationNode.playIfNeeded() {
|
||||
if animationNode.playIfNeeded() {
|
||||
startTime = .single(0.0)
|
||||
} else {
|
||||
startTime = self.animationNode.status
|
||||
startTime = animationNode.status
|
||||
|> map { $0.timestamp }
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue
|
||||
@ -1195,11 +861,11 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
return
|
||||
}
|
||||
|
||||
let heartbeatHaptic: ChatMessageHeartbeatHaptic
|
||||
let heartbeatHaptic: HeartbeatHaptic
|
||||
if let current = strongSelf.heartbeatHaptic {
|
||||
heartbeatHaptic = current
|
||||
} else {
|
||||
heartbeatHaptic = ChatMessageHeartbeatHaptic()
|
||||
heartbeatHaptic = HeartbeatHaptic()
|
||||
heartbeatHaptic.enabled = true
|
||||
strongSelf.heartbeatHaptic = heartbeatHaptic
|
||||
}
|
||||
|
||||
@ -201,7 +201,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView {
|
||||
deliveryFailedInset += 24.0
|
||||
}
|
||||
|
||||
let displaySize = CGSize(width: 212.0, height: 212.0)
|
||||
let displaySize = layoutConstants.instantVideo.dimensions
|
||||
|
||||
var automaticDownload = true
|
||||
for media in item.message.media {
|
||||
|
||||
@ -406,10 +406,12 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible {
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if let _ = media as? TelegramMediaAction {
|
||||
} else if media is TelegramMediaAction {
|
||||
viewClassName = ChatMessageBubbleItemNode.self
|
||||
} else if let _ = media as? TelegramMediaExpiredContent {
|
||||
} else if media is TelegramMediaExpiredContent {
|
||||
viewClassName = ChatMessageBubbleItemNode.self
|
||||
} else if media is TelegramMediaDice {
|
||||
viewClassName = ChatMessageAnimatedStickerItemNode.self
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -101,7 +101,7 @@ struct ChatMessageItemLayoutConstants {
|
||||
let image = ChatMessageItemImageLayoutConstants(bubbleInsets: UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0), statusInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 6.0, right: 6.0), defaultCornerRadius: 16.0, mergedCornerRadius: 8.0, contentMergedCornerRadius: 5.0, maxDimensions: CGSize(width: 440.0, height: 440.0), minDimensions: CGSize(width: 170.0, height: 74.0))
|
||||
let video = ChatMessageItemVideoLayoutConstants(maxHorizontalHeight: 250.0, maxVerticalHeight: 360.0)
|
||||
let file = ChatMessageItemFileLayoutConstants(bubbleInsets: UIEdgeInsets(top: 15.0, left: 9.0, bottom: 15.0, right: 12.0))
|
||||
let instantVideo = ChatMessageItemInstantVideoConstants(insets: UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0), dimensions: CGSize(width: 212.0, height: 212.0))
|
||||
let instantVideo = ChatMessageItemInstantVideoConstants(insets: UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0), dimensions: CGSize(width: 240.0, height: 240.0))
|
||||
let wallpapers = ChatMessageItemWallpaperLayoutConstants(maxTextWidth: 180.0)
|
||||
|
||||
return ChatMessageItemLayoutConstants(avatarDiameter: 37.0, timestampHeaderHeight: 34.0, bubble: bubble, image: image, video: video, text: text, file: file, instantVideo: instantVideo, wallpapers: wallpapers)
|
||||
@ -125,6 +125,7 @@ func chatMessageItemLayoutConstants(_ constants: (ChatMessageItemLayoutConstants
|
||||
let textInset: CGFloat = min(maxInset, ceil(maxInset * radiusTransition + minInset * (1.0 - radiusTransition)))
|
||||
result.text.bubbleInsets.left = textInset
|
||||
result.text.bubbleInsets.right = textInset
|
||||
result.instantVideo.dimensions = min(params.width, params.availableHeight) > 320.0 ? constants.1.instantVideo.dimensions : constants.0.instantVideo.dimensions
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
@ -13,23 +13,6 @@ import StickerResources
|
||||
import PhotoResources
|
||||
import TelegramStringFormatting
|
||||
|
||||
private func foldLineBreaks(_ text: String) -> String {
|
||||
var lines = text.split { $0.isNewline }
|
||||
var startedBothLines = false
|
||||
var result = ""
|
||||
for line in lines {
|
||||
if line.isEmpty {
|
||||
continue
|
||||
}
|
||||
if result.isEmpty {
|
||||
result += line
|
||||
} else {
|
||||
result += " " + line
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
|
||||
private let context: AccountContext
|
||||
private let tapButton: HighlightTrackingButtonNode
|
||||
|
||||
89
submodules/TelegramUI/Sources/HeartbeatHaptic.swift
Normal file
89
submodules/TelegramUI/Sources/HeartbeatHaptic.swift
Normal file
@ -0,0 +1,89 @@
|
||||
import Foundation
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
|
||||
final class HeartbeatHaptic {
|
||||
private var hapticFeedback = HapticFeedback()
|
||||
private var timer: SwiftSignalKit.Timer?
|
||||
private var time: Double = 0.0
|
||||
var enabled = false {
|
||||
didSet {
|
||||
if !self.enabled {
|
||||
self.reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var active: Bool {
|
||||
return self.timer != nil
|
||||
}
|
||||
|
||||
private func reset() {
|
||||
if let timer = self.timer {
|
||||
self.time = 0.0
|
||||
timer.invalidate()
|
||||
self.timer = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func beat(time: Double) {
|
||||
let epsilon = 0.1
|
||||
if fabs(0.0 - time) < epsilon || fabs(1.0 - time) < epsilon || fabs(2.0 - time) < epsilon {
|
||||
self.hapticFeedback.impact(.medium)
|
||||
} else if fabs(0.2 - time) < epsilon || fabs(1.2 - time) < epsilon || fabs(2.2 - time) < epsilon {
|
||||
self.hapticFeedback.impact(.light)
|
||||
}
|
||||
}
|
||||
|
||||
func start(time: Double) {
|
||||
self.hapticFeedback.prepareImpact()
|
||||
|
||||
if time > 2.0 {
|
||||
return
|
||||
}
|
||||
|
||||
var startTime: Double = 0.0
|
||||
var delay: Double = 0.0
|
||||
|
||||
if time > 0.0 {
|
||||
if time <= 1.0 {
|
||||
startTime = 1.0
|
||||
} else if time <= 2.0 {
|
||||
startTime = 2.0
|
||||
}
|
||||
}
|
||||
|
||||
delay = max(0.0, startTime - time)
|
||||
|
||||
let block = { [weak self] in
|
||||
guard let strongSelf = self, strongSelf.enabled else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.time = startTime
|
||||
strongSelf.beat(time: startTime)
|
||||
strongSelf.timer = SwiftSignalKit.Timer(timeout: 0.2, repeat: true, completion: { [weak self] in
|
||||
guard let strongSelf = self, strongSelf.enabled else {
|
||||
return
|
||||
}
|
||||
strongSelf.time += 0.2
|
||||
strongSelf.beat(time: strongSelf.time)
|
||||
|
||||
if strongSelf.time > 2.2 {
|
||||
strongSelf.reset()
|
||||
strongSelf.time = 0.0
|
||||
strongSelf.timer?.invalidate()
|
||||
strongSelf.timer = nil
|
||||
}
|
||||
|
||||
}, queue: Queue.mainQueue())
|
||||
strongSelf.timer?.start()
|
||||
}
|
||||
|
||||
if delay > 0.0 {
|
||||
Queue.mainQueue().after(delay, block)
|
||||
} else {
|
||||
block()
|
||||
}
|
||||
}
|
||||
}
|
||||
114
submodules/TelegramUI/Sources/ManagedDiceAnimationNode.swift
Normal file
114
submodules/TelegramUI/Sources/ManagedDiceAnimationNode.swift
Normal file
@ -0,0 +1,114 @@
|
||||
import Foundation
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SyncCore
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import AccountContext
|
||||
import StickerResources
|
||||
import ManagedAnimationNode
|
||||
|
||||
enum ManagedDiceAnimationState: Equatable {
|
||||
case rolling
|
||||
case value(Int32)
|
||||
}
|
||||
|
||||
final class ManagedDiceAnimationNode: ManagedAnimationNode, GenericAnimatedStickerNode {
|
||||
private let context: AccountContext
|
||||
private let emojis: [TelegramMediaFile]
|
||||
|
||||
private var diceState: ManagedDiceAnimationState? = nil
|
||||
private let disposable = MetaDisposable()
|
||||
|
||||
init(context: AccountContext, emojis: [TelegramMediaFile]) {
|
||||
self.context = context
|
||||
self.emojis = emojis
|
||||
|
||||
super.init(size: CGSize(width: 136.0, height: 136.0))
|
||||
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("DiceRolling"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0), duration: 0.3))
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disposable.dispose()
|
||||
}
|
||||
|
||||
func setState(_ diceState: ManagedDiceAnimationState) {
|
||||
let previousState = self.diceState
|
||||
self.diceState = diceState
|
||||
|
||||
if let previousState = previousState {
|
||||
switch previousState {
|
||||
case .rolling:
|
||||
switch diceState {
|
||||
case let .value(value):
|
||||
let file = self.emojis[Int(value) - 1]
|
||||
let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||
let fittedSize = dimensions.cgSize.aspectFilled(CGSize(width: 384.0, height: 384.0))
|
||||
|
||||
let fetched = freeMediaFileInteractiveFetched(account: self.context.account, fileReference: .standalone(media: file))
|
||||
let sticker = Signal<Void, NoError> { subscriber in
|
||||
let fetchedDisposable = fetched.start()
|
||||
let resourceDisposable = (chatMessageAnimationData(postbox: self.context.account.postbox, resource: file.resource, fitzModifier: nil, width: Int(fittedSize.width), height: Int(fittedSize.height), synchronousLoad: false)
|
||||
|> filter { data in
|
||||
return data.complete
|
||||
}).start(next: { next in
|
||||
subscriber.putNext(Void())
|
||||
})
|
||||
|
||||
return ActionDisposable {
|
||||
fetchedDisposable.dispose()
|
||||
resourceDisposable.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
self.disposable.set(sticker.start(next: { [weak self] data in
|
||||
if let strongSelf = self {
|
||||
strongSelf.trackTo(item: ManagedAnimationItem(source: .resource(strongSelf.context.account.postbox.mediaBox, file.resource), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0), duration: 0.3))
|
||||
}
|
||||
}))
|
||||
case .rolling:
|
||||
break
|
||||
}
|
||||
case let .value(currentValue):
|
||||
switch diceState {
|
||||
case .rolling:
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("DiceRolling"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0), duration: 0.3))
|
||||
case let .value(value):
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
switch diceState {
|
||||
case let .value(value):
|
||||
let file = self.emojis[Int(value) - 1]
|
||||
let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||
let fittedSize = dimensions.cgSize.aspectFilled(CGSize(width: 384.0, height: 384.0))
|
||||
|
||||
let fetched = freeMediaFileInteractiveFetched(account: self.context.account, fileReference: .standalone(media: file))
|
||||
let sticker = Signal<Void, NoError> { subscriber in
|
||||
let fetchedDisposable = fetched.start()
|
||||
let resourceDisposable = (chatMessageAnimationData(postbox: self.context.account.postbox, resource: file.resource, fitzModifier: nil, width: Int(fittedSize.width), height: Int(fittedSize.height), synchronousLoad: false)
|
||||
|> filter { data in
|
||||
return data.complete
|
||||
}).start(next: { next in
|
||||
subscriber.putNext(Void())
|
||||
})
|
||||
|
||||
return ActionDisposable {
|
||||
fetchedDisposable.dispose()
|
||||
resourceDisposable.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
self.disposable.set(sticker.start(next: { [weak self] data in
|
||||
if let strongSelf = self {
|
||||
strongSelf.trackTo(item: ManagedAnimationItem(source: .resource(strongSelf.context.account.postbox.mediaBox, file.resource), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0), duration: 0.3))
|
||||
}
|
||||
}))
|
||||
case .rolling:
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("DiceRolling"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0), duration: 0.3))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -34,7 +34,6 @@ final class OverlayInstantVideoDecoration: UniversalVideoDecoration {
|
||||
|
||||
self.contentContainerNode = ASDisplayNode()
|
||||
self.contentContainerNode.backgroundColor = .white
|
||||
self.contentContainerNode.cornerRadius = 60.0
|
||||
self.contentContainerNode.clipsToBounds = true
|
||||
|
||||
self.foregroundContainerNode = ASDisplayNode()
|
||||
@ -74,7 +73,7 @@ final class OverlayInstantVideoDecoration: UniversalVideoDecoration {
|
||||
|
||||
if let snapshot = snapshot {
|
||||
self.contentContainerNode.view.addSubview(snapshot)
|
||||
if let validLayoutSize = self.validLayoutSize {
|
||||
if let _ = self.validLayoutSize {
|
||||
snapshot.frame = CGRect(origin: CGPoint(), size: snapshot.frame.size)
|
||||
}
|
||||
}
|
||||
@ -84,6 +83,8 @@ final class OverlayInstantVideoDecoration: UniversalVideoDecoration {
|
||||
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
|
||||
self.validLayoutSize = size
|
||||
|
||||
self.contentContainerNode.cornerRadius = size.width / 2.0
|
||||
|
||||
let shadowInsets = UIEdgeInsets(top: 2.0, left: 3.0, bottom: 4.0, right: 3.0)
|
||||
transition.updateFrame(node: self.shadowNode, frame: CGRect(origin: CGPoint(x: -shadowInsets.left, y: -shadowInsets.top), size: CGSize(width: size.width + shadowInsets.left + shadowInsets.right, height: size.height + shadowInsets.top + shadowInsets.bottom)))
|
||||
|
||||
|
||||
@ -83,8 +83,12 @@ final class OverlayInstantVideoNode: OverlayMediaItemNode {
|
||||
self.updateLayout(self.bounds.size)
|
||||
}
|
||||
|
||||
override func preferredSizeForOverlayDisplay() -> CGSize {
|
||||
return CGSize(width: 120.0, height: 120.0)
|
||||
override func preferredSizeForOverlayDisplay(boundingSize: CGSize) -> CGSize {
|
||||
if min(boundingSize.width, boundingSize.height) > 320.0 {
|
||||
return CGSize(width: 150.0, height: 150.0)
|
||||
} else {
|
||||
return CGSize(width: 120.0, height: 120.0)
|
||||
}
|
||||
}
|
||||
|
||||
override func dismiss() {
|
||||
|
||||
@ -239,7 +239,7 @@ final class OverlayMediaControllerNode: ASDisplayNode, UIGestureRecognizerDelega
|
||||
location = groupLocation
|
||||
}
|
||||
}
|
||||
let nodeData = OverlayMediaVideoNodeData(node: node, location: location, isMinimized: false, currentSize: node.preferredSizeForOverlayDisplay())
|
||||
let nodeData = OverlayMediaVideoNodeData(node: node, location: location, isMinimized: false, currentSize: node.preferredSizeForOverlayDisplay(boundingSize: self.frame.size))
|
||||
self.videoNodes.append(nodeData)
|
||||
self.addSubnode(node)
|
||||
if let validLayout = self.validLayout {
|
||||
|
||||
@ -98,7 +98,7 @@ public final class OverlayUniversalVideoNode: OverlayMediaItemNode {
|
||||
self.updateLayout(self.bounds.size)
|
||||
}
|
||||
|
||||
override public func preferredSizeForOverlayDisplay() -> CGSize {
|
||||
override public func preferredSizeForOverlayDisplay(boundingSize: CGSize) -> CGSize {
|
||||
return self.content.dimensions.aspectFitted(CGSize(width: 300.0, height: 300.0))
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user