安卓自定義頻譜圖控件

引言

之前我寫過一篇文章铣猩,講的是安卓自定義電平流控件的實現(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末寻狂,一起剝皮案震驚了整個濱河市岁经,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蛇券,老刑警劉巖缀壤,帶你破解...
    沈念sama閱讀 211,265評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異纠亚,居然都是意外死亡塘慕,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評論 2 385
  • 文/潘曉璐 我一進(jìn)店門蒂胞,熙熙樓的掌柜王于貴愁眉苦臉地迎上來图呢,“玉大人,你說我怎么就攤上這事「蛑” “怎么了赴叹?”我有些...
    開封第一講書人閱讀 156,852評論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長瞳筏。 經(jīng)常有香客問我稚瘾,道長,這世上最難降的妖魔是什么姚炕? 我笑而不...
    開封第一講書人閱讀 56,408評論 1 283
  • 正文 為了忘掉前任摊欠,我火速辦了婚禮,結(jié)果婚禮上柱宦,老公的妹妹穿的比我還像新娘些椒。我一直安慰自己,他們只是感情好掸刊,可當(dāng)我...
    茶點故事閱讀 65,445評論 5 384
  • 文/花漫 我一把揭開白布免糕。 她就那樣靜靜地躺著,像睡著了一般忧侧。 火紅的嫁衣襯著肌膚如雪石窑。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,772評論 1 290
  • 那天蚓炬,我揣著相機與錄音松逊,去河邊找鬼。 笑死肯夏,一個胖子當(dāng)著我的面吹牛经宏,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播驯击,決...
    沈念sama閱讀 38,921評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼烁兰,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了徊都?” 一聲冷哼從身側(cè)響起沪斟,我...
    開封第一講書人閱讀 37,688評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎碟贾,沒想到半個月后币喧,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,130評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡袱耽,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,467評論 2 325
  • 正文 我和宋清朗相戀三年幻赚,在試婚紗的時候發(fā)現(xiàn)自己被綠了澄峰。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片郎任。...
    茶點故事閱讀 38,617評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡颖医,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情琼讽,我是刑警寧澤必峰,帶...
    沈念sama閱讀 34,276評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站钻蹬,受9級特大地震影響吼蚁,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜问欠,卻給世界環(huán)境...
    茶點故事閱讀 39,882評論 3 312
  • 文/蒙蒙 一肝匆、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧顺献,春花似錦旗国、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至肿轨,卻和暖如春寿冕,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背椒袍。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評論 1 265
  • 我被黑心中介騙來泰國打工蚂斤, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人槐沼。 一個月前我還...
    沈念sama閱讀 46,315評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像捌治,于是被迫代替她去往敵國和親岗钩。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,486評論 2 348

推薦閱讀更多精彩內(nèi)容

  • 引言 在無線電監(jiān)測方面肖油,需要對信號進(jìn)行展示兼吓,其中一項數(shù)據(jù)就是設(shè)備返回的電平數(shù)據(jù),需要對其實時展示森枪,一圖勝千言视搏,最好...
    AnuoF閱讀 449評論 0 4
  • 引言 之前寫過電平圖和頻譜圖的實現(xiàn)的文章,在這片文章中县袱,我將要講解頻譜瀑布圖的實現(xiàn)浑娜。 頻譜瀑布圖又叫譜陣圖,它是將...
    AnuoF閱讀 1,879評論 0 1
  • ¥開啟¥ 【iAPP實現(xiàn)進(jìn)入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個線程式散,因...
    小菜c閱讀 6,365評論 0 17
  • --繪圖與濾鏡全面解析 概述 在iOS中可以很容易的開發(fā)出絢麗的界面效果筋遭,一方面得益于成功系統(tǒng)的設(shè)計,另一方面得益...
    韓七夏閱讀 2,715評論 2 10
  • 坦白講最近被二寶折騰的夠嗆…… 兩歲多了白天尿布都不用可以自己穿脫褲子上廁所的娃了编饺,還不時要來一頓夜奶, 夜奶就夜...
    Rafen閱讀 604評論 0 1