最近的一个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 }
效果如下图: