¿Cómo funcionan los renderizadores MPAndroidChart y cómo escribo un renderizador personalizado?

Estoy usando la biblioteca MPAndroidChart pero no tiene toda la funcionalidad que deseo de fábrica.

He escuchado que es posible implementar la funcionalidad que quiero escribiendo un renderizador personalizado.

He revisado el código fuente de los renderizadores en el repository MPAndroidChart GitHub, pero no puedo entender los conceptos involucrados.

¿Cómo funcionan los renderizadores MPAndroidChart?

¿Cuál es el procedimiento de alto nivel para escribir un renderizador personalizado?

Nota: para muchas preguntas publicadas en SO para mpandroidchart, la solución es implementar algún tipo de procesador personalizado. Un comentario sobre tales preguntas “puede resolver este problema escribiendo un renderizador personalizado” es insatisfactorio si no hay una guía. Escribir una respuesta que incluya la solución completa para un requisito poco común e inusual puede llevar mucho tiempo. No existe una guía existente para escribir un renderizador personalizado y se espera que esta pregunta sirva como una utilidad para que los interrogados puedan ayudarse a sí mismos, sino un objective duplicado. Si bien he intentado mi propia respuesta aquí, otras respuestas, correcciones y comentarios son bienvenidos.

Comprensión de vistas y canvas

En primer lugar, se debe estudiar la guía Canvas y Drawables de la documentación oficial de Android. Particularmente, es importante tener en cuenta que LineChart , BarChart , etc. son subclases de View que se muestran sobrescribiendo la callback onDraw(Canvas c) de la superclase View. Tenga en cuenta también la definición de “canvas”:

Un canvas funciona para usted como un pretexto, o interfaz, a la superficie real sobre la que se dibujarán sus gráficos: contiene todas sus llamadas de “extracción”.

Cuando trabaje con renderizadores, tendrá que lidiar con la funcionalidad para dibujar líneas, barras, etc. en el canvas.

Traducción entre valores en el gráfico y píxeles en el canvas

Los puntos en el gráfico se especifican como valores xey con respecto a las unidades en el gráfico. Por ejemplo, en el cuadro a continuación, el centro de la primera barra está en x = 0 . La primera barra tiene el valor y de 52.28 .

un diagrama de barras MPAndroidChart

Esto claramente no corresponde a las coordenadas de píxeles en el canvas. En el canvas, x = 0 en el canvas sería un pixel del extremo izquierdo que está claramente en blanco. Del mismo modo, como la enumeración de píxeles comienza desde la parte superior como y = 0 , la punta de la barra claramente no está en 52.28 (el valor y en la tabla). Si utilizamos las opciones de desarrollador / ubicación del puntero, podemos ver que la punta de la primera barra es aproximadamente x = 165 y = 1150 .

Un Transformer es responsable de convertir los valores de los gráficos en coordenadas de píxeles (en pantalla) y viceversa. Un patrón común en los renderizadores es realizar cálculos utilizando valores de gráfico (que son más fáciles de entender) y luego, al final, usar el transformador para aplicar una transformación para renderizar en la pantalla.

Ver puerto y límites

Un puerto de visualización es una ventana, es decir, un área limitada en el gráfico. Los puertos de vista se utilizan para determinar qué parte del gráfico el usuario puede ver actualmente. Cada gráfico tiene un ViewPortHandler que encapsula la funcionalidad relacionada con los puertos de visualización. Podemos usar ViewPortHandler#isInBoundsLeft(float x) isInBoundsRight(float x) para determinar qué valores x el usuario puede ver actualmente.

En el cuadro que se muestra arriba, BarChart “conoce” BarEntry para 6 y más, pero debido a que están fuera de los límites y no en la ventana gráfica actual, 6 no se representan. Por lo tanto, los valores x de 0 a 5 están dentro de la ventana gráfica actual.

ChartAnimator

ChartAnimator proporciona una transformación adicional para aplicar al gráfico. Por lo general, esta es una simple multiplicación. Por ejemplo, supongamos que queremos una animación donde los puntos del gráfico comienzan en la parte inferior y aumentan gradualmente a su valor y correcto en más de 1 segundo. El animador proporcionará un phaseY que es un escalar simple que comienza en 0.000 a tiempo 0 0ms y se eleva gradualmente a 1.000 a 1.000 1000ms .

