Android -- [SelfView] 自定义圆盘指针时钟

Android -- [SelfView] 自定义圆盘指针时钟

复制代码
ps:
	简约圆盘指针时钟,颜色可调、自由搭配;支持阿拉伯数字、罗马数字刻度显示;
效果图
使用:
xml 复制代码
<!-- 自定义属性参考 attrs.xml 文件 -->
<com.nepalese.harinetest.player.VirgoCircleClock
            android:id="@+id/circleclock"
            android:layout_width="300dp"
            android:layout_height="300dp"
            app:paddingFrame="10dp"
            app:strokeSize="5dp"
            app:offsetMark="-1dp"
            app:offsetText="-1dp"
            app:rSmall="5px"
            app:rBig="8px"
            app:needBg="true"
            app:frameColor="@color/colorBlack30"
            app:bgColor="@color/colorEye"
            app:markColor1="@color/black"
            app:markColor2="@color/colorWhite"
            app:textColor="@color/black"
            app:txtSize="18sp"
            app:displayType="type_num"/>
java 复制代码
private VirgoCircleClock circleClock;

//使用
circleClock = findViewById(R.id.circleclock);
circleClock.startPlay();
 
//注销
if (circleClock != null) {
   circleClock.releaseView();
}
码源:
1. attrs.xml
xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<resources>
	<declare-styleable name="VirgoCircleClock">
        <!--是否绘制背景 默认透明-->
        <attr name="needBg" format="boolean" />
        <!--背景颜色 默认白色-->
        <attr name="bgColor" format="color|reference"/>
        <!--边框颜色 默认黑色-->
        <attr name="frameColor" format="color|reference"/>
        <!--小刻度颜色-->
        <attr name="markColor1" format="color|reference"/>
        <!--大刻度颜色-->
        <attr name="markColor2" format="color|reference"/>
        <!--文字颜色 & 时针、分针颜色-->
        <attr name="textColor" format="color|reference"/>
        <!--秒针颜色-->
        <attr name="secondColor" format="color|reference"/>
        <!--大、小刻度点半径-->
        <attr name="rBig" format="dimension|reference"/>
        <attr name="rSmall" format="dimension|reference"/>
        <attr name="offsetMark" format="dimension|reference"/>
        <attr name="offsetText" format="dimension|reference"/>
        <!--数字类型-->
        <attr name="displayType" format="integer">
            <enum name="type_num" value="1"/>
            <enum name="type_roma" value="2"/>
        </attr>
        <!--内缩间距-->
        <attr name="paddingFrame" format="dimension|reference"/>
        <!--文字大小-->
        <attr name="txtSize" format="dimension|reference"/>
        <!--边框厚度-->
        <attr name="strokeSize" format="dimension|reference"/>
    </declare-styleable>
</resources>
2. VirgoCircleClock.java
java 复制代码
package com.nepalese.harinetest.player;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;

import androidx.annotation.Nullable;

import com.nepalese.harinetest.R;

