Android学习笔记(八) 2D绘图

初步了解

  在C#中已经用过不少回GID+来绘图了,当时对GDI+不熟悉,对Graphic,Brush,Pen,Bitmap这类对象很模糊,但现在分清了,转到Android上,Canvas的作用跟Graphic等效了,里面一样附带着各种绘制操作,位图Bitmap仍然是那样,画笔全浓缩成Paint了。Point,Color这些还是类似,认识了这点的话从绘制方面来说还能过渡了。

  那么Canvas跟Graphic的作用一样的话,就是能起到绘制的作用,所绘制内容包括:文本,图形,图片。那下面则用一个简单的例子来尝试绘制

  这回还是用自定义视图来实现

复制代码
 1 public class SampleView extends View {
 2 
 3     private Bitmap mbitmap1,mbitmap2,mbitmap3;
 4     private Shader mshader;
 5     
 6     public SampleView(Context context) {
 7         super(context);
 8         // TODO Auto-generated constructor stub
 9         setFocusable(true);
10         InputStream is=context.getResources().openRawResource(R.drawable.icon_weibo_24);
11         mbitmap1=BitmapFactory.decodeStream(is);
12         mbitmap2=mbitmap1.extractAlpha();
13         mbitmap3=Bitmap.createBitmap(300, 300, Config.ALPHA_8);
14         drawIntoBitmap(mbitmap3);
15         mshader=new LinearGradient(0,0,100,70,
16                 new int[]{Color.RED,Color.GREEN,Color.BLUE},
17                 null,Shader.TileMode.MIRROR);
18         
19     }
20     
21     private static void drawIntoBitmap(Bitmap bm)
22     {
23         float x=bm.getWidth();
24         float y=bm.getHeight();
25         //为传入来的图片创建画布
26         Canvas c=new Canvas(bm);
27         Paint p=new Paint();
28         
29         p.setAntiAlias(true);
30         
31         //画圆设置透明度
32         p.setAlpha(0x80);    
33         c.drawCircle(x/2,y/2,x/2,p);
34         
35         //绘制文字
36         p.setAlpha(0x30);
37         p.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
38         p.setTextSize(60);
39         p.setTextAlign(Paint.Align.CENTER);
40         Paint.FontMetrics fm=p.getFontMetrics();
41         c.drawText("Alpha",x/2,(y-fm.ascent)/2,p);
42         
43     }
44 
45     @Override
46     protected void onDraw(Canvas canvas)
47     {
48         canvas.drawColor(Color.WHITE);// 绘制背景
49         Paint p=new Paint();
50         float y=10;
51         p.setColor(Color.RED);
52         canvas.drawBitmap(mbitmap1,10,y,p); //绘制图片1
53         y+=mbitmap1.getHeight()+10;
54         canvas.drawBitmap(mbitmap2,10,y,p);//绘制图片2
55         y+=mbitmap2.getHeight()+10;
56         p.setShader(mshader);//设置画笔 使用渐变色
57         canvas.drawBitmap(mbitmap3,10,y,p);//绘制图片3
58     }
59 }
复制代码

 从代码大致感觉得到,绘制图形,文字,图片这些方法都比较一目了然

这里列举一部分图形的,文字的,还有图片的

复制代码
void drawARGB(int a, int r, int g, int b)                // 将整体填充为某种颜色 
void drawColor(int color)                // 将整体填充为某种颜色 
void drawPoints(float[] pts, Paint paint)               // 绘制一个点 
void drawLines(float[] pts, Paint paint)             // 绘制一条线 
void drawRect(RectF rect, Paint paint)               // 绘制矩形 
void drawCircle(float cx, float cy, float radius, Paint paint)  // 绘制圆形 
void drawArc(RectF oval, float startAngle, float sweepAngle,               
        boolean useCenter, Paint paint)         // 绘制圆弧 

void drawText(String text, int start, int end, float x, float y, Paint paint)  
void drawText(char[] text, int index, int count, float x, float y, Paint paint)  
void drawText(String text, float x, float y, Paint paint)  
void drawText(CharSequence text, int start, int end, float x, float y, Paint paint) 

