天天看點

Android自定義View之Canvas基礎

畫布:通過畫筆繪制幾何圖形、文字、路徑(Path),位圖(Bitmap)等

繪制内容我們需要準備:

一個用于容納像素的位圖,

一個用于承載繪制調用的Canvas(寫入位圖),

一個繪制圖元(例如Rect,Path,文本,位圖),

一個繪制( 描述圖紙的顔色和樣式)。

Canvas常用的API大概分為:繪制、變換、狀态儲存和恢複。

一、變換

  • 平移
// 平移操作
canvas.drawCircle(200, 200, 150, mPaint);
// 畫布的起始點會移動到(200, 200)做個坐标點
canvas.translate(200,200);
mPaint.setColor(Color.RED);
canvas.drawCircle(200, 200, 150, mPaint);
           
Android自定義View之Canvas基礎
  • 縮放
// 縮放
canvas.drawRect(0, 0, 400, 400, mPaint);
// 将畫布縮放到原來一半
//canvas.scale(0.5f, 0.5f);

// 先平移(px,py),然後縮放scale(sx,sy),再反向平移(-px,-py)
// 也可以看成以(200,200)這個點進行縮放
canvas.scale(0.5f,0.5f,200,200);
//canvas.translate(200, 200);
//canvas.scale(0.5f, 0.5f);
//canvas.translate(-200, -200);

mPaint.setColor(Color.RED);
canvas.drawRect(0, 0, 400, 400, mPaint);
           
Android自定義View之Canvas基礎
Android自定義View之Canvas基礎

當縮放比例為負數的時候會根據縮放中心軸進行翻轉

// 以原點作為縮放點
// 為了使效果明顯,将坐标點圓點移動到畫布中心
canvas.translate(mWidth / 2, mHeght / 2);
canvas.drawRect(0, 0, 400, 400, mPaint);
canvas.scale(-0.5f, -0.5f);
mPaint.setColor(Color.RED);
canvas.drawRect(0, 0, 400, 400, mPaint);
           
Android自定義View之Canvas基礎
// 設定一個縮放點
// 為了使效果明顯,将坐标點圓點移動到畫布中心
canvas.translate(mWidth / 2, mHeght / 2);
canvas.drawRect(0, 0, 400, 400, mPaint);
//canvas.scale(-0.5f, -0.5f);
canvas.scale(-0.5f, -0.5f, 200, 0);
//canvas.translate(200, 0);
//canvas.scale(-0.5f, -0.5f);
//canvas.translate(-200, 0);

mPaint.setColor(Color.RED);
canvas.drawRect(0, 0, 400, 400, mPaint);
           
Android自定義View之Canvas基礎
  • 旋轉
canvas.translate(mWidth / 2, mHeght / 2);
canvas.drawRect(0, 0, 400, 400, mPaint);

// 順時針旋轉50度
//canvas.rotate(50);
// 設定順時針旋轉50度,旋轉點(200, 200)
canvas.rotate(50,200,200);

mPaint.setColor(Color.RED);
canvas.drawRect(0, 0, 400, 400, mPaint);
           
Android自定義View之Canvas基礎
  • 錯切

    skew (float sx, float sy)

    float sx:将畫布在x方向上傾斜相應的角度,sx傾斜角度的tan值,

    float sy:将畫布在y軸方向上傾斜相應的角度,sy為傾斜角度的tan值.

    變換後:

    X = x + sx * y

    Y = sy * x + y

canvas.translate(mWidth / 2 - 200, mHeght / 2 - 200);
canvas.drawRect(0, 0, 400, 400, mPaint);

// sx,sy就是三角函數中的tan值
//在X方向傾斜45度,Y軸逆時針旋轉45
//canvas.skew(1, 0);
// 在Y軸方向傾斜45度,X軸順時針旋轉45度
//canvas.skew(0,1);
canvas.skew(0.5f,0.5f);

mPaint.setColor(Color.RED);
canvas.drawRect(0, 0, 400, 400, mPaint);
           
Android自定義View之Canvas基礎
  • 切割

    clipRect(),clipPath(),clipOutRect(),clipOutPath()

canvas.translate(mWidth / 2 - 200, mHeght / 2 - 200);
canvas.drawRect(0, 0, 400, 400, mPaint);

