Android自定义控件之自定义时钟
这个是我从别的开源项目中挖出来的,真心写的很不错,然后继续下来以便不时之需,直接上代码:
WatcherBoard.java这个是自定义的时钟类
package cn.xiayiye.custormtext;
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.graphics.RectF;
import android.util.AttributeSet;
import android.view.View;
import java.util.Calendar;
/**
* ================================================
* 作 者:杨充
* 版 本:1.0
* 创建日期:2017/8/23
* 描 述:自定义钟表计时器
* 修订历史:
* ================================================
*/
public class WatcherBoard extends View {
private float mRadius; //外圆半径
private float mPadding; //边距
private float mTextSize; //文字大小
private float mHourPointWidth; //时针宽度
private float mMinutePointWidth; //分针宽度
private float mSecondPointWidth; //秒针宽度
private int mPointRadius; // 指针圆角
private float mPointEndLength; //指针末尾的长度
private int mColorLong; //长线的颜色
private int mColorShort; //短线的颜色
private int mHourPointColor; //时针的颜色
private int mMinutePointColor; //分针的颜色
private int mSecondPointColor; //秒针的颜色
private Paint mPaint; //画笔
public WatcherBoard(Context context) {
this(context, null);
}
public WatcherBoard(Context context, AttributeSet attrs) {
super(context, attrs);
obtainStyledAttrs(attrs); //获取自定义的属性
init(); //初始化画笔
}
private void obtainStyledAttrs(AttributeSet attrs) {
TypedArray array = null;
try {
array = getContext().obtainStyledAttributes(attrs, R.styleable.WatchBoard);
mPadding = array.getDimension(R.styleable.WatchBoard_wb_padding, DpToPx(10));
mTextSize = array.getDimension(R.styleable.WatchBoard_wb_text_size, SpToPx(16));
mHourPointWidth = array.getDimension(R.styleable.WatchBoard_wb_hour_pointer_width, DpToPx(5));
mMinutePointWidth = array.getDimension(R.styleable.WatchBoard_wb_minute_pointer_width, DpToPx(3));
mSecondPointWidth = array.getDimension(R.styleable.WatchBoard_wb_second_pointer_width, DpToPx(2));
mPointRadius = (int) array.getDimension(R.styleable.WatchBoard_wb_pointer_corner_radius, DpToPx(10));
mPointEndLength = array.getDimension(R.styleable.WatchBoard_wb_pointer_end_length, DpToPx(10));
mColorLong = array.getColor(R.styleable.WatchBoard_wb_scale_long_color, Color.argb(225, 0, 0, 0));
mColorShort = array.getColor(R.styleable.WatchBoard_wb_scale_short_color, Color.argb(125, 0, 0, 0));
mHourPointColor = array.getColor(R.styleable.WatchBoard_wb_hour_pointer_color, Color.BLACK);
mMinutePointColor = array.getColor(R.styleable.WatchBoard_wb_minute_pointer_color, Color.BLACK);
mSecondPointColor = array.getColor(R.styleable.WatchBoard_wb_second_pointer_color, Color.RED);
} catch (Exception e) {
//一旦出现错误全部使用默认值
mPadding = DpToPx(10);
mTextSize = SpToPx(16);
mHourPointWidth = DpToPx(5);
mMinutePointWidth = DpToPx(3);
mSecondPointWidth = DpToPx(2);
mPointRadius = (int) DpToPx(10);
mPointEndLength = DpToPx(10);
mColorLong = Color.argb(225, 0, 0, 0);
mColorShort = Color.argb(125, 0, 0, 0);
mMinutePointColor = Color.BLACK;
mSecondPointColor = Color.RED;
} finally {
if (array != null) {
array.recycle();
}
}
}
//Dp转px
private float DpToPx(int value) {
return SizeUtils.dp2px(value);
}
//sp转px
private float SpToPx(int value) {
return SizeUtils.sp2px(value);
}
//画笔初始化
private void init() {
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setDither(true);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = 1000; //设定一个最小值
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (widthMode == MeasureSpec.AT_MOST || widthMode == MeasureSpec.UNSPECIFIED ||
heightMeasureSpec == MeasureSpec.AT_MOST || heightMeasureSpec == MeasureSpec.UNSPECIFIED) {
try {
throw new NoDetermineSizeException("宽度高度至少有一个确定的值,不能同时为wrap_content");
} catch (NoDetermineSizeException e) {
e.printStackTrace();
}
} else { //至少有一个为确定值,要获取其中的最小值
if (widthMode == MeasureSpec.EXACTLY) {
width = Math.min(widthSize, width);
}
if (heightMode == MeasureSpec.EXACTLY) {
width = Math.min(heightSize, width);
}
}
setMeasuredDimension(width, width);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mRadius = (Math.min(w, h) - getPaddingLeft() - getPaddingRight()) / 2;
mPointEndLength = mRadius / 6; //尾部指针默认为半径的六分之一
}
//绘制外圆背景
public void paintCircle(Canvas canvas) {
mPaint.setColor(Color.WHITE);
mPaint.setStyle(Paint.Style.FILL);
canvas.drawCircle(0, 0, mRadius, mPaint);
}
@Override
protected void onDraw(Canvas canvas) {
canvas.save();
canvas.translate(getWidth() / 2, getHeight() / 2);
//绘制外圆背景
paintCircle(canvas);
//绘制刻度
paintScale(canvas);
//绘制指针
paintPointer(canvas);
canvas.restore();
//刷新
postInvalidateDelayed(1000);
}
private void paintPointer(Canvas canvas) {
Calendar calendar = Calendar.getInstance();
int hour = calendar.get(Calendar.HOUR_OF_DAY); //时
int minute = calendar.get(Calendar.MINUTE); //分
int second = calendar.get(Calendar.SECOND); //秒
int angleHour = (hour % 12) * 360 / 12; //时针转过的角度
int angleMinute = minute * 360 / 60; //分针转过的角度
int angleSecond = second * 360 / 60; //秒针转过的角度
//绘制时针
canvas.save();
canvas.rotate(angleHour); //旋转到时针的角度
RectF rectFHour = new RectF(-mHourPointWidth / 2, -mRadius * 3 / 5, mHourPointWidth / 2, mPointEndLength);
mPaint.setColor(mHourPointColor); //设置指针颜色
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(mHourPointWidth); //设置边界宽度
canvas.drawRoundRect(rectFHour, mPointRadius, mPointRadius, mPaint); //绘制时针
canvas.restore();
//绘制分针
canvas.save();
canvas.rotate(angleMinute);
RectF rectFMinute = new RectF(-mMinutePointWidth / 2, -mRadius * 3.5f / 5, mMinutePointWidth / 2, mPointEndLength);
mPaint.setColor(mMinutePointColor);
mPaint.setStrokeWidth(mMinutePointWidth);
canvas.drawRoundRect(rectFMinute, mPointRadius, mPointRadius, mPaint);
canvas.restore();
//绘制秒针
canvas.save();
canvas.rotate(angleSecond);
RectF rectFSecond = new RectF(-mSecondPointWidth / 2, -mRadius + 15, mSecondPointWidth / 2, mPointEndLength);
mPaint.setColor(mSecondPointColor);
mPaint.setStrokeWidth(mSecondPointWidth);
canvas.drawRoundRect(rectFSecond, mPointRadius, mPointRadius, mPaint);
canvas.restore();
//绘制中心小圆
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(mSecondPointColor);
canvas.drawCircle(0, 0, mSecondPointWidth * 4, mPaint);
}
//绘制刻度
private void paintScale(Canvas canvas) {
mPaint.setStrokeWidth(SizeUtils.dp2px(1));
int lineWidth = 0;
for (int i = 0; i < 60; i++) {
if (i % 5 == 0) { //整点
mPaint.setStrokeWidth(SizeUtils.dp2px(1.5f));
mPaint.setColor(mColorLong);
lineWidth = 40;
mPaint.setTextSize(mTextSize);
String text = ((i / 5) == 0 ? 12 : (i / 5)) + "";
Rect textBound = new Rect();
mPaint.getTextBounds(text, 0, text.length(), textBound);
mPaint.setColor(Color.BLACK);
canvas.save();
canvas.translate(0, -mRadius + DpToPx(5) + lineWidth + mPadding + (textBound.bottom - textBound.top) / 2);
mPaint.setStyle(Paint.Style.FILL);
canvas.rotate(-6 * i);
canvas.drawText(text, -(textBound.right + textBound.left) / 2, -(textBound.bottom + textBound.top) / 2, mPaint);
canvas.restore();
} else { //非整点
lineWidth = 30;
mPaint.setColor(mColorShort);
mPaint.setStrokeWidth(SizeUtils.dp2px(1));
}
canvas.drawLine(0, -mRadius + mPadding, 0, -mRadius + mPadding + lineWidth, mPaint);
canvas.rotate(6);
}
}
class NoDetermineSizeException extends Exception {
NoDetermineSizeException(String message) {
super(message);
}
}
}
WatcherBoard类里面需要使用到的两个类如下:
package cn.xiayiye.custormtext;
/**
* 创 建 者:下一页5(轻飞扬)
* 创建时间:2018/2/26.12:28
* 个人小站:http://wap.yhsh.ai(已挂)
* 最新小站:http://www.iyhsh.icoc.in
* 联系作者:企鹅 13343401268
* 博客地址:http://blog.csdn.net/xiayiye5
* 空间名称:XiaYiYeMap
* 项目包名:cn.xiayiye.custormtext
*/
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
/**
* <pre>
* author: Blankj
* blog : http://blankj.com
* time : 2016/08/02
* desc : 尺寸相关工具类
* </pre>
*/
public final class SizeUtils {
private SizeUtils() {
throw new UnsupportedOperationException("u can't instantiate me...");
}
/**
* dp转px
*
* @param dpValue dp值
* @return px值
*/
public static int dp2px(float dpValue) {
final float scale = Utils.getContext().getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
/**
* px转dp
*
* @param pxValue px值
* @return dp值
*/
public static int px2dp( float pxValue) {
final float scale = Utils.getContext().getResources().getDisplayMetrics().density;
return (int) (pxValue / scale + 0.5f);
}
/**
* sp转px
*
* @param spValue sp值
* @return px值
*/
public static int sp2px(float spValue) {
final float fontScale = Utils.getContext().getResources().getDisplayMetrics().scaledDensity;
return (int) (spValue * fontScale + 0.5f);
}
/**
* px转sp
*
* @param pxValue px值
* @return sp值
*/
public static int px2sp( float pxValue) {
final float fontScale = Utils.getContext().getResources().getDisplayMetrics().scaledDensity;
return (int) (pxValue / fontScale + 0.5f);
}
/**
* 各种单位转换
* <p>该方法存在于TypedValue</p>
*
* @param unit 单位
* @param value 值
* @param metrics DisplayMetrics
* @return 转换结果
*/
public static float applyDimension(int unit, float value, DisplayMetrics metrics) {
switch (unit) {
case TypedValue.COMPLEX_UNIT_PX:
return value;
case TypedValue.COMPLEX_UNIT_DIP:
return value * metrics.density;
case TypedValue.COMPLEX_UNIT_SP:
return value * metrics.scaledDensity;
case TypedValue.COMPLEX_UNIT_PT:
return value * metrics.xdpi * (1.0f / 72);
case TypedValue.COMPLEX_UNIT_IN:
return value * metrics.xdpi;
case TypedValue.COMPLEX_UNIT_MM:
return value * metrics.xdpi * (1.0f / 25.4f);
}
return 0;
}
/**
* 在onCreate中获取视图的尺寸
* <p>需回调onGetSizeListener接口,在onGetSize中获取view宽高</p>
* <p>用法示例如下所示</p>
* <pre>
* SizeUtils.forceGetViewSize(view, new SizeUtils.onGetSizeListener() {
* Override
* public void onGetSize(View view) {
* view.getWidth();
* }
* });
* </pre>
*
* @param view 视图
* @param listener 监听器
*/
public static void forceGetViewSize(final View view, final onGetSizeListener listener) {
view.post(new Runnable() {
@Override
public void run() {
if (listener != null) {
listener.onGetSize(view);
}
}
});
}
/**
* 获取到View尺寸的监听
*/
public interface onGetSizeListener {
void onGetSize(View view);
}
/**
* 测量视图尺寸
*
* @param view 视图
* @return arr[0]: 视图宽度, arr[1]: 视图高度
*/
public static int[] measureView(View view) {
ViewGroup.LayoutParams lp = view.getLayoutParams();
if (lp == null) {
lp = new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
);
}
int widthSpec = ViewGroup.getChildMeasureSpec(0, 0, lp.width);
int lpHeight = lp.height;
int heightSpec;
if (lpHeight > 0) {
heightSpec = View.MeasureSpec.makeMeasureSpec(lpHeight, View.MeasureSpec.EXACTLY);
} else {
heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
}
view.measure(widthSpec, heightSpec);
return new int[]{view.getMeasuredWidth(), view.getMeasuredHeight()};
}
/**
* 获取测量视图宽度
*
* @param view 视图
* @return 视图宽度
*/
public static int getMeasuredWidth(View view) {
return measureView(view)[0];
}
/**
* 获取测量视图高度
*
* @param view 视图
* @return 视图高度
*/
public static int getMeasuredHeight(View view) {
return measureView(view)[1];
}
}
package cn.xiayiye.custormtext;
/**
* 创 建 者:下一页5(轻飞扬)
* 创建时间:2018/2/26.12:30
* 个人小站:http://wap.yhsh.ai(已挂)
* 最新小站:http://www.iyhsh.icoc.in
* 联系作者:企鹅 13343401268
* 博客地址:http://blog.csdn.net/xiayiye5
* 空间名称:XiaYiYeMap
* 项目包名:cn.xiayiye.custormtext
*/
import android.annotation.SuppressLint;
import android.content.Context;
//import android.support.annotation.NonNull;
/**
* <pre>
* author: Blankj
* blog : http://blankj.com
* time : 16/12/08
* desc : Utils初始化相关
* </pre>
*/
public final class Utils {
@SuppressLint("StaticFieldLeak")
private static Context context;
private Utils() {
throw new UnsupportedOperationException("u can't instantiate me...");
}
/**
* 初始化工具类
*
* @param context 上下文
*/
/* public static void init(@NonNull Context context) {
Utils.context = context.getApplicationContext();
} */
public static void init( Context context) {
Utils.context = context.getApplicationContext();
}
/**
* 获取ApplicationContext
*
* @return ApplicationContext
*/
public static Context getContext() {
if (context != null) return context;
throw new NullPointerException("u should init first");
}
}
使用方法如下:
1.首先要在application里面进行自定义控件的初始化:
2.然后在XML布局文件中引用此控件:
<cn.xiayiye.custormtext.WatcherBoard
android:id="@+id/shi_zhong"
android:layout_width="match_parent"
android:layout_height="match_parent" />
3.在页面调用布局即可:看效果图,说明:截图反应卡顿慢,所以是5秒反应一次,正常源码里面是一秒走一次
如果看不明白的,可下载源码查看:源码下载