Android中的自定义动态图
[更新] 为了总结这个问题,我使用以下两种方法实现了我的图表(见下文)。 drawCurve()
接收一个 Canvas
和一个 float
数组。数组已正确填充(时间戳由数组中的值索引假定)并且从 0.0 到 1.0 变化。该数组被发送到 prepareWindowArray()
,后者以循环方式从位置 windowStart
获取 windowSize
值的数组块。
GraphView 和数据提供者(蓝牙设备)使用的数组是相同的。中间的类确保 GraphView 不会读取蓝牙设备正在写入的数据。由于 GraphView 总是循环遍历数组并在每次迭代时重绘它,因此它会根据蓝牙设备写入的数据进行更新,并且通过强制蓝牙设备的写入频率为 Graph 的刷新频率,我获得了平滑的效果我的信号的动画。
GraphView
的 invalidate()
方法由 Activity
调用,该方法运行 Timer
来刷新图表每x
毫秒。图表刷新的频率是动态设置的,以便它适应来自蓝牙设备的数据流(在其数据包的标头中指定其信号的频率)。
在我下面写的答案(在答案部分)中找到我的 GraphView
的完整代码。如果你们发现错误或优化方法,请告诉我;我们将不胜感激!
/**
* Read a buffer array of size greater than "windowSize" and create a window array out of it.
* A curve is then drawn from this array using "windowSize" points, from left
* to right.
* @param canvas is a Canvas object on which the curve will be drawn. Ensure the canvas is the
* later drawn object at its position or you will not see your curve.
* @param data is a float array of length > windowSize. The floats must range between 0.0 and 1.0.
* A value of 0.0 will be drawn at the bottom of the graph, while a value of 1.0 will be drawn at
* the top of the graph. The range is not tested, so you must ensure to pass proper values, or your
* graph will look terrible.
* 0.0 : draw at the bottom of the graph
* 0.5 : draw in the middle of the graph
* 1.0 : draw at the top of the graph
*/
private void drawCurve(Canvas canvas, float[] data){
// Create a reference value to determine the stepping between each points to be drawn
float incrementX = (mRightSide-mLeftSide)/(float) windowSize;
float incrementY = (mBottomSide - mTopSide);
// Prepare the array for the graph
float[] source = prepareWindowArray(data);
// Prepare the curve Path
curve = new Path();
// Move at the first point.
curve.moveTo(mLeftSide, source[0]*incrementY);
// Draw the remaining points of the curve
for(int i = 1; i < windowSize; i++){
curve.lineTo(mLeftSide + (i*incrementX), source[i] * incrementY);
}
canvas.drawPath(curve, curvePaint);
}
实现数组循环行为的 prepareWindowArray()
方法:
/**
* Extract a window array from the data array, and reposition the windowStart
* index for next iteration
* @param data the array of data from which we get the window
* @return an array of float that represent the window
*/
private float[] prepareWindowArray(float[] data){
// Prepare the source array for the graph.
float[] source = new float[windowSize];
// Copy the window from the data array into the source array
for(int i = 0; i < windowSize; i++){
if(windowStart+i < data.length) // If the windows holds within the data array
source[i] = data[windowStart + i]; // Simply copy the value in the source array
else{ // If the window goes beyond the data array
source[i] = data[(windowStart + 1)%data.length]; // Loop at the beginning of the data array and copy from there
}
}
// Reposition the buffer index
windowStart = windowStart + windowSize;
// If the index is beyond the end of the array
if(windowStart >= data.length){
windowStart = windowStart % data.length;
}
return source;
}
[/UPDATE]
我正在制作一个以固定速率从蓝牙设备读取数据的应用程序。每当我有新数据时,我都希望将它们绘制在右侧的图表上,并将图表的其余部分实时平移到左侧。基本上,就像示波器一样。
所以我制作了一个自定义视图,带有 xy 轴、标题和单位。为此,我只需在视图画布上绘制这些内容即可。现在我想画曲线。我设法使用这种方法从已经填充的数组中绘制静态曲线:
public void drawCurve(Canvas canvas){
int left = getPaddingLeft();
int bottom = getHeight()-getPaddingTop();
int middle = (bottom-10)/2 - 10;
curvePaint = new Paint();
curvePaint.setColor(Color.GREEN);
curvePaint.setStrokeWidth(1f);
curvePaint.setDither(true);
curvePaint.setStyle(Paint.Style.STROKE);
curvePaint.setStrokeJoin(Paint.Join.ROUND);
curvePaint.setStrokeCap(Paint.Cap.ROUND);
curvePaint.setPathEffect(new CornerPathEffect(10) );
curvePaint.setAntiAlias(true);
mCurve = new Path();
mCurve.moveTo(left, middle);
for(int i = 0; i < mData[0].length; i++)
mCurve.lineTo(left + ((float)mData[0][i] * 5), middle-((float)mData[1][i] * 20));
canvas.drawPath(mCurve, curvePaint);
}
它给了我这样的东西。
我的图表上仍有一些问题需要修复(子轴未正确缩放),但这些是细节我可以稍后修复。
现在我想用动态的东西来改变这个静态图(它接收非动态值矩阵),每 40 毫秒重新绘制曲线,将旧数据推到左边,将新数据绘制到右边,这样我就可以可视化蓝牙设备实时提供的信息。
我知道已经存在一些图形包,但我对这些东西有点菜鸟,我想通过自己实现这个图表来练习。另外,除了曲线部分之外,我的 GraphView 类的大部分内容都已完成。
第二个问题,我想知道如何将新值发送到图表。我应该使用 FIFO 堆栈之类的东西,还是可以通过简单的双精度矩阵来实现我想要的效果?
顺便说一句,底部的 4 个字段已经动态更新。好吧,他们有点伪造“动态”,他们一次又一次地循环相同的双矩阵,他们实际上并不采用新的值。
感谢您抽出时间!如果我的问题不清楚,请告诉我,我会更新更多详细信息。
[UPDATE]
To conclude this question, I implemented my graph using the following two methods (see below). drawCurve()
receives a Canvas
and an array of float
. The array is properly filled (timestamps are assumed by the value index in the array) and varies from 0.0 to 1.0. The array is sent to prepareWindowArray()
that takes a chunk of the array from position windowStart
for windowSize
-values, in a circular manner.
The array used by the GraphView and by the data provider (a Bluetooth device) is the same. A Class in the middle ensures that GraphView is not reading data that are being written by the Bluetooth device. Since the GraphView always loop thru the array and redraw it at every iteration, it will update according to the data written by the Bluetooth device, and by forcing the write frequency of the Bluetooth device to the refresh frequency of the Graph, I obtain a smooth animation of my signal.
The GraphView
's invalidate()
method is called by the Activity
, which run a Timer
to refresh the graph at every x
milliseconds. The frequency at which the graph is refreshed is dynamically set, so that it adapt to the flow of data from the Bluetooth device (which specify the frequency of its signal in the header of its packet).
Find the complete code of my GraphView
in the answer I wrote below (in the answer section). If you guys find errors or way to optimize it, please let me know; it would be greatly appreciated!
/**
* Read a buffer array of size greater than "windowSize" and create a window array out of it.
* A curve is then drawn from this array using "windowSize" points, from left
* to right.
* @param canvas is a Canvas object on which the curve will be drawn. Ensure the canvas is the
* later drawn object at its position or you will not see your curve.
* @param data is a float array of length > windowSize. The floats must range between 0.0 and 1.0.
* A value of 0.0 will be drawn at the bottom of the graph, while a value of 1.0 will be drawn at
* the top of the graph. The range is not tested, so you must ensure to pass proper values, or your
* graph will look terrible.
* 0.0 : draw at the bottom of the graph
* 0.5 : draw in the middle of the graph
* 1.0 : draw at the top of the graph
*/
private void drawCurve(Canvas canvas, float[] data){
// Create a reference value to determine the stepping between each points to be drawn
float incrementX = (mRightSide-mLeftSide)/(float) windowSize;
float incrementY = (mBottomSide - mTopSide);
// Prepare the array for the graph
float[] source = prepareWindowArray(data);
// Prepare the curve Path
curve = new Path();
// Move at the first point.
curve.moveTo(mLeftSide, source[0]*incrementY);
// Draw the remaining points of the curve
for(int i = 1; i < windowSize; i++){
curve.lineTo(mLeftSide + (i*incrementX), source[i] * incrementY);
}
canvas.drawPath(curve, curvePaint);
}
The prepareWindowArray()
method that implement the circular behavior of the array:
/**
* Extract a window array from the data array, and reposition the windowStart
* index for next iteration
* @param data the array of data from which we get the window
* @return an array of float that represent the window
*/
private float[] prepareWindowArray(float[] data){
// Prepare the source array for the graph.
float[] source = new float[windowSize];
// Copy the window from the data array into the source array
for(int i = 0; i < windowSize; i++){
if(windowStart+i < data.length) // If the windows holds within the data array
source[i] = data[windowStart + i]; // Simply copy the value in the source array
else{ // If the window goes beyond the data array
source[i] = data[(windowStart + 1)%data.length]; // Loop at the beginning of the data array and copy from there
}
}
// Reposition the buffer index
windowStart = windowStart + windowSize;
// If the index is beyond the end of the array
if(windowStart >= data.length){
windowStart = windowStart % data.length;
}
return source;
}
[/UPDATE]
I'm making an app that read data from a Bluetooth device at a fixed rate. Everytime that I have new data, I want them to be plotted on the graph to the right, and to translate the remainder of the graph to the left in realtime. Basically, like an oscilloscope would do.
So I made a custom View, with xy axis, a title and units. To do this, I simply draw those things on the View canvas. Now I want to draw the curve. I manage to draw a static curve from an already filled array using this method:
public void drawCurve(Canvas canvas){
int left = getPaddingLeft();
int bottom = getHeight()-getPaddingTop();
int middle = (bottom-10)/2 - 10;
curvePaint = new Paint();
curvePaint.setColor(Color.GREEN);
curvePaint.setStrokeWidth(1f);
curvePaint.setDither(true);
curvePaint.setStyle(Paint.Style.STROKE);
curvePaint.setStrokeJoin(Paint.Join.ROUND);
curvePaint.setStrokeCap(Paint.Cap.ROUND);
curvePaint.setPathEffect(new CornerPathEffect(10) );
curvePaint.setAntiAlias(true);
mCurve = new Path();
mCurve.moveTo(left, middle);
for(int i = 0; i < mData[0].length; i++)
mCurve.lineTo(left + ((float)mData[0][i] * 5), middle-((float)mData[1][i] * 20));
canvas.drawPath(mCurve, curvePaint);
}
It gives me something like this.
There are still things to fix on my graph (the sub-axis are not properly scaling), but these are details I can fix later.
Now I want to change this static graph (that receives a non-dynamic matrice of values) with something dynamic that would redraw the curve every 40ms, pushing the old data to the left and plotting the new data to the right, so I could visualise in real time the information provided by the Bluetooth device.
I know there are some graphing package that exists already, but I'm kinda noob with these things and I'd like to pratice by implementing this graph myself. Also, most of my GraphView class is done, except for the curve part.
Second question, I'm wondering how I should send the new values to the graph. Should I use something like a FIFO stack, or can I achieve what I want with a simple matrice of doubles?
On a side note, the 4 fields at the bottom are already dynamically updated. Well, they are kind of faking the "dynamic", they loop thru the same double matrice again and again, they don't actually take fresh values.
Thanks for your time! If something's unclear about my question, let me know and I'll update it with more details.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
正如我的问题中提到的,这是我设计用来解决我的问题的课程。
As mentioned in my question, here's the class that I designed to solve my problems.
好吧,我首先尝试用您拥有的代码和真实的动态数据重新绘制它。只有当这不够快时,你才需要尝试任何花哨的东西,比如滚动……
如果你需要花哨的东西,我会尝试这样的东西。
我会将图形的动态部分绘制到辅助位图中,将其保留在帧之间,而不是直接绘制到画布上。我会将图形的背景无动态部分放在另一个位图中,仅在重新缩放等时绘制。
在这个辅助动态位图中,当绘制新数据时,您首先需要清除要替换的旧数据,您可以通过绘制适当的切片来完成此操作将静态背景位图覆盖在陈旧数据的顶部,从而清除它并使背景再次变得漂亮和新鲜。然后您只需绘制新的动态数据即可。诀窍是,您从左到右绘制第二个位图,然后在末尾绕回左侧并重新开始。
为了从 soncodary 位图到你的 cancas,将位图分两部分绘制到画布上。刚刚添加的数据右侧的旧数据需要绘制到最终画布的左侧部分,而新数据需要立即绘制到其右侧。
对于发送数据,循环缓冲区对于此类数据来说是正常的事情,一旦它离开图表,您就不再关心它。
Well I would start by just trying to redraw it all with the code you have and real dynalic data. Only if that is not quick enough do you need to try anything fancy like scrolling...
If you need fancy I would try somthing like this.
I would draw the dynamic part of the graph into a secondary Bitmap that you keep between frames rather than directly to the canves. I would have the background none dynamic part of the graph in another bitmap that only gets drawen on rescale etc.
In this secondary dynamic bitmap when ploting new data you first need to clear the old data you are replacing you do this by drawing the apropriate slice of the static background bitmap over the top of the stale data, thus clearing it and geting the background nice and fresh again. You then just need to draw your new bit of dynamic data. The trick is that You draw into this second bitmap left to right then just wrap back to the left at the end and start over.
To get from the soncodary bitmap to your cancas draw the bitmap to the canvas in two parts. The older data to the right of what you just added needs to be drawn onto the left part of your final canvas and the new data needs to be drawn imediatly to the right of it.
For sending the data a circular buffer would be the normal thing for this sort of data where once it's off the graph you don't care about it.