Android 画布Canvas与画笔Paint (附带会动的钟表)

一、Canvas绘制图形的直接对象

1、两种绘制环境:

(1)、使用普通View的canvas画图,

(2)、使用专门的SurfaceView的canvas来画图。

canvas主要是区别:

第一种适合处理量比较小,帧率比较小的动画,比如说象棋游戏之类的;

第二种主要用在游戏,高品质动画方面的画图。

因为SurfaceView中定义一个专门的线程来完成画图工作,应用程序不需要等待View的刷图,提高了性能。

View一般用于绘制静态页面或者界面元素跟随用户的操作(点击、拖拽等)而被动的改变位置、大小等

SurfaceView一般用于无需用户操作,界面元素就需要不断的刷新的情况(例如打飞机游戏不断移动的背景)

2.canvas四个常用方法:
         canvas.save();            保存画布,将之前绘制的图像全部保存起来,后续的操作好像就在一个新图层一样
         canvas.restore();        合并图层,将save之前和save之后的图像合并起来
         canvas.translate(x,y); 画布平移,,默认绘图坐标在零点,平移之后,原点由(0,0)移到(x,y),(x,y)为新原点
        canvas.rotate();         画布旋转

3.Canvas的常用的绘制方法:

(1)drawPoint(float x, float y, Paint paint)        

画点,

x:          水平x轴坐标,

y:          垂直y轴坐标,

paint:    Paint对象。

(2)drawLine(float startX, float startY, float stopX, float stopY, Paint paint) 

画线,

startX:    起始点的x轴坐标位置,

startY:    始点的y轴坐标位置,

stopX:    终点的x轴水平坐标位置,

stopY:    y轴垂直坐标位置,

paint:    Paint 画刷对象。

(3)drawRect(RectF rect, Paint paint)              

    绘制区域,

    rect:    RectF为一个区域 

(4)drawPath(Path path, Paint paint) 

    绘制一个路径,

    path:    为Path路径对象

(5)drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint)  

贴图,

bitmap:    常规的Bitmap对象,

src:          源区域(这里是bitmap),

dst:         目标区域(应该在canvas的位置和大小),

paint:       画刷对象,

因为用到了缩放和拉伸的可能,当原始Rect不等于目标Rect时性能将会有大幅损失。

(6)drawText(String text, float x, float y, Paint paint)  

渲染文本,

text:String类型的文本,

x:         参数二x轴坐标,

y:         参数三y轴坐标,

paint:    是Paint对象。

drawText的详细解释

(7)drawOval(RectF oval, Paint paint)

画椭圆,

oval:参数一是扫描区域,参数二为paint对象;

(8)drawCircle(float cx, float cy, float radius,Paint paint)

绘制圆,

cx:        圆心的x坐标,

cy:        圆心的y轴坐标,

radius:   是半径

(9)drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint)

画弧,

oval:                是RectF对象,一个区域界限用于定义在形状、大小、圆弧,

startAngle:       起始角(度)在圆弧的开始位置,

sweepAngle:    描角(度)开始顺时针测量的,

useCenter:       如果这是真的话,将是一个扇形,如果它是假这将是一个弧线,

起始角度不理解的,请看下图


二、简单的画笔Paint

Paint 代表了Canvas上的画笔、画刷、颜料等等;

Paint类常用方法:

setARGB(int a, int r, int g, int b)         设置 Paint对象颜色,参数一为alpha透明值
setAlpha(int a)                                   设置alpha不透明度,范围为0~255

setAntiAlias(boolean aa)                    是否抗锯齿
setColor(int color)                             设置颜色,这里Android内部定义的有Color类包含了一些常见颜色定义

                                                         颜色的获取方法:ContextCompat.getDrawable(getContext(), R.color.colorAccent)
setTextScaleX(float scaleX)                 设置文本缩放倍数,1.0f为原始
setTextSize(float textSize)                    设置字体大小
setUnderlineText(booleanunderlineText)     设置下划线

三、举例

通过上面讲解我们画一个简单的钟表,加深理解

钟表分为四部分:外层大圆盘,四个长时间刻度线,短时间刻度线,两个指针

说白了也是一个自定义View


1、新建java类继承View,并添加构造方法


