1. 程式人生 > >自定義元件開發三 Graphics 繪製動態效果

自定義元件開發三 Graphics 繪製動態效果

概述

上文https://blog.csdn.net/u011733020/article/details/80220513主要介紹了Graphics相關的api的繪圖方法。繪製的都是靜態的,這裡使用Graphics 來實現動態效果繪圖,來達到讓畫面動起來,或者讓圖案與我們的手指互動。

過去我們在 ImageView 上繪圖,這裡嘗試定義一個繼承自 View 的
子類,重寫 onDraw(),在該方法中繪圖,當 View 顯示時會回撥 onDraw()方法
展方法,用於繪製元件的外觀。

public class MyView extends View{
    public void onDraw
(Canvas canvas){ } }

View 的子類必須定義三個不同版本的構造方法。

invalidate()

View 類定義了一組 invalidate()方法,該方法有好幾個版本:

public void invalidate()
public void invalidate(int l, int t, int r, int b)
public void invalidate(Rect dirty)

invalidate()用於重繪元件,不帶引數表示重繪整個檢視區域,帶引數表示重繪指定的區域。如果要去追溯該方法的原始碼,大概就是將重繪請求一級級往上提交到 ViewRoot,呼叫 ViewRoot的 scheduleTraversals()方法重新發起重繪請求,scheduleTraversals()方法會發送一個非同步訊息,呼叫 performTraversals()方法執行重繪,而 performTraversals()方法最終呼叫 onDraw()方法。所以,簡單來說,呼叫 View 的 invalidate()方法就相當於呼叫了 onDraw()方法,而 onDraw()方法中就是我們編寫的繪圖程式碼。

如果要重新整理元件或者讓畫面動起來,我們只需呼叫 invalidate()方法即可。通過改變資料來影響繪製結果,這是實現元件重新整理或實現動畫的基本思路。

invalidate()方法只能在 UI 執行緒中呼叫,如果是在子執行緒中重新整理元件,View 類還定義了另一組名為 postInvalidate 的方法:

public void postInvalidate()
public void postInvalidate(int left, int top, int right, int bottom)

先拿小車來演示一下,讓小車在 View 的 Canvas 中水平往返移動。
這裡寫圖片描述

package bczm.graphics.view;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;

import bczm.graphics.R;



public class CarMoveView extends View {
    /**
     * 小車的水平位置
     */
    private int x;
    /**
     * 小車的垂直位置,固定為 100
     */
    private static final int y = 100;
    /**
     * 小車的寬度
     */
    private static final int carWidth = 320;

    private static final int COLOR = Color.RED;
    private Paint paint;
    /**
     * 移動的方向
     */
    private boolean direction;
    private Bitmap newBitMap;

    public CarMoveView(Context context) {
        super(context);
    }

    public CarMoveView(Context context, AttributeSet attrs) {
        super(context, attrs);
        //初始化畫筆,引數表示去鋸齒
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
//        paint.setColor(COLOR);
        x = 0;

        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.space_art);
        newBitMap = calculate(bitmap);
    }

    public CarMoveView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public Bitmap calculate(Bitmap bm) {
        // 獲得圖片的寬高
        int width = bm.getWidth();
        int height = bm.getHeight();
        // 設定想要的大小
        int newWidth = 320;
        int newHeight = 320;
        // 計算縮放比例
        float scaleWidth = ((float) newWidth) / width;
        float scaleHeight = ((float) newHeight) / height;
        // 取得想要縮放的matrix引數
        Matrix matrix = new Matrix();
        matrix.postScale(scaleWidth, scaleHeight);
        // 得到新的圖片
        Bitmap newbm = Bitmap.createBitmap(bm, 0, 0, width, height, matrix,
                true);
        return newbm;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //根據 x、y 的座標值 初始小車位置
        canvas.drawBitmap(newBitMap, x, y, paint);
        //改變 x 座標的值,呼叫 invalidate()方法後,
        //小車將因 x 的值發生改變而產生移動的效果
        int width = this.getMeasuredWidth();//獲取元件的寬度
        if (x <= carWidth) {
            direction = true;
        }
        if (x >= width - carWidth) {
            direction = false;

        }
        x = direction ? x + 5 : x - 5;
    }
}

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    xmlns:android="http://schemas.android.com/apk/res/android">
    <bczm.graphics.view.CarMoveView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/ballview"
        />
</LinearLayout>
public class CarViewActivity extends Activity {
    private CarMoveView ballview;
    @Override
    protected void onCreate( Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_ball_move);
        ballview = (CarMoveView) findViewById(R.id.ballview);
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                ballview.postInvalidate();
            }
        }, 200, 50);
    }
}

