¿Cómo funcionan los renderizadores de MPAndroidChart y cómo escribo un renderizador personalizado?
(1)
Estoy usando la biblioteca MPAndroidChart pero no tiene toda la funcionalidad que quiero fuera de la caja.
He oído que es posible implementar la funcionalidad que quiero escribiendo un renderizador personalizado.
He mirado el código fuente de los renderizadores en el repositorio 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 renderizador personalizado. Un comentario sobre tales preguntas "puede resolver este problema escribiendo un renderizador personalizado" no es satisfactorio 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 pueda servir como una utilidad para que los interrogadores puedan ayudarse a sí mismos, si no un objetivo duplicado. Si bien he intentado mi propia respuesta aquí, otras respuestas, correcciones y comentarios son bienvenidos.
Comprender las vistas y el lienzo
Primero, uno debe estudiar la
Guía
de
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 anulando la devolución de llamada
onDraw(Canvas c)
de la superclase View.
Tenga en cuenta también la definición de "lienzo":
Un lienzo funciona para usted como un pretexto o interfaz para la superficie real sobre la que se dibujarán sus gráficos: contiene todas sus llamadas de "dibujar".
Cuando trabaje con renderizadores, se ocupará de la funcionalidad para dibujar líneas, barras, etc. en el lienzo.
Traducción entre valores en el gráfico y píxeles en el lienzo
Los puntos en el gráfico se especifican como valores x e y 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
.
Esto claramente no se corresponde con las coordenadas de píxeles en el lienzo.
En el lienzo,
x = 0
en el lienzo sería un píxel del extremo izquierdo que está claramente en blanco.
Del mismo modo, debido a que la enumeración de píxeles comienza desde arriba como
y = 0
, la punta de la barra claramente no está en
52.28
(el valor y en el gráfico).
Si utilizamos las opciones de desarrollador / ubicación del puntero, podemos ver que la punta de la primera barra es aproximadamente
x = 165
e
y = 1150
.
Un
Transformer
es responsable de convertir los valores del gráfico 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áficos (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 vista es una ventana, es decir, un área delimitada en el gráfico.
Los puertos de visualización se utilizan para determinar qué parte del gráfico puede ver el usuario 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 de x puede ver el usuario actualmente.
En el gráfico que se muestra arriba, BarChart "conoce" el
BarEntry
para 6 y superiores, pero debido a que están fuera de los límites y no en la vista actual, 6 y hacia arriba no se representan.
Por lo tanto, los valores de x de
0
a
5
están dentro de la ventana gráfica actual.
ChartAnimator
ChartAnimator
proporciona una transformación adicional que se 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 comiencen en la parte inferior y aumenten gradualmente a su valor y correcto durante 1 segundo.
El animador proporcionará una
phaseY
que es un escalar simple que comienza en
0.000
a tiempo 0
0ms
y aumenta gradualmente a
1.000
a
1.000
1000ms
.
Un ejemplo de código de renderizador
Ahora que entendemos los conceptos básicos involucrados, echemos un vistazo a 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 del bucle de renderizador.
Tenga en cuenta que obtenemos la
phaseY
del ChartAnimator, el Transformador, y calculamos los límites del puerto de vista.
El bucle
for
básicamente significa "para cada punto que está dentro de los límites izquierdo y derecho del puerto de vista".
No tiene sentido representar valores x que no se pueden ver.
Dentro del ciclo, obtenemos el valor x y el valor y para la entrada actual usando
dataSet.getEntryForIndex(j)
y creamos una ruta entre eso y la entrada anterior.
Observe cómo la ruta se multiplica por la
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 lienzo con
mBitmapCanvas.drawPath(cubicPath, mRenderPaint);
Escribir un renderizador personalizado
El primer paso es elegir la clase correcta para la subclase.
Tenga en cuenta las clases en el paquete
com.github.mikephil.charting.renderer
incluyendo
XAxisRenderer
y
LineChartRenderer
etc. Una vez que cree una subclase, simplemente puede anular el método apropiado.
Según el código de ejemplo anterior, anularíamos
void drawHorizontalBezier(ILineDataSet dataSet)
sin llamar a
super
(para no invocar la etapa de renderizado dos veces) y reemplazarlo con la funcionalidad que queremos.
Si lo está haciendo bien, el método anulado debería parecerse al menos un poco al método que está anulando:
- Obtención de un controlador en el transformador, animador y límites.
- Recorriendo los valores de x visibles (los valores de x que están dentro de los límites del puerto de vista)
- Preparación de puntos para representar en valores de gráfico
- Transformando los puntos en píxeles en el lienzo
-
Usando los métodos de la clase
Canvas
para dibujar en el lienzo
Debe estudiar los métodos en la
clase Canvas
(
drawBitmap
etc.) para ver qué operaciones puede realizar en el bucle de renderizador.
Si el método que necesita anular no está expuesto, es posible que tenga que subclasificar un renderizador base como
LineRadarRenderer
para lograr la funcionalidad deseada.
Una vez que haya diseñado la subclase de renderizador que desea, puede consumirla fácilmente con
Chart#setRenderer(DataRenderer renderer)
o
BarLineChartBase#setXAxisRenderer(XAxisRenderer renderer)
y otros métodos.