英文:
Swift custom bubble view component
问题
我需要创建一个能够以气泡形式显示许多跑步者进度的组件。我已经尝试使用很棒的Charts库,但没有取得太多成果。
这是代码:
override func viewDidLoad() {
super.viewDidLoad()
self.configureChart(raceChartData: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], raceDistance: 10)
}
func configureChart(raceChartData: [Int], raceDistance: Double) {
// ...(代码中的其他配置)
}
这是结果:
这是我需要创建的真实组件:
假设你需要跑10公里,当前的市场线和带有头像的气泡是你(应用程序用户)以及你已经跑过的距离。其他气泡代表附近的其他人和他们的距离。所有数据来自套接字(后端),我需要以这种形式显示它们。你有任何关于如何做到这一点的想法吗?我在考虑从头开始创建一个组件(基于UIView),但我不知道这是否是一个好计划。
英文:
I need to create a component capable of displaying the progress of many runners in the form of a bubble. I have tried to do it with the Charts library which is great but I have not achieved much.
This is the code:
override func viewDidLoad() {
super.viewDidLoad()
self.configureChart(raceChartData: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], raceDistance: 10)
}
func configureChart(raceChartData: [Int], raceDistance: Double) {
chartView.chartDescription.enabled = false
chartView.dragEnabled = false
chartView.pinchZoomEnabled = false
chartView.xAxis.enabled = true
//chartView.maxVisibleCount = 0
chartView.autoScaleMinMaxEnabled = false
chartView.leftAxis.enabled = false
chartView.rightAxis.enabled = false
chartView.legend.enabled = false
chartView.legend.drawInside = false
chartView.setScaleEnabled(false)
let xAxis: XAxis = chartView.xAxis
xAxis.axisLineColor = #colorLiteral(red: 1, green: 0.4823529412, blue: 0.1450980392, alpha: 1).withAlphaComponent(0.1)
xAxis.drawAxisLineEnabled = false
xAxis.drawLabelsEnabled = false
xAxis.gridColor = #colorLiteral(red: 1, green: 0.4823529412, blue: 0.1450980392, alpha: 1).withAlphaComponent(0.3)
//xAxis.axisMinimum = 1
//xAxis.axisMaximum = 10
var calculatedChartDistance = 0.0
if raceChartData.count != 0 {
var chartdistanceArray = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
if activeRaceFormat == "SPDWRK" {
calculatedChartDistance = raceDistance
} else {
chartView.xAxis.centeredEntries = [0, 0.5, 0.4]
chartView.xAxis.centerAxisLabelsEnabled = true
calculatedChartDistance = raceDistance
}
for i in 0...9 {
chartdistanceArray[i] = calculatedChartDistance * Double((i + 1))
}
let yVals1 = [
BubbleChartDataEntry(x: chartdistanceArray[0], y: 0, size: CGFloat(raceChartData[0])),
BubbleChartDataEntry(x: chartdistanceArray[1], y: 0, size: CGFloat(raceChartData[1])),
BubbleChartDataEntry(x: chartdistanceArray[2], y: 0, size: CGFloat(raceChartData[2])),
BubbleChartDataEntry(x: chartdistanceArray[3], y: 0, size: CGFloat(raceChartData[3])),
BubbleChartDataEntry(x: chartdistanceArray[4], y: 0, size: CGFloat(raceChartData[4])),
BubbleChartDataEntry(x: chartdistanceArray[5], y: 0, size: CGFloat(raceChartData[5])),
BubbleChartDataEntry(x: chartdistanceArray[6], y: 0, size: CGFloat(raceChartData[6])),
BubbleChartDataEntry(x: chartdistanceArray[7], y: 0, size: CGFloat(raceChartData[7])),
BubbleChartDataEntry(x: chartdistanceArray[8], y: 0, size: CGFloat(raceChartData[8])),
BubbleChartDataEntry(x: chartdistanceArray[9], y: 0, size: CGFloat(raceChartData[9]))
]
let set1 = BubbleChartDataSet(entries: yVals1)
set1.drawIconsEnabled = false
set1.setColor(ChartColorTemplates.colorful()[1], alpha: 0.5)
set1.drawValuesEnabled = true
set1.normalizeSizeEnabled = false
let data = [set1] as BubbleChartData
data.setDrawValues(true)
data.setValueFont(UIFont(name: "HelveticaNeue-Light", size: 7)!)
data.setHighlightCircleWidth(0.5)
chartView.data = data
}
}
And this is the result:
Here is the real component I need to create:
Suppose you have to run 10 KM, the current market line and the bubble with avatar are you (the app user) and the distance you have running. The other bubbles is the amount of people near one from other and their distance. All the data comes from socket (backend) and I need to display in that form. Any idea of how I can do this?, I'm thinking in do a component from scratch (based on UIView) but I don't know if this is a good plan at all.
答案1
得分: 0
以下是翻译好的部分:
// 以下是Swift代码的注释部分,无需翻译
// Una clase para representar un gráfico de burbujas con varios corredores
class DiyBubbleChartView: UIView {
// Un arreglo para almacenar los corredores
var currentRunners: [Runner] = []
// Una constante para la distancia total a recorrer
let totalDistance = 100.0
// Un método para actualizar corredores en el gráfico
func updateRunners(_ runners: [Runner]) {
self.currentRunners.removeAll()
self.currentRunners = runners
self.setNeedsDisplay() // Llama al método draw para actualizar el gráfico
}
// Un método para agregar un corredor al gráfico
func addRunner(_ runner: Runner) {
self.currentRunners.append(runner)
self.setNeedsDisplay() // Llama al método draw para actualizar el gráfico
}
// Un方法para dibujar el gráfico de burbujas
override func draw(_ rect: CGRect) {
super.draw(rect)
// Obtiene el contexto gráfico actual
guard let context = UIGraphicsGetCurrentContext() else { return }
self.drawGradientBackground()
self.drawGridLines()
// Define el color y el ancho de las líneas
context.setStrokeColor(UIColor.black.cgColor)
context.setLineWidth(0.1)
// Agrupa los corredores que están juntos en un diccionario con la distancia como clave y el número de corredores como valor
var groups: [Double: Int] = [:]
for runner in currentRunners {
groups[runner.distance, default: 0] += 1
}
// Para cada grupo de corredores, dibuja una burbuja con un radio proporcional al número de corredores y una posición proporcional a la distancia
for (distance, count) in groups {
var radius = CGFloat(count) // El radio de la burbuja es 5 veces el número de corredores
// Verifica si el radio es mayor que la mitad de la altura de la vista
if radius > (bounds.height / 2) - 20.0 {
// Si lo es, reduce el radio a ese valor (20.0 es el margen)
radius = (bounds.height / 2) - 20.0
}
var x = 10 + CGFloat(distance / totalDistance) * (bounds.width - 20) // La posición x de la burbuja es proporcional a la distancia recorrida
let y = bounds.height / 2 // La posición y de la burbuja es la mitad de la altura del gráfico
// Verifica si la posición x más el radio es mayor que el ancho de la vista
if x + radius > bounds.width {
// Si lo es, resta la diferencia al valor de x para mover la burbuja hacia la izquierda
x -= (x + radius) - bounds.width
}
// Dibuja un círculo con el centro y el radio dados
// Establece el color de relleno del contexto gráfico
context.setFillColor(UIColor.red.withAlphaComponent(0.3).cgColor)
context.addArc(center: CGPoint(x: x, y: y), radius: radius, startAngle: 0, endAngle: .pi * 2, clockwise: true)
context.fillPath()
context.strokePath()
}
self.drawBubbleWithUserAvatar(withGroups: groups)
}
// Crea una instancia de CAGradientLayer y le asigna un tamaño igual al de la vista
func drawGradientBackground() {
// Remover todas las sublayers para evitar problemas
self.layer.sublayers?.removeAll()
let gradientLayer = CAGradientLayer()
gradientLayer.frame = self.bounds
// Define los colores para el degradado
let firstColor = UIColor(named: "firstColor")!.withAlphaComponent(0.3)
let secondColor = UIColor(named: "secondColor")!.withAlphaComponent(0.3)
// Define los colores del degradado como un arreglo de CGColor
gradientLayer.colors = [firstColor.cgColor, secondColor.cgColor]
// Define los puntos de inicio y fin del degradado como CGPoint
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
gradientLayer.endPoint = CGPoint(x: 1, y: 1)
// Agrega el degradado como una subcapa de la vista
self.layer.insertSublayer(gradientLayer, at: 0)
}
// Dibuja las líneas horizontales de la gráfica
func drawGridLines() {
// Obtiene el contexto gráfico actual
guard let context = UIGraphicsGetCurrentContext() else { return }
let lineColor = #colorLiteral(red: 1, green: 0.4823529412, blue: 0.1450980392, alpha: 1).withAlphaComponent(0.3)
// Define una constante para la distancia entre las líneas
let lineSpacing = 32.0
context.setStrokeColor(lineColor.cgColor)
context.setLineWidth(1.0)
// Define una constante para el número de líneas a dibujar
let numberOfLines = Int((bounds.width - 20) / lineSpacing)
// Para cada línea, calcula su posición x y dibuja una línea vertical desde el borde superior hasta el borde inferior de la vista
for i in 0...numberOfLines {
let x = 10 + CGFloat(i) * lineSpacing // La posición x de la línea es igual al margen izquierdo más el espaciado multiplicado por el índice de la línea
context.move(to: CGPoint(x: x, y: 0)) // Mueve el punto inicial al borde superior de la vista
context.addLine(to: CGPoint(x: x, y: bounds.height)) // Agrega una línea al borde inferior de la vista
context.strokePath() // Dibuja la línea
}
}
// 以下是未翻译的部分,如需要继续翻译,请提供具体内容
// ...
}
// 以下是测试用的Swift代码的注释部分,无需翻译
// To test, in some view controller:
let chart = DiyBubbleChartView(frame: CGRect(x: 0, y: 0, width: 300, height: 110))
var runners: [Runner] = []
var distances = [Double]()
// Rellenar las distancias posibles desde 1 hasta 100
for i in 1...100 {
distances.append
<details>
<summary>英文:</summary>
I have ended done this so far:
```swift
//
// DiyBubbleChartView.swift
// Test Charts
//
// Created by Fidel Hernández Salazar on 5/29/23.
//
import Foundation
import UIKit
// Una clase para representar un gráfico de burbujas con varios corredores
class DiyBubbleChartView: UIView {
// Un arreglo para almacenar los corredores
var currentRunners: [Runner] = []
// Una constante para la distancia total a recorrer
let totalDistance = 100.0
// Un método para actualizar corredores en el gráfico
func updateRunners(_ runners: [Runner]) {
self.currentRunners.removeAll()
self.currentRunners = runners
self.setNeedsDisplay() // Llama al método draw para actualizar el gráfico
}
// Un método para agregar un corredor al gráfico
func addRunner(_ runner: Runner) {
self.currentRunners.append(runner)
self.setNeedsDisplay() // Llama al método draw para actualizar el gráfico
}
// Un método para dibujar el gráfico de burbujas
override func draw(_ rect: CGRect) {
super.draw(rect)
// Obtiene el contexto gráfico actual
guard let context = UIGraphicsGetCurrentContext() else { return }
self.drawGradientBackground()
self.drawGridLines()
// Define el color y el ancho de las líneas
context.setStrokeColor(UIColor.black.cgColor)
context.setLineWidth(0.1)
// // Dibuja una línea horizontal para representar la distancia total
// context.move(to: CGPoint(x: 10, y: bounds.height / 2))
// context.addLine(to: CGPoint(x: bounds.width - 10, y: bounds.height / 2))
// context.strokePath()
// // Dibuja una etiqueta con la distancia total
// let label = UILabel(frame: CGRect(x: bounds.width - 50, y: bounds.height / 2 - 20, width: 40, height: 20))
// label.text = "\(totalDistance)"
// label.textColor = .black
// label.font = .systemFont(ofSize: 12)
// addSubview(label)
// Agrupa los corredores que están juntos en un diccionario con la distancia como clave y el número de corredores como valor
var groups: [Double: Int] = [:]
for runner in currentRunners {
groups[runner.distance, default: 0] += 1
}
// Para cada grupo de corredores, dibuja una burbuja con un radio proporcional al número de corredores y una posición proporcional a la distancia
for (distance, count) in groups {
var radius = CGFloat(count) // El radio de la burbuja es 5 veces el número de corredores
// Verifica si el radio es mayor que la mitad de la altura de la vista
if radius > (bounds.height / 2) - 20.0 {
// Si lo es, reduce el radio a ese valor (20.0 es el margin)
radius = (bounds.height / 2) - 20.0
}
var x = 10 + CGFloat(distance / totalDistance) * (bounds.width - 20) // La posición x de la burbuja es proporcional a la distancia recorrida
let y = bounds.height / 2 // La posición y de la burbuja es la mitad de la altura del gráfico
// Verifica si la posición x más el radio es mayor que el ancho de la vista
if x + radius > bounds.width {
// Si lo es, resta la diferencia al valor de x para mover la burbuja hacia la izquierda
x -= (x + radius) - bounds.width
}
// Dibuja un círculo con el centro y el radio dados
// Establece el color de relleno del contexto gráfico
context.setFillColor(UIColor.red.withAlphaComponent(0.3).cgColor)
context.addArc(center: CGPoint(x: x, y: y), radius: radius, startAngle: 0, endAngle: .pi * 2, clockwise: true)
context.fillPath()
context.strokePath()
// // Dibuja una etiqueta con el número de corredores dentro de la burbuja
// let label = UILabel(frame: CGRect(x: x - radius, y: y - radius, width: radius * 2, height: radius * 2))
// label.text = "\(count)"
// label.textColor = .black
// label.font = UIFont(name: "HelveticaNeue-Light", size: 7)! // .systemFont(ofSize: 12)
// label.textAlignment = .center
// addSubview(label)
}
self.drawBubbleWithUserAvatar(withGroups: groups)
}
// Crea una instancia de CAGradientLayer y le asigna un tamaño igual al de la vista
func drawGradientBackground() {
// Remover todas las sublayers para evitar problemas
self.layer.sublayers?.removeAll()
let gradientLayer = CAGradientLayer()
gradientLayer.frame = self.bounds
// Define los colores para el degradado
let firstColor = UIColor(named: "firstColor")!.withAlphaComponent(0.3)
let secondColor = UIColor(named: "secondColor")!.withAlphaComponent(0.3)
// Define los colores del degradado como un arreglo de CGColor
gradientLayer.colors = [firstColor.cgColor, secondColor.cgColor]
// Define los puntos de inicio y fin del degradado como CGPoint
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
gradientLayer.endPoint = CGPoint(x: 1, y: 1)
// Agrega el degradado como una subcapa de la vista
self.layer.insertSublayer(gradientLayer, at: 0)
}
// Dibuja las lineas horizontales de la grafica
func drawGridLines() {
// Obtiene el contexto gráfico actual
guard let context = UIGraphicsGetCurrentContext() else { return }
let lineColor = #colorLiteral(red: 1, green: 0.4823529412, blue: 0.1450980392, alpha: 1).withAlphaComponent(0.3)
// Define una constante para la distancia entre las líneas
let lineSpacing = 32.0
context.setStrokeColor(lineColor.cgColor)
context.setLineWidth(1.0)
// Define una constante para el número de líneas a dibujar
let numberOfLines = Int((bounds.width - 20) / lineSpacing)
// Para cada línea, calcula su posición x y dibuja una línea vertical desde el borde superior hasta el borde inferior de la vista
for i in 0...numberOfLines {
let x = 10 + CGFloat(i) * lineSpacing // La posición x de la línea es igual al margen izquierdo más el espaciado multiplicado por el índice de la línea
context.move(to: CGPoint(x: x, y: 0)) // Mueve el punto inicial al borde superior de la vista
context.addLine(to: CGPoint(x: x, y: bounds.height)) // Agrega una línea al borde inferior de la vista
context.strokePath() // Dibuja la línea
}
}
func drawBubbleWithUserAvatar(withGroups: [Double: Int]) {
// Define una constante con el nombre del corredor principal
let mainRunnerName = "User 1"
// Obtiene la distancia recorrida por el corredor principal
let mainRunnerDistance = currentRunners.first(where: { $0.name == mainRunnerName })!.distance
// Obtiene el número de corredores en el mismo grupo que el corredor principal
let mainRunnerCount = withGroups[mainRunnerDistance]!
// Calcula el radio y la posición x de la burbuja del corredor principal usando las mismas fórmulas que antes
let mainRunnerRadius = CGFloat(mainRunnerCount * 10)
let mainRunnerX = 10 + CGFloat(mainRunnerDistance / totalDistance) * (bounds.width - 20)
// Crea una instancia de UIImageView con una imagen de prueba (puedes cambiarla por la imagen real del perfil del usuario)
let imageView = UIImageView(image: UIImage(named: "user-avatar"))
// Calcula el radio de la imagen como el mismo que el de la burbuja del usuario
var imageRadius = mainRunnerRadius
// Verifica si el radio es mayor que la mitad de la altura de la vista
if imageRadius > (bounds.height / 2) - 20.0 {
// Si lo es, reduce el tamaño de la imagen a ese valor
imageRadius = (bounds.height / 2) - 20.0
}
// // Le asigna un tamaño y una posición iguales a los de la burbuja del corredor principal
// imageView.frame = CGRect(x: mainRunnerX - mainRunnerRadius, y: bounds.height / 2 - mainRunnerRadius, width: mainRunnerRadius * 2, height: mainRunnerRadius * 2)
// Le asigna un tamaño y una posición iguales a los de la burbuja del usuario
imageView.frame = CGRect(x: mainRunnerX - imageRadius, y: bounds.height / 2 - imageRadius, width: imageRadius * 2, height: imageRadius * 2)
// Le da una forma circular recortando las esquinas
//imageView.layer.cornerRadius = mainRunnerRadius
imageView.layer.cornerRadius = imageRadius
imageView.clipsToBounds = true
// Agrega la imagen como una subvista de la vista
addSubview(imageView)
// Obtiene el contexto gráfico actual
guard let context = UIGraphicsGetCurrentContext() else { return }
context.setStrokeColor(UIColor.init(named: "firstColor")!.cgColor)
context.setLineWidth(1.0)
// Calcula las posiciones y del inicio y fin de la línea como el borde superior e inferior de la vista principal
let startY = 0.0
let endY = bounds.height
// Dibuja una línea vertical con los puntos dados
context.move(to: CGPoint(x: mainRunnerX, y: startY))
context.addLine(to: CGPoint(x: mainRunnerX, y: endY))
context.strokePath()
/// .......
// Crea una instancia de CALayer y le asigna un tamaño y una posición que cubran el área deseada
let backgroundLayer = CALayer()
backgroundLayer.frame = CGRect(x: 0, y: 0, width: mainRunnerX, height: bounds.height)
// Define el color del fondo como un CGColor
backgroundLayer.backgroundColor = UIColor.init(named: "secondColor")!.withAlphaComponent(0.2).cgColor
// Agrega el fondo como una subcapa de la vista
self.layer.insertSublayer(backgroundLayer, at: 1)
}
}
To test, in some view controller:
let chart = DiyBubbleChartView(frame: CGRect(x: 0, y: 0, width: 300, height: 110))
var runners: [Runner] = []
var distances = [Double]()
// Rellenar las distancias posibles desde 1 hasta 100
for i in 1...100 {
distances.append(Double(i))
}
for i in 1...100 {
self.runners.append(Runner(name: "User \(i)", distance: distances.randomElement() ?? 1.0, isUser: false))
}
self.updateBubbleView(withRunners: self.runners)
And the updateBubbleView func
func updateBubbleView(withRunners: [Runner]) {
// Crea una instancia del gráfico de burbujas y le agrega algunos corredores de prueba
self.chart.updateRunners(withRunners)
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论