android 自定义view

本文我们将自定义一个View,来实现一个时钟,先看一下效果图。
在这里插入图片描述这里只是截取了一个静态图,实际上可以秒针是运动的。至于其他的更好看的效果在这基础上可以自己添加。

1、属性设定

在res/values 目录下新建立一个文件attrs.xml, 我们将在里面定义时钟所需要的属性,这里我只是定义了4个属性,分别是时、分、秒、背景颜色。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="WatchView">
        <attr name="hour" format="integer"/>
        <attr name="min" format="integer"/>
        <attr name="sec" format="integer"/>
        <attr name="background_color" format="color"/>
    </declare-styleable>
</resources>

2、自定义WatchView

新建一个类WatchView继承自View,然后添加上画笔、指针数字、长度等等成员。在构造函数中,我们获取布局文件中定义的属性值。

public class WatchView extends View {
    private static final String TAG = "WatchView";

    private Paint mPaint;

    private int mCircleWidth = 20;
    private int radius = 500;
    // 这些下面都会重新计算
    private int lenHour = 200;
    private int lenMin = 300;
    private int lenSec = 400;

    private int mProgressSecond = 0;
    private int mProgressMin = 30;
    private int mProgressHour = 11;

    // background
    private int mBackgroundColor;
    public WatchView(Context context) {
        this(context,null);
    }

    public WatchView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);

    }

    public WatchView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        // 获取布局文件中定义的属性值并转换
        TypedArray array = context.obtainStyledAttributes(attrs,R.styleable.WatchView);
        mProgressHour = array.getInteger(R.styleable.WatchView_hour,0);
        mProgressHour %= 12;
        mProgressMin = array.getInteger(R.styleable.WatchView_min,0);
        mProgressMin %= 60;
        mProgressSecond = array.getInteger(R.styleable.WatchView_sec,0);
        mProgressSecond %= 60;
        mBackgroundColor = array.getColor(R.styleable.WatchView_background_color,getResources().getColor(R.color.white));
        // 一定不要忘记资源回收
        array.recycle();

    }
    
}

接下来初始化画笔的各个属性。

    private void Init(){
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeCap(Paint.Cap.ROUND);
        mPaint.setColor(getResources().getColor(R.color.black));
        mPaint.setStrokeWidth(10); 
        mPaint.setAntiAlias(true);
        mPaint.setTextSize(80);
        mPaint.setTextAlign(Paint.Align.CENTER);

    }

上面这些属性的含义比较简单,如果有不懂的一看源码就清楚了。

自定义View还需要处理的是wrap_content和match_parent这两个属性,在View的源码中,它对这两个的处理是相同的,所以你会看到自己定义的View设置成wrap_content和match_parent其大小都是一样的,因此我们需要给wrap_content设置一个值,自己确定就好了。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSepcSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        // 继承自自定义View,需要给wrap_content这种模式指定一个具体的值,否则它的大小和match_parent没有区别
        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(500,500);
        }else if (widthSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(500,heightSpecSize);
        }else if (heightSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(widthSepcSize,500);
        }

    }

接下来我们就需要对最开始定义的那些属性初始化了,圆盘半径啊,指针长度这些,我这里半径是根据整个View的长宽来确定的,那需要在哪里去确定这些数值呢?如果我们在初始化函数或者是onMeasure函数中设置,将会发现getWidth大小是0 , 因为这个时候view 的大小还没有确定完成,而如果我们把这些计算都放在onDraw里面去完成的话,View的绘制效率又会变得很低,因为每次刷新都会调用一次onDraw函数,所以不宜将过多的工作放在里面完成,因此可以将这些工作放在onLayout函数里面完成。

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        // 初始化半径,指针长度等,这些计算不能放到初始化函数和onMeasure中完成,因为那时宽度还没有确定,getWidth = 0.
        // 也不要放到onDraw里面做,因为onDraw调用比较频繁,最好不要放太多操作进去
        int w = getWidth() - getPaddingLeft() - getPaddingRight();
        int h = getHeight() - getPaddingTop() - getPaddingBottom();
        if (w > h){
            radius = h / 2;
        }else {
            radius = w / 2;
        }
        // 确定指针长度
        lenSec = (int) ((float)(radius) * 3 / 4);
        lenMin = (int) ((float)(radius) / 2);
        lenHour = (int) ((float)(radius) / 4);
    }

3、开始绘制

开始绘制之前,我们先理解以下View中获取坐标,宽高的一些方法

