前言
除了使用已有的图片之外,Android应用常常需要在运行时根据场景动态生成2D图片,比如手机游戏,这就需要借助于Android2D绘图的支持。本篇博客主要讲解一下Android下使用Canvas进行绘图的相关操作。最后将以一个简单的Demo演示如何使用Canvas在ImageView上画图并保存。
本篇博客的主要内容:
在Android下进行2D绘图需要Canvas类的支持,它位于"android.graphics.Canvas"包下,直译过来为画布的意思,用于完成在View上的绘图。
Canvas为提供了两个构造函数:
- Canvas():创建一个空的Canvas对象。
- Canvas(Bitmap bitmap):创建一个以bitmap位图为背景的Canvas。
既然Canvas主要用于2D绘图,那么它也提供了很多相应的drawXxx()方法,方便我们在Canvas对象上画画,drawXxx()具有多种类型,可以画出:点、线、矩形、圆形、椭圆、文字、位图等的图形,这里就不再一一介绍了,只介绍几个Canvas中常用的方法:
- void drawBitmap(Bitmap bitmap,float left,float top,Paint paint):在指定坐标绘制位图。
- void drawLine(float startX,float startY,float stopX,float stopY,Paint paint):根据给定的起始点和结束点之间绘制连线。
- void drawPath(Path path,Paint paint):根据给定的path,绘制连线。
- void drawPoint(float x,float y,Paint paint):根据给定的坐标,绘制点。
- void drawText(String text,int start,int end,Paint paint):根据给定的坐标,绘制文字。
- int getHeight():得到Canvas的高度。
- int getWidth():得到Canvas的宽度。
从上面列举的几个Canvas.drawXxx()的方法看到,其中都有一个类型为paint的参数,可以把它理解为一个"画笔",通过这个画笔,在Canvas这张画布上作画。 它位于"android.graphics.Paint"包下,主要用于设置绘图风格,包括画笔颜色、画笔粗细、填充风格等。
Paint中提供了大量设置绘图风格的方法,这里仅列出一些常用的,高级的内容有时间再详细讲解:
- setARGB(int a,int r,int g,int b):设置ARGB颜色。
- setColor(int color):设置颜色。
- setAlpha(int a):设置透明度。
- setPathEffect(PathEffect effect):设置绘制路径时的路径效果。
- setShader(Shader shader):设置Paint的填充效果。
- setAntiAlias(boolean aa):设置是否抗锯齿。
- setStrokeWidth(float width):设置Paint的笔触宽度。
- setStyle(Paint.Style style):设置Paint的填充风格。
- setTextSize(float textSize):设置绘制文本时的文字大小。
既然已经简单讲解了Android下2D绘图的两个重要类,Canvas和Paint,那么下面通过一个简单的Demo来演示一下,这样加深大家的理解。
在这个Demo中,将实现一个画图板的功能,当用户在触摸屏上移动的时候,即可在屏幕上绘制任意的图形。实现手绘功能其实是一种假象:表面上看起来可以随着用户在触摸屏上自由地画线,实际上依然利用的是Canvas的drawLine()方法画直线,每条直线都是从上一个移动事件发生点画到本次移动事件的的发生点。当用户在触摸屏上连续移动的时候,每次移动点之间的距离很小,多此极短的连线,肉眼看起来就是一个依照手指触摸移动的轨迹的连线。
需要指出的是,如果程序每次都只是从上次移动事件的发生点绘制一条直线到本次拖动事件的发生点,那么当用户手指一旦离开触摸屏,再次引发触摸移动事件的时候,会导致前面绘制的内容被丢失。为了保留用户之前绘制的内容,程序需要借助于一个"双缓冲"的机制。
之前讲解SurfaceView的时候,有讲到SurfaceView会自己维护一个双缓冲的缓冲区,但是在这里使用ImageView来展示绘图效果,它需要我们去维护双缓冲的机制。当用户在ImageView上进行"绘制"的时候,程序并不直接"绘制"到该ImageView组件上,而是先绘制到一个内存中的Bitmap对象(缓冲)上,等到内存中的Bitmap绘制好之后,再一次性的将Bitmap对象"绘制"到ImageView上。
在这个Demo中,会监听ImageView的View.OnTouchListener事件的发生,它主要用户监听在View上的触摸事件。其中需要重写onTouch()方法,当用户触摸View的时候会调用这个方法,以下是它的完整签名:
boolean onTouch(View v,MotionEvent event)
它的返回值用于指定是否连续捕获触摸事件,而在它的参数中,View为当前引发触摸事件的View,而MotionEvent是当前引发触摸事件一些属性,这个类中定义了一系列的静态常量,用于表示当前触摸的动作,比如:
- MotionEvent.ACTION_DOWN:手指触摸屏幕。
- MotionEvent.ACTION_MOVE:手指移动。
- MotionEvent.ACTION_UP:手指离开屏幕。
Demo中的主要内容已经讲解清楚,下面直接上代码了,代码中注释比较详细,就不再赘述了。
布局代码:activity_main.xml
1 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 2 xmlns:tools="http://schemas.android.com/tools" 3 android:layout_width="match_parent" 4 android:layout_height="match_parent" 5 android:orientation="vertical" 6 android:paddingBottom="@dimen/activity_vertical_margin" 7 android:paddingLeft="@dimen/activity_horizontal_margin" 8 android:paddingRight="@dimen/activity_horizontal_margin" 9 android:paddingTop="@dimen/activity_vertical_margin" 10 tools:context=".MainActivity" > 11 12 <LinearLayout 13 android:layout_width="match_parent" 14 android:layout_height="wrap_content" 15 android:orientation="horizontal" > 16 17 <Button 18 android:id="@+id/btn_resume" 19 android:layout_width="wrap_content" 20 android:layout_height="wrap_content" 21 android:text="重新画图" /> 22 23 <Button 24 android:id="@+id/btn_save" 25 android:layout_width="match_parent" 26 android:layout_height="wrap_content" 27 android:text="保存图片" /> 28 </LinearLayout> 29 30 <ImageView 31 android:id="@+id/iv_canvas" 32 android:layout_width="match_parent" 33 android:layout_height="match_parent" /> 34 35 </LinearLayout>
实现代码:MainActivity.java
1 package cn.bgtx.canvasdemo; 2 3 import java.io.File; 4 import java.io.FileOutputStream; 5 import android.net.Uri; 6 import android.os.Bundle; 7 import android.os.Environment; 8 import android.app.Activity; 9 import android.content.Intent; 10 import android.graphics.Bitmap; 11 import android.graphics.Bitmap.CompressFormat; 12 import android.graphics.Canvas; 13 import android.graphics.Color; 14 import android.graphics.Paint; 15 import android.view.MotionEvent; 16 import android.view.View; 17 import android.view.View.OnClickListener; 18 import android.view.View.OnTouchListener; 19 import android.widget.Button; 20 import android.widget.ImageView; 21 import android.widget.Toast; 22 23 public class MainActivity extends Activity { 24 private Button btn_save, btn_resume; 25 private ImageView iv_canvas; 26 private Bitmap baseBitmap; 27 private Canvas canvas; 28 private Paint paint; 29 30 @Override 31 protected void onCreate(Bundle savedInstanceState) { 32 super.onCreate(savedInstanceState); 33 setContentView(R.layout.activity_main); 34 35 // 初始化一个画笔,笔触宽度为5,颜色为红色 36 paint = new Paint(); 37 paint.setStrokeWidth(5); 38 paint.setColor(Color.RED); 39 40 iv_canvas = (ImageView) findViewById(R.id.iv_canvas); 41 btn_save = (Button) findViewById(R.id.btn_save); 42 btn_resume = (Button) findViewById(R.id.btn_resume); 43 44 btn_save.setOnClickListener(click); 45 btn_resume.setOnClickListener(click); 46 iv_canvas.setOnTouchListener(touch); 47 } 48 49 private View.OnTouchListener touch = new OnTouchListener() { 50 // 定义手指开始触摸的坐标 51 float startX; 52 float startY; 53 54 @Override 55 public boolean onTouch(View v, MotionEvent event) { 56 switch (event.getAction()) { 57 // 用户按下动作 58 case MotionEvent.ACTION_DOWN: 59 // 第一次绘图初始化内存图片,指定背景为白色 60 if (baseBitmap == null) { 61 baseBitmap = Bitmap.createBitmap(iv_canvas.getWidth(), 62 iv_canvas.getHeight(), Bitmap.Config.ARGB_8888); 63 canvas = new Canvas(baseBitmap); 64 canvas.drawColor(Color.WHITE); 65 } 66 // 记录开始触摸的点的坐标 67 startX = event.getX(); 68 startY = event.getY(); 69 break; 70 // 用户手指在屏幕上移动的动作 71 case MotionEvent.ACTION_MOVE: 72 // 记录移动位置的点的坐标 73 float stopX = event.getX(); 74 float stopY = event.getY(); 75 76 //根据两点坐标,绘制连线 77 canvas.drawLine(startX, startY, stopX, stopY, paint); 78 79 // 更新开始点的位置 80 startX = event.getX(); 81 startY = event.getY(); 82 83 // 把图片展示到ImageView中 84 iv_canvas.setImageBitmap(baseBitmap); 85 break; 86 case MotionEvent.ACTION_UP: 87 88 break; 89 default: 90 break; 91 } 92 return true; 93 } 94 }; 95 private View.OnClickListener click = new OnClickListener() { 96 97 @Override 98 public void onClick(View v) { 99 100 switch (v.getId()) { 101 case R.id.btn_save: 102 saveBitmap(); 103 break; 104 case R.id.btn_resume: 105 resumeCanvas(); 106 break; 107 default: 108 break; 109 } 110 } 111 }; 112 113 /** 114 * 保存图片到SD卡上 115 */ 116 protected void saveBitmap() { 117 try { 118 // 保存图片到SD卡上 119 File file = new File(Environment.getExternalStorageDirectory(), 120 System.currentTimeMillis() + ".png"); 121 FileOutputStream stream = new FileOutputStream(file); 122 baseBitmap.compress(CompressFormat.PNG, 100, stream); 123 Toast.makeText(MainActivity.this, "保存图片成功", 0).show(); 124 125 // Android设备Gallery应用只会在启动的时候扫描系统文件夹 126 // 这里模拟一个媒体装载的广播,用于使保存的图片可以在Gallery中查看 127 Intent intent = new Intent(); 128 intent.setAction(Intent.ACTION_MEDIA_MOUNTED); 129 intent.setData(Uri.fromFile(Environment 130 .getExternalStorageDirectory())); 131 sendBroadcast(intent); 132 } catch (Exception e) { 133 Toast.makeText(MainActivity.this, "保存图片失败", 0).show(); 134 e.printStackTrace(); 135 } 136 } 137 138 /** 139 * 清除画板 140 */ 141 protected void resumeCanvas() { 142 // 手动清除画板的绘图,重新创建一个画板 143 if (baseBitmap != null) { 144 baseBitmap = Bitmap.createBitmap(iv_canvas.getWidth(), 145 iv_canvas.getHeight(), Bitmap.Config.ARGB_8888); 146 canvas = new Canvas(baseBitmap); 147 canvas.drawColor(Color.WHITE); 148 iv_canvas.setImageBitmap(baseBitmap); 149 Toast.makeText(MainActivity.this, "清除画板成功,可以重新开始绘图", 0).show(); 150 } 151 } 152 }
效果展示(随手涂鸦了个笑脸,发现还是蛮有喜感的,大家将就着看看吧):