void drawBitmap(Bitmap bitmap, Matrix matrix, Paint paint) // 指定 Matrix 绘制位图 
void drawBitmap(int[] colors, int offset, int stride,           
            float x, float y, int width, int height,                  
            boolean hasAlpha, Paint paint)   // 指定数组作为 Bitmap 绘制       
void drawBitmap(Bitmap bitmap, Rect src, RectF dst, Paint paint)// 自动缩放到目标矩形的绘制
复制代码

  在活动的类里面就很简单了

 

复制代码
1 public class TestDraw_1 extends Activity {
2 
3     @Override
4     protected void onCreate(Bundle savedInstanceState)
5     {
6         super.onCreate(savedInstanceState);
7         setContentView(new SampleView(this));
8     }
9 }
复制代码

只是把刚刚定义的视图设置进去,按照以上面的形式来试着接下来的尝试。

 

文字对齐

在绘制文字当中,通过Paint的setTextAlign来设置文字的对齐方式,那么先看看下面这段放在onDraw里面的方法

复制代码
 1          canvas.drawLine(x, y, x, y+DY*3, mpaint);
 2          mpaint.setColor(Color.BLACK);             
 3          canvas.translate(0, DY);              
 4          mpaint.setTextAlign(Paint.Align.LEFT);      // 绘制左对齐的文本             
 5          canvas.drawText("LEFT", x, y, mpaint);             
 6          canvas.translate(0, DY);                         
 7          mpaint.setTextAlign(Paint.Align.CENTER);    // 绘制中对齐的文本             
 8          canvas.drawText("CENTER", x, y, mpaint);             
 9          canvas.translate(0, DY);                         
10          mpaint.setTextAlign(Paint.Align.RIGHT);     // 绘制右对齐的文本             
11          canvas.drawText("RIGHT", x, y, mpaint);
12             
复制代码

 

  单纯看上去好像挺不对劲的,左对齐应该在左边,右对齐应该在右边,但这里刚好相反了,如果有留意到中间那条辅助线就明白了,她是相对于输入的坐标(x,y)靠左对齐以及靠右对齐。

  其实对于绘制文字来说不仅单单drawText这一个方法,还有drawPosText和drawTextOnPath两种方法,drawPosText是以文字本身的宽度来顶一个位置,按照那个位置来对齐,那么看看下面这段代码

复制代码
 1          canvas.translate(0, DY*2);              
 2          mpaint.setColor(0xBB00FF00);             
 3          for (int i = 0; i < pos.length/2; i++) {                 
 4              canvas.drawLine(pos[i*2+0], pos[i*2+1]-DY,                                 
 5                      pos[i*2+0], pos[i*2+1]+DY*2, mpaint);             
 6          }   
 7          
 8          mpaint.setColor(Color.BLACK);             
 9          mpaint.setTextAlign(Paint.Align.LEFT);                 
10          canvas.drawPosText(sContext, pos, mpaint); // 绘制左对齐的文本            
11          canvas.translate(0, DY);             
12          mpaint.setTextAlign(Paint.Align.CENTER);               
13          canvas.drawPosText(sContext, pos, mpaint);  // 绘制中对齐的文本            
14          canvas.translate(0, DY);             
15          mpaint.setTextAlign(Paint.Align.RIGHT);                
16          canvas.drawPosText(sContext, pos, mpaint);  // 绘制右对齐的文本
复制代码

  drawPosText方法则需要传入一个float的数组,这个数组就存放着字符串之中各个字符的定位坐标,这个数组存放的要求如下,它长度是字符串长度的两倍,每两个坐标代表着一个字符的坐标,比如0和1两个元素就代表着第一个字符的x和y坐标,如此类推,在本例中它需要计算出来的,计算的方法如下

复制代码
 1     private float[] buildTextPositions(String text, float y, Paint paint) {
 2         
 3             float[] widths = new float[text.length()];
 4             int n = paint.getTextWidths(text, widths);
 5             float[] pos = new float[n * 2];
 6             float accumulatedX = 0;
 7             for (int i = 0; i < n; i++) {
 8                 pos[i*2 + 0] = accumulatedX;
 9                 pos[i*2 + 1] = y;
10                 accumulatedX += widths[i];
11             }
12             return pos;
13         }
复制代码

