android自定义view之自定义时钟wacthview

本文介绍如何自定义一个显示当前时间的时钟View,包括时针、分针和秒针的绘制过程。通过继承View并重写onMeasure和onDraw方法实现自定义View的宽高测量及动态绘制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  1. 问题描述
    自定义一个显示当前时间的时钟view,具有时针,分针,秒针。
  2. 解决方案
    自定义一个view,继承view重写ondraw方法和onmesure方法,重新测量控件宽高和重绘控件
    先上图
    这里写图片描述

首先我们在attrs文件里面声明自定义属性

<declare-styleable name="TestView">
        <attr name="src" format="reference" />
        <attr name="hour_color" format="color" />
</declare-styleable>

然后在布局文件里面定义这些自定义属性

<com.song.watchview.WatchView
        android:id="@+id/watchView1"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        custom:hour_color="#ffcc3324"
        custom:src="@drawable/photo_cheetah" />

但是不要忘记声明自己的命名空间

xmlns:custom="http://schemas.android.com/apk/res-auto"

下面就是在自定义view的构造方法里面去获得这些定义的属性,如下

public WatchView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        // attrs 中存了设定的值,R.styleable.TestView取出这一组值,
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.TestView, defStyleAttr, 0);
        Drawable drawable = array.getDrawable(R.styleable.TestView_src);
        if (drawable != null && drawable instanceof BitmapDrawable) {
            bitmap = ((BitmapDrawable) drawable).getBitmap();
        }
        color = array.getColor(R.styleable.TestView_hour_color, Color.WHITE);
        array.recycle();
        start();
    }

通过TypedArray对象获取设置的src属性和color属性,最后别忘记调用

array.recycle();

回收TypedArray对象

我们在构造方法里面启动一个间隔一秒不断让view重绘的线程