我們定義了一個 BallMoveView 類,繼承自 View,並且重寫了 onDraw()方法,該方法是用於在元件上繪圖的方法,同時,我們定義了有兩個引數的構造方法,如果在佈局檔案中定義了該元件,則會呼叫此構造方法來建立物件。

在 onDraw()方法中,畫布 Canvas 物件自動傳入,第一章我們已經知道該物件的來源,顯示在 View 上的內容都最終都必須繪製在該 canvs 物件上。我們呼叫 canvas 物件的 drawCircle()方法

那麼,小車是如何水平移動的呢?在 onDraw()方法中,我們發現該方法的最後幾行程式碼會
根據 direction 來修改成員變數 x 的值,如果 direction 為 true,x 累加 5,否則累加-5。正如上面所說,我們在 Activity 中恰恰是通過定時器週期性呼叫了 invalidate()方法不斷重繪元件,也就是不斷呼叫 onDraw()方法,因為小車的位置由 x 來決定,onDraw()每呼叫一次,x 的值就會變化一次,小車繪製的位置自然也會跟著一起改變,最後形成了小車移動的效果。

座標轉換

預設情況下,畫布座標的原點就是繪圖區的左上角,向左為負,向右為正,向上為負,向下為正,但是通過 Canvas 提供的方法可以對座標進行轉換。轉換的方式主要有 4 種:平移、旋轉、縮放和拉斜:

public void translate(float dx, float dy)

座標平移,在當前原點的基礎上水平移動 dx 個距離,垂直移動 dy 個距離,正負符號決定方向。座標原點改變後,所有的座標都是以新的原點為參照進行定位。

這裡寫圖片描述

下面兩段程式碼是等效的:

程式碼段 1:canvas.drawPoint(10, 10, paint);
程式碼段 2:canvas.translate(10, 10); canvas.drawPoint(0, 0, paint);

public void rotate(float degrees)

將畫布的座標以當前原點為中心旋轉指定的角度,如果角度為正,則為順時針旋轉,
否則為逆時針旋轉。

這裡寫圖片描述

public final void rotate(float degrees, float px, float py)

以點(px, py)為中心對畫布座標進行旋轉 degrees 度,為正表示順時針,為負表示逆時針。

public void scale(float sx, float sy)

縮放畫布的座標,sx、sy 分別是 x 方向和 y 方向的縮放比例,小於 1 表示縮小,等於1 表示不變,大於 1 表示放大。畫布縮放後,繪製在畫布上的圖形也會等比例縮放。縮放的單位是倍數,比如 sx 為 0.5 時,就是在 x 方向縮小 0.5 倍。

public final void scale(float sx, float sy, float px, float py)

以(px,py)為中心對畫布進行縮放。

public void skew(float sx, float sy)

將畫布分別在 x 方向和 y 方向拉斜一定的角度,sx 為 x 方向傾斜角度的 tan 值,sy 為y 方向傾斜角度的 tan 值,比如我們打算在 X 軸方向上傾斜 45 度,則 tan45=1,寫成:canvas.skew(1, 0)。

座標轉換後,後面的圖形繪製功能將跟隨新座標,轉換前已經繪製的圖形不會有任何的變化。另外,為了能恢復到座標變化之前的狀態,Canvas 定義了兩個方法用於儲存現場和恢復現場:

public int save()
儲存現場。

public void restore()
恢復現場到 save()執行之前的狀態。

Android 中定義了一個名為 Matrix 的類,該類定義了一個 3 * 3 的矩陣,通過 Matrix 同樣可以實現座標的變換,相關的方法如下:

移位

public void setTranslate(float dx, float dy)

旋轉

public void setRotate(float degrees, float px, float py)
public void setRotate(float degrees)

縮放

public void setScale(float sx, float sy)
public void setScale(float sx, float sy, float px, float py)

拉斜

public void setSkew(float kx, float ky)
public void setSkew(float kx, float ky, float px, float py)

Matrix 的應用範圍很廣,Canvas、Shader 等都支援通過 Matrix 實現移位、旋轉、縮放等效果。Matrix 的基本使用形如:

Matrix matrix = new Matrix();
matrix.setTranslate(10, 10);
canvas.setMatrix(matrix);

剪下區( Clip )

clip 是指剪下區,理解“剪下區”這個概念不需要費什麼周折,我們想象一下,春暖花開的季節,您在海岸邊的豪華別墅裡,面朝海邊的牆上開了一個窗戶,您和美麗的妻子依偎在窗戶旁,一起看潮起潮落、鳥來鳥往,伴隨著落日餘輝和萬丈晚霞,好不愜意。這裡的剪下區就是在 Canvas上開一個口子, 開了這個口子後,接下來繪製的內容只有通過該口子才能看到,口子外的圖形就看不到了。
這裡寫圖片描述

