安卓上實現(xiàn)的自定義動態(tài)心電波形控件

1、前言

大概前年的樣子块请,實驗室要做一個智能睡眠床墊的項目娜氏,需要用安卓手機 端進行心電和呼吸波形的展示,當時由于時間緊墩新,波形的展示用的是第三方控件achartengine贸弥。雖然能運行,但是有些信息不能顯示海渊,總之 沒有自己實現(xiàn)的方便绵疲,想加什么功能就加什么功能哲鸳。直到今年,項目組又要做一個智能背心的項目盔憨,還是需要 顯示心電和呼吸波形徙菠,趁這次時間比較充裕,就自己實現(xiàn)了一個郁岩。

2婿奔、控件功能

image
image

根據(jù)項目要求,結(jié)合自己的實現(xiàn)驯用,本控件具有以下功能和特點:

1脸秽、控件能顯示單元格的時間和幅度信息,從而可以根據(jù)波形的背景和單位蝴乔,可以從圖中得到波形的真實幅度大小和周期记餐,以便于專業(yè)人士從波形中獲取有效信息(尤其是時間信息);

2薇正、波形能自適應調(diào)整幅值大小片酝,可以通過手勢對時間軸進行放大或者縮小挖腰;

3雕沿、波形以掃屏方式進行;
4猴仑、可以通過單手上拉或者下拉快速實現(xiàn)波形的基線調(diào)整审轮,通過雙手上下擴張或者收縮完成波形的倍數(shù)放大和縮小。

3辽俗、實現(xiàn)

控件一共用到了一個類EcgWaveView和一個接口EcgViewInterface疾渣。
其中,接口包含兩個函數(shù)

public interface EcgViewInterface{

        void onError(Exception e);

        void onShowMessage(String t,inti);

}

接口主要用于接口回調(diào)崖飘,通知父窗體關于背景單元格的時間軸和Y軸的單位變化榴捡。

類EcgWaveView繼承View,整個控件的實現(xiàn)機制大體可以概括為:控件提供了兩個bitmap:machineBitmap和cacheBitmap朱浴,cacheBitmap用于更新波形吊圾,machineBitmap是最終畫到設備的畫圖區(qū)。 類提供了一個供外部調(diào)用的方法drawWave(fy)翰蠢,fy即為需要畫的點的Y值(并不是像素值)项乒。控件將此Y值轉(zhuǎn)為像素值后梁沧,在machineBitmap進行drawline操作板丽,然后用cacheBitmap對machineBitmap畫完點后的[10,20]的像素區(qū)域進行刷新,然后調(diào)用canvas.drawBitmap(machineBitmap,0,0,bmpPaint);將machineBitmap滑到屏幕上。

下面根據(jù)功能分別介紹其實現(xiàn)原理埃碱。

3.1猖辫、背景圖

  private void drawBackGrid()
    {
        //繪制網(wǎng)格 圖片
        int m, n;
        backBitmap=Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        canvasBackground=new Canvas();
        canvasBackground.setBitmap(backBitmap);
        Paint paint1=new Paint();

        paint1.setColor(Color.WHITE);
        paint1.setStyle(Paint.Style.FILL);
        paint1.setStrokeWidth(2);
        paint1.setAntiAlias(true);
        paint1.setDither(true);
        paint1.setStrokeJoin(Paint.Join.ROUND);

        canvasBackground.drawRect(0, 0, bx, by, paint1);
        paint1.setStyle(Paint.Style.STROKE);
        paint1.setColor(Color.argb(128, 220, 190, 50));
        for (m = 0; m < bx; m = m + 10)
            for (n = 0; n < by; n = n + 10)
                canvasBackground.drawPoint(m, n, paint1);

        for (m = 0; m < bx; m = m + 50)
            canvasBackground.drawLine(m, 0, m, by-5, paint1);
        for (n = 0; n < by; n = n + 50)
            canvasBackground.drawLine(0, n, bx, n,paint1);

    }

