Android 中动态绘图的最佳方法是什么?

Which is the best approach for dynamic drawing in Android?

我想为Android中的录音机制作波形图。普通的带lines/bars,比如这个:

更重要的是,我希望在录制歌曲的同时现场直播。我的应用程序已经通过 AudioRecord 计算 RMS。但我不确定在处理、资源、电池等方面实际绘图的最佳方法是什么

Visualizer 没有显示任何有意义的东西,IMO(这些图表或多或少是随机的东西??)。

我看过 canvas 方法和布局方法(可能还有更多?)。在 layout approach 中,您在水平布局中添加细垂直布局。优点是你不需要每 1/n 秒重新绘制整个东西,你只需每 1/n 秒添加一个布局......但你需要数百个布局(取决于 n)。在 canvas 布局中,你需要每秒重新绘制整个东西(对吗??)n 次。有些人甚至为每幅画创建位图...

那么,哪个更便宜,为什么?现在有什么更好的吗?多少频率更新(即 n)对于通用低端设备来说太多了?

EDIT1

多亏了@cactustictacs 在他的回答中教给我的漂亮技巧,我才能够轻松地实现它。然而,图像奇怪地呈现出一种“运动模糊”:

波形从右向左运行。您可以很容易地看到模糊运动,最左边和最右边的像素被另一端“污染”。我想我可以同时切断两个极端...

如果我让我的位图更大(即,使 widthBitmap 更大),这会呈现得更好,但是 onDraw 会更重...

这是我的完整代码:

package com.floritfoto.apps.ave;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;

import java.util.Arrays;

public class Waveform extends androidx.appcompat.widget.AppCompatImageView {
    //private float lastPosition = 0.5f; // 0.5 for drawLine method, 0 for the others
    private int lastPosition = 0;
    private final int widthBitmap = 50;
    private final int heightBitmap = 80;
    private final int[] transpixels = new int[heightBitmap];
    private final int[] whitepixels = new int[heightBitmap];
    //private float top, bot;            // float for drawLine method, int for the others
    private int aux, top;
    //private float lpf;
    private int width = widthBitmap;
    private float proportionW = (float) (width/widthBitmap);
    Boolean firstLoopIsFinished = false;
    Bitmap MyBitmap = Bitmap.createBitmap(widthBitmap, heightBitmap, Bitmap.Config.ARGB_8888);
    //Canvas canvasB = new Canvas(MyBitmap);
    Paint MyPaint = new Paint();
    Paint MyPaintTrans = new Paint();
    Rect rectLbit, rectRbit, rectLdest, rectRdest;

    public Waveform(Context context, AttributeSet attrs) {
        super(context, attrs);
        MyPaint.setColor(0xffFFFFFF);
        MyPaint.setStrokeWidth(1);
        MyPaintTrans.setColor(0xFF202020);
        MyPaintTrans.setStrokeWidth(1);
        Arrays.fill(transpixels, 0xFF202020);
        Arrays.fill(whitepixels, 0xFFFFFFFF);
    }

    public void drawNewBar() {
        // For drawRect or drawLine
        /*
        top = ((1.0f - Register.tone) * heightBitmap / 2.0f);
        bot = ((1.0f + Register.tone) * heightBitmap / 2.0f);

        // Using drawRect
        //if (firstLoopIsFinished) canvasB.drawRect(lastPosition, 0, lastPosition+1, heightBitmap, MyPaintTrans);  // Delete last stuff
        //canvasB.drawRect(lastPosition, top, lastPosition+1, bot, MyPaint);

        // Using drawLine
        if (firstLoopIsFinished) canvasB.drawLine(lastPosition, 0, lastPosition, heightBitmap, MyPaintTrans);  // Delete previous stuff
        canvasB.drawLine(lastPosition ,top, lastPosition, bot, MyPaint);
        */
        // Using setPixel (no tiene sentido, mucho mejor setPixels.
        /*
        int top = (int) ((1.0f - Register.tone) * heightBitmap / 2.0f);
        int bot = (int) ((1.0f + Register.tone) * heightBitmap / 2.0f);
        if (firstLoopIsFinished) {
            for (int i = 0; i < top; ++i) {
                MyBitmap.setPixel(lastPosition, i, 0xFF202020);
                MyBitmap.setPixel(lastPosition, heightBitmap - i-1, 0xFF202020);
            }
        }
        for (int i = top ; i < bot ; ++i) {
            MyBitmap.setPixel(lastPosition,i,0xffFFFFFF);
        }
        //System.out.println("############## "+top+" "+bot);
        */
        // Using setPixels. Works!!
        top = (int) ((1.0f - Register.tone) * heightBitmap / 2.0f);
        if (firstLoopIsFinished)
            MyBitmap.setPixels(transpixels,0,1,lastPosition,0,1,heightBitmap);
        MyBitmap.setPixels(whitepixels, top,1, lastPosition, top,1,heightBitmap-2*top);

        lastPosition++;

        aux = (int) (width - proportionW * (lastPosition));
        rectLbit.right = lastPosition;
        rectRbit.left = lastPosition;
        rectLdest.right = aux;
        rectRdest.left = aux;

        if (lastPosition >= widthBitmap) { firstLoopIsFinished = true; lastPosition = 0; }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        width = w;
        proportionW = (float) width/widthBitmap;
        rectLbit = new Rect(0, 0, widthBitmap, heightBitmap);
        rectRbit = new Rect(0, 0, widthBitmap, heightBitmap);
        rectLdest = new Rect(0, 0, width, h);
        rectRdest = new Rect(0, 0, width, h);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawNewBar();
        canvas.drawBitmap(MyBitmap, rectLbit, rectRdest, MyPaint);
        canvas.drawBitmap(MyBitmap, rectRbit, rectLdest, MyPaint);
    }
}

