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);

在这里插入图片描述

  • 缩放
// 缩放
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实例
  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);
    }
}

在这里插入图片描述
现在的例子在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);
        }
    }
}

在这里插入图片描述

示例代码Github路径

参考文章
  1. Gcssloop自定义View系列Canvas操作
  2. 有兴趣可以去学习Gcssloop关于自定义View的所有文章
    Gcssloop自定义View系列所有文章
  3. HenCoder Android 开发进阶:自定义 View 1-4 Canvas 对绘制的辅助 clipXXX() 和 Matrix
  4. Google官方Canvas文档
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

吃骨头不吐股骨头皮

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值