背景圖沒啥好說的,基本就是定義好背景bitmap和用于畫背景的canvas砚殿,然后每50個像素分別畫橫的和豎的直線啃憎,每10*10的范圍內(nèi)畫一個點。
繪制好的backBitmap似炎,用作給上文提到的machineBitmap和cacheBitmap做背景圖:

 machineBitmap =Bitmap.createBitmap(backBitmap,0,0,width,height);
 cacheBitmap =Bitmap.createBitmap(backBitmap,0,0,width,height);

3.2 畫曲線

 public void drawWave( int y)  {

        int ecgy_new=changeOut(y);
        machineCanvas.drawLine(n_ecgx, ecgy, n_ecgx + n_step, ecgy_new, paint);
        n_ecgx=n_ecgx+n_step;
        ScreenResh();
        invalidate();
        ecgy=ecgy_new;

    }

畫曲線的思路就是先將輸入的Y值轉(zhuǎn)為手機屏幕對應的像素值辛萍,然后在machineCanvas進行描點操作,再進行掃屏的更新操作羡藐,最后將machineCanvas所對應的bitmap投向手機屏幕贩毕。
其中changeout是將Y值轉(zhuǎn)為像素的方法

 private int changeOut(int temp)
    {
        float a;
        int b;
        //temp-欲轉(zhuǎn)換的數(shù)值
        //a表示放大之前電壓的真實范圍。SampleV 是輸入的電壓范圍:比如[0,10]mv,則SampleV 就是10仆嗦,而[0,10]所對應的Y值是[0,4096],則SampleR就是4096
        a = (float)SampleV * temp / SampleR;
        //這個公式的意思是(真實電壓-基線電壓)/每格代表的電壓*每格所擁有的50個像素
        b = (short)(change_h - (a - change_y) * change_50n / change_nV);
// y_max 和y_min 用于記錄像素最大值和最小值辉阶,當當前的像素點操過了最大值,表明曲線將要超出屏幕瘩扼,需要進行波形的自適應調(diào)整谆甜。
        if (b < y_min)
            y_min = b;
        if (b > y_max)
            y_max = b;

        if (b > by)
        {
            b = (short)by - 1;
            b_autoResize = true;
            n_aStep = 1;
        }

        if (b <= 0)
        {
            b = 1;
            b_autoResize = true;
            n_aStep = 1;
        }
        return b;
    }

在上述drawWave方法中,調(diào)用invalidate()時集绰,會調(diào)用View的onDraw()方法:

 public void onDraw(Canvas canvas) {

        canvas.drawBitmap(machineBitmap,0,0,bmpPaint);

    } 

ScreenResh()是掃屏方法规辱,用于更新曲線。其原理就是栽燕,對描好點的machineBitmap罕袋,需要用cacheBitmap(cacheBitmap就是一張干凈的背景圖)去更新。比如machineBitmap的描點已經(jīng)畫到第N點碍岔,則用cacheBitmap的((N+10浴讯,0),(N+20,by))(by是圖的高度)區(qū)域去更新machineBitmap的((N+10付秕,0)兰珍,(N+20,by))區(qū)域侍郭。
當判斷曲線到頭時询吴,需要判斷波形是否需要自動調(diào)整,如果需要調(diào)整亮元,就調(diào)用調(diào)整方法猛计。

    private void ScreenResh(){

        if (n_ecgx > bx - 5)    //如果曲線到頭
        {
            //判斷(ymax-ymin)的值是不是小于高度一半,如果小于爆捞,就要自動調(diào)整
            if((y_max-y_min)*2<by){
                b_autoResize = true;
                n_aStep = 1;
            }
            n_ecgx = 0;
            Rect rect=new Rect(0, 0, 20, height);    //表示更新的區(qū)域,從0到20的X軸
            machineCanvas.drawBitmap(cacheBitmap, rect, rect, bmpPaint);
            if (b_autoResize&&waveAdapter){
                AutoResize();
            }
            checkRange();
        }
        //
        Rect rect=new Rect(n_ecgx + n_step+10, 0, n_ecgx + n_step+20, height);
        machineCanvas.drawBitmap(cacheBitmap, rect, rect, bmpPaint);

    }

