哪个是Android中动态绘画的最佳方法?
我想为Android中的录音机制作波形图。像这样的通常具有线/条的通常是:
更重要的是,我希望它在录制歌曲时。我的应用程序已经通过audiorecord
计算RMS。但是我不确定哪种方法是处理,资源,电池等方面实际绘制的最佳方法
。 ??)。
我已经看到了画布方法和布局方法(可能还有更多吗?)。在
所以,哪个更便宜,为什么?如今有什么更好的吗?对于通用低端设备而言,多少频率更新(即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 我能够防止仅使用null
作为paint
在canvas.drawbitmap
中的绘画:
canvas.drawBitmap(MyBitmap, rectLbit, rectRdest, null);
canvas.drawBitmap(MyBitmap, rectRbit, rectLdest, null);
无需油漆。
I want to make a waveform drawing for an audio recorder in Android. The usual one with lines/bars, like this one:
More importantly, I want it live, while the song is being recorded. My app already computes the RMS through AudioRecord
. But I am not sure which is the best approach for the actual drawing in terms of processing, resources, battery, etc.
The Visualizer
does not show anything meaningful, IMO (are those graphs more or less random stuff??).
I've seen the canvas approach and the layout approach (there are probably more?). In the layout approach you add thin vertical layouts in a horizontal layout. The advantage is that you don't need to redraw the whole thing each 1/n secs, you just add one layout each 1/n secs... but you need hundreds of layouts (depending on n). In the canvas layout, you need to redraw the whole thing (right??) n times per second. Some even create bitmaps for each drawing...
So, which is cheaper, and why? Is there anything better nowadays? How much frequency update (i.e., n) is too much for generic low end devices?
EDIT1
Thanks to the beautiful trick @cactustictacs taught me in his answer, I was able to implement this with ease. Yet, the image is strangely rendered kind of "blurry by movement":
The waveform runs from right to left. You can easily see the blur movement, and the left-most and right-most pixels get "contaminated" by the other end. I guess I can just cut both extremes...
This renders better if I make my Bitmap bigger (i.e., making widthBitmap
bigger), but then the onDraw
will be heavier...
This is my full code:
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
I was able to prevent the blurring just using null
as Paint
in the canvas.drawBitmap
:
canvas.drawBitmap(MyBitmap, rectLbit, rectRdest, null);
canvas.drawBitmap(MyBitmap, rectRbit, rectLdest, null);
No Paints needed.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
您的基本自定义视图方法是实现
OnDraw
,并重新绘制当前数据的每个帧。您可能会保留某种循环buffer
持有最新 n 振幅值,因此您要迭代这些帧,并使用drawRect 绘制条形图(您将计算
onsizezechanged
中的宽度,高度缩放,启动和结束位置等诸如rect> rect S)。
那本身可能很好!您真正判断呼叫的唯一方法是对它们进行基准测试,因此您可以尝试这种方法并查看它的发展。个人资料以查看花费多少时间,CPU尖峰等
。对于创建
迭代器
s的循环函数,并且以相同的方式创建paint
一次,而不是一遍又一遍地创建它们代码>,您可以通过为需要绘制的每个栏设置其坐标来重复使用单个对象。
您可以尝试的另一种方法是在自定义视图中创建一个工作
bitmap
,您可以控制并调用drawbitmap
inon Drawd
将其绘制到画布
。 应该是一个相当便宜的电话,并且可以根据需要轻松拉伸以适应视图。那里的想法是,您可以获得新数据,然后将其绘制到位图上。由于波形的外观(如块),并且您可以将其扩展到它的事实,实际上您需要的只是每个值的单个像素的垂直线线,对吗?因此,随着数据的进来,您将一条额外的线绘制在已经存在的位图上,并添加到图像中。您只是添加新的块,而不是每个框架绘制整个波形块。
当您“填充”位图时,那里的并发症 - 现在您必须将所有像素移向左侧,将最古老的像素放在左侧,因此您可以在右侧绘制新的像素。因此,您需要一种方法来做到这一点!
另一种方法是类似于圆形缓冲区的想法。如果您不知道那是什么,那么想法是您使用一个开始和结束的普通缓冲区,但是您将其中一个索引视为数据的 start Point Point ,将您点击缓冲区的最后一个索引,然后在击中索引时
停下如有必要?因此,您始终添加了最新的6个值。您只需要从正确的索引范围中处理读取,从
开始 - &gt; lastIndex
,然后firstIndex-&gt; end
您可以使用位图做同样的事情 - 从左侧启动“填充”,增加
end
,以便您可以绘制下一条垂直线。满满后,通过将结束
从左开始填充。当您实际绘制位图时,您不是绘制整个内容(723456
),而是将其绘制为两个部分(23456
,然后7
)。有意义吗?当您为画布绘制位图时,有一个调用源rect
和一个目标,因此您可以将其绘制在两个块中。您始终可以从刮擦每帧(清除并绘制垂直线)重新绘制位图,因此您基本上每次都重新绘制了整个数据缓冲区。对于每个值,可能仍然比
drawRect
方法快,但是说实话,比“将位图视为另一个圆形缓冲区”方法不容易得多。如果您已经管理了一个圆形缓冲区,那就不多了 - 因为缓冲区和位图将具有相同数量的值(在位图的情况下水平像素),您可以使用相同的start> start> start
和end
两个Your basic custom view approach would be to implement
onDraw
and redraw your current data each frame. You'd probably keep some kind of circularBuffer
holding your most recent n amplitude values, so each frame you'd iterate over those, and usedrawRect
to draw the bars (you'd calculate things like width, height scaling, start and end positions etc inonSizeChanged
, and use those values when defining the coordinates for theRect
s).That in itself might be fine! The only way you can really tell how expensive draw calls are is to benchmark them, so you could try this approach out and see how it goes. Profile it to see how much time it takes, how much the CPU spikes etc.
There are a few things you can do to make
onDraw
as efficient as possible, mostly things like avoiding object allocations - so watch out for loop functions that createIterator
s, and in the same way you're supposed to create aPaint
once instead of creating them over and over inonDraw
, you could reuse a singleRect
object by setting its coordinates for each bar you need to draw.Another approach you could try is creating a working
Bitmap
in your custom view, which you control, and callingdrawBitmap
insideonDraw
to paint it onto theCanvas
. That should be a pretty inexpensive call, and it can easily be stretched as required to fit the view.The idea there, is that very time you get new data, you paint it onto the bitmap. Because of how your waveform looks (like blocks), and the fact you can scale it up, really all you need is a single vertical line of pixels for each value, right? So as the data comes in, you paint an extra line onto your already-existing bitmap, adding to the image. Instead of painting the entire waveform block by block every frame, you're just adding the new blocks.
The complication there is when you "fill" the bitmap - now you have to "shift" all the pixels to the left, dropping the oldest ones on the left side, so you can draw the new ones on the right. So you'll need a way to do that!
Another approach would be something similar to the circular buffer idea. If you don't know what that is, the idea is you take a normal buffer with a start and an end, but you treat one of the indices as your data's start point, wrap around to 0 when you hit the last index of the buffer, and stop when you hit the index you're calling your end point:
See how once it's full, you shift the start and end one step "right", wrapping around if necessary? So you always have the most recent 6 values added. You just have to handle reading from the correct index ranges, from
start -> lastIndex
and thenfirstIndex -> end
You could do the same thing with a bitmap - start "filling" it from the left, increasing
end
so you can draw the next vertical line. Once it's full, start filling from the left by movingend
there. When you actually draw the bitmap, instead of drawing the whole thing as-is (723456
) you draw it in two parts (23456
then7
). Make sense? When you draw a bitmap to the canvas, there's a call that takes a sourceRect
and a destination one, so you can draw it in two chunks.You could always redraw the bitmap from scratch each frame (clear it and draw the vertical lines), so you're basically redrawing your whole data buffer each time. Probably still faster than the
drawRect
approach for each value, but honestly not much easier than the "treat the bitmap as another circular buffer" method. If you're already managing one circular buffer, it's not much more work - since the buffer and the bitmap will have the same number of values (horizontal pixels in the bitmap's case) you can use the samestart
andend
values for both您永远不会使用布局来执行此操作。布局用于预制组件。它们是组件的高级组合,您不想经常从中添加或删除视图。为此,您使用带有画布的自定义视图。布局甚至都不是这样的选择。
You would never do this with layouts. Layouts are for premade components. They're high level combinations of components and you don't want to dynamically add or remove views from it frequently. For this, you use a custom view with a canvas. Layouts aren't even an option for something like this.