Canvas 提供了剪下區的功能,剪下區可以是一個 Rect 或者是一個 Path,兩個剪下區還能進行圖形運算,得到更加複雜的剪下區。我們來看看相關的方法:

public boolean clipRect(Rect rect)
public boolean clipRect(RectF rect)
public boolean clipRect(float left, float top, float right, float bottom)
public boolean clipRect(int left, int top, int right, int bottom)

以上 4 個方法定義一個矩形的剪下區

public boolean clipPath(Path path)

以上方法定義一個 Path 剪下區,用於定義更加複雜的區域。

下面用一個例子演示

public class ClipView extends View {

    public ClipView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Bitmap srcBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.sun);
        //繪製完整照片
        canvas.drawBitmap(srcBitmap, 0, 0, null);
        //平移座標
        canvas.translate(0, 500);
        //定義剪下區
        canvas.clipRect(new Rect(100, 100, 500, 500));
        //再次繪製裁剪後的照片
        canvas.drawBitmap(srcBitmap, 0, 0, null);
    }
}

最終的效果是下面這樣:

這裡寫圖片描述

從上圖看出,平移座標後,在 Rect(100, 100, 500, 500)區域定義了一個剪下區,接下來繪製的圖片只有該剪下區才會顯示了。

剪下區還能進行圖形運算,前面學習 Path 時我們接觸過 Op,事實上剪下區的 Op 運算也沒什麼太大的不同,一共有 6 種:

public static enum Op {
DIFFERENCE,
INTERSECT,
REPLACE,
REVERSE_DIFFERENCE,
UNION,
XOR
}

這裡寫圖片描述

這裡寫圖片描述

這裡寫圖片描述

這裡寫圖片描述

這裡寫圖片描述

這裡寫圖片描述

與剪下區 Op 運算相關的方法如下:

public boolean clipRect(RectF rect, Op op)
public boolean clipRect(Rect rect, Op op)
public boolean clipRect(float left, float top, float right, float bottom, Op op)
public boolean clipPath(Path path, Op op)

用Op 的UNION來顯示裁剪區程式碼如下:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        Bitmap srcBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.sun);
        //繪製完整原始照片
        canvas.drawBitmap(srcBitmap, 0, 0, null);
        //平移座標
        canvas.translate(0, 500);
        //定義剪下區
        canvas.clipRect(new Rect(100, 100, 500, 500));
        //定義一個新的剪下區,與上一剪下區做 Op 運算
        Path path = new Path();
        path.addCircle(400, 320, 200, Path.Direction.CCW);
        canvas.clipPath(path, Region.Op.UNION);
        //再次繪製照片
        canvas.drawBitmap(srcBitmap, 0, 0, null);
    }

這裡寫圖片描述

利用剪下區還可以實現幀動畫的播放
這裡寫圖片描述

上面一張大圖包含了 7 幀,定義一個 1/7 大小的剪下區,每隔一段時間按照順序連續播放其中的一幀,原理類似於以前的膠片電影,這樣就構成了一個動感十足的動畫。

這裡寫圖片描述

播放過程中,剪下區(clip)是固定不動的,實際上移動的恰恰是圖片,圖片每次向左移動一幀。假設圖片總長度為 70,顯示第一幀時,圖片的 left 為 0,然後向左移動一幀,left為-10,向左移動兩幀,left 為-20……向左移動 6 幀,left 為-60,此時,整個動畫播放完畢。如果要迴圈播放,將 left 的值重新置 0 即可,具體實現請看下面的程式碼。
···
public ClipBombView(Context context, AttributeSet attrs) {
super(context, attrs);
bmpBoom = BitmapFactory.decodeResource(getResources(), R.drawable.boom);
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    //獲取位置的寬度和高度
    int width = bmpBoom.getWidth();
    int height = bmpBoom.getHeight();
    //剪下區
    int frameWidth = width / 7;
    Rect rect = new Rect(0, 0, frameWidth, height);
    canvas.save();
    canvas.translate(100, 100);//平移座標
    canvas.clipRect(rect);//設定剪下區
    canvas.drawBitmap(bmpBoom, -i * frameWidth, 0, null);//播放一幀
    canvas.restore();
    i++; //i 加 1 以播放下一幀
    if (i == 7) i = 0;//播放完畢後將 i 重置為 0 重新播放
}

···

謝謝認真觀讀本文的每一位小夥伴,衷心歡迎小夥伴給我指出文中的錯誤,也歡迎小夥伴與我交流學習。
歡迎愛學習的小夥伴加群一起進步:230274309