// 裁剪畫布
canvas.clipRect(200, 200, 600, 600);
/*if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
         canvas.clipOutRect(200f,200f,600f,600f);
}*/

mPaint.setColor(Color.RED);
// 超出畫布區域的部分不能被繪制
canvas.drawRect(0, 0, 300, 300, mPaint);
mPaint.setColor(Color.MAGENTA);
// 在畫布範圍内可以被繪制
canvas.drawRect(200, 200, 500, 500, mPaint);
           
Android自定義View之Canvas基礎
  • 使用矩陣方法使用Canvas的變換

    隻使用了常用的,還有很多其他的方法,使用到的時候再去檢視源碼或者文檔都可以,隻需要記住一些常用的,具體需要使用的時候知道知道有這個東西,不那麼迷茫就行, O(∩_∩)O哈哈~。

// 矩陣操作canvas的變換
//canvas.translate(mWidth / 2 - 200, mHeght / 2 - 200);
canvas.drawRect(0, 0, 400, 400, mPaint);

// 使用矩陣
Matrix matrix = new Matrix();
// 使用矩陣提供的平移方法
matrix.setTranslate(200, 200);
matrix.setRotate(45);
matrix.setRotate(45, 0, 0);
matrix.setScale(0.5f, 0.5f);
matrix.setScale(0.5f, 0.5f, 100, 0);
matrix.setSkew(1,0);
matrix.setSkew(0,1);
canvas.setMatrix(matrix);

mPaint.setColor(Color.RED);
canvas.drawRect(0, 0, 400, 400, mPaint);
           

二、繪制

圖形繪制、文字繪制、路徑繪制、位圖繪制等

三、狀态儲存和恢複

Canvas調用了平移,旋轉,縮放,錯切,裁剪等變換後,後續的操作都是基于變換後的Canvas,後續的操作都會受到影響,對于我們後續的操作不是很友善。Canvas提供了:

canvas.save();

canvas.saveLayer();

canvas.saveLayerAlpha();

canvas.restore();

canvas.restoreToCount();

來儲存和恢複狀态

1.canvas内部對于狀态的儲存存放在棧中
2.可以多次調用save儲存canvas的狀态,并且可以通過getSaveCount方法擷取儲存的狀态個數
3.可以通過restore方法傳回最近一次save前的狀态,也可以通過restoreToCount傳回指定save狀态。指定save狀态之後的狀态全部被清除
4.saveLayer可以建立新的圖層,之後的繪制都會在這個圖層之上繪制,直到調用restore方法
注意:繪制的坐标系不能超過圖層的範圍, saveLayerAlpha對圖層增加了透明度資訊
           
  • 儲存狀态,save()
canvas.drawRect(0, 0, 500, 500, mPaint);
// 儲存Canvas狀态
canvas.save();

// 平移
canvas.translate(100, 100);
mPaint.setColor(Color.RED);
canvas.drawRect(0, 0, 500, 500, mPaint);
// 再次儲存Canvas的狀态
canvas.save();

// 平移
canvas.translate(100, 100);
mPaint.setColor(Color.MAGENTA);
canvas.drawRect(0, 0, 500, 500, mPaint);

// 復原Canvas狀态一次
canvas.restore();

// 繪制直線
mPaint.setColor(Color.BLACK);
canvas.drawLine(0, 0, 400, 400, mPaint);
           

我們發現,canvas.restore()隻是復原到前一次儲存的狀态

Android自定義View之Canvas基礎

如果我們需要復原狀态到最初的原點的狀态,儲存多少次就復原多少次,可以達到我們需要的效果

canvas.drawRect(0, 0, 500, 500, mPaint);
// 儲存Canvas狀态
canvas.save();

// 平移
canvas.translate(100, 100);
mPaint.setColor(Color.RED);
canvas.drawRect(0, 0, 500, 500, mPaint);
// 再次儲存Canvas的狀态
canvas.save();

// 平移
canvas.translate(100, 100);
mPaint.setColor(Color.MAGENTA);
canvas.drawRect(0, 0, 500, 500, mPaint);

// 復原Canvas狀态一次
canvas.restore();
// 再次復原一次到最初的狀态
canvas.restore();

// 繪制直線
mPaint.setColor(Color.BLACK);
canvas.drawLine(0, 0, 400, 400, mPaint);
           
Android自定義View之Canvas基礎