Un ejemplo de código de renderizador

Ahora que entendemos los conceptos básicos involucrados, veamos algunos códigos de LineChartRenderer :

 protected void drawHorizontalBezier(ILineDataSet dataSet) { float phaseY = mAnimator.getPhaseY(); Transformer trans = mChart.getTransformer(dataSet.getAxisDependency()); mXBounds.set(mChart, dataSet); cubicPath.reset(); if (mXBounds.range >= 1) { Entry prev = dataSet.getEntryForIndex(mXBounds.min); Entry cur = prev; // let the spline start cubicPath.moveTo(cur.getX(), cur.getY() * phaseY); for (int j = mXBounds.min + 1; j <= mXBounds.range + mXBounds.min; j++) { prev = cur; cur = dataSet.getEntryForIndex(j); final float cpx = (prev.getX()) + (cur.getX() - prev.getX()) / 2.0f; cubicPath.cubicTo( cpx, prev.getY() * phaseY, cpx, cur.getY() * phaseY, cur.getX(), cur.getY() * phaseY); } } // if filled is enabled, close the path if (dataSet.isDrawFilledEnabled()) { cubicFillPath.reset(); cubicFillPath.addPath(cubicPath); // create a new path, this is bad for performance drawCubicFill(mBitmapCanvas, dataSet, cubicFillPath, trans, mXBounds); } mRenderPaint.setColor(dataSet.getColor()); mRenderPaint.setStyle(Paint.Style.STROKE); trans.pathValueToPixel(cubicPath); mBitmapCanvas.drawPath(cubicPath, mRenderPaint); mRenderPaint.setPathEffect(null); } 

Las primeras líneas antes del bucle for son la configuración para el bucle del renderizador. Tenga en cuenta que obtenemos el phaseY del ChartAnimator, el Transformer y calculamos los límites del puerto de visualización.

El bucle for significa básicamente "para cada punto que está dentro de los límites izquierdo y derecho del puerto de visualización". No tiene sentido hacer que los valores de x que no se pueden ver.

Dentro del ciclo, obtenemos el valor xy el valor y de la entrada actual utilizando dataSet.getEntryForIndex(j) y creamos una ruta entre eso y la entrada anterior. Observe cómo todos los caminos se multiplican por el phaseY para la animación.

Finalmente, después de calcular las rutas, se aplica una transformación con trans.pathValueToPixel(cubicPath); y las rutas se representan en el canvas con mBitmapCanvas.drawPath(cubicPath, mRenderPaint);

Escribir un renderizador personalizado

El primer paso es elegir la clase correcta a la subclase. Observe las clases en el paquete com.github.mikephil.charting.renderer incluyendo XAxisRenderer y LineChartRenderer etc. Una vez que crea una subclase, puede simplemente anular el método apropiado. Según el código de ejemplo anterior, void drawHorizontalBezier(ILineDataSet dataSet) sin llamar a super (para no invocar la etapa de renderizado dos veces) y la reemplazaremos con la funcionalidad que queremos. Si lo haces bien, el método reemplazado debería verse al menos un poco como el método que estás anulando:

  1. Obtener un control sobre el transformador, el animador y los límites
  2. Looping a través de los valores x visibles (los valores x que están dentro de los límites del puerto de visualización)
  3. Preparación de puntos para representar en valores de gráfico
  4. Transformar los puntos en píxeles en el canvas
  5. Usar los métodos de la clase Canvas para dibujar en el canvas

Debería estudiar los métodos en la clase Canvas ( drawBitmap etc.) para ver qué operaciones tiene permitido realizar en el bucle del renderizador.

Si el método que debe sobrescribir no está expuesto, es posible que deba LineRadarRenderer una subclase de un renderizador base como LineRadarRenderer para lograr la funcionalidad deseada.

Una vez que haya diseñado la subclase del renderizador que desea, puede consumirla fácilmente con Chart#setRenderer(DataRenderer renderer) o BarLineChartBase#setXAxisRenderer(XAxisRenderer renderer) y otros métodos.