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();
    }
}
相关推荐
沅霖1 小时前
android 怎么查看依赖包的大小
android
开心呆哥1 小时前
如何使用 Python 控制 Android 设备的蓝牙和 WiFi
android·python
bytebeats4 小时前
Kotlin 注解全面指北
android·java·kotlin
q567315235 小时前
Python 中的字符串匹配算法
android·java·javascript·python·算法
jzlhll1235 小时前
kotlin android Handler removeCallbacks runnable不生效的一种可能
android·开发语言·kotlin
大耳猫6 小时前
Android Studio 多工程公用module引用
android·java·kotlin·android studio
良技漫谈7 小时前
Rust移动开发:Rust在Android端集成使用介绍
android·程序人生·rust·kotlin·学习方法
Erorrs10 小时前
Android13 系统/用户证书安装相关分析总结(二) 如何增加一个安装系统证书的接口
android·java·数据库
东坡大表哥11 小时前
【Android】常见问题集锦
android
ShuQiHere12 小时前
【ShuQiHere】️ 深入了解 ADB(Android Debug Bridge):您的 Android 开发利器!
android·adb