但是我們知道Android的設計不可能這麼雞肋,還要我們一次一次的去復原,這就用到另一個復原方法了,canvas.restoreToCount(saveId),圖示就是上面同樣的。

canvas.drawRect(0, 0, 500, 500, mPaint);
// 儲存Canvas狀态
int saveId = canvas.save();

// 平移
canvas.translate(100, 100);
mPaint.setColor(Color.RED);
canvas.drawRect(0, 0, 500, 500, mPaint);
// 再次儲存Canvas的狀态
canvas.save();

// 平移
canvas.translate(100, 100);
mPaint.setColor(Color.MAGENTA);
canvas.drawRect(0, 0, 500, 500, mPaint);

// 復原Canvas狀态一次
//canvas.restore();
// 再次復原一次到最初的狀态
//canvas.restore();
// 直接復原到儲存的Id指引的位置,将它棧頂儲存的狀态全部出棧,将自己放在棧頂
canvas.restoreToCount(saveId);

// 繪制直線
mPaint.setColor(Color.BLACK);
canvas.drawLine(0, 0, 400, 400, mPaint);
           
  • 儲存圖層狀态 saveLayer()

    儲存圖層之後的繪制,如果是超出圖層範圍的部分是不會被繪制的。

canvas.drawRect(200, 200, 700, 700, mPaint);

int layerId = canvas.saveLayer(0, 0, 700, 700, mPaint, Canvas.ALL_SAVE_FLAG);
mPaint.setColor(Color.BLUE);
Matrix matrix = getMatrix();
// 平移到(100, 100)的位置
matrix.setTranslate(100, 100);
canvas.setMatrix(matrix);
//由于平移操作,導緻繪制的矩形超出了圖層的大小,是以繪制不完全
canvas.drawRect(0, 0, 700, 700, mPaint);
canvas.restoreToCount(layerId);

mPaint.setColor(Color.RED);
canvas.drawRect(0, 0, 100, 100, mPaint);
           
Android自定義View之Canvas基礎

Canvas執行個體

  1. 粒子爆炸效果

JavaBean實體類

public class Cell {
    // 粒子顔色
    public int color;
    // 粒子半徑
    public float radius;
    // 粒子的坐标(x, y)
    public float x;
    public float y;
    // 粒子的速度
    public float vx;
    public float vy;
    // 粒子的加速度
    public float ax;
    public float ay;
}
           

自定義View實作粒子爆炸

public class CanvasView2 extends View {
    private Paint mPaint;
    private Bitmap mBitmap;
    private List<Cell> cells;
    private float defaultRadius = 1.5f;
    private ValueAnimator mAnimator;

    private int mWidth, mHeight;

    public CanvasView2(Context context) {
        this(context, null);
    }

    public CanvasView2(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CanvasView2(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        init();
    }

    private void init() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        cells = new ArrayList<>();

        mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.beauty);
        int bmWidth = mBitmap.getWidth();
        int bmHeight = mBitmap.getHeight();
        for (int i = 0; i < bmWidth; i++) {
            for (int j = 0; j < bmHeight; j++) {
                Cell cell = new Cell();
                // 擷取像素點顔色
                cell.color = mBitmap.getPixel(i, j);
                cell.radius = defaultRadius;
                cell.x = i * 2 * defaultRadius + defaultRadius;
                cell.y = j * 2 * defaultRadius + defaultRadius;
                cells.add(cell);

                // 速度(-20,20)
                cell.vx = (float) (Math.pow(-1, Math.ceil(Math.random() * 1000)) * 20 * Math.random());
                cell.vy = rangInt(-15, 25);
            }
        }

        // 初始化動畫
        mAnimator = ValueAnimator.ofFloat(0, 1);
        mAnimator.setRepeatCount(-1);
        mAnimator.setDuration(2000);
        mAnimator.setInterpolator(new LinearInterpolator());
        mAnimator.addUpdateListener(animation -> {
            updateCell();
            postInvalidate();
        });
    }

    private int rangInt(int i, int j) {
        int max = Math.max(i, j);
        int min = Math.min(i, j) - 1;
        //在0到(max - min)範圍内變化,取大于x的最小整數 再随機
        return (int) (min + Math.ceil(Math.random() * (max - min)));
    }

    private void updateCell() {
        //更新粒子的位置
        for (Cell cell : cells) {
            cell.x += cell.vx;
            cell.y += cell.vy;

            cell.vx += cell.ax;
            cell.vy += cell.ay;
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = w;
        mHeight = h;
    }

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

        canvas.translate(mWidth / 4, mHeight / 6);

        for (Cell cell : cells) {
            mPaint.setColor(cell.color);
            canvas.drawCircle(cell.x, cell.y, defaultRadius, mPaint);
        }
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            mAnimator.start();
        }
        return super.onTouchEvent(event);
    }
}
           