2、重写onMeasure,确定View大小,也可以跳过此步骤,此时自定义View会充满父布局

    //第一步,我们需要知道绘制图形的大小,这个过程在OnMeasure中进行,如果不重写OnMeasure方法,自定义的View会默认充满父布局
    //首先重写onMeasure,按住ctrl点击super.onMeasure查看,发现其实是调用的setMeasuredDimension(mWidth,mHeihgt);方法,此方法将测量的宽高穿进去从而完成测量工作
    //所以重写onMeasure方法,就是把参数传给setMeasuredDimension

    //对宽高重新进行定义
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //我们调用自己的自定义方法measureSize对宽高重新定义,参数宽和高是MeasureSpec对象
        //MeasureSpec对象有两个常用方法
        //MeasureSpec.getMode(measureSpec)  得到测量模式
        //MeasureSpec.getSize(measureSpec)  得到测量大小
        mWidth = measureSize(widthMeasureSpec);
        mHeihgt = measureSize(heightMeasureSpec);
        setMeasuredDimension(mWidth,mHeihgt);
        //初始化图形
        //initView();
    }

    //我们通过测量模式,给出不同的测量值
    //当specMode = EXACTLY时,直接指定specSize即可
    //当specMode != EXACTLY时,需要指出默认大小
    //当specMode = AT_MOST时,即指定了wrap_content属性时,需要取出我们指定大小和specSize中最小的一个为最后测量值
    private int measureSize(int measureSpec) {
        int result = 0;

        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        if (specMode == MeasureSpec.EXACTLY){
            result = specSize;
        }else {
            result = 180;   //指定默认大小
            if (specMode == MeasureSpec.AT_MOST){
                result = Math.min(result,specSize);
            }
        }
        return result;
    }

3.重写onDraw方法

(1)外层大圆

首先设置大圆的的画布属性

        circlePaint = new Paint();
        circlePaint.setStyle(Paint.Style.STROKE);
        circlePaint.setAntiAlias(true);
        circlePaint.setStrokeWidth(5);
        circlePaint.setColor(ContextCompat.getColor(this.getContext(), R.color.colorAccent));

画出大圆

        radius = Math.min(mWidth,mHeihgt) / 2;
        radius = radius * 0.8f;     //让大圆占父布局的4/5
        canvas.drawCircle(mWidth/2,mHeihgt/2,radius,circlePaint);


(2)刻度线

    通过drawLine方法设置起始和结束端点就可以了。

    不过要注意有一部分刻度是斜着的,这样就难以计算坐标了,通过三角函数计算工作量大。

    所以使用使用相对的概念,我们将画布以圆心为坐标原点,每画好一条线段,就把画布旋转一定角度,当当画布重新旋转到原点时,所有刻度就已经画好了,这样就避免了三角函数的计算

初始化刻度线画笔:

        //刻度线
        degreePaint = new Paint();
        degreePaint.setStyle(Paint.Style.STROKE);
        degreePaint.setAntiAlias(true);
        degreePaint.setStrokeWidth(3);
        degreePaint.setColor(ContextCompat.getColor(this.getContext(), android.R.color.white));

绘制刻度线

        //刻度线
        //初始刻度12点的索引为0
        //之后中间空四个小刻度,然后是一个大刻度,即每加5是一个大刻度
        //所以5的0和5的倍数是一个大刻度,其他为小刻度
        //每格的度数
        int rotateAngel = 360 / 60;
        int txt;
        String time;
        for (int i = 0; i < 60; i++){

            if (i / 5 == 0){
                txt = 12;
            }else {
                txt = i / 5;
            }
            time = String.valueOf(txt);

            if (i % 5 == 0){
                //大刻度
                degreePaint.setStrokeWidth(5);
                degreePaint.setTextSize(30);
                canvas.drawLine(
                        mWidth / 2,mHeihgt / 2-radius,
                        mWidth / 2,mHeihgt / 2-radius+30,
                        degreePaint
                );

                canvas.drawText(
                        time,
                        mWidth / 2 - degreePaint.measureText(time) / 2,
                        mHeihgt / 2-radius+60,
                        degreePaint
                );
            }else {
                //小刻度
                degreePaint.setStrokeWidth(3);
                degreePaint.setTextSize(15);
                canvas.drawLine(
                        mWidth / 2,mHeihgt / 2-radius,
                        mWidth / 2,mHeihgt / 2-radius+15,
                        degreePaint
                );
            }
            /**
             * 参数
             * degrees 旋转的角度
             * (x,y)相对旋转点坐标
             * px x坐标位置
             * py y坐标位置
             */
            canvas.rotate(rotateAngel,mWidth/2,mHeihgt/2);
        }//for

