bitmap xml大小 安卓_安卓自定义频谱图控件

引言

之前我写过一篇文章,讲的是安卓自定义电平流控件的实现,在这片文章中我要讲的是频谱图的实现。相信我们大多数人都接触过或者是知道频谱吧,频谱图就是显示无线电信号在一定带宽范围内,信号强弱的变化,一目了然的可以看到信号有无,或者信号的变化等特征。这里我也就不过多的阐述,接下来主要讲解如何实现安卓客户端上的频谱图控件。

首先看下效果图:

4c2b386e0eff4db063d31916f73fefa9.png

4c2b386e0eff4db063d31916f73fefa9.png

b21062aae0c00bfcd31ae455bc737904.png

实现了,最大值、最小值、实时值的绘制,同时Y轴拖动,以及框选显示(X轴缩放)等。

实现

布局

要画一个图形控件,首先是布局,要画哪些元素,元素的位置布局,元素的颜色、字体等等,这些东西捋清楚之后,就可以动手画了。您在网上随便一搜频谱图,应该就可以看到大致长啥样了,基本都差不多,我的频谱图布局如下:

ab51de03c9448e3ed79b6836dc6f5860.png

接下来就应该是编码实现了。

编码

首先新建一个类SpectrumView继承View,实现必要的构造函数:

public class SpectrumView extends View implements View.OnTouchListener {

      public SpectrumView(Context context, AttributeSet attrs, int defStypeAttr) {
        super(context, attrs, defStypeAttr);
        initView(context, attrs);
    }

    public SpectrumView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView(context, attrs);
    }

    public SpectrumView(Context context) {
        super(context);
        initView();
    }
}

在attr.xml中定义控件的属性,便于调用这可以定制某些属性,比如颜色、字体大小等,并在初始化initView()中读取设置的值:

<declare-styleable name="SpectrumView">
        <!--单位字体大小-->
        <attr name="unit_font_size" format="integer" />
        <!--单位-->
        <attr name="unit_sv" format="string" />
        <!--单位颜色-->
        <attr name="unit_color_sv" format="color" />
        <!--格子颜色-->
        <attr name="grid_color_sv" format="color" />
        <!--格子数,几等分-->
        <attr name="grid_count_sv" format="integer" />

        ...

接下来就是绘制,首先需要重载onMeasure()和onDraw()方法,在onMeasure()中确定View的高和宽:

_width = getMeasuredWidth();
    _height = getMeasuredHeight();

在onDraw()中就是具体的绘制了,首先是drawUnit()绘制左中的单位“电平(dBuV)”,drawAxis()绘制坐标轴和网格,这两个个的实现基本和电平流控件的那里完全一样,就不再阐述。至此,基本的背景绘制已完成,效果图如下:

d087407902da24669eb7feb25b7d303b.png

当setData()来数据之后,就开始绘制频谱了drawSpectrum():

/**
 * 画频谱
 *
 * @param canvas
 */
