引言
之前写过电平图和频谱图的实现的文章,在这片文章中,我将要讲解频谱瀑布图的实现。
频谱瀑布图又叫谱阵图,它是将振动信号的功率谱或幅值谱随转速变化而叠置而成的三维谱图,显示振动信号中各谐波成分随转速变化的情况。普通频谱图x轴是频率,y轴是幅度;而瀑布图x轴是频率,y轴是时间,幅度则用不同颜色表示,随着时间的的变化,整个频谱由上到下移动,看起来像瀑布,所以叫瀑布图。
瀑布图也叫雨点图,对于查看猝发信号或者跳频信号的效果十分明显,可以很清晰的看到信号的变化,这里就不模拟和展示了。
首先看下效果图:
实现了,色带自定义,频谱信号缩放,手指按下显示当前点的频率等。
实现
布局
这个的布局相对比较简单,就是一个色带区和信号绘制区,如下图:
左边是一个渐变的色带,其对应右边信号幅度的强弱。
编码
自定义控件,前面的步骤都差不多,新建类,attrs.xml,初始化,onMeasure(),onDraw(),我们直接从onDraw()开始吧。
在我最初实现第一个版本的时候,性能问题非常严重,绘制的点数多了之后,界面卡顿非常严重,后来我在绘图的机制和绘图的效率方面做了优化之后,效果非常好,测试Y轴1000行,X轴801个点,非常流畅,毫无压力。
我的第一个优化点是,采用离屏Canvas机制:即异步方式绘制图形,完成之后再回调通知onDraw()方法进行屏幕绘制。
第二个优化点是,在对数据的处理时,不再保存矩阵数据,不做横向和纵向的矩阵遍历。已绘制了的信号直接裁剪Bitmap,新到的信号才遍历绘制,然后贴在一起,就完成信号的绘制。这就完成性能质的飞跃啊。
那我们就看代码吧
在onDraw()中,我们首先绘制左边色带drawGradientRect():
/**
* 画色带
*
* @param canvas
*/
private void drawGradientRect(Canvas canvas) {
LinearGradient linearGradient = new LinearGradient(0, 0, _marginLeft, _height, _colors, null, Shader.TileMode.CLAMP);
_paint.setShader(linearGradient);
canvas.drawRect(0, 0, _marginLeft, _height, _paint);
}
效果图如下:
然后就在setData()等待频谱数据,数据来了之后就启动线程池执行绘制:
public void setData(final double frequency, final double span, final float[] data) {
if (frequency != _frequency || span != _spectrumSpan) {
_startIndex = 0;
_endIndex = data.length;
_frequency = frequency;
_spectrumSpan = span;
_dataLength = data.length;
}
if (_wCanvas != null) {
_executorService.execute(new Runnable() {
@Override
public void run() {
if (_wCanvas != null) {
_wCanvas.setData(frequency, span, data);
}
}
});
}
}
在启动线程绘制的过程中,首先是根据频谱的幅度值,映射到色带中的颜色值(怎么映射,请看代码),然后才是绘制drawWaterfall()
/**
* 绘制瀑布图。为保证效率,不需要每次全部重绘,而是绘制新增的数据即可。
*/
private void drawWaterfall() {
if (_data == null || _data.size() == 0)
return;
if (_waterFallView == null || _waterFallView._bitmap == null)
return;
float perWidth = (_width) / (float) (_endIndex - _startIndex); // 每个方格的 宽
float perHeight = (_height) / (float) _rainRow; // 每个方格的 高
if (_data.size() == 1) {
for (int h = _startIndex; h < _endIndex; h++) {
int width = (int) ((h - _startIndex) * perWidth);
int height = 0;
_rainPaint.setColor(_colors[_colors.length - 1 - _data.get(0)[h]]);
_canvas.drawRect(width, height, width + perWidth, height + perHeight, _rainPaint);
}
}else {
// 先绘制之前的 Bitmap,然后再画新的数据
Bitmap bitmap = Bitmap.createBitmap(_waterFallView._bitmap, 0, (int) perHeight, _width, (int) ((_data.size() - 1) * perHeight)); // perHeight 必须 >= 1,也就是 _rainRow 必须 <= _height
_canvas.drawBitmap(bitmap, 0, 0, _rainPaint);
bitmap.recycle();
for (int h = _startIndex; h < _endIndex; h++) {
if (h >= _endIndex)
break;
int width = (int) ((h - _startIndex) * perWidth);
int height = (int) ((_data.size() - 1) * perHeight);
_rainPaint.setColor(_colors[_colors.length - 1 - _data.get(_data.size() - 1)[h]]); // 只画最后一包
_canvas.drawRect(width, height, width + perWidth, height + perHeight, _rainPaint);
}
}
}
上面的方法就是我在之前说的第二个优化点。在绘制完成之后,再调用回调通知onDraw()绘制图形到屏幕:
_callback.onDrawFinished();
@Override
protected void onDraw(final Canvas canvas) {
drawGradientRect(canvas);
if (_bitmap != null) {
canvas.drawBitmap(_bitmap, _marginLeft, _marginTop, _paint);
}
drawSelectRect(canvas);
super.onDraw(canvas);
}
最后是信号的放大和缩小,这个的实现和频谱图的实现基本一致,即根据手指的位置来确定_startIndex和_endIndex,然后取相应的数据进行绘制即可。
好了,就到这里吧,文章只说个大概,详情请看代码,或者联系我:
/**
* @Title: WaterfallView.java
* @Package: com.an.view
* @Description: 自定义频谱瀑布图控件
* @Author: AnuoF
* @QQ/WeChat: 188512936
* @Date 2019.08.14 11:50
* @Version V1.0
*/
最后,奉上源码,自由、开源:https://github.com/AnuoF/android_customview
AnuoF
Chengdu
Aug 20,2019