引言
之前我寫過一篇文章铣猩,講的是安卓自定義電平流控件的實現(xiàn),在這片文章中我要講的是頻譜圖的實現(xiàn)俩莽。相信我們大多數(shù)人都接觸過或者是知道頻譜吧洲鸠,頻譜圖就是顯示無線電信號在一定帶寬范圍內(nèi),信號強弱的變化教馆,一目了然的可以看到信號有無逊谋,或者信號的變化等特征。這里我也就不過多的闡述土铺,接下來主要講解如何實現(xiàn)安卓客戶端上的頻譜圖控件胶滋。
首先看下效果圖:
實現(xiàn)了,最大值悲敷、最小值究恤、實時值的繪制,同時Y軸拖動后德,以及框選顯示(X軸縮放)等部宿。
實現(xiàn)
布局
要畫一個圖形控件,首先是布局瓢湃,要畫哪些元素理张,元素的位置布局,元素的顏色绵患、字體等等雾叭,這些東西捋清楚之后,就可以動手畫了落蝙。您在網(wǎng)上隨便一搜頻譜圖织狐,應(yīng)該就可以看到大致長啥樣了,基本都差不多筏勒,我的頻譜圖布局如下:
接下來就應(yīng)該是編碼實現(xiàn)了移迫。
編碼
首先新建一個類SpectrumView繼承View,實現(xiàn)必要的構(gòu)造函數(shù):
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中定義控件的屬性奏寨,便于調(diào)用這可以定制某些屬性起意,比如顏色、字體大小等病瞳,并在初始化initView()中讀取設(shè)置的值:
<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" />
<!--格子數(shù)揽咕,幾等分-->
<attr name="grid_count_sv" format="integer" />
...
接下來就是繪制悲酷,首先需要重載onMeasure()和onDraw()方法,在onMeasure()中確定View的高和寬:
_width = getMeasuredWidth();
_height = getMeasuredHeight();
在onDraw()中就是具體的繪制了亲善,首先是drawUnit()繪制左中的單位“電平(dBuV)”设易,drawAxis()繪制坐標(biāo)軸和網(wǎng)格,這兩個個的實現(xiàn)基本和電平流控件的那里完全一樣蛹头,就不再闡述顿肺。至此,基本的背景繪制已完成渣蜗,效果圖如下:
當(dāng)setData()來數(shù)據(jù)之后屠尊,就開始繪制頻譜了drawSpectrum():
/**
* 畫頻譜
*
* @param canvas
*/
private void drawSpectrum(Canvas canvas) {
if (_data.length == 0 || _startIndex >= _endIndex)
return; // 沒有數(shù)據(jù)時不需要繪制
_paint.setColor(_realTimeLineColor);
_paint.setStyle(Paint.Style.STROKE);
int maxValue = _maxValue + _offsetY - _zoomOffsetY;
int minValue = _minValue + _offsetY + _zoomOffsetY;
int scaleHeight = _height - _marginTop - _marginBottom; // 繪制區(qū)總高度
int scaleWidth = _width - _marginLeft - _marginRight - _scaleLineLength; // 繪制區(qū)總寬度
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); // 畫最小值
}
// 覆蓋上邊和下邊耕拷,使頻譜看上去是在指定區(qū)域進(jìn)行繪制的
_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);
}
這里的關(guān)鍵點在于計算出幅度與屏幕上所在的位置浸赫,并不復(fù)雜,具體可以參見代碼赃绊,畫了Path之后既峡,在上邊和下邊各畫一個矩形,覆蓋在上面碧查,這樣當(dāng)頻譜的圖形移動到上面去的時候并不會越過網(wǎng)格界限运敢,看起來就是繪制在網(wǎng)格以內(nèi),實際上您要是不繪制矩形的話么夫,可以試試者冤,看下又會是什么效果。
畫到這里档痪,頻譜圖的靜態(tài)展示效果就完成了涉枫,這還沒完,還需要加上一些交互事件:圖形上下拖動腐螟,局部縮放等愿汰。下面是具體實現(xiàn)步驟,先重載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中乐纸,主要根據(jù)手指按下的位置衬廷,來確定當(dāng)前操作是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中主要是根據(jù)當(dāng)前的操作類型,來確定_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中,主要是固化字段的狀態(tài)跌宛。
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) {
// 放大酗宋。 根據(jù) _startX 和 _endX 來確定 _startIndex 和 _endIndex,以及中心頻率和帶寬
if (_data.length == 0 || _endIndex - _startIndex <= 2) { // 沒有數(shù)據(jù)疆拘,或者只要小于2個點時蜕猫,不再放大
_handleType = HandleType.NONE;
return;
}
float perScaleLength = (_width - _marginLeft - _scaleLineLength - _marginRight) / (float) (_endIndex - _startIndex); // 一格的距離
// 在放大的基礎(chǔ)上再次放大,巧妙啊哎迄,佩服我自己了回右,哈哈哈
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;
}
至此,頻譜控件的交互事件也基本完成漱挚。最后翔烁,我們再對外提供一些方法,便于調(diào)用:
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);
好了棱烂,控件的大致實現(xiàn)過程就是如上所述租漂,程序也并不復(fù)雜,只要細(xì)心點颊糜,把各種情況的考慮下,就沒啥問題秃踩。
最后衬鱼,如果要集成使用控件,可能還需添加其他功能或方法憔杨,才能完善系統(tǒng)鸟赫,可以定制開發(fā),如果有需要可以聯(lián)系本人消别。
/**
* @Title: SpectrumView.java
* @Package: com.an.view
* @Description: 自定義頻譜圖控件
* @Author: AnuoF
* @QQ/WeChat: 188512936
* @Date 2019.08.09 20:27
* @Version V1.0
*/
奉上源碼抛蚤,自由、開源:https://github.com/AnuoF/android_customview
AnuoF
Chengdu
Aug 20,2019