畫布:通過畫筆繪制幾何圖形、文字、路徑(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);
- 縮放
// 縮放
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);
當縮放比例為負數的時候會根據縮放中心軸進行翻轉
// 以原點作為縮放點
// 為了使效果明顯,将坐标點圓點移動到畫布中心
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);
// 設定一個縮放點
// 為了使效果明顯,将坐标點圓點移動到畫布中心
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);
- 旋轉
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);
-
錯切
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);
-
切割
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);
-
使用矩陣方法使用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()隻是復原到前一次儲存的狀态
如果我們需要復原狀态到最初的原點的狀态,儲存多少次就復原多少次,可以達到我們需要的效果
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的設計不可能這麼雞肋,還要我們一次一次的去復原,這就用到另一個復原方法了,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);
Canvas執行個體
- 粒子爆炸效果
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);
}
}
現在的例子在onDraw()進行了頻繁繪制,造成UI線程卡頓,是以可以試着修改,這裡隻是展示一個例子,┭┮﹏┭┮
-
加載動畫
加載動畫:
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);
}
}
}
示例代碼Github路徑
參考文章
- Gcssloop自定義View系列Canvas操作
-
有興趣可以去學習Gcssloop關于自定義View的所有文章
Gcssloop自定義View系列所有文章
- HenCoder Android 開發進階:自定義 View 1-4 Canvas 對繪制的輔助 clipXXX() 和 Matrix
- Google官方Canvas文檔