1、前言
大概前年的樣子,實(shí)驗(yàn)室要做一個智能睡眠床墊的項(xiàng)目,需要用安卓手機(jī) 端進(jìn)行心電和呼吸波形的展示,當(dāng)時由于時間緊,波形的展示用的是第三方控件achartengine。雖然能運(yùn)行,但是有些信息不能顯示,總之 沒有自己實(shí)現(xiàn)的方便,想加什么功能就加什么功能。直到今年,項(xiàng)目組又要做一個智能背心的項(xiàng)目,還是需要 顯示心電和呼吸波形,趁這次時間比較充裕,就自己實(shí)現(xiàn)了一個。
2、控件功能


根據(jù)項(xiàng)目要求,結(jié)合自己的實(shí)現(xiàn),本控件具有以下功能和特點(diǎn):
1、控件能顯示單元格的時間和幅度信息,從而可以根據(jù)波形的背景和單位,可以從圖中得到波形的真實(shí)幅度大小和周期,以便于專業(yè)人士從波形中獲取有效信息(尤其是時間信息);
2、波形能自適應(yīng)調(diào)整幅值大小,可以通過手勢對時間軸進(jìn)行放大或者縮?。?/p>
3、波形以掃屏方式進(jìn)行;
4、可以通過單手上拉或者下拉快速實(shí)現(xiàn)波形的基線調(diào)整,通過雙手上下擴(kuò)張或者收縮完成波形的倍數(shù)放大和縮小。
3、實(shí)現(xiàn)
控件一共用到了一個類EcgWaveView和一個接口EcgViewInterface。
其中,接口包含兩個函數(shù)
public interface EcgViewInterface{
void onError(Exception e);
void onShowMessage(String t,inti);
}
接口主要用于接口回調(diào),通知父窗體關(guān)于背景單元格的時間軸和Y軸的單位變化。
類EcgWaveView繼承View,整個控件的實(shí)現(xiàn)機(jī)制大體可以概括為:控件提供了兩個bitmap:machineBitmap和cacheBitmap,cacheBitmap用于更新波形,machineBitmap是最終畫到設(shè)備的畫圖區(qū)。 類提供了一個供外部調(diào)用的方法drawWave(fy),fy即為需要畫的點(diǎn)的Y值(并不是像素值)??丶⒋薡值轉(zhuǎn)為像素值后,在machineBitmap進(jìn)行drawline操作,然后用cacheBitmap對machineBitmap畫完點(diǎn)后的[10,20]的像素區(qū)域進(jìn)行刷新,然后調(diào)用canvas.drawBitmap(machineBitmap,0,0,bmpPaint);將machineBitmap滑到屏幕上。
下面根據(jù)功能分別介紹其實(shí)現(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)畫一個點(diǎn)。
繪制好的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)為手機(jī)屏幕對應(yīng)的像素值,然后在machineCanvas進(jìn)行描點(diǎn)操作,再進(jìn)行掃屏的更新操作,最后將machineCanvas所對應(yīng)的bitmap投向手機(jī)屏幕。
其中changeout是將Y值轉(zhuǎn)為像素的方法
private int changeOut(int temp)
{
float a;
int b;
//temp-欲轉(zhuǎn)換的數(shù)值
//a表示放大之前電壓的真實(shí)范圍。SampleV 是輸入的電壓范圍:比如[0,10]mv,則SampleV 就是10,而[0,10]所對應(yīng)的Y值是[0,4096],則SampleR就是4096
a = (float)SampleV * temp / SampleR;
//這個公式的意思是(真實(shí)電壓-基線電壓)/每格代表的電壓*每格所擁有的50個像素
b = (short)(change_h - (a - change_y) * change_50n / change_nV);
// y_max 和y_min 用于記錄像素最大值和最小值,當(dāng)當(dāng)前的像素點(diǎn)操過了最大值,表明曲線將要超出屏幕,需要進(jìn)行波形的自適應(yīng)調(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()是掃屏方法,用于更新曲線。其原理就是,對描好點(diǎn)的machineBitmap,需要用cacheBitmap(cacheBitmap就是一張干凈的背景圖)去更新。比如machineBitmap的描點(diǎn)已經(jīng)畫到第N點(diǎn),則用cacheBitmap的((N+10,0),(N+20,by))(by是圖的高度)區(qū)域去更新machineBitmap的((N+10,0),(N+20,by))區(qū)域。
當(dāng)判斷曲線到頭時,需要判斷波形是否需要自動調(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);
}
自適應(yīng)方法AutoResize,主要是調(diào)整波形的幅度和基線。首先是判斷整屏范圍內(nèi)的波形最大值和最小值的絕對值和波形控件的高度進(jìn)行相比,如果幅度大于高度,則需要將每格所代表的幅度值放大一倍(即波形幅度變小一倍),反之則將每格所代表的幅度值縮小一倍,如此反復(fù),到最后會使波形的高度大于控件高度一半,但會小于控件高度。此時,再調(diào)整基線即可保證,波形能完整的在控件中顯示。
//自適應(yīng)調(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 控件的手勢操作
控件可以通過兩點(diǎn)進(jìn)行時間軸的放大或者縮小操作。這里主要用到view的onTouchEvent事件。主要思路是:在第二個手指觸摸屏幕事件中,計(jì)算兩個手指之間的距離,并保存;在第一個點(diǎn)離開屏幕時,再次計(jì)算兩個手指之間的距離,通過計(jì)算這個兩個距離的值來進(jìn)行判斷是否需要進(jìn)行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:
//工具鼠標(biāo)上下移動距離調(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文件以及如何導(dǎo)入aar文件,我會在另一篇博客給出。
4.1庫包含以下公開方法:
| 方法 | 參數(shù) | 功能 | 備注 |
|---|---|---|---|
| init() | 用于初始化控件 | ||
| setWaveAdapter(boolean waveAdapter) | waveAdapter:true->波形自適應(yīng),false->不自適應(yīng) | 設(shè)置波形是否需要自適應(yīng)調(diào)整 | |
| setN_frequency(double n_frequency) | n_frequency:輸入的點(diǎn)的頻率 | 設(shè)置需要掃描點(diǎn)的頻率 | 假設(shè)一秒鐘需要畫100個點(diǎn),則頻率就是100 |
| setSampleV(int sampleVa) | sampleVa:輸入的點(diǎn)的真實(shí)電壓范圍 | 設(shè)置需要掃描點(diǎn)的真實(shí)電壓范圍 | 假設(shè)電壓范圍是[0,10]mv,則sampleVa=10 |
| setSampleRe(int sampleRe)) | sampleRe:輸入的點(diǎn)的數(shù)值范圍 | 設(shè)置需要掃描點(diǎn)的數(shù)值范圍 | 控件接收的數(shù)據(jù)是數(shù)字量接收的,假如其范圍是[0,4096](代表真實(shí)電壓[0,10]mv),則sampleRe是4096 |
| setListener(EcgViewInterface ecgViewListener)) | ecgViewListener:用于回調(diào)的接口 | 設(shè)置回調(diào)接口 | EcgViewInterface 是 用于回調(diào)的接口,用于回傳控件單元格的時間單位和幅度單位 |
| drawWave( int y) | y):需要花點(diǎn)的y坐標(biāo),對應(yīng)于上述的[0,4096] | 畫圖 |
4.2使用示例
控件需要一個layout進(jìn)行加載。假設(shè)在布局文件中,我們?yōu)檫@個庫設(shè)置Linearlayout進(jìn)行加載,并命命為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); //設(shè)置接口回調(diào)
ecgWaveView1.init();
lLayout3.addView(bcgWaveView3);
view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
});
}
其中,用于接口回調(diào)的實(shí)現(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。喜歡的朋友可以點(diǎn)贊或者贊賞,謝謝!