自適應方法AutoResize,主要是調(diào)整波形的幅度和基線奉瘤。首先是判斷整屏范圍內(nèi)的波形最大值和最小值的絕對值和波形控件的高度進行相比,如果幅度大于高度,則需要將每格所代表的幅度值放大一倍(即波形幅度變小一倍)盗温,反之則將每格所代表的幅度值縮小一倍藕赞,如此反復,到最后會使波形的高度大于控件高度一半卖局,但會小于控件高度斧蜕。此時,再調(diào)整基線即可保證砚偶,波形能完整的在控件中顯示批销。

 //自適應調(diào)整
    public void AutoResize()
    {
        if (n_aStep == 1)
        {
            //波形的幅度小于畫布高度,并且波形幅度的2倍大于畫布高度染坯,說明波形幅度合適均芽,此時只要調(diào)整基線
            if ((y_max - y_min) * 2 >= by && (y_max - y_min) <= by)
                n_aStep = 2;
            else
            {
                if (y_max - y_min >= by)       //表示波形范圍超過畫布高度
                {
                    change_nV = change_nV*2;
                    listener.onShowMessage(change_nV+"",1);

                }
                else if ((y_max - y_min) * 2 <= by)   //如果波形幅度的兩倍都小于畫布高度,說明波形幅度過小单鹿,需要波形像素調(diào)整放大
                {
                    change_nV = change_nV/2;
                    listener.onShowMessage(change_nV+"",1);

                }

                y_max = -3*by;
                y_min =3*by;
                return;
            }
        }

        if (n_aStep == 2)
        {
            n_top = (by - (y_max + y_min)) / 2;
            this.change_y += n_top * change_nV/ change_50n;
        }
            b_autoResize = false;
    }

3.3 控件的手勢操作

控件可以通過兩點進行時間軸的放大或者縮小操作掀宋。這里主要用到view的onTouchEvent事件。主要思路是:在第二個手指觸摸屏幕事件中羞反,計算兩個手指之間的距離布朦,并保存;在第一個點離開屏幕時昼窗,再次計算兩個手指之間的距離是趴,通過計算這個兩個距離的值來進行判斷是否需要進行X軸的放大或者縮小操作。

  @Override
    public boolean onTouchEvent( MotionEvent event) {

        switch (event.getAction() & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN:
                Log.i("tag","手勢放下");
                OldY = event.getY();
                mode = 1;
                break;
            case MotionEvent.ACTION_UP:


                mode = 0;
                break;
            case MotionEvent.ACTION_POINTER_UP:
                Log.i("tag","手勢拿起"+spacingX(event));
                newDistanceX = spacingX(event);
                mode -= 1;
                if (newDistanceX > oldDistanceX +DISTANCE){
                    n_step=n_step*2;
                    n_Btime = (int)(n_Ptime * change_50n / n_step);
                    listener.onShowMessage(n_Btime+"",0);
                    Log.i("tag","n_step-->"+n_step+";n_Btime-->"+n_Btime);
                }else if (newDistanceX < oldDistanceX -DISTANCE){
                    if (n_step>=2){
                        n_step=n_step/2;
                        n_Btime = (int)(n_Ptime * change_50n / n_step);
                        listener.onShowMessage(n_Btime+"",0);
                        Log.i("tag", "n_step-->" + n_step + ";n_Btime-->" + n_Btime);
                    }
                }

                //這是Y軸方向
                newDistanceY = spacingY(event);
                if (newDistanceY > oldDistanceY +DISTANCE){
                    change_nV = change_nV/2;   //波形放大   //如果波形幅度的兩倍都小于畫布高度澄惊,說明波形幅度過小唆途,需要波形像素調(diào)整放大
                    listener.onShowMessage(change_nV+"",1);
                    Log.i("tag","change_nV縮小-->"+change_nV);
                }else if (newDistanceY < oldDistanceY -DISTANCE){    //波形放大
                    change_nV = change_nV*2;
                    listener.onShowMessage(change_nV+"",1);
                    Log.i("tag", "change_nV放大-->" + change_nV);
                }

                break;
            case MotionEvent.ACTION_POINTER_DOWN:
                Log.i("tag","手勢放下"+spacingX(event));
                oldDistanceX = spacingX(event);
                oldDistanceY = spacingY(event);

                mode += 1;
                break;

            case MotionEvent.ACTION_MOVE:
                //工具鼠標上下移動距離調(diào)整波形上下位置
                this.change_y -= 0.4 * (OldY - event.getY()) * change_nV * 2 / 25;
                OldY = event.getY();
                break;
        }
        return true;
    }