至于对齐方式,则按照给定的坐标来靠左或靠右

剩下的drawTextOnPath方法则是按照给定的路径来描绘字符

复制代码
 1 // 绘制在路径上的的字串            
 2          Paint mPathPaint = new Paint();
 3          mPathPaint = new Paint(); 
 4          mPathPaint.setAntiAlias(true);             
 5          mPathPaint.setColor(0x800000FF);             
 6          mPathPaint.setStyle(Paint.Style.STROKE); 
 7          
 8          mpaint.setTextSize(30);
 9          canvas.translate(-100, DY*2);        // 重定画布的位置            
10          canvas.drawPath(mpath, mPathPaint);             
11          mpaint.setTextAlign(Paint.Align.LEFT);                
12          canvas.drawTextOnPath(sContext+" LEFT", mpath, 0, 0, mpaint);  // 绘制对齐路径的文本            
13          canvas.translate(0, DY*1.5f);             
14          canvas.drawPath(mpath, mPathPaint);             
15          mpaint.setTextAlign(Paint.Align.CENTER);             
16          canvas.drawTextOnPath(sContext+" Center", mpath, 0, 0, mpaint);  // 绘制对齐路径的文本             
17          canvas.translate(0, DY*1.5f);  
18          canvas.drawPath(mpath, mPathPaint);             
19          mpaint.setTextAlign(Paint.Align.RIGHT);             
20          canvas.drawTextOnPath(sContext+ "RIGHT", mpath, 0, 0, mpaint);  // 绘制对齐路径的文本
复制代码

那么路径通过下面这个方法来确定

1     private void MakePath(Path path)
2     {
3         path.moveTo(10, 0);
4         path.cubicTo(100, -50, 200, 50, 300, 0);
5     }

这种绘制的对齐方式最符合我们平时的对对齐这个概念的认识。

 

路径绘制

  其实在上面例子中也用到了路径绘制,画出了一条条用于参考的曲线,当然那是最简单的路径绘制,利用PathEffect类可以给路径增加各种样式,不过PathEffect只是一个基类,它的子类就可以让路径有不同的效果,只要通过paint的setPathEffect方法则可

  现在构造函数里面对画笔、路径和颜色都进行设置

复制代码
 1         paint = new Paint();  
 2         paint.setStyle(Paint.Style.STROKE);  
 3         paint.setStrokeWidth(4);  
 4         //创建,并初始化Path  
 5         path = new Path();  
 6         path.moveTo(0, 0);  
 7         for(int i = 1; i<= 15; i++)  
 8         {  
 9             //生成15个点,随机生成它们的坐标,并将它们连成一条Path  
10             path.lineTo(i*20, (float)Math.random()*60);  
11         }  
12         //初始化七个颜色  
13         colors = new int[] {  
14                 Color.BLACK,Color.BLUE,Color.CYAN,  
15                 Color.GREEN,Color.MAGENTA,Color.RED,Color.YELLOW  
16         };  
复制代码

 

重绘方法里面就可以对不同的PathEffect进行设置,最后描绘出来

复制代码
 1         //将背景填充成白色  
 2         canvas.drawColor(Color.WHITE);  
 3         //-------下面开始初始化7中路径的效果  
 4         //使用路径效果  
 5         effects[0] = null;  
 6         //使用CornerPathEffect路径效果  
 7         effects[1] = new CornerPathEffect(10);  
 8         //初始化DiscretePathEffect  
 9         effects[2] = new DiscretePathEffect(3.0f,5.0f);  
