文章目录
一、概念
画布,通过画笔绘制几何图形、文本、路径和位图等。
二、常用api类型
常用api分为绘制、变换、状态保存和恢复
2.1 绘制集合图形,文本,位图等
//在指定座标绘制位图
public void drawBitmap(@NonNull Bitmap bitmap, float left, float top, @Nullable Paint paint)
//根据指定的起始点、结束点之间绘制连线
public void drawLine(float startX, float startY, float stopX, float stopY,
@NonNull Paint paint)
//根据指定的path,绘制连线
public void drawPath(@NonNull Path path, @NonNull Paint paint)
//根据指定的座标,绘制点
public void drawPoint(float x, float y, @NonNull Paint paint)
//根据指定的座标,绘制文字
public void drawText(@NonNull String text, int start, int end, float x, float y,
@NonNull Paint paint)
2.2 位置、形状变换
//平移操作
void translate(float dx, float dy)
//缩放操作
void scale(float sx, float sy)
//旋转操作
void rotate(float degrees)
//倾斜操作
void skew(float sx, float sy)
//切割操作,参数指定区域内不可以绘制
void clipxxx(......)
//反向切割操作,参数指定区域内不可以绘制
void clipOutxxx(......)
//可通过matrix实现平移、缩放、旋转等操作
void setMatrix(@Nullable Matrix matrix)
2.2.1 平移操作
首先要初始化画笔Paint:
private void init(){
mPaint = new Paint();
mPaint.setColor(Color.RED);
mPaint.setStrokeWidth(4);
mPaint.setStyle(Paint.Style.STROKE);
}
然后进行平移操作:
//1.平移操作:先画出矩形,再平移、改色画新矩形
canvas.drawRect(0,0,100,100,mPaint);
canvas.translate(50,50);
mPaint.setColor(Color.GRAY);
canvas.drawRect(0,0,100,100,mPaint);
效果:
矩形经过了平移,在draw的时候传入的起始点座标为(0,0),但是translate函数将其平移了(50,50),于是就出现了这样的效果。我们还可以在下面再绘制一条直线来验证:
//1.平移操作:先画出矩形,再平移、改色画新矩形
canvas.drawRect(0,0,100,100,mPaint);
canvas.translate(50,50);
mPaint.setColor(Color.GRAY);
canvas.drawRect(0,0,100,100,mPaint);
canvas.drawLine(0,0,200,200,mPaint);//画直线
以后绘制任何内容,都是经过translate平移后开始的。
2.2.2 缩放操作
scale()方法,参数需要传入x、y方向的缩放比例。这里我们全部都传入0.5f:
//2.缩放操作
canvas.drawRect(100,100,300,300,mPaint);
canvas.scale(0.5f,0.5f);
mPaint.setColor(Color.GRAY);
canvas.drawRect(100,100,300,300,mPaint);
这个scale()方法会将画布进行缩小,因此才会出现这种效果。
也有一个重载方法:
//2.缩放操作
canvas.drawRect(100,100,300,300,mPaint);
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.GRAY);
canvas.drawRect(100,100,300,300,mPaint);
这里的效果有点区别,矩形的长宽缩小了,但是绘制起点发生了改变。这个方法的意义就是先translate操作,后scale操作,最后反向translate。
2.2.3 旋转操作
rotate方法,参数传递是旋转角度。默认顺时针旋转,旋转后再去绘制矩形。
//3.旋转操作
canvas.drawRect(0,0,300,300,mPaint);
canvas.rotate(45);
mPaint.setColor(Color.GRAY);
canvas.drawRect(0,0,300,300,mPaint);
当然我们也可以先将画布进行平移操作后再看一下效果:
//3.旋转操作
canvas.translate(50,50);//平移画布
canvas.drawRect(0,0,300,300,mPaint);
canvas.rotate(45);
mPaint.setColor(Color.GRAY);
canvas.drawRect(0,0,300,300,mPaint);
旋转中心,是平移之后的原点。
当然,rotate方法也有重载方法,参数分别表示:旋转角度、旋转中心的x座标、旋转中心的y座标:
//先画矩形,然后将旋转圆心定位矩形中心,改变画笔颜色,重新绘制矩形
canvas.drawRect(200,200,600,600,mPaint);
canvas.rotate(45,400,400);
mPaint.setColor(Color.GRAY);
canvas.drawRect(200,200,600,600,mPaint);
2.2.4 倾斜操作
skew方法,参数表示x、y方向的tan值
//4.倾斜操作
canvas.drawRect(0,0,600,600,mPaint);
canvas.skew(1,0); //在x方向倾斜45度
mPaint.setColor(Color.GRAY);
canvas.drawRect(0,0,600,600,mPaint);
效果就是:在x方向倾斜45度,就是y轴逆时针旋转45度
如果是y方向倾斜45度的话:
//4.倾斜操作
canvas.drawRect(0,0,600,600,mPaint);
canvas.skew(0,1); //在y方向倾斜45度
mPaint.setColor(Color.GRAY);
canvas.drawRect(0,0,600,600,mPaint);
这个效果就是x轴顺时针旋转45度。
2.2.5 切割
可以切割矩形、路径。切割矩形,需要传入一个需要切割的矩形区域。首先确定切割区域,接下来的操作只会在这个切割区域内有效
//5.切割操作
canvas.drawRect(20,20,70,70,mPaint);
mPaint.setColor(Color.GRAY);
canvas.drawRect(20,80,70,100,mPaint);
canvas.clipRect(20,20,70,70); //画布被裁减
canvas.drawCircle(10,10,10,mPaint); //座标超出裁减区域,无法绘制
原本应该绘制出来的圆并没有出现,因为我们已经通过clipRect切割了画布,圆的绘制并不在切割区域,所以无法绘制显示。
现在如果在裁减区域内画圆:
//5.切割操作
canvas.drawRect(200,200,700,700,mPaint);
mPaint.setColor(Color.GRAY);
canvas.drawRect(200,800,700,1000,mPaint);
canvas.clipRect(200,200,700,700);
// canvas.drawCircle(100,100,100,mPaint);
canvas.drawCircle(300,300,100,mPaint);
圆的座标位于裁减区域内,成功绘制出来了。
反向裁减
//反向裁减
canvas.drawRect(200,200,700,700,mPaint);
mPaint.setColor(Color.GRAY);
canvas.drawRect(200,800,700,1000,mPaint);
canvas.clipOutRect(200,200,700,700);
canvas.drawCircle(100,100,100,mPaint);
canvas.drawCircle(300,300,100,mPaint);
这样的操作和上面的操作效果是相反的。裁减区域以外的才能正常绘制,裁减区域内的绘制无效。
2.2.6 矩阵
//6. matrix
canvas.drawRect(200,200,700,700,mPaint);
Matrix matrix = new Matrix();
matrix.setTranslate(50,50); //平移
// matrix.setRotate(45); //旋转45度
// matrix.setScale(0.5f,0.5f); //缩放
canvas.setMatrix(matrix);
mPaint.setColor(Color.GRAY);
canvas.drawRect(200,200,700,700,mPaint);
2.3 状态保存和恢复
Canvas调用translate、scale、rotate、skew、clipRect等变换后,后续的操作都是基于变换后的Canvas,都收到了影响,对以后的操作不利。Canvas提供了sava、saveLayer、saveLayerAlpha、restore、restoreToCount来保存和恢复状态
//绘制矩形
canvas.drawRect(200,200,700,700,mPaint);
//平移
canvas.translate(50,50);
//更改画笔颜色,绘制另一个矩形
mPaint.setColor(Color.BLUE);
canvas.drawRect(0,0,500,500,mPaint);
运行以上代码,看一下初步效果。
如果我将画布反向平移后,再画一条线:
//绘制矩形
canvas.drawRect(200,200,700,700,mPaint);
//平移
canvas.translate(50,50);
//更改画笔颜色,绘制另一个矩形
mPaint.setColor(Color.BLUE);
canvas.drawRect(0,0,500,500,mPaint);
//我想把画布再平移回去
canvas.translate(-50,-50);
canvas.drawLine(0,0,400,500,mPaint);
如果在初次平移画布后,我想在原点画一条线,需要先进行反向平移。针对这种情况,需要用到canvas提供的save方法进行状态的保存、restore方法进行状态恢复:
//绘制矩形
canvas.drawRect(200,200,700,700,mPaint);
//保存状态
canvas.save();
//平移
canvas.translate(50,50);
//更改画笔颜色,绘制另一个矩形
mPaint.setColor(Color.BLUE);
canvas.drawRect(0,0,500,500,mPaint);
//无需反向平移,直接恢复状态
canvas.restore();
canvas.drawLine(0,0,400,500,mPaint);
效果和上面的效果是一样的。调用sava后,无需关心后续会进行什么操作,直接一个restore方法就可以进行状态恢复。
restore方法可以多次调用:
//绘制矩形
canvas.drawRect(200,200,700,700,mPaint);
//保存状态
canvas.save();
//平移
canvas.translate(50,50);
//更改画笔颜色,绘制另一个矩形
mPaint.setColor(Color.BLUE);
canvas.drawRect(0,0,500,500,mPaint);
canvas.save();
canvas.translate(50,50);
mPaint.setColor(Color.GREEN);
canvas.drawRect(0,0,500,500,mPaint);
//无需反向平移,直接恢复状态
canvas.restore();
canvas.drawLine(0,0,400,500,mPaint);
①保存画布的状态,将画布平移到(50,50),绘制一个蓝色矩形;②保存画布状态,将画布平移到(50,50),画一个绿色矩形;③恢复画布状态,画一条直线
经过运行发现,restore将画布的状态恢复到了上一次sava时,直线从(50,50)处绘制。如果想让直线从原点开始绘制,需要再额外多调用一次restore方法将画布状态恢复到最初。
效果:
实际上,Canvas内部维护了一个状态栈,每调用一次sava方法都会进行一次压栈操作。当调用restore方法后会将状态恢复到上一次,也就是把最顶层的状态进行出栈操作。
当然,我们也可以通过getSaveCount()方法来查看保存的状态的个数(默认保存的状态初始值为1,restore最小只能将状态数恢复至1,继续调用会报错)。有兴趣的可以通过打印日志查看保存状态的个数。
restoreToCount()方法可以将状态恢复到指定的状态下,这个状态是由sava方法所返回的:
//绘制矩形
canvas.drawRect(200,200,700,700,mPaint);
//保存状态
int state = canvas.save();
//平移
canvas.translate(50,50);
//更改画笔颜色,绘制另一个矩形
mPaint.setColor(Color.BLUE);
canvas.drawRect(0,0,500,500,mPaint);
canvas.save();
canvas.translate(50,50);
mPaint.setColor(Color.GREEN);
canvas.drawRect(0,0,500,500,mPaint);
//将状态恢复到指定的程度
canvas.restoreToCount(state);
除了使用save保存状态之外,还可以使用saveLayer方法保存状态,同样返回int类型的值。保存之后同样可以通过restoreToCount,将画布状态恢复至指定程度。在图层混合模式的离屏绘制时用到了它,它会新创建一个图层,将saveLayer和restoreToCount之间的代码先绘制到图层上,然后再将最后的这个图层绘制到Canvas上。
值得注意的是,saveLayer方法可以指定图层的大小:
//先绘制一个矩形
canvas.drawRect(200,200,700,700,mPaint);
int layerId = canvas.saveLayer(0, 0, 700, 700, mPaint);
//将画笔调整颜色
mPaint.setColor(Color.BLUE);
//使用Matrix实现变换
Matrix matrix = new Matrix();
//移动到指定座标
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);
运行效果如下:
首先绘制了一个(200,200,700,700)的矩形,然后创建了(0, 0, 700, 700)的图层。 移动画布,绘制与图层大小等同的矩形,但是没有绘制完全,右下两个边已经超出了图层范围。恢复状态,重新再原点绘制一个矩形。
三、案例
粒子散落效果,得到bitmap的水平、竖直方向上各有多少个像素。通过bitmap的getPixel方法得到指定座标位置的像素的颜色值,如此就能将每一个点都封装到粒子对象中。
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.pic);
bitmap.getWidth(); //宽表示水平方向有多少像素
bitmap.getHeight(); //高表示竖直方向有多少像素
int pixel = bitmap.getPixel(0, 0);//返回当前位置像素的颜色值
首先我们来定义这么一个粒子对象:
/**
* 粒子封装对象
*/
public class Ball {
/**
* 这些已经能完全描述出一个圆
*/
//像素点的颜色值
public int color;
//粒子圆心座标
public float x,y;
//粒子半径
public float r;
/**
* 让圆动起来,进行位置变换。变换过程,模仿自由落体运动。
* 所以要定义加速度、速度
*/
//运动的x、y方向的速度
public float vX,vY;
//运动的x、y方向的加速度
public float aX,aY;
}
接下来创建自定义view:
//遍历整张bitmap的每个像素点,将其相关属性封装到Ball对象中。
//在onDraw方法中绘制这个List<Ball>集合中的每个粒子
//现在的效果,绘制的并非是一个图片,而是很多个圆
private void init(){
mPaint = new Paint();
mBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.pic);
//循环遍历这个bitmap的每个像素
for (int i = 0; i < mBitmap.getWidth(); i++) {
for (int j = 0; j < mBitmap.getHeight(); j++) {
Ball ball = new Ball();
//将每个像素点的颜色交给Ball对象
ball.color=mBitmap.getPixel(i,j);
//粒子的圆心座标x、y
ball.x=i*d+d/2;
ball.y=j*d+d/2;
//粒子半径
ball.r=d/2;
list.add(ball);
}
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//先平移画布
canvas.translate(500,500);
//遍历集合,实现绘制
for (Ball ball:list){
mPaint.setColor(ball.color);
canvas.drawCircle(ball.x,ball.y,ball.r,mPaint);
}
}
继续,当点击图片时需要产生爆炸碎裂效果,此时需要重写onTouchEvent方法:
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction()==MotionEvent.ACTION_DOWN){
//执行属性动画
}
return super.onTouchEvent(event);
}
接下来需要定义属性动画:
mValueAnimator = ValueAnimator.ofFloat(0, 1); //从0到1开始变换
mValueAnimator.setRepeatCount(-1); //重复运行
mValueAnimator.setDuration(2000); //执行时间
mValueAnimator.setInterpolator(new LinearInterpolator()); //设置线性插值器
//设置监听
mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
updateBall(); //更新粒子
invalidate(); //触发onDraw方法
}
});
关于更新粒子的updateBall()方法,需要遍历List集合,不断的修改粒子的速度、位置
/**
* 更新粒子
*/
private void updateBall() {
for (Ball ball:list){
//更新粒子的位置
ball.x+=ball.vX;
ball.y+=ball.vY;
//更新粒子速度
ball.vX+=ball.aX;
ball.vY+=ball.aY;
}
}
别忘了,在init()的for循环的时候,需要对速度、加速度都要进行初始化,给他们一个初始值:
//初始化粒子的速度、加速度。可以看出来,速度为某一个范围的随机值,
//加速度初始值水平方向为0,竖直方向为0.98f
//速度(-20,20)
ball.vX = (float) (Math.pow(-1, Math.ceil(Math.random() * 1000)) * 20 * Math.random());
ball.vY = rangInt(-15, 35);
//加速度
ball.aX = 0;
ball.aY = 0.98f;
速度、加速度搞定之后,接下来就该在DOWN事件中调用方法:
//执行属性动画
mValueAnimator.start();
点击DOWN事件后,会触发属性动画。在回调中会更新粒子的位置,之后继续触发onDraw方法。
完整代码如下:
Ball.java
/**
* 粒子封装对象
*/
public class Ball {
/**
* 这些已经能完全描述出一个圆
*/
//像素点的颜色值
public int color;
//粒子圆心座标
public float x,y;
//粒子半径
public float r;
/**
* 让圆动起来,进行位置变换。变换过程,模仿自由落体运动。
* 所以要定义加速度、速度
*/
//运动的x、y方向的速度
public float vX,vY;
//运动的x、y方向的加速度
public float aX,aY;
}
SplitView.java
public class SplitView extends View {
private Paint mPaint;
private Bitmap mBitmap;
//负责接收Bitmap对象转换的Ball对象
private List<Ball> list = new ArrayList<>();
private float d=3;//粒子直径
private ValueAnimator mValueAnimator;
public SplitView(Context context) {
super(context);
init();
}
public SplitView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public SplitView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init(){
mPaint = new Paint();
mBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.pic);
//循环遍历这个bitmap的每个像素
for (int i = 0; i < mBitmap.getWidth(); i++) {
for (int j = 0; j < mBitmap.getHeight(); j++) {
Ball ball = new Ball();
//将每个像素点的颜色交给Ball对象
ball.color=mBitmap.getPixel(i,j);
//粒子的圆心座标x、y
ball.x=i*d+d/2;
ball.y=j*d+d/2;
//粒子半径
ball.r=d/2;
//初始化粒子的速度、加速度
//速度(-20,20)
ball.vX = (float) (Math.pow(-1, Math.ceil(Math.random() * 1000)) * 20 * Math.random());
ball.vY = rangInt(-15, 35);
//加速度
ball.aX = 0;
ball.aY = 0.98f;
list.add(ball);
}
}
mValueAnimator = ValueAnimator.ofFloat(0, 1); //从0到1开始变换
mValueAnimator.setRepeatCount(-1); //重复运行
mValueAnimator.setDuration(2000); //执行时间
mValueAnimator.setInterpolator(new LinearInterpolator()); //设置线性插值器
//设置监听
mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
updateBall(); //更新粒子
invalidate(); //触发onDraw方法
}
});
}
/**
* 更新粒子
*/
private void updateBall() {
for (Ball ball:list){
//更新粒子的位置
ball.x+=ball.vX;
ball.y+=ball.vY;
//更新粒子速度
ball.vX+=ball.aX;
ball.vY+=ball.aY;
}
}
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)));
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//先平移画布
canvas.translate(500,500);
//遍历集合,实现绘制
for (Ball ball:list){
mPaint.setColor(ball.color);
canvas.drawCircle(ball.x,ball.y,ball.r,mPaint);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction()==MotionEvent.ACTION_DOWN){
//执行属性动画
mValueAnimator.start();
}
return super.onTouchEvent(event);
}
}