MPAndroidChart 渲染器如何工作以及如何编写自定义渲染器?

How do MPAndroidChart renderers work and how do I write a custom renderer?

我正在使用 MPAndroidChart 库,但它没有我想要的所有开箱即用的功能。

我听说可以通过编写自定义渲染器来实现我想要的功能。

我看过 MPAndroidChart GitHub 库中的 source code for the renderers,但我无法理解所涉及的概念。

MPAndroidChart 渲染器如何工作?

编写自定义呈现器的高级过程是什么?

理解观点和Canvas

首先,应该研究 Android 官方文档中的 Canvas and Drawables Guide。特别要注意 LineChartBarChart 等是 View 的子 class,它们通过覆盖视图的 onDraw(Canvas c) 回调来显示自己超级class。另请注意 "canvas":

的定义

A Canvas works for you as a pretense, or interface, to the actual surface upon which your graphics will be drawn — it holds all of your "draw" calls.

当您使用渲染器时,您将处理在 canvas.

上绘制线条、条形等的功能。

图表上的值与 canvas

上的像素之间的转换

图表上的点指定为相对于图表上单位的 x 和 y 值。例如,在下图中,第一个柱的中心位于 x = 0。第一个柱形的 y 值为 52.28.

这显然与 canvas 上的像素坐标不对应。在 canvas 上,canvas 上的 x = 0 将是一个明显空白的最左边像素。同样,由于像素枚举从顶部开始 y = 0,条形的尖端显然不在 52.28(图表上的 y 值)。如果我们使用 Developer options/Pointer 位置,我们可以看到第一个柱的顶端大约是 x = 165y = 1150

A Transformer 负责将图表值转换为像素(屏幕上)坐标,反之亦然。渲染器中的一个常见模式是使用图表值(更容易理解)执行计算,然后在最后使用转换器应用转换以渲染到屏幕上。

查看端口和边界

视口是 window,即图表上的边界区域。查看端口用于确定用户当前可以看到图表的哪一部分。每个图表都有一个 ViewPortHandler 封装了与视图端口相关的功能。我们可以使用 ViewPortHandler#isInBoundsLeft(float x) isInBoundsRight(float x) 来确定用户当前可以看到哪些 x 值。

在上图中,6及以上的BarChart "knows about" BarEntry 但因为它们越界且不在当前视口中,所以6及以上的未呈现。因此,x 值 05 在当前视口内。

ChartAnimator

ChartAnimator 提供了应用于图表的附加转换。通常这是一个简单的乘法。例如,假设我们想要一个动画,其中图表的点从底部开始,并在 1 秒内逐渐上升到正确的 y 值。动画师将提供一个 phaseY,它是一个简单的标量,在 0ms 时从 0.000 开始,并在 1000ms 时逐渐上升到 1.000

渲染器代码示例

现在我们了解了所涉及的基本概念,让我们看一下来自 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);
}

for 循环之前的前几行是渲染器循环的设置。请注意,我们从 ChartAnimator、Transformer 获取 phaseY,并计算视口边界。

for 循环基本上意味着 "for each point that is within the left and right bounds of the view port"。渲染看不到的 x 值没有意义。

在循环中,我们使用 dataSet.getEntryForIndex(j) 获取当前条目的 x 值和 y 值,并在该条目和前一个条目之间创建一条路径。注意路径是如何乘以 phaseY 的动画。

最后,在计算出路径后,使用 trans.pathValueToPixel(cubicPath); 应用转换,并使用 mBitmapCanvas.drawPath(cubicPath, mRenderPaint);

将路径渲染到 canvas

编写自定义渲染器

第一步是选择正确的 class 到 subclass。注意 classes 在包 com.github.mikephil.charting.renderer 中,包括 XAxisRendererLineChartRenderer 等。创建子 class 后,您可以简单地覆盖适当的方法。根据上面的示例代码,我们将在不调用 super 的情况下覆盖 void drawHorizontalBezier(ILineDataSet dataSet) (以便不调用渲染阶段两次)并将其替换为我们想要的功能。如果你做对了,被覆盖的方法应该看起来至少有点像你正在覆盖的方法:

  1. 获取转换器、动画器和边界的句柄
  2. 遍历可见的 x 值(视口边界内的 x 值)
  3. 准备要在图表值中呈现的点
  4. 将 canvas
  5. 上的点转换为像素
  6. 使用Canvasclass方法绘制canvas

您应该研究 Canvas classdrawBitmap 等)中的方法,以了解允许您在渲染器循环中执行哪些操作。

如果您需要重写的方法未公开,您可能需要子class 像 LineRadarRenderer 这样的基础渲染器才能实现所需的功能。

一旦你设计了你想要的渲染器子class,你可以使用Chart#setRenderer(DataRenderer renderer)BarLineChartBase#setXAxisRenderer(XAxisRenderer renderer)和其他方法轻松使用它。