4、如何使用這個庫

這個庫我會放在github和csdn上掸驱,為了避免資源沖突肛搬,整個庫都使用java代碼編寫,沒有使用任何第三方庫毕贼,在android studio下編譯為aar文件温赔。至于如何生成aar文件以及如何導入aar文件,我會在另一篇博客給出鬼癣。

4.1庫包含以下公開方法:

方法 參數(shù) 功能 備注
init() 用于初始化控件
setWaveAdapter(boolean waveAdapter) waveAdapter:true->波形自適應陶贼,false->不自適應 設置波形是否需要自適應調(diào)整
setN_frequency(double n_frequency) n_frequency:輸入的點的頻率 設置需要掃描點的頻率 假設一秒鐘需要畫100個點,則頻率就是100
setSampleV(int sampleVa) sampleVa:輸入的點的真實電壓范圍 設置需要掃描點的真實電壓范圍 假設電壓范圍是[0,10]mv待秃,則sampleVa=10
setSampleRe(int sampleRe)) sampleRe:輸入的點的數(shù)值范圍 設置需要掃描點的數(shù)值范圍 控件接收的數(shù)據(jù)是數(shù)字量接收的芬沉,假如其范圍是[0,4096](代表真實電壓[0,10]mv)瘦材,則sampleRe是4096
setListener(EcgViewInterface ecgViewListener)) ecgViewListener:用于回調(diào)的接口 設置回調(diào)接口 EcgViewInterface 是 用于回調(diào)的接口逸嘀,用于回傳控件單元格的時間單位和幅度單位
drawWave( int y) y):需要花點的y坐標制市,對應于上述的[0,4096] 畫圖

4.2使用示例
控件需要一個layout進行加載。假設在布局文件中,我們?yōu)檫@個庫設置Linearlayout進行加載聊替,并命命為graph1_father楼肪。

  <LinearLayout
            android:id="@+id/graph1_father"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="5"
            android:background="@color/floralwhite"
            android:gravity="center"
            android:orientation="vertical"
            android:paddingLeft="5dp"
            android:paddingRight="5dp"
            android:weightSum="5">

        </LinearLayout>

然后在activity中,使用

 @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        relativeLayout=(RelativeLayout)findViewById(R.id.root);
        view = getWindow().getDecorView();
       relativeLayout.setOnTouchListener(this);
        initView();
    }

    private void initView() {

        view.getViewTreeObserver().addOnGlobalLayoutListener(
                new ViewTreeObserver.OnGlobalLayoutListener() {
                    @Override
                    public void onGlobalLayout() {


                        lLayout3 = (LinearLayout) view.findViewById(R.id.graph1_father);
                        int width = lLayout3.getWidth();
                        int height = lLayout3.getHeight();

                       ecgWaveView3 = new EcgWaveView(getBaseContext(), width, height);
                 
                        ecgWaveView3 .setN_frequency(50);
                        ecgWaveView3 .setSampleRe(4096);
                        ecgWaveView3 .setSampleV(10);
                        ecgWaveView1.setListener(ecgViewListener);  //設置接口回調(diào)
                        ecgWaveView1.init();


                        lLayout3.addView(bcgWaveView3);

                        view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
                    }
                });
    }