private void drawSpectrum(Canvas canvas) {
    if (_data.length == 0 || _startIndex >= _endIndex)
        return;  // 没有数据时不需要绘制

    _paint.setColor(_realTimeLineColor);
    _paint.setStyle(Paint.Style.STROKE);

    int maxValue = _maxValue + _offsetY - _zoomOffsetY;
    int minValue = _minValue + _offsetY + _zoomOffsetY;

    int scaleHeight = _height - _marginTop - _marginBottom;     // 绘制区总高度
    int scaleWidth = _width - _marginLeft - _marginRight - _scaleLineLength;       // 绘制区总宽度
    float perHeight = scaleHeight / (float) Math.abs(maxValue - minValue);      // 每一格的高度
    float perWidth = scaleWidth / (float) (_endIndex - _startIndex);

    Path realTimePath = new Path();
    Path maxValuePath = null;
    Path minValuePath = null;

    for (int i = _startIndex; i <= _endIndex; i++) {    // 此处需要加上=,确保最后一个点可以绘制
        if (i >= _data.length)  // 防止越界
            continue;

        float level = _data[i];
        int x = (int) ((i - _startIndex) * perWidth) + _marginLeft + _scaleLineLength;
        int y = (int) ((maxValue - level) * perHeight) + _marginTop;

        if (i == _startIndex) {
            realTimePath.moveTo(x, y);
        } else {
            realTimePath.lineTo(x, y);
        }

        if (_drawMaxValue) {
            if (maxValuePath == null) {
                maxValuePath = new Path();
            }

            float maxLevel = _maxData[i];
            int max_x = (int) ((i - _startIndex) * perWidth) + _marginLeft + _scaleLineLength;
            int max_y = (int) ((maxValue - maxLevel) * perHeight) + _marginTop;

            if (i == _startIndex) {
                maxValuePath.moveTo(max_x, max_y);
            } else {
                maxValuePath.lineTo(max_x, max_y);
            }
        }

        if (_drawMinValue) {
            if (minValuePath == null) {
                minValuePath = new Path();
            }

            float minLevel = _minData[i];
            int min_x = (int) ((i - _startIndex) * perWidth) + _marginLeft + _scaleLineLength;
            int min_y = (int) ((maxValue - minLevel) * perHeight) + _marginTop;

            if (i == _startIndex) {
                minValuePath.moveTo(min_x, min_y);
            } else {
                minValuePath.lineTo(min_x, min_y);
            }
        }
    }

    canvas.drawPath(realTimePath, _paint);
    if (maxValuePath != null) {
        _paint.setColor(_maxValueLineColor);
        canvas.drawPath(maxValuePath, _paint);    // 画最大值
    }
    if (minValuePath != null) {
        _paint.setColor(_minValueLineColor);
        canvas.drawPath(minValuePath, _paint);    // 画最小值
    }

    // 覆盖上边和下边,使频谱看上去是在指定区域进行绘制的
    _paint.setStyle(Paint.Style.FILL);
    Drawable background = getBackground();
    if (background instanceof ColorDrawable) {
        ColorDrawable colorDrawable = (ColorDrawable) background;
        int color = colorDrawable.getColor();
        _paint.setColor(color);
        canvas.drawRect(_marginLeft + _scaleLineLength, 0, _width - _marginRight, _marginTop, _paint);
        canvas.drawRect(_marginLeft + _scaleLineLength, _height - _marginBottom + 1, _width - _marginRight, _height + 1, _paint);
    }

    // 计算并绘制中心频率和带宽
    double perFreq = _spectrumSpan / _data.length / 1000;
    double span = perFreq * (_endIndex - _startIndex) / 2 * 1000;

    String centerFreqStr, startFreqStr, endFreqStr;
    // 如果是全景,则显示中心频率和带宽,局部缩放则显示起始、终止频率和中心点频率
    if (_startIndex == 0 && _endIndex == _data.length) {
        centerFreqStr = String.format("%.3f", _frequency) + "MHz";
        startFreqStr = "-" + String.format("%.3f", span) + "kHz";
        endFreqStr = "+" + String.format("%.3f", span) + "kHz";
    } else {
        int centerIndex = (_startIndex + (_endIndex - _startIndex) / 2);
        centerFreqStr = String.format("%.3f", centerIndex * perFreq + (_frequency - _spectrumSpan / 2 / 1000)) + " MHz";
        startFreqStr = String.format("%.3f", _startIndex * perFreq + (_frequency - _spectrumSpan / 2 / 1000)) + " MHz";
        endFreqStr = String.format("%.3f", _endIndex * perFreq + (_frequency - _spectrumSpan / 2 / 1000)) + " MHz";
    }

    Rect freqRect = new Rect();
    _paint.setColor(_gridColor);
    _paint.getTextBounds(centerFreqStr, 0, centerFreqStr.length(), freqRect);
    canvas.drawText(centerFreqStr, _width - _marginRight - scaleWidth / 2 - freqRect.width() / 2, _height - _marginBottom + freqRect.height() + 5, _paint);
    canvas.drawText(startFreqStr, _marginLeft + _scaleLineLength, _height - _marginBottom + freqRect.height() + 5, _paint);
    canvas.drawText(endFreqStr, _width - _marginRight - (float) _paint.measureText(endFreqStr), _height - _marginBottom + freqRect.height() + 5, _paint);
}

这里的关键点在于计算出幅度与屏幕上所在的位置,并不复杂,具体可以参见代码,画了Path之后,在上边和下边各画一个矩形,覆盖在上面,这样当频谱的图形移动到上面去的时候并不会越过网格界限,看起来就是绘制在网格以内,实际上您要是不绘制矩形的话,可以试试,看下又会是什么效果。

画到这里,频谱图的静态展示效果就完成了,这还没完,还需要加上一些交互事件:图形上下拖动,局部缩放等。下面是具体实现步骤,先重载onTouch()方法:

@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
    switch (motionEvent.getAction() & MotionEvent.ACTION_MASK) {
        case MotionEvent.ACTION_DOWN:
            actionDown(motionEvent);
            break;
        case MotionEvent.ACTION_POINTER_DOWN:
            actionPointerDown(motionEvent);
            break;
        case MotionEvent.ACTION_MOVE:
            actionMove(motionEvent);
            break;
        case MotionEvent.ACTION_POINTER_UP:
            actionPointerUp(motionEvent);
            break;
        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP:
            actionCancelUp(motionEvent);
            break;
    }

    return true;
}

在ACTION_DOWN中,主要根据手指按下的位置,来确定当前操作是Y轴拖动还是X轴缩放:

private void actionDown(MotionEvent event) {
    if (Utils.IsPointInRect(0, 0, _marginLeft + _scaleLineLength, _height, (int) event.getX(), (int) event.getY())) {
        _handleType = HandleType.DRAG;    // 纵轴拖动
        _startY = event.getY();
    } else if (Utils.IsPointInRect(_marginLeft + _scaleLineLength, _marginTop, _width - _marginRight, _height - _marginBottom, (int) event.getX(), (int) event.getY())) {
        _handleType = HandleType.ZONE;    // 缩放频谱
        _startX = _endX = event.getX();
    }
}