宽高可以直接通过getwidth()和getHeight()获得,也可以通过
getRight() - getLeft()获取到宽。getX() 和 getY()可以获取到左上角顶点的坐标。
一定要注意的是,这些数据都是相对坐标,都是在父容器中的相对位置。利用canvas绘制图形的时候却又不一样,它会将canvas的左上角坐标定为(0,0),这时考虑坐标点又不是相对于父布局了, 因此绘制过程中要特别注意。如果想要获取到绝对坐标,可以通过

        int[] xy = new int[2];
        getLocationOnScreen(xy);
        Log.d(TAG, "onDraw X: "+xy[0]);
        Log.d(TAG, "onDraw y: "+xy[1]);

在处理点击等事件时,MotionEvent中的getRawX()获取到的也是绝对坐标。

在onDraw函数中开始我们的绘制工作,首先是绘制表盘和背景颜色

        int center;
        if (getWidth() > getHeight()){
            center = getHeight() / 2;
        }else {
            center = getWidth() / 2;
        }
        // 圆心坐标
        float xCenter = center;
        float yCenter = center;
        // 表框
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(mCircleWidth);
        mPaint.setColor(getResources().getColor(R.color.black));
        canvas.drawCircle(center,center,radius,mPaint);
        // 在指针的旋转中心画一个小圈圈
        canvas.drawCircle(xCenter,yCenter,10,mPaint);
        // 绘制背景
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setColor(mBackgroundColor);
        canvas.drawCircle(center,center,radius - mCircleWidth / 2,mPaint);
        mPaint.setStrokeWidth(mCircleWidth);
        mPaint.setColor(getResources().getColor(R.color.black));

接下来绘制表盘上的刻度,对于大刻度和小刻度要分开处理,总共绘制了60个刻度,计算时,我是按照时钟的数字从0点开始顺时针计算的,每个刻度之间的间隔是6度。

        // 绘制12个大刻度和其他小刻度
        for (int i = 0; i < 60; i++) {
            // 大刻度
            int len = 0;
            if (i%5 == 0){
                len = 30;
                mPaint.setStrokeWidth(20);
            }else {
                len = 20;
                mPaint.setStrokeWidth(10);
            }
            double hudu = i * 6 * Math.PI / 180;
            double sin1 = Math.sin(hudu);
            double cos1 = Math.cos(hudu);
            float xEnd = (float) (radius * sin1)+xCenter;
            float yEnd = -(float) (radius * cos1)+yCenter;

            float xStart = (float) ((radius-len) * sin1)+xCenter;
            float yStart = -(float) ((radius-len) * cos1)+yCenter;

            canvas.drawLine(xStart,yStart,xEnd,yEnd,mPaint);
        }

下一步绘制时针、分针、秒钟等,坐标计算的公式都差不多,但是看起来会比较难以理解,比如计算时针角度时,假设现在是8点,以12点为0度开始的地方,那它所占的角度应该是8 / 12 * 360, 但是小时我们是用mProgressHour表示的,它是一个整型,8 / 12 为0 ,除非我们先将它转为float类型,所以计算时我调整了一下计算的顺序,看时需要自己揣摩一下。

        /**
         * 下面我计算弧度时算式看起来很混乱,这是因为时分秒都是int类型的,
         * 比如计算mProgressMin/60, 我们希望得到的结果是0.5, 但是实际上是0, 所以我调整了一下计算顺序,
         * 也可以先转换类型再计算
         */
        // 绘制秒针
        mPaint.setStrokeWidth(10);
        double sec = mProgressSecond * 360 * Math.PI / 60 / 180;
        float sStart = (float) (lenSec * Math.sin(sec)) + xCenter;
        float sEnd = - (float) (lenSec * Math.cos(sec)) + yCenter;
        canvas.drawLine(xCenter, yCenter, sStart, sEnd, mPaint);

        // 绘制分针
        mPaint.setStrokeWidth(20);
        double min = mProgressMin * 360 * Math.PI / 60 / 180;
        float mStart = (float) (lenMin * Math.sin(min)) + xCenter;
        float mEnd = - (float) (lenMin * Math.cos(min)) + yCenter;
        canvas.drawLine(xCenter, yCenter, mStart, mEnd, mPaint);

        // 绘制时针,时针应该按照分针偏移一定角度
        mPaint.setStrokeWidth(30);
        // 先计算所占的角度mProgressHour / 12 * 360, 再计算弧度 / 180 * Math.PI
        double hour = mProgressHour * 360 * Math.PI / 12 / 180;
        // 分会导致时针偏移一定的角度
        hour += mProgressMin * 30 * Math.PI / 180 / 60;
        float hStart = (float) (lenHour * Math.sin(hour)) + xCenter;
        float hEnd = - (float) (lenHour * Math.cos(hour)) + yCenter;
        canvas.drawLine(xCenter, yCenter, hStart, hEnd, mPaint);