import java.util.Calendar;

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

    private static final int DEF_RADIUS_BIG = 3;
    private static final int DEF_RADIUS_SMALL = 2;
    private static final int DEF_OFF_MARK = 2;//刻度与边框间距
    private static final int DEF_OFF_TEXT = 3;//数字与边框间距
    private static final int TYPE_NUM = 1;//阿拉伯数字
    private static final int TYPE_ROMA = 2;//罗马数字
    private static final int DEF_PADDING = 15;//内缩间距
    private static final float DEF_SIZE_TEXT = 18f;//数字大小
    private static final float DEF_FRAME_STROKE = 5f;//边框厚度
    private static final float RATE_HOUR = 0.5f;//时针与半径比例
    private static final float RATE_HOUR_TAIL = 0.05f;//时针尾巴与半径比例
    private static final float RATE_HOUR_WIDTH = 70f;//时针尾巴与半径比
    private static final float RATE_MINUTE = 0.6f;//分针与半径比例
    private static final float RATE_MINUTE_TAIL = 0.08f;//分针尾巴与半径比例
    private static final float RATE_MINUTE_WIDTH = 120f;//分针尾巴与半径比
    private static final float RATE_SECOND = 0.7f;//秒针与半径比例
    private static final float RATE_SECOND_TAIL = 0.1f;//秒针尾巴与半径比例
    private static final float RATE_SECOND_WIDTH = 240f;//秒针宽度与半径比

    private Paint paintMain;//刻度+指针
    private Paint paintFrame;//外环边框
    private Calendar calendar;

    private boolean needBg;//是否绘制背景 默认透明
    private int bgColor;//背景颜色 默认白色
    private int frameColor;//边框颜色
    private int markColor1;//小刻度颜色
    private int markColor2;//大刻度颜色
    private int textColor;//文字颜色 & 时针、分针颜色
    private int secondColor;//秒针颜色
    private int radiusBig, radiusSmall;//大、小刻度点半径
    private int offMark, offText;
    private int markY, txtY;
    private int displayStyle;//数字类型
    private int padding;//内缩间距
    private int radius;//表盘半径
    private int hours, minutes, seconds;//当前时分秒
    private int centerX, centerY;//表盘中心坐标
    private float textSize;//文字大小
    private float strokeSize;//边框厚度

    public VirgoCircleClock(Context context) {
        this(context, null);
    }

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

    public VirgoCircleClock(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

    private void init(Context context, AttributeSet attrs) {
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.VirgoCircleClock);
        needBg = typedArray.getBoolean(R.styleable.VirgoCircleClock_needBg, false);
        bgColor = typedArray.getColor(R.styleable.VirgoCircleClock_bgColor, Color.WHITE);
        frameColor = typedArray.getColor(R.styleable.VirgoCircleClock_frameColor, Color.BLACK);
        markColor1 = typedArray.getColor(R.styleable.VirgoCircleClock_markColor1, Color.BLACK);
        markColor2 = typedArray.getColor(R.styleable.VirgoCircleClock_markColor2, Color.DKGRAY);
        textColor = typedArray.getColor(R.styleable.VirgoCircleClock_textColor, Color.BLACK);
        secondColor = typedArray.getColor(R.styleable.VirgoCircleClock_secondColor, Color.RED);

        radiusBig = typedArray.getDimensionPixelSize(R.styleable.VirgoCircleClock_rBig, DEF_RADIUS_BIG);
        radiusSmall = typedArray.getDimensionPixelSize(R.styleable.VirgoCircleClock_rSmall, DEF_RADIUS_SMALL);
        offMark = typedArray.getDimensionPixelSize(R.styleable.VirgoCircleClock_offsetMark, DEF_OFF_MARK);
        offText = typedArray.getDimensionPixelSize(R.styleable.VirgoCircleClock_offsetText, DEF_OFF_TEXT);
        displayStyle = typedArray.getInteger(R.styleable.VirgoCircleClock_displayType, TYPE_NUM);
        textSize = typedArray.getDimension(R.styleable.VirgoCircleClock_txtSize, DEF_SIZE_TEXT);
        strokeSize = typedArray.getDimension(R.styleable.VirgoCircleClock_strokeSize, DEF_FRAME_STROKE);
        padding = typedArray.getDimensionPixelSize(R.styleable.VirgoCircleClock_paddingFrame, DEF_PADDING);

        initData();
    }

    /**
     * 设置|更改布局时调用
     *
     * @param width  容器宽
     * @param height 容器高
     */
    public void initLayout(int width, int height) {
        Log.d(TAG, "initLayout: " + width + " - " + height);
        //取小
        //表盘直径
        int diameter = Math.min(width, height);

        //半径
        radius = (int) ((diameter - padding - strokeSize) / 2);

        //圆心
        centerX = diameter / 2;
        centerY = diameter / 2;

        markY = (int) (padding + strokeSize + offMark);
        txtY = markY + radiusBig * 2 + offText;
    }

    private void initData() {
        paintMain = new Paint();
        paintMain.setAntiAlias(true);
        paintMain.setStyle(Paint.Style.FILL);

        paintFrame = new Paint();
        paintFrame.setAntiAlias(true);
        paintFrame.setStyle(Paint.Style.STROKE);
        paintFrame.setStrokeWidth(strokeSize);

        calendar = Calendar.getInstance();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (w > 0 && h > 0) {
            initLayout(w, h);
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (radius < 1) {
            return;
        }
        getTimes();

        //画表盘
        drawPlate(canvas);

        //画指针
        drawPointHour(canvas);
        drawPointMinutes(canvas);
        drawPointSeconds(canvas);
    }

    //刷新时间
    private void getTimes() {
        calendar.setTimeInMillis(System.currentTimeMillis());
        hours = calendar.get(Calendar.HOUR_OF_DAY);
        minutes = calendar.get(Calendar.MINUTE);
        seconds = calendar.get(Calendar.SECOND);
    }

    //画表盘
    private void drawPlate(Canvas canvas) {
        //背景
        if(needBg){
            paintMain.setColor(bgColor);
            canvas.drawCircle(centerX, centerY, radius, paintMain);
        }

        //边框
        paintFrame.setColor(frameColor);
        canvas.drawCircle(centerX, centerY, radius, paintFrame);

        canvas.save();
        //刻度
        for (int i = 0; i < 60; i++) {
            if (i % 5 == 0) {
                //大刻度
                paintMain.setColor(markColor1);
                canvas.drawCircle(centerX - radiusBig / 2f, markY, radiusBig, paintMain);
            } else {
                paintMain.setColor(markColor2);
                canvas.drawCircle(centerX - radiusSmall / 2f, markY, radiusSmall, paintMain);
            }
            canvas.rotate(6, centerX, centerY);
        }

        canvas.restore();

        paintMain.setColor(textColor);
        paintMain.setTextSize(textSize);
        //数字
        for (int i = 1; i <= 12; i++) {
            //计算每个数字所在位置的角度
            double radians = Math.toRadians(30 * i); //将角度转换为弧度,以便计算正弦值和余弦值
            String hourText;
            if (displayStyle == TYPE_ROMA) {
                hourText = getHoursGreece(i);
            } else {
                hourText = String.valueOf(i);
            }

            Rect rect = new Rect(); //获取数字的宽度和高度
            paintMain.getTextBounds(hourText, 0, hourText.length(), rect);
            int textWidth = rect.width();
            int textHeight = rect.height();
            canvas.drawText(hourText,
                    (float) (centerX + (radius - txtY) * Math.sin(radians) - textWidth / 2),
                    (float) (centerY - (radius - txtY) * Math.cos(radians) + textHeight / 2),
                    paintMain); //通过计算出来的坐标进行数字的绘制
        }
    }

    private void drawPointHour(Canvas canvas) {
        //画时针
        drawPoint(canvas, 360 / 12 * hours + (30 * minutes / 60), RATE_HOUR, RATE_HOUR_TAIL, RATE_HOUR_WIDTH);
    }

    private void drawPointMinutes(Canvas canvas) {
        //画分针
        drawPoint(canvas, 360 / 60 * minutes + (6 * seconds / 60), RATE_MINUTE, RATE_MINUTE_TAIL, RATE_MINUTE_WIDTH);
    }

    private void drawPointSeconds(Canvas canvas) {
        paintMain.setColor(secondColor);
        //画秒针
        drawPoint(canvas, 360 / 60 * seconds, RATE_SECOND, RATE_SECOND_TAIL, RATE_SECOND_WIDTH);
    }

    /**
     * 画指针
     *
     * @param canvas    画布
     * @param degree    指针走过角度
     * @param rateLen   正向长度与半径比
     * @param rateTail  尾部长度与半径比
     * @param rateWidth 宽度占半径比
     */
    private void drawPoint(Canvas canvas, int degree, float rateLen, float rateTail, float rateWidth) {
        //角度的计算由当前的小时占用的角度加上分针走过的百分比占用的角度之和
        double radians = Math.toRadians(degree);
        //时针的起点为圆的中点
        //通过三角函数计算时针终点的位置,时针最短,取长度的0.5倍
        int endX = (int) (centerX + radius * Math.cos(radians) * rateLen); //计算直线终点x坐标
        int endY = (int) (centerY + radius * Math.sin(radians) * rateLen); //计算直线终点y坐标
        canvas.save();
        paintMain.setStrokeWidth(radius / rateWidth);
        //初始角度是0,应该从12点钟开始算,所以要逆时针旋转90度
        canvas.rotate((-90), centerX, centerY); // 因为角度是从x轴为0度开始计算的,所以要逆时针旋转90度,将开始的角度调整到与y轴重合
        canvas.drawLine(centerX, centerY, endX, endY, paintMain); //根据起始坐标绘制时针
        radians = Math.toRadians(degree - 180); //时针旋转180度,绘制小尾巴
        endX = (int) (centerX + radius * Math.cos(radians) * rateTail);
        endY = (int) (centerY + radius * Math.sin(radians) * rateTail);
        canvas.drawLine(centerX, centerY, endX, endY, paintMain);
        canvas.restore();
    }

    private String getHoursGreece(int i) {
        switch (i) {
            case 1:
                return "I";
            case 2:
                return "II";
            case 3:
                return "III";
            case 4:
                return "IV";
            case 5:
                return "V";
            case 6:
                return "VI";
            case 7:
                return "VII";
            case 8:
                return "VIII";
            case 9:
                return "IX";
            case 10:
                return "X";
            case 11:
                return "XI";
            case 12:
            default:
                return "XII";
        }
    }

    //===================================================
    private final int MSG_UPDATE_TIME = 1;

    private final Handler handler = new Handler(new Handler.Callback() {
        @Override
        public boolean handleMessage(Message msg) {
            if (msg.what == MSG_UPDATE_TIME) {
                invalidate();
                startTask();
            }
            return false;
        }
    });

    private void startTask() {
        handler.removeMessages(MSG_UPDATE_TIME);
        handler.sendEmptyMessageDelayed(MSG_UPDATE_TIME, 1000L);
    }

    private void pauseTask() {
        removeMsg();
    }

    private void stopTask() {
        removeMsg();
    }

    private void removeMsg() {
        handler.removeMessages(MSG_UPDATE_TIME);
    }

    //==========================api========================================
    public void startPlay() {
        startTask();
    }

    public void pausePlay() {
        pauseTask();
    }

    public void continuePlay() {
        startTask();
    }

    public void releaseView() {
        stopTask();
    }
}
相关推荐
似霰2 小时前
安卓adb shell串口基础指令
android·adb
fatiaozhang95274 小时前
中兴云电脑W102D_晶晨S905X2_2+16G_mt7661无线_安卓9.0_线刷固件包
android·adb·电视盒子·魔百盒刷机·魔百盒固件
CYRUS_STUDIO5 小时前
Android APP 热修复原理
android·app·hotfix
鸿蒙布道师6 小时前
鸿蒙NEXT开发通知工具类(ArkTs)
android·ios·华为·harmonyos·arkts·鸿蒙系统·huawei
鸿蒙布道师6 小时前
鸿蒙NEXT开发网络相关工具类(ArkTs)
android·ios·华为·harmonyos·arkts·鸿蒙系统·huawei
大耳猫6 小时前
【解决】Android Gradle Sync 报错 Could not read workspace metadata
android·gradle·android studio
ta叫我小白6 小时前
实现 Android 图片信息获取和 EXIF 坐标解析
android·exif·经纬度
dpxiaolong7 小时前
RK3588平台用v4l工具调试USB摄像头实践(亮度,饱和度,对比度,色相等)
android·windows
tangweiguo030519878 小时前
Android 混合开发实战:统一 View 与 Compose 的浅色/深色主题方案
android
老狼孩111229 小时前
2025新版懒人精灵零基础及各板块核心系统视频教程-全分辨率免ROOT自动化开发
android·机器人·自动化·lua·脚本开发·懒人精灵·免root开发