安卓自定义瀑布图控件

引言

之前写过电平图和频谱图的实现的文章,在这片文章中,我将要讲解频谱瀑布图的实现。

频谱瀑布图又叫谱阵图,它是将振动信号的功率谱或幅值谱随转速变化而叠置而成的三维谱图,显示振动信号中各谐波成分随转速变化的情况。普通频谱图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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容