其中惹悄,用于接口回調(diào)的實現(xiàn)

    private EcgViewInterface ecgViewListener=new EcgViewInterface() {
        @Override
        public void onError(Exception e) {

        }

        @Override
        public void onShowMessage(String t, int i) {
            Log.i("tag", "心電接口回調(diào)--》" + t);
            if (i==0){   
               Toast.makeText(getApplication(),"時間:" + t + "ms/格",Toast.LENGTH_SHORT).show();
            }else if (i==1){
                Toast.makeText(getApplication(),"電壓:"+t+"mv/格",Toast.LENGTH_SHORT).show();
               
            }
        }
    };

庫文件淹辞,示例以及apk演示程序可參考:
https://download.csdn.net/download/andyzhu_2005/10764191。喜歡的朋友可以點贊或者贊賞俘侠,謝謝象缀!

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市爷速,隨后出現(xiàn)的幾起案子央星,更是在濱河造成了極大的恐慌,老刑警劉巖惫东,帶你破解...
    沈念sama閱讀 211,290評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件莉给,死亡現(xiàn)場離奇詭異,居然都是意外死亡廉沮,警方通過查閱死者的電腦和手機颓遏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評論 2 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來滞时,“玉大人叁幢,你說我怎么就攤上這事∑夯” “怎么了曼玩?”我有些...
    開封第一講書人閱讀 156,872評論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長窒百。 經(jīng)常有香客問我黍判,道長,這世上最難降的妖魔是什么篙梢? 我笑而不...
    開封第一講書人閱讀 56,415評論 1 283
  • 正文 為了忘掉前任顷帖,我火速辦了婚禮,結(jié)果婚禮上渤滞,老公的妹妹穿的比我還像新娘贬墩。我一直安慰自己,他們只是感情好蔼水,可當我...
    茶點故事閱讀 65,453評論 6 385
  • 文/花漫 我一把揭開白布震糖。 她就那樣靜靜地躺著录肯,像睡著了一般趴腋。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,784評論 1 290
  • 那天优炬,我揣著相機與錄音颁井,去河邊找鬼。 笑死蠢护,一個胖子當著我的面吹牛雅宾,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播葵硕,決...
    沈念sama閱讀 38,927評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼眉抬,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了懈凹?” 一聲冷哼從身側(cè)響起蜀变,我...
    開封第一講書人閱讀 37,691評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎介评,沒想到半個月后库北,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,137評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡们陆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,472評論 2 326
  • 正文 我和宋清朗相戀三年寒瓦,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片坪仇。...
    茶點故事閱讀 38,622評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡杂腰,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出椅文,到底是詐尸還是另有隱情颈墅,我是刑警寧澤,帶...
    沈念sama閱讀 34,289評論 4 329
  • 正文 年R本政府宣布雾袱,位于F島的核電站恤筛,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏芹橡。R本人自食惡果不足惜毒坛,卻給世界環(huán)境...
    茶點故事閱讀 39,887評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望林说。 院中可真熱鬧煎殷,春花似錦、人聲如沸腿箩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽珠移。三九已至弓乙,卻和暖如春末融,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背暇韧。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工勾习, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人懈玻。 一個月前我還...
    沈念sama閱讀 46,316評論 2 360
  • 正文 我出身青樓巧婶,卻偏偏與公主長得像,于是被迫代替她去往敵國和親涂乌。 傳聞我的和親對象是個殘疾皇子艺栈,可洞房花燭夜當晚...
    茶點故事閱讀 43,490評論 2 348

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