| 操作 | API 類 | 備注 |
|---|---|---|
| 繪制圖片 | drawBitmap | -- |
| 錄制繪制過程 | Picture | -- |
一、繪制圖片
| API | 備注 |
|---|---|
| drawBitmap(Bitmap bitmap, Matrix matrix, Paint paint) | 根據(jù)特定的 Matrix 進行繪制 |
| drawBitmap(int[] colors, int offset, int stride, float x, float y, int width, int height, boolean hasAlpha, Paint paint) | API 21廢棄 |
| drawBitmap(int[] colors, int offset, int stride, int x, int y, int width, int height, boolean hasAlpha, Paint paint) | API 21廢棄 |
| drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint) | 將指定區(qū)域的內(nèi)容利用移動或者縮放的方式填充指定繪制區(qū)域 |
| drawBitmap(Bitmap bitmap, RectF src, Rect dst, Paint paint) | 將指定區(qū)域的內(nèi)容利用移動或者縮放的方式填充指定繪制區(qū)域 |
| drawBitmap(Bitmap bitmap, float left, float top, Paint paint) | 以 (left, top) 為左上角,繪制圖片 |
| drawBitmapMesh(Bitmap bitmap, int meshWidth, int meshHeight, float[] verts, int vertOffset, int[] colors, int colorOffset, Paint paint) | 圖片扭曲形變 |
在自定義 View 時,難免會遇到難以繪制的圖標(biāo)和背景,這個時候,我們就需要用到繪制圖片。在 android 中,當(dāng)我們使用到圖片的時候,通常會使用到兩個類:Drawable 和 Bitmap。
這兩個類在開發(fā)中用的不少,想必都已經(jīng)很熟悉了。由于繪制方法是用的 Bitmap ,這里只講獲取 Bitmap 的方法。
就通常而言,獲取 Bitmap 對象有兩種方法
- 1.利用 Bitmap 構(gòu)造器獲取,這種方式獲取只能復(fù)制位圖或者新建位圖
- 2.利用 BitmapFactory 獲取,這種方式可以根據(jù)傳入的參數(shù)返回指定的位圖
由于圖片資源的位置不同,獲取相應(yīng)位圖的方法也會不同,但是基本只要使用下面的兩個方法,就可以應(yīng)對大部分的情況:
BitmapFactory.decodeResource() //獲取 drawable 文件夾下資源文件
BitmapFactory.decodeStream() //將指定路徑的文件轉(zhuǎn)化為 IO 流后,獲取指定位圖
拋開廢棄方法不看,我們發(fā)現(xiàn)實際上,繪制 bitmap 的方法有四個:
drawBitmap(Bitmap bitmap, Matrix matrix, Paint paint)
drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint)
drawBitmap(Bitmap bitmap, float left, float top, Paint paint)
drawBitmapMesh(Bitmap bitmap, int meshWidth, int meshHeight, float[] verts, int vertOffset, int[] colors, int colorOffset, Paint paint)
1)其中第一個方法中,根據(jù) Matrix 來繪制圖片,這里涉及到 Matrix 的使用,有興趣的可以自己了解一下。這里不展開講了。
2)第二個方法中間兩個參數(shù):
- src 源視圖的顯示部分
- dst 畫布上允許的繪制區(qū)域
演示代碼:
Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.alu);
Rect src = new Rect(0, 0, 200, 200);
Rect dst = new Rect(0, 0, 200, 150);
canvas.drawBitmap(bitmap,src,dst,mPaint);

其中右邊為原圖,左邊為繪制的圖片。比較后,可以看出,這個方法,將原圖 200200 區(qū)域的圖像,經(jīng)過變形繪制在 200150 的畫布上。
3)第三個方法可以讓我們控制繪制圖像所在在的畫布位置
Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.alu);
canvas.drawBitmap(bitmap,400,400,mPaint);

4)第四個方法主要用于圖像的扭曲
參數(shù)說明:
- bitmap:指定需要扭曲的源位圖;
- meshWidth:該參數(shù)控制在橫向上把該源位圖劃分成多少格;
- meshHeight:該參數(shù)控制在縱向上把該源位圖劃分成多少格;
- verts:該參數(shù)是一個長度為 (meshWidth+1) * (meshHeight+1) * 2 的數(shù)組,它記錄了扭曲后的位圖各“頂點”位置。雖然它是個數(shù)組,實際上它記錄的數(shù)據(jù)是形如 (x0,y0)、(x1,y1)、(x2,y2)....(Nx,Ny) 格式的數(shù)據(jù),這些數(shù)組元素控制對bitmap位圖的扭曲效果;
- vertOffset:控制verts數(shù)組中從第幾個數(shù)組元素開始才對bitmap進行扭曲。
從方法參數(shù)中,可以看到方法會根據(jù)參數(shù)將圖片用網(wǎng)格分割。
這里我們用了一張帶有 20 * 20 網(wǎng)格的圖片做例子:

圖片分割為 20 * 20 個方格,這每個方格成為一個拉伸單元。方法中會計算出這個圖片中,所有交點的原始坐標(biāo)組 origins,當(dāng)你傳入了改變的坐標(biāo)數(shù)組 verts 時,它會將 origins 對應(yīng)坐標(biāo)圍成的單元逐個進行拉伸,變換為計算后的樣子。比如,這里我隨便點了一下。

大致原理是這樣,分的網(wǎng)格越多,形變控制的越精細(xì)。這里最重要的是交點變化的算法。
貼上代碼:
public class MeshView extends View {
private Bitmap bitmap;
//定義兩個常量,這兩個常量指定該圖片橫向、縱向上都被劃分為20格。
private final int WIDTH = 20;
private final int HEIGHT = 20;
//記錄該圖片上包含441個頂點
private final int COUNT = (WIDTH + 1) * (HEIGHT + 1);
//定義一個數(shù)組,保存Bitmap上的21 * 21個點的座標(biāo)
private final float[] verts = new float[COUNT * 2];
//定義一個數(shù)組,記錄Bitmap上的21 * 21個點經(jīng)過扭曲后的座標(biāo)
//對圖片進行扭曲的關(guān)鍵就是修改該數(shù)組里元素的值。
private final float[] orig = new float[COUNT * 2];
private Paint mPaint;
public MeshView(Context context, int drawableId) {
super(context);
setFocusable(true);
//根據(jù)指定資源加載圖片
bitmap = BitmapFactory.decodeResource(context.getResources(),
drawableId);
//獲取圖片寬度、高度
float bitmapWidth = bitmap.getWidth();
float bitmapHeight = bitmap.getHeight();
int index = 0;
for (int y = 0; y <= HEIGHT; y++) {
float fy = bitmapHeight * y / HEIGHT;
for (int x = 0; x <= WIDTH; x++) {
float fx = bitmapWidth * x / WIDTH;
/*
* 初始化orig、verts數(shù)組。
* 初始化后,orig、verts兩個數(shù)組均勻地保存了21 * 21個點的x,y座標(biāo)
*/
orig[index * 2 + 0] = verts[index * 2 + 0] = fx;
orig[index * 2 + 1] = verts[index * 2 + 1] = fy;
index += 1;
}
}
//設(shè)置背景色
setBackgroundColor(Color.WHITE);
mPaint = new Paint();
mPaint.setColor(Color.RED);
mPaint.setStrokeWidth(1);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setAntiAlias(true);
}
@Override
protected void onDraw(Canvas canvas) {
/* 對bitmap按verts數(shù)組進行扭曲
* 從第一個點(由第5個參數(shù)0控制)開始扭曲
*/
canvas.drawBitmapMesh(bitmap, WIDTH, HEIGHT, verts
, 0, null, 0, null);
}
//工具方法,用于根據(jù)觸摸事件的位置計算verts數(shù)組里各元素的值
private void warp(float cx, float cy) {
for (int i = 0; i < COUNT * 2; i += 2) {
float dx = cx - orig[i + 0];
float dy = cy - orig[i + 1];
float dd = dx * dx + dy * dy;
//計算每個座標(biāo)點與當(dāng)前點(cx、cy)之間的距離
float d = (float) Math.sqrt(dd);
//計算扭曲度,距離當(dāng)前點(cx、cy)越遠(yuǎn),扭曲度越小
float pull = 80000 / ((float) (dd * d));
//對verts數(shù)組(保存bitmap上21 * 21個點經(jīng)過扭曲后的座標(biāo))重新賦值
if (pull >= 1) {
verts[i + 0] = cx;
verts[i + 1] = cy;
} else {
//控制各頂點向觸摸事件發(fā)生點偏移
verts[i + 0] = orig[i + 0] + dx * pull;
verts[i + 1] = orig[i + 1] + dy * pull;
}
}
//通知View組件重繪
invalidate();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//調(diào)用warp方法根據(jù)觸摸屏事件的座標(biāo)點來扭曲verts數(shù)組
warp(event.getX(), event.getY());
return true;
}
}
二、Picture
抄一段官方翻譯:
A Picture records drawing calls (via the canvas returned by beginRecording) and can then play them back into Canvas (via draw(Canvas) or drawPicture(Picture)).For most content (e.g. text, lines, rectangles), drawing a sequence from a picture can be faster than the equivalent API calls, since the picture performs its playback without incurring any method-call overhead.
簡而言之,就是錄制一個繪制過程,然后在需要的時候,可以把這個過程重現(xiàn)。
| API | 備注 |
|---|---|
| beginRecording(int width, int height) | -- |
| endRecording() | -- |
| draw(Canvas canvas) | -- |
| getHeight() | -- |
| getWidth() | -- |
| createFromStream(InputStream stream) | deprecated in API level 18 |
| writeToStream(OutputStream stream) | deprecated in API level 18 |
Picture 的 api 方法比較簡單,基本就是方法名所代表的意思,下面主要演示用法和需要注意的地方。
錄制一段繪制操作
private void initPicture() {
if (mPicture == null) {
mPicture = new Picture();
Canvas canvas = mPicture.beginRecording(200, 200);
canvas.translate(150, 150);
canvas.drawCircle(0, 0, 100, mPaint);
mPicture.endRecording();
}
}
上面的代碼就已經(jīng)錄制好了一段繪畫操作,值得注意的是,在這之后,即便你改變了 mPaint 的屬性,或者移動旋轉(zhuǎn)了 onDraw 方法中的畫布,錄制中的圖像并不會有所改變,再次繪制的時候,只會和第一次錄制時一樣。單就這一點而言,和錄像機還真是相像。
繪制錄像中繪制的圖片
下面我們來看,如何把這個 picture 繪制到畫布上去。想要把已經(jīng)錄制好的圖像繪制到畫布上,一共有三種方法:
Picture#draw(Canvas canvas)
Canvas#drawPicture(Picture picture)
PictureDrawable#draw(Canvas)
1)Picture#draw(Canvas canvas)
我們知道,在調(diào)用錄制方法的時候,返回了 canvas 對象,而我們的繪制操作就是對這個畫布進行的操作。這里將 onDraw 中的畫布傳入 picture 進行繪制,需要注意的是在某些低版本的機型上,繪制結(jié)束后,所有在錄像過程中進行的操作都會被實際作用在你傳入的畫布上,因此這個方法是不推薦使用的。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPicture.draw(canvas);
}
2)Canvas#drawPicture(Picture picture)
我們可以在 onDraw 方法中直接調(diào)用 drawPicture 方法:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawPicture(mPicture);
}
當(dāng)然,如果你已經(jīng)開始使用了,會發(fā)現(xiàn),它還可以再添加一個參數(shù),像這樣:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
RectF rectF = new RectF(0, 0, 200, 100);
canvas.drawPicture(mPicture,rectF);
}