private void start() {
        if (bitmap == null) {
            bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.photo_cheetah);
        }
        new Thread() {
            @Override
            public void run() {
                while (true) {
                    // 子线程中,提醒刷新
                    postInvalidate();
                    // 主线程中刷新
                    // invalidate();
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();
    }

核心是调用postInvalidate()方法,重新调用view的ondraw方法
在ondraw方法之前,需要先测量控件的宽高,我们重写onmesure方法

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        switch (widthMode) {
            case MeasureSpec.AT_MOST:// 在一定范围内,尽可能展示完全,在wrap_content时,传入
                widthSize = Math.min(widthSize, DEFAULT_SIZE);
                break;
            case MeasureSpec.EXACTLY:// 规定大小,在match_parent或指定尺寸的时候.
                break;
            case MeasureSpec.UNSPECIFIED:// 未知大小,完全展示,在ListView子控件的高度等
                widthSize = DEFAULT_SIZE;
                break;
        }
        switch (heightMode) {
            case MeasureSpec.AT_MOST:// 在一定范围内,尽可能展示完全,在wrap_content时,传入
                heightSize = Math.min(heightSize, DEFAULT_SIZE);
                break;
            case MeasureSpec.EXACTLY:// 规定大小,在match_parent或指定尺寸的时候.
                break;
            case MeasureSpec.UNSPECIFIED:// 未知大小,完全展示,在ListView子控件的高度等
                heightSize = DEFAULT_SIZE;
                break;
        }
        if (widthMode == MeasureSpec.AT_MOST) {
            widthSize = Math.min(widthSize, heightSize);
        }
        if (heightMode == MeasureSpec.AT_MOST) {
            heightSize = Math.min(widthSize, heightSize);
        }
        widthMeasureSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
        heightMeasureSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

onMeasure方法具体实现注释已经很清楚,核心就是拿到定义的宽高,如果不满足默认的宽高就设置为默认的宽高,无需多讲

下面就是核心的onDraw方法,我们每秒钟重绘一次,所以,绘制的时针,分针和秒针都是当前的时间,那么就有思路了

private void drawWatch(Canvas canvas) {
        // 让canvas入栈,保存当前的信息
        canvas.save();
        float temp = Math.min(getWidth() / 200.0f, getHeight() / 200.0f);
        // 将画布以200位基准缩放,那么无论view宽高多大,都可以看做宽200,高200的view
        canvas.scale(temp, temp);
        // view填充黑色
        canvas.drawColor(Color.BLACK);
        {
            // 坐标100,100为圆心,半径为100的画圆,并且设置背景图片
            Paint paint = new Paint();
            // 给paint设置渲染背景图片,画出的圆带有背景图片,使用REPEAT模式渲染
            paint.setShader(new BitmapShader(bitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT));
            canvas.drawCircle(100, 100, 100, paint);
        }
        // 画表的白色边界
        Paint paint = new Paint();
        paint.setColor(Color.WHITE);
        paint.setStyle(Paint.Style.STROKE);
        // 设置画笔抗锯齿
        paint.setAntiAlias(true);
        canvas.drawCircle(100, 100, 100, paint);
        // 保存当前canvas状态
        canvas.save();
        // 画表刻度,总共有12个刻度,其中4个粗刻度
        for (int i = 0; i < 12; i++) {
            if (i % 3 == 0) {
                paint.setStrokeWidth(3);
            } else {
                paint.setStrokeWidth(1);
            }
            canvas.drawLine(100, 0, 100, 10, paint);
            // 画完一个刻度后,顺时针旋转canvas30度,然后画下一个刻度
            canvas.rotate(30, 100, 100);
        }
        // 回到上次保存的canvas状态,未旋转
        canvas.restore();
        Calendar calendar = Calendar.getInstance();
        // 同上
        canvas.save();
        // 绘制时针
        // 以100,100为基准旋转,一个小时旋转30度,当前的时刻乘以30就是时针位置
        canvas.rotate(30 * calendar.get(Calendar.HOUR) + 30 * calendar.get(Calendar.MINUTE) / 60.0f, 100, 100);
        Path path = new Path();
        // 绘制菱形时针
        path.moveTo(100, 30);
        path.lineTo(110, 100);
        path.lineTo(100, 110);
        path.lineTo(90, 100);
        paint.setStyle(Paint.Style.FILL);
        paint.setColor(color);
        paint.setAntiAlias(true);
        canvas.drawPath(path, paint);
        canvas.restore();
        canvas.save();
        // 绘制分针
        // 以100,100位基准,一分钟旋转6度,当前的时刻乘以6就是分针位置
        canvas.rotate(6 * calendar.get(Calendar.MINUTE) + 6 * calendar.get(Calendar.SECOND) / 60.0f, 100, 100);
        Path path1 = new Path();
        path1.moveTo(100, 10);
        path1.lineTo(105, 100);
        path1.lineTo(100, 110);
        path1.lineTo(95, 100);
        paint.setColor(Color.GREEN);
        paint.setAntiAlias(true);
        canvas.drawPath(path1, paint);
        canvas.restore();
        canvas.save();
        // 绘制秒针
        // 以100,100位基准,一秒钟旋转6度,当前的时刻乘以6就是秒针位置
        canvas.rotate(6 * calendar.get(Calendar.SECOND) + 6 * calendar.get(Calendar.MILLISECOND) / 1000.0f, 100, 100);
        paint.setColor(Color.RED);
        canvas.drawLine(100, 10, 100, 110, paint);
        canvas.restore();
        // 当前canvas栈还保存数据,将所有的栈数据清空
        canvas.restore();
    }

其中重要的是学会旋转canvas,来绘制时针

如果我们需要暂停表,或者重启表,那么只需要控制线程的开启与暂停就可以了,本质上还是使用手机的时间。

核心的代码和解释都介绍了,下面贴出代码地址,欢迎大家留言。
http://download.youkuaiyun.com/detail/xiangxi101/9679079

/**
* --------------
* 欢迎转载 | 转载请注明
* --------------
* 如果对你有帮助,请点击|顶|
* --------------
* 请保持谦逊 | 你会走的更远
* --------------
* @author css
* @github https://github.com/songsongbrother
* @blog http://blog.youkuaiyun.com/xiangxi101
*/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值