10         //初始化DashPathEffect  
11         effects[3] = new DashPathEffect(new float[]{20,10,5,10},phase);  
12         //初始化PathDashPathEffect  
13         Path p = new Path();  
14         p.addRect(0, 0, 8, 8, Path.Direction.CCW);  
15         effects[4] = new PathDashPathEffect(p,12,phase,PathDashPathEffect.Style.ROTATE);  
16         //初始化PathDashPathEffect  
17         effects[5] = new ComposePathEffect(effects[2],effects[4]);  
18         effects[6] = new SumPathEffect(effects[4],effects[3]);  
19         //将画布移到8,8处开始绘制  
20         canvas.translate(8, 8);  
21         //依次使用7中不同路径效果,7种不同的颜色来绘制路径  
22         for(int i = 0; i < effects.length; i++)  
23         {  
24             paint.setPathEffect(effects[i]);  
25             paint.setColor(colors[i]);  
26             canvas.drawPath(path, paint);  
27             canvas.translate(0, 60);  
28         }  
29         //改变phase值,形成动画效果  
30         phase += 1;  
31         invalidate();
复制代码

 

 

裁剪效果

  Canvas能对画布区域进行裁剪,调用的方法主要是clipPath和clipRect,具体重载的会在代码例子中看出来,在经过一系列尝试之后,感觉这个裁剪是从画布中挖出一部分指定的区域填充上要填充的内容,至于挖出的区域是怎么样的,要靠代码去控制。

首先上来的是要画图的方法

复制代码
 1      private void drawScene(Canvas canvas)
 2      {
 3          canvas.clipRect(0, 0, 100, 100); 
 4          
 5          canvas.drawColor(Color.WHITE);
 6          mPaint.setColor(Color.RED);
 7         canvas.drawLine(0, 0, 100, 100, mPaint); 
 8         mPaint.setColor(Color.GREEN);
 9         canvas.drawCircle(30, 70, 30, mPaint);
10         mPaint.setColor(Color.BLUE);
11         canvas.drawText("Clipping", 100, 30, mPaint);
12      }
复制代码

传入一个画布,然后画上线,图形和字。其实这里也用到了clip的方法了。

那么先看一下正常的绘图的

1          canvas.translate(10, 10);
2          drawScene(canvas);
3          canvas.restore();
4          canvas.save();

 

  这里相当于在画布往下往右个评议10个像素后,在原点处才拣出一个100*100的矩形来填充。绘图完毕后调用了restore和save方法,这两个方法有必要说一下,这两个方法都会在onDraw这个绘制的方法里面经常出现,restore用来恢复Canvas之前保存的状态,防止save后对Canvas执行的操作对后续的绘制有影响。save:用来保存Canvas的状态。save之后,可以调用Canvas的平移、放缩、旋转、错切、裁剪等操作。save和restore要配对使用(restore可以比save少,但不能多),如果restore调用次数比save多,就会抛异常。

那么再看看别的情况

1          canvas.translate(160, 10);
2          canvas.clipRect(10,10,90,90);  //在这部分矩形上描绘
3          canvas.clipRect(30,30,70,70,Region.Op.DIFFERENCE); //这部矩形去除
4          drawScene(canvas);
5          canvas.restore();
6          canvas.save();

 

  由于上面对画布的操作重置了,所以现在平移是从新的屏幕原点去平移,对先前描绘的图形没有影响,这里是画了一个矩形,80*80的,然后再画了另一个矩形,40*40的,刚好位于先前画的矩形的中心,这里还多传了一个参数Region.Op.DIFFERENCE(差),意思是画的这个矩形是从先前的矩形中挖去那部分,那么这两次裁剪的操作合并起来,成的图就是一个“回”字形

 

1          canvas.translate(10, 160);
2          mPath.reset();
3          canvas.clipPath(mPath); // 做一个圆             
4          mPath.addCircle(50, 50, 50, Path.Direction.CCW);             
5          canvas.clipPath(mPath, Region.Op.REPLACE); // 圆外部分被去除            
6          drawScene(canvas);             
7          canvas.restore();             
8          canvas.save();

  这里先不管原本画出了什么样的区域,后来画出了一个圆,而且传入了Region.Op.REPLACE这个参数,就相当于这个圆替代了之前裁剪的区域,图案将会画到这个新区域上

 