如果需要把文字放正,还需要计算三角函数,此处略过

(2)时针/分针/秒针

注意三个指针的长度宽度均不一样

初始化指针画笔:

        //指针
        fingerPaint = new Paint();
        fingerPaint.setStyle(Paint.Style.STROKE);
        fingerPaint.setAntiAlias(true);

绘制指针:

        //时针
        canvas.translate(mWidth/2,mHeihgt/2);
        fingerPaint.setStrokeWidth(15);
        fingerPaint.setColor(ContextCompat.getColor(this.getContext(), android.R.color.black));
        canvas.drawLine(0,0,0,-mWidth / 6,fingerPaint);
        //分针
        fingerPaint.setStrokeWidth(10);
        fingerPaint.setColor(ContextCompat.getColor(this.getContext(), android.R.color.holo_red_light));
        canvas.drawLine(0,0,0,-mWidth / 5,fingerPaint);
        //秒针
        fingerPaint.setStrokeWidth(5);
        fingerPaint.setColor(ContextCompat.getColor(this.getContext(), android.R.color.holo_blue_light));
        canvas.drawLine(0,0,0,-mWidth / 4,fingerPaint);

        //绘制一个圆心:
        canvas.drawCircle(0, 0, 5, degreePaint);

4.让钟表走起来:

View一般用于绘制静态页面或者界面元素跟随用户的操作(点击、拖拽等)而被动的改变位置、大小等

SurfaceView一般用于无需用户操作,界面元素就需要不断的刷新的情况(例如打飞机游戏不断移动的背景)

但并不是不能完成画面的更新,

只要在onDraw方法中调用this.invalidate();即可实时更新,因为invalidate会调用onDraw方法

此处我们使用View,之后在另一篇文章里我会用SurfaceView


要想和系统时间一样,首先要得到系统时间:

        curTime.setToNow();             // 取得系统时间。
        int hour = curTime.hour;       // 0-23
        int minute = curTime.minute;
        int second = curTime.second;
        //Log.e("Liang", "Time获取当前日期"+hour+":"+minute+":"+second );
之后就通过算法,旋转对应的角度即可:如时针
canvas.rotate( hour % 12 / 12f * 360, 0, 0);

先对12取余是因为时间可能是24进制的,

最后时分秒指针画完后,只要在onDraw方法中调用this.invalidate()即可实时更新,因为invalidate会调用onDraw方法

时分针画法:

        curTime.setToNow();                  // 取得系统时间。
        int hour = curTime.hour;            // 0-23
        int minute = curTime.minute  + 1 ;  //0-59
        int second = curTime.second + 1;    //0-59
        //Log.e("Liang", "Time获取当前日期"+hour+":"+minute+":"+second );

        float degress = 0;      //画布旋转角度
        //时针
        canvas.rotate( -degress, 0, 0);                 //还原画布方向
        degress = hour % 12 / 12f * 360 +  minute / 60f * 30;   //旋转的角度
        canvas.rotate(  degress , 0, 0);                //旋转画布
        fingerPaint.setStrokeWidth(15);
        fingerPaint.setColor(ContextCompat.getColor(this.getContext(), android.R.color.black));
        //画指针,其中注意x轴正方向朝右,y轴正方向朝下
        canvas.drawLine(0,0,0,-hourPointerLength,fingerPaint);

        //分针
        canvas.rotate( -degress , 0, 0);            //还原画布方向
        degress = minute / 60f * 360 + second / 60f * 6 ;   //旋转的角度
        canvas.rotate( degress , 0, 0);             //旋转画布
        fingerPaint.setStrokeWidth(10);
        fingerPaint.setColor(ContextCompat.getColor(this.getContext(), android.R.color.holo_red_light));
        //画指针,其中注意x轴正方向朝右,y轴正方向朝下
        canvas.drawLine(0,0,0,-minutePointerLength,fingerPaint);
        //秒针
        canvas.rotate( -degress, 0, 0);             //还原画布方向
        degress = second / 60f * 360;                       //旋转的角度
        canvas.rotate( degress, 0, 0);              //旋转画布
        fingerPaint.setStrokeWidth(5);
        fingerPaint.setColor(ContextCompat.getColor(this.getContext(), android.R.color.holo_blue_light));
        //画指针,其中注意x轴正方向朝右,y轴正方向朝下
        canvas.drawLine(0,0,0,-secondPointerLength,fingerPaint);

        //绘制一个圆心:
        canvas.drawCircle(0, 0, 5, degreePaint);
        canvas.restore();
        this.invalidate();//调用onDraw方法,这样就会一直更新onDraw

