前言
除了使用已有的圖片之外,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 }
效果展示(隨手塗鴉了個笑臉,發現還是蠻有喜感的,大家將就着看看吧):