1          canvas.translate(160, 160); 
2          canvas.clipRect(0, 0, 60, 60);             
3          canvas.clipRect(40, 40, 100, 100, Region.Op.UNION);        //与前面区域联合     
4          drawScene(canvas);             
5          canvas.restore();             
6          canvas.save();

  这里先在左上方裁剪一个60的正方形,然后在右下角裁一个边长同样为60的正方形,如果不混合图层(也就是传入Region.Op.UNION的话)得出的结果将是两个正方形相交的那部分, 那么这里用的是UNION(和),就是将之前裁剪的区域合并起来,就得出以下的图形

 

1          canvas.translate(10, 310);             
2          canvas.clipRect(0, 0, 60, 60);             
3          canvas.clipRect(40, 40, 100, 100, Region.Op.XOR);        //     相同为0,相异为1,重合为空,不重合不为空
4          drawScene(canvas);             
5          canvas.restore();             
6          canvas.save();

 

  这段代码与上面的很像,唯一的区别在于混合图层的方式不一样,这里用了XOR(异或),也就是不同的为1相同的为0,那么在上面裁剪的区域中,两个正方形描绘到的和相交的都是不保留的,保留的就下面图片的部分

 

1          canvas.translate(160, 310);             
2          canvas.clipRect(0, 0, 60, 60);             
3          canvas.clipRect(40, 40, 100, 100, Region.Op.REVERSE_DIFFERENCE);       //求差再将当前取反      
4          drawScene(canvas);             
5          canvas.restore(); 
6          canvas.save();

 

  还是裁剪同样的矩形,但是这里用的是REVERSE_DIFFERENCE(保留差异),个人理解是在之前的裁剪区域的基础上对当前的图形去掉公共部分,余下当前图形与之前所有图形没相交的部分。

 

记录绘制过程

绘制过程这里主要涉及到两个类,一个是Drawable,另一个是Picture。

  • Drawable就是一个可画的对象,其可能是一张位图(BitmapDrawable), 也可能是一个图形(ShapeDrawable),还有可能是一个图层(LayerDrawable),我们根据画图的需求,创建相应的可画对象,就可以将这个可画对象当作一块“画布(Canvas)”,在其上面操作可画对象,并最终将这种可画对象显示在画布上,有点类似于“内存画布“。
  • Picture在android.graphics.Picture包中,相对于Drawable和Bitmap而言,Picture对象就小巧的多,它并不存储实际的像素,仅仅记录了每个绘制的过程。整个类提供了两个重载形式,其中比较特别的是Picture(Picture src) 从一个Picture对象去实例化操作,我们可以看到整个类中方法不多,但主要的beginRecording和endRecording记录开始和结束,最终通过draw方法绘制到画布上去,而createFromStream和writeToStream可以帮助我们从流中读写这个Picture对象,我们可以将整个绘制过程通过FileInputStream或FileOutputStream放到文件中去。它位于android.graphics.drawable.PictureDrawable 是从Drawable类继承而来的,它的构造方法只有一个就是从Picture对象中实例化。

Pictue可以说是一个绘图的内容,它记录着绘图的步骤,也可以说是图像中有什么内容了,但是Drawable则是一个给图像的载体,在现实中可以这个图像的载体可以使纸张,布片,墙壁或者屏幕,回到程序中,Drawable则是一个位于内存的载体。

下面则看看一个例子

复制代码
 1     Picture mpicture ;
 2     Drawable mdrawable;
 3     
 4     public DrawableTestView(Context context) {
 5         super(context);
 6         // TODO Auto-generated constructor stub
 7         setFocusable(true);
 8         mpicture=new Picture();
 9         DrawShape(this.mpicture.beginRecording(200, 100));
10         this.mpicture.endRecording();
11         this.mdrawable=new PictureDrawable(this.mpicture);
12     }
13 
14     private void DrawShape(Canvas canvas)
15     {
16         Paint p=new Paint();
17         p.setColor(Color.RED);
18         canvas.drawCircle(60, 60, 50, p);
19         p.setColor(Color.BLUE);
20         canvas.drawCircle(60, 60, 40, p);
21         p.setColor(Color.BLACK);
22         p.setTextSize(30);
23         canvas.drawText("Picture", 110, 50, p);
24     }
25     
26     
27     @Override
28     protected void onDraw(Canvas canvas)
29     {
30         
31         canvas.drawPicture(mpicture);
32         canvas.drawPicture(mpicture, new Rect(0, 110, getWidth(), 200));
33 
34         mdrawable.setBounds(0, 200, getWidth(), 360);
35         mdrawable.draw(canvas);
36         
37          ByteArrayOutputStream os = new ByteArrayOutputStream();             
38          mpicture.writeToStream(os); 
39          InputStream is = new ByteArrayInputStream(os.toByteArray()); 
40          canvas.translate(0, 300);
41          canvas.drawPicture(Picture.createFromStream(is));
42     }
复制代码

