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 分钟前
20241121 android中树结构列表(使用recyclerView实现)
android·gitee
Y多了个想法43 分钟前
RK3568 android11 适配敦泰触摸屏 FocalTech-ft5526
android·rk3568·触摸屏·tp·敦泰·focaltech·ft5526
NotesChapter2 小时前
Android吸顶效果,并有着ViewPager左右切换
android
_祝你今天愉快3 小时前
分析android :The binary version of its metadata is 1.8.0, expected version is 1.5.
android
暮志未晚Webgl3 小时前
109. UE5 GAS RPG 实现检查点的存档功能
android·java·ue5
麦田里的守望者江4 小时前
KMP 中的 expect 和 actual 声明
android·ios·kotlin
Dnelic-4 小时前
解决 Android 单元测试 No tests found for given includes:
android·junit·单元测试·问题记录·自学笔记
佛系小嘟嘟4 小时前
Android Studio不显示需要的tag日志解决办法《All logs entries are hidden by the filter》
android·ide·android studio
mariokkm4 小时前
Django一分钟:django中收集关联对象关联数据的方法
android·django·sqlite
长亭外的少年5 小时前
如何查看 Android 项目的依赖结构树
android