在ACTION_POINTER_DOWN中主要是记录两个手指按下时的初始距离:

private void actionPointerDown(MotionEvent event) {
    if (event.getPointerCount() == 2) {
        _handleType = HandleType.ZOOM;
        _oldDistanceY = Math.abs(event.getY(0) - event.getY(1));
    }
}

在ACTION_MOVE中主要是根据当前的操作类型,来确定_offsetY/_zoomOffsetY/_endX等字段的值,并实时刷新绘制图形:

private void actionMove(MotionEvent event) {
    if (_handleType == HandleType.DRAG) {
        float currrentY = event.getY();
        int spanScale = (int) ((currrentY - _startY) / ((_height - _marginTop - _marginBottom) / Math.abs((_maxValue - _minValue))));
        if (spanScale != 0) {
            _offsetY = spanScale;
            postInvalidate();
        }
    } else if (_handleType == HandleType.ZOOM && event.getPointerCount() == 2) {
        float currentDistanceY = Math.abs(event.getY(0) - event.getY(1));
        float perScaleHeight = (_height - _marginTop - _marginBottom) / (float) Math.abs(_maxValue - _minValue);
        int spanScale = (int) ((currentDistanceY - _oldDistanceY) / perScaleHeight);
        if (spanScale != 0 && ((_maxValue - spanScale) - (_minValue + spanScale) >= _gridCount)) {  // 防止交叉越界,并且在放大到 总刻度长为 _gridCount 时,不能再放大
            _zoomOffsetY = spanScale;
            postInvalidate();
        }
    } else if (_handleType == HandleType.ZONE) {
        _endX = event.getX();
        if (_endX < _marginLeft + _scaleLineLength) {
            _endX = _marginLeft + _scaleLineLength;
        } else if (_endX > _width - _marginRight) {
            _endX = _width - _marginRight;
        }  // 此处的判断是为了防止Rect越界

        postInvalidate();
    }
}

在ACTION_POINTER_UP和ACTION_CANCEL或ACTION_UP中,主要是固化字段的状态。

private void actionPointerUp(MotionEvent event) {
    if (_handleType == HandleType.ZOOM) {
        _maxValue -= _zoomOffsetY;
        _minValue += _zoomOffsetY;
        _zoomOffsetY = 0;
    }

    _handleType = HandleType.NONE;
}

private void actionCancelUp(MotionEvent event) {
    if (_handleType == HandleType.DRAG) {
        _maxValue += _offsetY;
        _minValue += _offsetY;
        _offsetY = 0;
    } else if (_handleType == HandleType.ZONE) {
        // 这里需要读取索引
        if (_startX > _endX) {
            // 缩小
            _startIndex = 0;
            _endIndex = _data.length;
            postInvalidate();
        } else if (_startX < _endX) {
            // 放大。 根据 _startX 和 _endX 来确定 _startIndex 和 _endIndex,以及中心频率和带宽
            if (_data.length == 0 || _endIndex - _startIndex <= 2) {  // 没有数据,或者只要小于2个点时,不再放大
                _handleType = HandleType.NONE;
                return;
            }

            float perScaleLength = (_width - _marginLeft - _scaleLineLength - _marginRight) / (float) (_endIndex - _startIndex); //  一格的距离
            // 在放大的基础上再次放大,巧妙啊,佩服我自己了,哈哈哈
            int tempEndIndex = _startIndex + (int) ((_endX - _marginLeft - _scaleLineLength) / perScaleLength);
            int tempStartIndex = _startIndex + (int) ((_startX - _marginLeft - _scaleLineLength) / perScaleLength);
            if (tempEndIndex > tempStartIndex) {   // 保证至少有2个点(一条直线)
                _endIndex = tempEndIndex;
                _startIndex = tempStartIndex;
                postInvalidate();
            }
        }
    }

    _handleType = HandleType.NONE;
}

至此,频谱控件的交互事件也基本完成。最后,我们再对外提供一些方法,便于调用:

public void setData(double frequency, double spectrunSpan, float[] data);
public void offsetY(int offset);
public void zoomY(int zoom);
public void clear();
public void autoView();
public void setMaxValueLineVisible(boolean visible);
public void setMinValueLineVisible(boolean visible);

好了,控件的大致实现过程就是如上所述,程序也并不复杂,只要细心点,把各种情况的考虑下,就没啥问题。

最后,如果要集成使用控件,可能还需添加其他功能或方法,才能完善系统,可以定制开发,如果有需要可以联系本人。

最后1,我的联系方式,呃,,如果您真的需要的话,随便那种方式应该都可以在到我的,哈哈,提醒一下,在代码里面也有,例如:

/**
 * @Title: SpectrumView.java
 * @Package: com.an.view
 * @Description: 自定义频谱图控件
 * @Author: AnuoF
 * @QQ/WeChat: 188512936
 * @Date 2019.08.09 20:27
 * @Version V1.0
 */

最后2:奉上源码,自由、开源:https://github.com/AnuoF/android_customview

最后,定稿!!!!

AnuoF
Chengdu
Aug 20,2019

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值