最后是绘制表盘上的数字,这里需要让数字居中显示,具体的绘制原理和显示方式参考

https://www.jianshu.com/p/8b97627b21c4

        // 绘制数字
        mPaint.setStrokeWidth(radius/60);
        mPaint.setTextSize((float) (radius / 3.5));
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setTextAlign(Paint.Align.CENTER);
        // 绘制数字时,为了使数字居中对其,应该给y坐标一定的偏移量
        Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
        float distance = (fontMetrics.bottom - fontMetrics.top)/2 - fontMetrics.bottom;
        for (int i = 0; i < 12; i++) {
            double d = (i+1) * 30 * Math.PI / 180;
            float x = (float) ((radius - 100) * Math.sin(d) + xCenter);
            float y = (float) (-(radius - 100) * Math.cos(d) + yCenter);
            float baseline = y + distance;

            canvas.drawText(String.valueOf(i+1),x,baseline,mPaint);
        }

绘制工作到这里就大功告成了。接下来开启一个线程,每秒钟刷新一次数据。

        // 绘图线程
        new Thread(){
            @Override
            public void run() {
                while (true){
                    mProgressSecond +=1;
                    if (mProgressSecond == 60){
                        mProgressSecond = 0;
                        mProgressMin += 1;
                        // 处理时针
                        if (mProgressMin == 60){
                            mProgressMin = 0;
                            mProgressHour += 1;
                        }
                    }
                    // 重新绘制
                    postInvalidate();
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();

最后,附上全部代码。

public class WatchView extends View {
    private static final String TAG = "WatchView";

    private Paint mPaint;

    private int mCircleWidth = 20;
    private int radius = 500;
    // 这些下面都会重新计算
    private int lenHour = 200;
    private int lenMin = 300;
    private int lenSec = 400;

    private int mProgressSecond = 0;
    private int mProgressMin = 30;
    private int mProgressHour = 11;

    // background
    private int mBackgroundColor;
    public WatchView(Context context) {
        this(context,null);
    }

    public WatchView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);

    }

    public WatchView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        Init();
        TypedArray array = context.obtainStyledAttributes(attrs,R.styleable.WatchView);
        mProgressHour = array.getInteger(R.styleable.WatchView_hour,0);
        mProgressHour %= 12;
        mProgressMin = array.getInteger(R.styleable.WatchView_min,0);
        mProgressMin %= 60;
        mProgressSecond = array.getInteger(R.styleable.WatchView_sec,0);
        mProgressSecond %= 60;
        mBackgroundColor = array.getColor(R.styleable.WatchView_background_color,getResources().getColor(R.color.white));
        array.recycle();

    }

    public int getmProgressSecond() {
        return mProgressSecond;
    }

    public void setmProgressSecond(int mProgressSecond) {
        this.mProgressSecond = mProgressSecond % 60;
    }

    public int getmProgressMin() {
        return mProgressMin;
    }

    public void setmProgressMin(int mProgressMin) {
        this.mProgressMin = mProgressMin % 60;
    }

    public int getmProgressHour() {
        return mProgressHour;
    }

    public void setmProgressHour(int mProgressHour) {
        this.mProgressHour = mProgressHour % 12;
    }

    private void Init(){
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeCap(Paint.Cap.ROUND);
        mPaint.setColor(getResources().getColor(R.color.black));
        mPaint.setStrokeWidth(10);  // 圆环宽度10
        mPaint.setAntiAlias(true);
        mPaint.setTextSize(100);
        mPaint.setTextAlign(Paint.Align.CENTER);

        // 绘图线程
        new Thread(){
            @Override
            public void run() {
                while (true){
                    mProgressSecond +=1;
                    if (mProgressSecond == 60){
                        mProgressSecond = 0;
                        mProgressMin += 1;
                        // 处理时针
                        if (mProgressMin == 60){
                            mProgressMin = 0;
                            mProgressHour += 1;
                        }
                    }

                    // 重新绘制
                    postInvalidate();
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSepcSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        // 继承自自定义View,需要给wrap_content这种模式指定一个具体的值,否则它的大小和match_parent没有区别
        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(500,500);
        }else if (widthSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(500,heightSpecSize);
        }else if (heightSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(widthSepcSize,500);
        }

    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        // 初始化半径,指针长度等,这些计算不能放到初始化函数和onMeasure中完成,因为那时宽度还没有确定,getWidth = 0.
        // 也不要放到onDraw里面做,因为onDraw调用比较频繁,最好不要放太多操作进去
        int w = getWidth() - getPaddingLeft() - getPaddingRight();
        int h = getHeight() - getPaddingTop() - getPaddingBottom();
        if (w > h){
            radius = h / 2;
        }else {
            radius = w / 2;
        }
        // 确定指针长度
        lenSec = (int) ((float)(radius) * 3 / 4);
        lenMin = (int) ((float)(radius) / 2);
        lenHour = (int) ((float)(radius) / 4);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int center;
        if (getWidth() > getHeight()){
            center = getHeight() / 2;
        }else {
            center = getWidth() / 2;
        }
        // 圆心坐标
        float xCenter = center;
        float yCenter = center;
        // 表框
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(mCircleWidth);
        mPaint.setColor(getResources().getColor(R.color.black));
        canvas.drawCircle(center,center,radius,mPaint);
        // 在指针的旋转中心画一个小圈圈
        canvas.drawCircle(xCenter,yCenter,10,mPaint);
        // 绘制背景
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setColor(mBackgroundColor);
        canvas.drawCircle(center,center,radius - mCircleWidth / 2,mPaint);
        mPaint.setStrokeWidth(mCircleWidth);
        mPaint.setColor(getResources().getColor(R.color.black));


        // 绘制12个大刻度和其他小刻度
        for (int i = 0; i < 60; i++) {
            // 大刻度
            int len = 0;
            if (i%5 == 0){
                len = 30;
                mPaint.setStrokeWidth(20);
            }else {
                len = 20;
                mPaint.setStrokeWidth(10);
            }
            double hudu = i * 6 * Math.PI / 180;
            double sin1 = Math.sin(hudu);
            double cos1 = Math.cos(hudu);
            float xEnd = (float) (radius * sin1)+xCenter;
            float yEnd = -(float) (radius * cos1)+yCenter;

            float xStart = (float) ((radius-len) * sin1)+xCenter;
            float yStart = -(float) ((radius-len) * cos1)+yCenter;

            canvas.drawLine(xStart,yStart,xEnd,yEnd,mPaint);
        }
        /**
         * 下面我计算弧度时算式看起来很混乱,这是因为时分秒都是int类型的,
         * 比如计算mProgressMin/60, 我们希望得到的结果是0.5, 但是实际上是0, 所以我调整了一下计算顺序,
         * 也可以先转换类型再计算
         */
        // 绘制秒针
        mPaint.setStrokeWidth(10);
        double sec = mProgressSecond * 360 * Math.PI / 60 / 180;
        float sStart = (float) (lenSec * Math.sin(sec)) + xCenter;
        float sEnd = - (float) (lenSec * Math.cos(sec)) + yCenter;
        canvas.drawLine(xCenter, yCenter, sStart, sEnd, mPaint);

        // 绘制分针
        mPaint.setStrokeWidth(20);
        double min = mProgressMin * 360 * Math.PI / 60 / 180;
        float mStart = (float) (lenMin * Math.sin(min)) + xCenter;
        float mEnd = - (float) (lenMin * Math.cos(min)) + yCenter;
        canvas.drawLine(xCenter, yCenter, mStart, mEnd, mPaint);

        // 绘制时针,时针应该按照分针偏移一定角度
        mPaint.setStrokeWidth(30);
        // 先计算所占的角度mProgressHour / 12 * 360, 再计算弧度 / 180 * Math.PI
        double hour = mProgressHour * 360 * Math.PI / 12 / 180;
        // 分会导致时针偏移一定的角度
        hour += mProgressMin * 30 * Math.PI / 180 / 60;
        float hStart = (float) (lenHour * Math.sin(hour)) + xCenter;
        float hEnd = - (float) (lenHour * Math.cos(hour)) + yCenter;
        canvas.drawLine(xCenter, yCenter, hStart, hEnd, mPaint);

        // 绘制数字
        mPaint.setStrokeWidth(radius/60);
        mPaint.setTextSize((float) (radius / 3.5));
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setTextAlign(Paint.Align.CENTER);
        // 绘制数字时,为了使数字居中对其,应该给y坐标一定的偏移量
        Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
        float distance = (fontMetrics.bottom - fontMetrics.top)/2 - fontMetrics.bottom;
        for (int i = 0; i < 12; i++) {
            double d = (i+1) * 30 * Math.PI / 180;
            float x = (float) ((radius - 100) * Math.sin(d) + xCenter);
            float y = (float) (-(radius - 100) * Math.cos(d) + yCenter);
            float baseline = y + distance;

            canvas.drawText(String.valueOf(i+1),x,baseline,mPaint);
        }

    }

}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值