EDIT2 我能够在 canvas.drawBitmap:

中使用 null 作为 Paint 来防止模糊
        canvas.drawBitmap(MyBitmap, rectLbit, rectRdest, null);
        canvas.drawBitmap(MyBitmap, rectRbit, rectLdest, null);

不需要油漆。

您永远不会对布局执行此操作。布局用于预制组件。它们是组件的高级组合,您不想频繁地动态添加或删除视图。为此,您使用带有 canvas 的自定义视图。布局甚至不是这样的选项。

您的基本自定义视图方法是实施 onDraw 并在每一帧重新绘制当前数据。你可能会保留某种循环 Buffer 来保存你最近的 n 振幅值,所以每一帧你都会迭代这些值,并使用 drawRect 来绘制条形图(您将在 onSizeChanged 中计算宽度、高度比例、开始和结束位置等内容,并在定义 Rect 的坐标时使用这些值)。

这本身就可以了!真正了解绘制调用有多昂贵的唯一方法是对它们进行基准测试,这样您就可以尝试这种方法,看看效果如何。分析它以查看需要多少时间,CPU 尖峰等

您可以采取一些措施来使 onDraw 尽可能高效,主要是避免对象分配 - 因此请注意创建 Iterator 的循环函数,并且在同样,你应该创建一个 Paint 一次,而不是在 onDraw 中一遍又一遍地创建它们,你可以通过为你需要的每个栏设置其坐标来重用单个 Rect 对象画.


您可以尝试的另一种方法是在您控制的自定义视图中创建一个有效的 Bitmap,然后在 onDraw 中调用 drawBitmap 将其绘制到 Canvas. 应该 是一个相当便宜的调用,并且可以很容易地根据需要拉伸以适应视图。

那里的想法是,当您获得新数据时,将其绘制到位图上。因为你的波形看起来(像块),而且你可以放大它,你真正需要的是每个值的像素垂直线,对吧?因此,随着数据的到来,您在 already-existing 位图上绘制了一条额外的线,添加到图像中。您只需添加新块,而不是每帧逐块绘制整个波形。


当您“填充”位图时出现的复杂情况 - 现在您必须将所有像素“移动”到左侧,将最旧的像素放在左侧,这样您就可以在右侧绘制新像素.所以你需要一种方法来做到这一点!

另一种方法类似于循环缓冲区的想法。如果您不知道那是什么,我们的想法是您采用一个带有开始和结束的普通缓冲区,但是您将其中一个索引视为数据的 起点 ,环绕当您到达缓冲区的最后一个索引时为 0,当您到达您调用的索引时停止 end point:

Partially filled buffer:

|start
123400
   |end
Data: 1234

Full buffer:

|start
123456
     |end
Data: 123456

After adding one more item:

 |start
723456
|end
Data: 234567

看看一旦它满了,你如何将开始和结束“右移”一步,必要时环绕?所以你总是添加了最近的 6 个值。您只需要处理从 start -> lastIndexfirstIndex -> end

的正确索引范围的读取

您可以对位图做同样的事情 - 从左边开始“填充”它,增加 end 这样您就可以绘制下一条垂直线。一旦它满了,就从左边移动 end 开始填充。当您实际绘制位图时,不是绘制整个 as-is (723456),而是将其分为两部分绘制(23456 然后 7)。说得通?当您将位图绘制到 canvas 时,有一个调用需要一个源 Rect 和一个目标,因此您可以将其分成两块绘制。


您总是可以在每一帧从头开始重新绘制位图(清除它并绘制垂直线),因此基本上每次都在重新绘制整个数据缓冲区。对于每个值,可能仍然比 drawRect 方法快,但老实说并不比“将位图视为另一个循环缓冲区”方法容易多少。如果您已经在管理一个循环缓冲区,那么工作就不会多了——因为缓冲区和位图将具有相同数量的值(在位图的情况下是水平像素),您可以使用相同的 startend 两者的值