自定義 View - Canvas - 繪制圖片

操作 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);
效果圖-g.png

其中右邊為原圖,左邊為繪制的圖片。比較后,可以看出,這個方法,將原圖 200200 區(qū)域的圖像,經(jīng)過變形繪制在 200150 的畫布上。

3)第三個方法可以讓我們控制繪制圖像所在在的畫布位置

Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.alu);
canvas.drawBitmap(bitmap,400,400,mPaint);
效果圖-g.png

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)格的圖片做例子:

target2.png

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

效果圖-g.png

大致原理是這樣,分的網(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);

}
效果圖-g.png

由于原圖為 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ū)域大的時候,圖形不會伸縮,只會被裁剪:

效果圖-g.png


感謝:

1.Android drawBitmapMesh扭曲圖像
2.Picture
3.GcsSloop 自定義 View 系列

以上。

謝謝觀賞
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容