最近的一個Android項目中,需要在特定坐標軸上繪制一個數據波形(虛擬儀器之類),並且需要在不同分辨率的設備上保持波形上數據點的個數以及與坐標軸的一致性。
思路如下:
1、首先采用SurfaceView進行繪圖操作,SurfaceView是View的繼承類,繪圖的效率較高。具體的使用方法是自定義視圖類繼承SurfaceView並實現SurfaceHolder.Callback接口。定義一個繪圖線程類,由於SurfaceView的特性,SurfaceView上的繪圖工作在Surface創建之后進行,在Surface銷毀之前結束,所以在復寫的surfaceCreated(SurfaceHolder holder)方法中打開繪圖線程,在復寫的surfaceCreated(SurfaceHolder holder)方法中結束線程。
2、在畫圖線程中使用SurfaceHolder,用來操縱Surface。畫圖的操作都在SurfaceHolder.lockCanvas()和SurfaceHolder.unlockCanvasAndPost(Canvas canvas)之間進行,鎖定畫布得到畫布對象,解鎖后將畫圖內容顯示。
到目前為止都是按部就班的SurfaceView操作,具體的解釋和實例可以參考這篇博文:http://www.cnblogs.com/xuling/archive/2011/06/06/android.html
關於繪圖線程的打開和結束要多說幾句,完全按照上述方法進行打開和結束線程會遇到一個問題,當按HOME鍵返回主界面后再次進入程序時,會報線程已打開的錯,即使在surfaceDestroyed(SurfaceHolder holder)方法中設置了線程結束的標志(結束線程較安全較普遍的方法是設置一個flag,線程循環運行時每次進行判斷,為true時繼續循環執行,需要結束線程時將flag設置為false,線程自行結束)。這個問題的解決方法是將myThread = new MyThread(holder)由構造方法中移到surfaceCreated(SurfaceHolder holder)方法中,並在surfaceCreated(SurfaceHolder holder)中將myThread指為null。
1 @Override 2 public void surfaceCreated(SurfaceHolder holder) { 3 // TODO Auto-generated method stub 4 myThread = new MyPressThread(holder); 5 6 myThread.isRun = true; 7 myThread.start(); 8 9 } 10 11 @Override 12 public void surfaceDestroyed(SurfaceHolder holder) { 13 // TODO Auto-generated method stub 14 System.out.println("PressView surfaceDestroyed"); 15 myThread.isRun = false; 16 myThread = null; 17 }
3、坐標軸的實現:
我具體實現坐標軸上繪圖的方法很簡單,首先將一張坐標軸圖片作為SurfaceView的背景。但是在實際操作中發現了問題,直接將繼承SurfaceView的視圖控件(我將其自定義為一個控件)背景變為圖片之后會發生只顯示圖片不出現圖形的情況,解決方法是將視圖控件嵌套在LinearLayout中,將坐標軸圖片設置為Linearayout的背景圖片,然后將SurfaceView視圖控件設置為透明。
布局如下:
<LinearLayout android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="@drawable/picture" > <com.example.MyView android:id="@+id/myView" android:layout_width="fill_parent" android:layout_height="fill_parent" /> </LinearLayout>
SurfaceView默認的背景顏色是黑的,在網上學到一種將其變為透明的方法:在繼承SurfaceView的自定義視圖類的構造函數中添加這兩句:
1 public PressView(Context context, AttributeSet attrs) { 2 super(context, attrs); 3 // TODO Auto-generated constructor stub 4 holder = this.getHolder(); 5 holder.addCallback(this); 6 7 //將surfaceView背景變為透明 8 setZOrderOnTop(true); 9 getHolder().setFormat(PixelFormat.TRANSLUCENT); 10 }
4、關於自適應坐標軸的圖像畫法:
接下來是真正的畫圖,其中最關鍵的要怎樣畫出與坐標軸(就是一個背景圖片)相一致的圖形,首先分析一下畫圖的工具:
Canvas.drawLine(float startX, float startY, float stopX, float stopY, Paint paint)
這是最基本的一個畫線方法,前4個參數分別是線段頭尾的坐標,最后一個參數是畫筆。這里一開始讓我困惑的是為什么坐標是浮點數?屏幕上像素點明明是一個一個的,難不成還能把每個像素點切開來?如果真是根據浮點數畫圖,那要將波形圖像與坐標軸對應就簡單了,只要將圖片坐標軸均分成要畫的點數段,則直接根據這些非整數段的實際位置畫圖即可。
實際上常識告訴我們在屏幕上顯示圖像時,像素點就是最小單位,不可能再分割。我對drawine等畫圖方法參數是浮點數的理解是:此方法支持非整數坐標,而后方法自動將非整數坐標轉換成實際屏幕上最接近的像素點。就好象我們在畫圖軟件中畫一條寬度一個像素的線,可以隨便畫不考慮下筆是否對准了某個像素點,如果將這條線放大來看,則實際是畫在了最接近的像素點上。
因此,要使畫的波形與坐標軸對應,首先要確定需要畫多少個點,然后確定各個點的實際橫坐標。如果實際橫坐標完全按照算得的非整數來畫,則很有可能因為自動轉化為最接近像素點坐標(整數)而導致畫的點數與實際應畫點數不一致。比如說,我需要畫400個點,又得到我的SurfaceView坐標區域寬度為731pix,則400個點的間距應該是731/400=1.8275,如果按x = x + 1.8275這樣的坐標關系來畫這400個點,則點的實際坐標(像素點)為x = (取整)( x +1.8275),這時當x = 731時,實際點數小於400。
由此,我的方法是:首先確定得到這400個點橫坐標(浮點數),然后將這400個橫坐標四舍五入取整,得到整數后將其代入drawine等方法的參數進行畫圖。當然,畫完全所有點的首要條件是繪圖區域的像素點數一定要大於點數,否則就需要省略一些數據點。
5、SurfaceView清除畫布
采用以下語句清除畫布:
1 canvas = holder.lockCanvas(null); 2 canvas.drawColor(Color.TRANSPARENT,PorterDuff.Mode.CLEAR);// 清除畫布 3 holder.unlockCanvasAndPost(canvas);
6、SurfaceView畫圖殘影問題
在實際畫圖中,發現畫新的數據點的時候會出現以前數據的殘影,即使清除屏幕也沒用,經過研究與網上搜索發現是畫布全部鎖定刷新導致的畫圖效率問題,解決方法是每次畫圖前鎖定畫布時,不鎖定整個畫布,而只鎖定需要刷新的區域,對於畫波形來說,也就是下一個點所在的大概區域。
canvas = holder.lockCanvas(new Rect((int)oldX, 0, (int)X+3, MyView.Height));
以下是我繪圖線程run()方法的部分內容,繪制波形的數據為160個數據循環顯示,坐標軸為時間,長度8秒,數據每20毫秒畫一個,一共400個點。縱坐標直接用浮點數,Y=0的實際坐標根據比例計算。實際畫圖中不是畫的線而是畫的多邊形並填色。在這里Surface的寬度實際上就是坐標軸上0到8秒的距離,未加兩邊的空白部分,所以view的寬度就是坐標長度。
1 @Override 2 public void run() { 3 4 int[] dData = new int[160]; 5 float x = 0; 6 int X = 0;//真x,實際畫在圖上的x 7 int oldX = X;//真oldx,實際畫在圖上的oldx,上一個X的坐標 8 float[] y= new float[160];//上一個縱軸坐標 9 float y0 = (46*MyView.Height)/56f;//縱軸0點位置 10 int[] dFlag = new int[160]; 11 int j = 0; 12 13 //得到數據 14 for(int i = 0; i<160; i++) { 15 dData[i] = MainActivity.dummyData[i]; 16 } 17 18 //得到標志 19 for(int i = 0; i<160; i++) { 20 dFlag[i] = MainActivity.dummyFlag[i]; 21 } 22 23 //將數據轉換為實際點縱坐標 24 for(int i = 0; i<160; i++) { 25 y[i] = ((40 - dData[i])*41f*MyView.Height)/(40f*56f)+5*MyView.Height/56f; 26 } 27 28 while(isRun) { 29 Canvas c = null; 30 try { 31 synchronized (holder) { 32 33 if(x >= MyView.Width){ 34 // 清除畫布 35 //clear(); 36 c = holder.lockCanvas(new Rect((int)oldX, 0, (int)X+3, MyView.Height)); 37 c.drawColor(Color.TRANSPARENT,PorterDuff.Mode.CLEAR); 38 holder.unlockCanvasAndPost(c); 39 x = 0; 40 X = 0; 41 oldX = X; 42 } 43 else { 44 c = holder.lockCanvas(new Rect((int)oldX, 0, (int)X+3, MyView.Height)); 45 c.drawColor(Color.TRANSPARENT,PorterDuff.Mode.CLEAR);// 清除畫布 46 Paint p = new Paint(); 47 48 //根據標志設置不同顏色 49 if(dIeFlag[j] == 0) 50 p.setColor(Color.GREEN); 51 else 52 p.setColor(Color.YELLOW); 53 54 p.setStrokeWidth(3); 55 56 //第一個點 57 if(j == 0) { 58 j++; 59 } 60 //如果y=0則不畫多邊形而畫直線 61 if(dPress[j-1] == 0) { 62 c.drawLine(oldX, y[j-1], X, y[j++], p); 63 } 64 //如果y不等於0則畫多邊形填色 65 else { 66 p.setStyle(Paint.Style.FILL); 67 Path path = new Path(); 68 path.moveTo(oldX, y[j-1]); 69 path.lineTo(X, y[j]); 70 path.lineTo(X, y0); 71 path.lineTo(oldX, y0); 72 path.close(); 73 c.drawPath(path, p); 74 j++; 75 } 76 oldX = X; 77 if(j == 160) j = 0; 78 79 //需要按照坐標畫點,若不省略點,則一共需要畫:8秒/20毫秒=400個點 80 //首先算出精確的x坐標,是浮點數,然后四舍五入成整數畫在畫布上,保證點數正確 81 x = x + MyView.Width/400f; 82 X = Math.round(x); 83 holder.unlockCanvasAndPost(c); 84 } 85 Thread.sleep(20); 86 } 87 } catch (Exception e) { 88 // TODO: handle exception 89 e.printStackTrace(); 90 } 91 92 } 93 }
效果如下圖: