自定义的Swift气泡视图组件

huangapple go评论51阅读模式
英文:

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) {
    // ...(代码中的其他配置)
}

这是结果:

自定义的Swift气泡视图组件

这是我需要创建的真实组件:

自定义的Swift气泡视图组件

假设你需要跑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:

自定义的Swift气泡视图组件

Here is the real component I need to create:

自定义的Swift气泡视图组件

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&#225;ndez Salazar on 5/29/23.
//

import Foundation
import UIKit

// Una clase para representar un gr&#225;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&#233;todo para actualizar corredores en el gr&#225;fico
    func updateRunners(_ runners: [Runner]) {
        self.currentRunners.removeAll()
        self.currentRunners = runners
        self.setNeedsDisplay() // Llama al m&#233;todo draw para actualizar el gr&#225;fico
    }

    // Un m&#233;todo para agregar un corredor al gr&#225;fico
    func addRunner(_ runner: Runner) {
        self.currentRunners.append(runner)
        self.setNeedsDisplay() // Llama al m&#233;todo draw para actualizar el gr&#225;fico
    }

    // Un m&#233;todo para dibujar el gr&#225;fico de burbujas
    override func draw(_ rect: CGRect) {
        super.draw(rect)

        // Obtiene el contexto gr&#225;fico actual
        guard let context = UIGraphicsGetCurrentContext() else { return }

        self.drawGradientBackground()
        self.drawGridLines()

        // Define el color y el ancho de las l&#237;neas
        context.setStrokeColor(UIColor.black.cgColor)
        context.setLineWidth(0.1)

//        // Dibuja una l&#237;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 = &quot;\(totalDistance)&quot;
//        label.textColor = .black
//        label.font = .systemFont(ofSize: 12)
//        addSubview(label)

        // Agrupa los corredores que est&#225;n juntos en un diccionario con la distancia como clave y el n&#250;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&#250;mero de corredores y una posici&#243;n proporcional a la distancia
        for (distance, count) in groups {
            var radius = CGFloat(count) // El radio de la burbuja es 5 veces el n&#250;mero de corredores

            // Verifica si el radio es mayor que la mitad de la altura de la vista
            if radius &gt; (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&#243;n x de la burbuja es proporcional a la distancia recorrida
            let y = bounds.height / 2 // La posici&#243;n y de la burbuja es la mitad de la altura del gr&#225;fico

            // Verifica si la posici&#243;n x m&#225;s el radio es mayor que el ancho de la vista
            if x + radius &gt; 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&#237;rculo con el centro y el radio dados
            // Establece el color de relleno del contexto gr&#225;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&#250;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 = &quot;\(count)&quot;
//            label.textColor = .black
//            label.font = UIFont(name: &quot;HelveticaNeue-Light&quot;, size: 7)! // .systemFont(ofSize: 12)
//            label.textAlignment = .center
//            addSubview(label)
        }

        self.drawBubbleWithUserAvatar(withGroups: groups)
    }

    // Crea una instancia de CAGradientLayer y le asigna un tama&#241;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: &quot;firstColor&quot;)!.withAlphaComponent(0.3)
        let secondColor = UIColor(named: &quot;secondColor&quot;)!.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&#225;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&#237;neas
        let lineSpacing = 32.0

        context.setStrokeColor(lineColor.cgColor)
        context.setLineWidth(1.0)

        // Define una constante para el n&#250;mero de l&#237;neas a dibujar
        let numberOfLines = Int((bounds.width - 20) / lineSpacing)

        // Para cada l&#237;nea, calcula su posici&#243;n x y dibuja una l&#237;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&#243;n x de la l&#237;nea es igual al margen izquierdo m&#225;s el espaciado multiplicado por el &#237;ndice de la l&#237;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&#237;nea al borde inferior de la vista
            context.strokePath() // Dibuja la l&#237;nea
        }
    }

    func drawBubbleWithUserAvatar(withGroups: [Double: Int]) {
        // Define una constante con el nombre del corredor principal
        let mainRunnerName = &quot;User 1&quot;

        // Obtiene la distancia recorrida por el corredor principal
        let mainRunnerDistance = currentRunners.first(where: { $0.name == mainRunnerName })!.distance

        // Obtiene el n&#250;mero de corredores en el mismo grupo que el corredor principal
        let mainRunnerCount = withGroups[mainRunnerDistance]!

        // Calcula el radio y la posici&#243;n x de la burbuja del corredor principal usando las mismas f&#243;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: &quot;user-avatar&quot;))

        // 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 &gt; (bounds.height / 2) - 20.0 {
            // Si lo es, reduce el tama&#241;o de la imagen a ese valor
            imageRadius = (bounds.height / 2) - 20.0
        }

//        // Le asigna un tama&#241;o y una posici&#243;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&#241;o y una posici&#243;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&#225;fico actual
        guard let context = UIGraphicsGetCurrentContext() else { return }

        context.setStrokeColor(UIColor.init(named: &quot;firstColor&quot;)!.cgColor)
        context.setLineWidth(1.0)

        // Calcula las posiciones y del inicio y fin de la l&#237;nea como el borde superior e inferior de la vista principal
        let startY = 0.0
        let endY = bounds.height

        // Dibuja una l&#237;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&#241;o y una posici&#243;n que cubran el &#225;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: &quot;secondColor&quot;)!.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: &quot;User \(i)&quot;, 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&#225;fico de burbujas y le agrega algunos corredores de prueba
    self.chart.updateRunners(withRunners)
}

huangapple
  • 本文由 发表于 2023年5月29日 09:49:08
  • 转载请务必保留本文链接:https://go.coder-hub.com/76354254.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定