画布:通过画笔绘制几何图形、文字、路径(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);
}
}
}