Android自定義View之Canvas基礎

現在的例子在onDraw()進行了頻繁繪制,造成UI線程卡頓,是以可以試着修改,這裡隻是展示一個例子,┭┮﹏┭┮

  1. 加載動畫

    加載動畫:

    Progress(6個小圓組成),Progress離散聚合動畫,最後繪制一個水波紋顯示正文

public class CanvasView3 extends View {
    // 旋轉圓的畫筆(小圓球)
    private Paint mPaint;
    // 水波紋圓的畫筆
    private Paint mRipplePaint;
    // 屬性動畫
    private ValueAnimator mValueAnimator;

    // 背景色
    private int mBgColor = Color.WHITE;
    // 6個小圓的顔色
    private int[] mCircleColors;

    //表示旋轉圓的中心坐标(6個小球圍成的圓)
    private float mCenterX;
    private float mCenterY;
    //表示斜對角線長度的一半,擴散圓最大半徑
    private float mDistance;

    //6個小球的半徑
    private float mCircleRadius = 18;
    //旋轉大圓的半徑
    private float mRotateRadius = 90;

    //目前大圓的旋轉角度(預設是0)
    private float mCurrentRotateAngle = 0F;
    //目前大圓的半徑(半徑是會變化的)
    private float mCurrentRotateRadius = mRotateRadius;
    //擴散圓的半徑
    private float mCurrentRippleRadius = 0F;
    //表示旋轉動畫的時長
    private int mRotateDuration = 1200;

    private SplashState mState;

    public CanvasView3(Context context) {
        this(context, null);
    }