在运行结果中看不出什么,但是对于程序内部而言差别在于一次绘制完毕和多次分部分绘制。

 

动画效果

  这里介绍的动画只是局限在帧动画,就类似于gif那样已一定的时间间隔快速切换一组连续的图片实现动画的效果。

制作这种帧动画主要用到AnimationDrawable这个类,通过以下代码可以获取到一个实例

frameAnim=(AnimationDrawable) getResources().getDrawable(R.drawable.bullet_anim);

 

可以把图片用重绘界面的形式描绘出来,但是那种方式现在我实现不了,取而代之的是用过一个ImageView设置它的BackgroundDrawable属性来实现,

imgView.setBackgroundDrawable(frameAnim);

Drawable的资源可以通过下面这种形式,在drawable文件夹中放置那一组连续的图片,然后添加一个xml文件来描述各个图片以及切换的时间间隔

1 <?xml version="1.0" encoding="utf-8"?>
2 <animation-list xmlns:android="http://schemas.android.com/apk/res/android" android:oneshot="false"  >
3     <!-- 定义一个动画帧,Drawable为img0,持续时间50毫秒 -->
4  <item android:drawable="@drawable/img0" android:duration="50" />
5 <item android:drawable="@drawable/img1" android:duration="50" />
6 </animation-list>

AnimationDrawable的实例中有start()方法和stop()方法,来控制动画的播放与停止,通过isRunning()方法来判断动画的状态。

整个类的声明如下

复制代码
 1 public class AnimateTestView extends LinearLayout {
 2 
 3     private AnimationDrawable frameAnim;
 4     private Button btnStart,btnEnd;
 5     private ImageView imgView;
 6     
 7     public AnimateTestView(Context context) {
 8         super(context);
 9         // TODO Auto-generated constructor stub
10         this.setFocusable(true);
11         frameAnim=(AnimationDrawable) getResources().getDrawable(R.drawable.bullet_anim);
12         
13         
14         btnStart=new Button(context);
15         btnStart.setText("Start");
16         
17         btnEnd=new Button(context);
18         btnEnd.setText("End");
19         
20         imgView=new ImageView(context);
21         imgView.setImageDrawable(frameAnim);    
22         
23         this.setOrientation( LinearLayout.VERTICAL);
24         
25         btnStart.setOnClickListener(new OnClickListener() {
26             
27             @Override
28             public void onClick(View arg0) {
29                 // TODO Auto-generated method stub
30                 start();
31             }
32         });
33         
34         btnEnd.setOnClickListener(new OnClickListener() {
35             
36             @Override
37             public void onClick(View arg0) {
38                 // TODO Auto-generated method stub
39                 stop();
40             }
41         });
42         
43         this.addView(imgView);
44         this.addView(btnStart);
45         this.addView(btnEnd);
46     }
47 
48     // 开始播放
49     protected void start() {
50         if (frameAnim != null && !frameAnim.isRunning()) {
51             frameAnim.start();
52             Toast.makeText(this.getContext(), "开始播放", 0).show();
53         }
54     }
55 
56     // 停止播放
57     protected void stop() {
58         if (frameAnim != null && frameAnim.isRunning()) {
59             frameAnim.stop();
60             Toast.makeText(this.getContext(), "停止播放", 0).show();
61         }
62     }
复制代码

 

 

  不过这上面的例子举得的局限性好大,希望日后的开发能把对绘图的认识丰富起来。

 

 

posted @   猴健居士  阅读(615)  评论(0编辑  收藏  举报
努力加载评论中...
点击右上角即可分享
微信分享提示