全部代码:

package myView;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.icu.util.Calendar;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.text.format.Time;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;

import com.liang.myview.R;

/**
 * Created by Amarao on 2018/6/24.
 */

public class MyClock extends View {

    private int mWidth;
    private int mHeihgt;

    private Paint circlePaint,degreePaint,fingerPaint;
    private float radius;

    private Time curTime;
    //秒针长度
    private float secondPointerLength;
    //分针长度
    private float minutePointerLength;
    //时针长度
    private float hourPointerLength;


    public MyClock(Context context) {
        super(context);
    }

    public MyClock(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        curTime = new Time();
    }

    //第一步,我们需要知道绘制图形的大小,这个过程在OnMeasure中进行,如果不重写OnMeasure方法,自定义的View会默认充满父布局
    //首先重写onMeasure,按住ctrl点击super.onMeasure查看,发现其实是调用的setMeasuredDimension(mWidth,mHeihgt);方法,此方法将测量的宽高穿进去从而完成测量工作
    //所以重写onMeasure方法,就是把参数传给setMeasuredDimension

    //对宽高重新进行定义
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //我们调用自己的自定义方法measureSize对宽高重新定义,参数宽和高是MeasureSpec对象
        //MeasureSpec对象有两个常用方法
        //MeasureSpec.getMode(measureSpec)  得到测量模式
        //MeasureSpec.getSize(measureSpec)  得到测量大小
        mWidth = measureSize(widthMeasureSpec);
        mHeihgt = measureSize(heightMeasureSpec);
        setMeasuredDimension(mWidth,mHeihgt);
        //初始化图形
        //initView();
    }

    //我们通过测量模式,给出不同的测量值
    //当specMode = EXACTLY时,直接指定specSize即可
    //当specMode != EXACTLY时,需要指出默认大小
    //当specMode = AT_MOST时,即指定了wrap_content属性时,需要取出我们指定大小和specSize中最小的一个为最后测量值
    private int measureSize(int measureSpec) {
        int result = 0;

        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        if (specMode == MeasureSpec.EXACTLY){
            result = specSize;
        }else {
            result = 180;   //指定默认大小
            if (specMode == MeasureSpec.AT_MOST){
                result = Math.min(result,specSize);
            }
        }
        return result;
    }

    private void initPaint(){
        //外层大圆
        circlePaint = new Paint();
        circlePaint.setStyle(Paint.Style.STROKE);
        circlePaint.setAntiAlias(true);
        circlePaint.setStrokeWidth(5);
        circlePaint.setColor(ContextCompat.getColor(this.getContext(), R.color.colorAccent));
        //刻度线
        degreePaint = new Paint();
        degreePaint.setStyle(Paint.Style.STROKE);
        degreePaint.setAntiAlias(true);
        degreePaint.setStrokeWidth(3);
        degreePaint.setColor(ContextCompat.getColor(this.getContext(), android.R.color.white));
        //指针
        fingerPaint = new Paint();
        fingerPaint.setStyle(Paint.Style.STROKE);
        fingerPaint.setAntiAlias(true);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        initPaint();
        super.onDraw(canvas);
        //外层大圆
        radius = Math.min(mWidth,mHeihgt) / 2;
        radius = radius * 0.8f;     //让大圆占父布局的4/5
        canvas.drawCircle(mWidth/2,mHeihgt/2,radius,circlePaint);
        //刻度线
        //初始刻度12点的索引为0
        //之后中间空四个小刻度,然后是一个大刻度,即每加5是一个大刻度
        //所以5的0和5的倍数是一个大刻度,其他为小刻度
        //每格的度数
        int rotateAngel = 360 / 60;
        int txt;
        String time;
        for (int i = 0; i < 60; i++){

            if (i / 5 == 0){
                txt = 12;
            }else {
                txt = i / 5;
            }
            time = String.valueOf(txt);

            if (i % 5 == 0){
                //大刻度
                degreePaint.setStrokeWidth(5);
                degreePaint.setTextSize(30);
                canvas.drawLine(
                        mWidth / 2,mHeihgt / 2-radius,
                        mWidth / 2,mHeihgt / 2-radius+30,
                        degreePaint
                );
                degreePaint.setStrokeWidth(3);
                canvas.drawText(
                        time,
                        mWidth / 2 - degreePaint.measureText(time) / 2,
                        mHeihgt / 2-radius+60,
                        degreePaint
                );
            }else {
                //小刻度
                degreePaint.setStrokeWidth(3);
                degreePaint.setTextSize(15);
                canvas.drawLine(
                        mWidth / 2,mHeihgt / 2-radius,
                        mWidth / 2,mHeihgt / 2-radius+15,
                        degreePaint
                );
            }
            /**
             * 参数
             * degrees 旋转的角度
             * (x,y)相对旋转点坐标
             * px x坐标位置
             * py y坐标位置
             */
            canvas.rotate(rotateAngel,mWidth/2,mHeihgt/2);
        }//for

        //将坐标中心移到圆心
        canvas.translate(mWidth/2,mHeihgt/2);
        hourPointerLength = mWidth * 0.3f;
        minutePointerLength = mWidth * 0.35f;
        secondPointerLength = mWidth * 0.4f;

        curTime.setToNow();                  // 取得系统时间。
        int hour = curTime.hour;            // 0-23
        int minute = curTime.minute ;  //0-59
        int second = curTime.second ;    //0-59
        //Log.e("Liang", "Time获取当前日期"+hour+":"+minute+":"+second );

        float degress = 0;      //画布旋转角度
        //时针
        canvas.rotate( -degress, 0, 0);                 //还原画布方向
        degress = hour % 12 / 12f * 360 +  minute / 60f * 30;   //旋转的角度
        canvas.rotate(  degress , 0, 0);                //旋转画布
        fingerPaint.setStrokeWidth(15);
        fingerPaint.setColor(ContextCompat.getColor(this.getContext(), android.R.color.black));
        //画指针,其中注意x轴正方向朝右,y轴正方向朝下
        canvas.drawLine(0,0,0,-hourPointerLength,fingerPaint);

        //分针
        canvas.rotate( -degress , 0, 0);            //还原画布方向
        degress = minute / 60f * 360 + second / 60f * 6 ;   //旋转的角度
        canvas.rotate( degress , 0, 0);             //旋转画布
        fingerPaint.setStrokeWidth(10);
        fingerPaint.setColor(ContextCompat.getColor(this.getContext(), android.R.color.holo_red_light));
        //画指针,其中注意x轴正方向朝右,y轴正方向朝下
        canvas.drawLine(0,0,0,-minutePointerLength,fingerPaint);
        //秒针
        canvas.rotate( -degress, 0, 0);             //还原画布方向
        degress = second / 60f * 360;                       //旋转的角度
        canvas.rotate( degress, 0, 0);              //旋转画布
        fingerPaint.setStrokeWidth(5);
        fingerPaint.setColor(ContextCompat.getColor(this.getContext(), android.R.color.holo_blue_light));
        //画指针,其中注意x轴正方向朝右,y轴正方向朝下
        canvas.drawLine(0,0,0,-secondPointerLength,fingerPaint);

        //绘制一个圆心:
        canvas.drawCircle(0, 0, 5, degreePaint);
        canvas.restore();
        this.invalidate();//调用onDraw方法,这样就会一直更新onDraw

    }//onDraw

    private void runClock(){

    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Amarao

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

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

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

打赏作者

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

抵扣说明:

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

余额充值