由于原圖為 200 * 200 的圓形,要將其放入 200 * 100 的矩形區(qū)域內(nèi),圖形發(fā)生的拉伸。上圖中,右邊為原圖,左邊為實際繪制的圖形。
3)PictureDrawable#draw(Canvas)
這個方法讓我挺郁悶的,因為我像這樣調(diào)用,是沒有任何效果的:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
PictureDrawable drawable = new PictureDrawable(mPicture);
// 設(shè)置繪制區(qū)域 -- 注意此處所繪制的實際內(nèi)容不會縮放
//drawable.setBounds(0,0,mPicture.getWidth(),mPicture.getHeight());
// 繪制
drawable.draw(canvas);
}
在沒有調(diào)用 drawable.setBounds 時,不會有任何圖像被繪制,因為在 PictureDrawable 源碼中,onDraw 方法是這樣寫的:
@Override
public void draw(Canvas canvas) {
if (mPicture != null) {
Rect bounds = getBounds();
canvas.save();
canvas.clipRect(bounds);
canvas.translate(bounds.left, bounds.top);
canvas.drawPicture(mPicture);
canvas.restore();
}
}
@NonNull
public final Rect getBounds() {
if (mBounds == ZERO_BOUNDS_RECT) {
mBounds = new Rect();
}
return mBounds;
}
也就是說,只有調(diào)用 drawable.setBounds 才會有對應(yīng)的繪制區(qū)域。而當(dāng)繪制區(qū)域比實際區(qū)域大的時候,圖形不會伸縮,只會被裁剪:

感謝:
1.Android drawBitmapMesh扭曲圖像
2.Picture
3.GcsSloop 自定義 View 系列
以上。