    public CanvasView3(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CanvasView3(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        init();
    }

    private void init() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

        mRipplePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mRipplePaint.setStyle(Paint.Style.STROKE);
        mRipplePaint.setColor(mBgColor);

        mCircleColors = getResources().getIntArray(R.array.splash_circle_colors);

    }


    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mCenterX = w * 1f / 2;
        mCenterY = h * 1f / 2;
        // sqrt(x^2 + y^2)
        mDistance = (float) (Math.hypot(w, h) / 2);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mState == null) {
            mState = new RotateState();
        }
        mState.drawState(canvas);
    }

    /**
     * 繪制狀态的類
     */
    private abstract class SplashState {
        abstract void drawState(Canvas canvas);
    }


    /**
     * 1、旋轉狀态,就是6個旋轉小圓
     */
    private class RotateState extends SplashState {

        private RotateState() {
            // 屬性動畫,并且設定動畫的取值範圍
            mValueAnimator = ValueAnimator.ofFloat(0, (float) (2 * Math.PI));
            // 執行次數
            mValueAnimator.setRepeatCount(2);
            mValueAnimator.setDuration(mRotateDuration);
            // 插值器
            mValueAnimator.setInterpolator(new LinearInterpolator());
            // 監聽動畫執行
            mValueAnimator.addUpdateListener(animation -> {
                // 更新旋轉角度(因為我們的動畫範圍剛好就是0..2PI)
                mCurrentRotateAngle = (float) animation.getAnimatedValue();
                invalidate();
            });
            mValueAnimator.addListener(new AnimatorListenerAdapter() {

                @Override
                public void onAnimationEnd(Animator animation) {
                    super.onAnimationEnd(animation);
                    mState = new DiffState();
                }

                // 動畫結束的時候,并沒有走這個結束監聽
                /*@Override
                public void onAnimationEnd(Animator animation, boolean isReverse) {
                    mState = new DiffState();
                }*/
            });
            mValueAnimator.start();
        }

        @Override
        void drawState(Canvas canvas) {
            //繪制背景
            drawBackground(canvas);
            //繪制6個小球
            drawCircles(canvas);
        }
    }

    /**
     * 繪制6個小圓
     *
     * @param canvas
     */
    private void drawCircles(Canvas canvas) {
        // 每個小球之間的角度(這裡求的是弧度)
        float rotateAngle = (float) (Math.PI * 2 / mCircleColors.length);
        for (int i = 0; i < mCircleColors.length; i++) {
            //float angle = i * rotateAngle;
            // 小球的角度(因為旋轉動畫,是以我們需要加上旋轉過的角度)
            float angle = i * rotateAngle + mCurrentRotateAngle;
            // 小球的圓心坐标
            // x = r * cos(a) + centX;
            // y = r * sin(a) + centY;
            //float cx = (float) (Math.cos(angle) * mRotateRadius + mCenterX);
            //float cy = (float) (Math.sin(angle) * mRotateRadius + mCenterY);
            // 更改旋轉圓的半徑,因為擴散的時候,半徑一直在變化,是以相應的小圓的圓心坐标也需要改變
            float cx = (float) (Math.cos(angle) * mCurrentRotateRadius + mCenterX);
            float cy = (float) (Math.sin(angle) * mCurrentRotateRadius + mCenterY);
            mPaint.setColor(mCircleColors[i]);
            canvas.drawCircle(cx, cy, mCircleRadius, mPaint);
        }
    }

    /**
     * 繪制背景
     *
     * @param canvas
     */
    private void drawBackground(Canvas canvas) {
        // 判斷當水波紋圓半徑大于0,說明走到第三步繪制水波紋圓
        if (mCurrentRippleRadius > 0) {
            // 空心圓邊框寬度
            float strokeWidth = mDistance - mCurrentRippleRadius;
            // 空心圓半徑
            float radius = strokeWidth / 2 + mCurrentRippleRadius;
            mRipplePaint.setStrokeWidth(strokeWidth);
            canvas.drawCircle(mCenterX, mCenterY, radius, mRipplePaint);
        } else {
            canvas.drawColor(mBgColor);
        }
    }


    /**
     * 2、擴散狀态
     */
    private class DiffState extends SplashState {

        private DiffState() {
            // 動畫取值範圍,小圓半徑到大圓半徑
            mValueAnimator = ValueAnimator.ofFloat(mCircleRadius, mRotateRadius);
            // 執行次數
            //mValueAnimator.setRepeatCount(2);
            mValueAnimator.setDuration(mRotateDuration);
            // 插值器
            mValueAnimator.setInterpolator(new OvershootInterpolator(10f));
            // 監聽動畫執行
            mValueAnimator.addUpdateListener(animation -> {
                // 更新目前旋轉圓的半徑
                mCurrentRotateRadius = (float) animation.getAnimatedValue();
                invalidate();
            });
            mValueAnimator.addListener(new AnimatorListenerAdapter() {

                @Override
                public void onAnimationEnd(Animator animation) {
                    mState = new RippleState();
                }
            });
            mValueAnimator.reverse();
        }

        @Override
        void drawState(Canvas canvas) {
            drawBackground(canvas);
            drawCircles(canvas);
        }
    }

    private class RippleState extends SplashState {

        private RippleState() {
            // 動畫取值範圍,小圓半徑到水波紋圓的半徑
            mValueAnimator = ValueAnimator.ofFloat(mCircleRadius, mDistance);
            // 執行次數
            //mValueAnimator.setRepeatCount(2);
            mValueAnimator.setDuration(mRotateDuration);
            // 插值器
            mValueAnimator.setInterpolator(new LinearInterpolator());
            // 監聽動畫執行
            mValueAnimator.addUpdateListener(animation -> {
                // 更新目前旋轉圓的半徑
                mCurrentRippleRadius = (float) animation.getAnimatedValue();
                invalidate();
            });
            mValueAnimator.start();
        }

        @Override
        void drawState(Canvas canvas) {
            drawBackground(canvas);
        }
    }
}
           
Android自定義View之Canvas基礎

示例代碼Github路徑

參考文章

  1. Gcssloop自定義View系列Canvas操作
  2. 有興趣可以去學習Gcssloop關于自定義View的所有文章

    Gcssloop自定義View系列所有文章

  3. HenCoder Android 開發進階:自定義 View 1-4 Canvas 對繪制的輔助 clipXXX() 和 Matrix
  4. Google官方Canvas文檔