Android动态秒针插件app,安卓自定义View,仿小米秒钟

效果图:

a3c46ca150d8

这里写图片描述

前言:

自定义view,是开发者必备的技能之一,也是找工作时面试官必问的题目。

有文章把自定义控件归纳为三种:

一、自绘控件,即继承View,在onDraw()内使用canvas绘制;

二、组合控件,即把常用的控件组合在一起,变成新的控件;

三、继承控件,即继承一个常用的View,修改、增加某个方法等。

组合控件最常用,自绘控件最体现水平。网上很多入门教程也很详细,本篇也会通过实例细讲绘制过程。总结下来就是更多的:“计算”(计算位置、计算距离等等),所以打开AndroidStudio的同时,也请准备好计算器。

正文

新建StopwatchView 继承View ,除了构造方法外,有两个方法必须得重写:测量尺寸onMeasure(xxx)和绘制图形onDraw(xxx)

public class StopwatchView extends View {

public StopwatchView(Context context, @Nullable AttributeSet attrs) {

super(context, attrs);

}

@Override

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

super.onMeasure(widthMeasureSpec, heightMeasureSpec);

}

@Override

protected void onDraw(Canvas canvas) {

super.onDraw(canvas);

}

}

一、onMeasure方法:

系统在绘制图形前,会先测量图形尺寸等相关参数,然后根据尺寸进行绘制。

在Demo中,我们的秒表始终保持圆形,但View的宽高设定可以有三种情况:match_parent、wrap_content、定值,所以我们重写onMeasure()来适配这三种情况

@Override

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

//重新定义尺寸,保证为正方形

int width = measuredDimension(widthMeasureSpec);

int height = measuredDimension(heightMeasureSpec);

mLen = Math.min(width, height);

//小三角形指针端点到外圆之间的距离,用于计算三角形坐标[这里取整体宽度的1/16]

mTriangleLen = (float) mLen / 16.0f;

//提交设置 新的值

setMeasuredDimension(mLen, mLen);

}

//适配不同尺寸

private int measuredDimension(int measureSpec) {

int defaultSize = 800; //默认大小

int mode = MeasureSpec.getMode(measureSpec); //宽高度设定方式

int size = MeasureSpec.getSize(measureSpec); //宽高度测量大小

switch (mode) {

case MeasureSpec.EXACTLY: //尺寸指定

return size;

case MeasureSpec.AT_MOST: //match_parent

return size;

case MeasureSpec.UNSPECIFIED: //wrap_content

return defaultSize;

default:

return defaultSize;

}

}

说明:1、mLen 是最终外围宽高度。内部其他各元素的宽高、大小等都要以此为基准。简单来说,就是其他各元素都要按照mLen的值进行比例分配,不能设定死。否则可能出现不同尺寸下,内部元素比例不协调的情况 2、MeasureSpec 看起来比较陌生,其实内部只有三个常量、三个方法,如上面的代码所写,重写目的一是保证宽、高相同,二是在wrap_content时给一个默认值

二、StopwatchView构造方法:

在写onDraw()前,先提一下画笔。因为本例是一个动画效果,需要不停的重复执行ondraw(),所以一些不变的对象,如画笔等应该放在构造方法里。分析全局,需要四个画笔:三角形画笔指针(mTrianglePaint)、mLinePaint(mLinePaint)、文字画笔(mTextPaint)、内部圆形画笔(mInnerCirclePaint)

public StopwatchView(Context context, @Nullable AttributeSet attrs) {

super(context, attrs);

//三角形指针画笔

mTrianglePaint = new Paint();

mTrianglePaint.setColor(Color.WHITE);

mTrianglePaint.setAntiAlias(true); //抗锯齿

//刻度线的画笔

mLinePaint = new Paint();

mLinePaint.setAntiAlias(true);

mLinePaint.setStrokeWidth(2); //设线宽

//文字画笔

mTextPaint = new Paint();

mTextPaint.setTextAlign(Paint.Align.CENTER); //文字居中

mTextPaint.setColor(Color.WHITE);

mTextPaint.setAntiAlias(true);

mTextPaint.setStrokeWidth(2);

//内部圆形画笔

mInnerCirclePaint = new Paint();

mInnerCirclePaint.setColor(Color.WHITE);

mInnerCirclePaint.setStyle(Paint.Style.STROKE); //无填充

mInnerCirclePaint.setAntiAlias(true);

}

三、onDraw方法:

本例主要的变量为秒表计时的毫秒值mMilliseconds,

再根据mMilliseconds值计算出外圆三角形指针的角度outerAngle和内部小圆的角度innerAngle,其他图形的绘制是根据这三个参数来进行;

另一个需要强调的是,参考小米秒钟,共设定240条刻度线,并预先设定好每个角度的值:

float eachLineAngle = 360f / 240f; //两个刻度线之间的角度1.5° 共240条线 240间隔

1、calculateValue() 计算相关值

//计算相关值【根据当前毫秒值,计算外指针角度和内圆指针角度】

private void calculateValue() {

//显示文字

int hours = mMilliseconds / (1000 * 60 * 60);

int minutes = (mMilliseconds % (1000 * 60 * 60)) / (1000 * 60);

int seconds = (mMilliseconds - hours * (1000 * 60 * 60) - minutes * (1000 * 60)) / 1000;

int milliSec = mMilliseconds % 1000 / 100;

if (hours == 0) {

mShowContent = toDoubleDigit(minutes) + ":" + toDoubleDigit(seconds) + "." + milliSec;

} else {

mShowContent = toDoubleDigit(hours) + ":" + toDoubleDigit(minutes) + ":" + toDoubleDigit(seconds) + "." + milliSec;

}

//外角度

outerAngle = 360 * (mMilliseconds % 60000) / 60000;

//内角度

innerAngle = 360 * (mMilliseconds % 1000) / 1000;

}

2、drawTriangle(Canvas canvas) 根据角度绘制三角形

//根据角度绘制三角形

private void drawTriangle(Canvas canvas) {

canvas.save();

//确定坐标

canvas.translate(mLen / 2, mLen / 2);

canvas.rotate(outerAngle);

//画三角形

Path p = new Path();

//指针点

p.moveTo(0, mLen / 2 - mTriangleLen);

//左右侧点

p.lineTo(0.5f * mTriangleLen, mLen / 2 - 0.134f * mTriangleLen);

p.lineTo(-0.5f * mTriangleLen, mLen / 2 - 0.134f * mTriangleLen);

p.close();

canvas.drawPath(p, mTrianglePaint);

canvas.restore();

}

说明:mTriangleLen是之前计算的指针顶点到外边缘的距离。因为没有三角形的api,所以根据路径来绘制。其中:0.5f * mTriangleLen 和 mLen / 2 - 0.134f * mTriangleLen 分别表示以三角形指针另两点的x和y的距离[0.5=sin30°,0.134=(1-cos30°)]

3、drawLine(Canvas canvas) 绘制外部刻度线

//绘制外部刻度线

private void drawLine(Canvas canvas) {

canvas.save();

canvas.translate(mLen / 2, mLen / 2);

int totalLines = (int) (360f / eachLineAngle); //240条线

int lastLine = (int) (outerAngle / eachLineAngle); //最亮的线条

int firstLine = lastLine - ((int) (90 / eachLineAngle)); //最暗的一条

boolean negativeFlag = false; //负数标志【即表示跨过了0起始坐标】

if (firstLine < 0) {

negativeFlag = true;

firstLine = totalLines - Math.abs(firstLine);

}

int count = 0;

for (int i = 0; i < totalLines; i++) {

canvas.rotate(eachLineAngle);

int color = 0;

if (!negativeFlag) {

//没有跨过起始点标志

if (i >= firstLine && i <= lastLine && count < (totalLines / 4)) {

count++;

color = Color.argb(255 - ((totalLines / 4 - count) * 3), 255, 255, 255);

} else {

color = Color.argb(255 - (int) (360f * 3 / (eachLineAngle * 4)), 255, 255, 255);

}

} else {

//跨过起始点

if (i >= 0 && i < lastLine) {

if (count == 0) {

count = totalLines / 4 - lastLine;

} else {

count++;

}

color = Color.argb(255 - ((totalLines / 4 - count) * 3), 255, 255, 255);

} else if (mMilliseconds!=0&&i < totalLines && i >= firstLine) { //mMilliseconds!=0 条件限制,目的是初始化时 都是灰色线条

Log.i("TAG6", "firstLine" + firstLine + " lastLine" + lastLine);

count++;

color = Color.argb(255 - ((totalLines / 4 - (i - firstLine)) * 3), 255, 255, 255);

} else {

color = Color.argb(255 - (int) (360f * 3 / (eachLineAngle * 4)), 255, 255, 255);

}

}

mLinePaint.setColor(color);

//mTriangleLen/5距离 目的是为了三角形到线条之间保留的距离

canvas.drawLine(0, (float) (mLen / 2 - (mTriangleLen+mTriangleLen/5)), 0, (float) (mLen / 2 - (2 * mTriangleLen+mTriangleLen/5)), mLinePaint);

}

canvas.restore();

}

说明:绘制线条,先要计算总的线条数,然后for循环,循环中每次旋转eachLineAngle角度。同时要根据当前角度来设定画笔的颜色来达到渐变效果。因为有跨过0°和未跨过0°的情况,所以代码中分别对此做了处理。当然也可能有其它更好的计算方法。其中的有判断 mMilliseconds!=0情况,表示初始情况或重置情况下,颜色不做改变

4、drawText(Canvas canvas) 绘制文字

//绘制文字

private void drawText(Canvas canvas) {

canvas.save();

canvas.translate(mLen / 2, mLen / 2);

mTextPaint.setTextSize(mLen / 10);

canvas.drawText(mShowContent, 0, 0, mTextPaint);

canvas.restore();

}

5、drawSecondHand(Canvas canvas) 根据角度绘制内部秒针

//根据角度绘制内部秒针

private void drawSecondHand(Canvas canvas) {

canvas.save();

canvas.translate(mLen / 2, (float) mLen * 3 / 4.0f - mLen / 16);

canvas.drawCircle(0, 0, mLen / 12, mInnerCirclePaint);

canvas.drawCircle(0, 0, mLen / 80, mInnerCirclePaint);

canvas.rotate(innerAngle);

canvas.drawLine(0, mLen / 80, 0, mLen / 14, mInnerCirclePaint);

canvas.restore();

}

四、增加对外交互的方法

//开始

public void start() {

if (mTimer == null) {

mTimer = new Timer();

mTimer.schedule(new TimerTask() {

@Override

public void run() {

if (!isPause) {![这里写图片描述](https://img-blog.csdn.net/20180415133612672?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2Fyc29uNjYzMzAw/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70)

mMilliseconds += 50;

//工作线程中用postInvalidate(); UI线程用invalidate()

postInvalidate();

}

}

}, 50, 50);

} else {

resume();

}

}

//暂停

public void pause() {

isPause = true;

}

//继续

private void resume() {

isPause = false;

}

//重置

public void reset() {

if (mTimer != null) {

mTimer.cancel();

mTimer = null;

}

isPause = false;

mMilliseconds = 0;

invalidate();

}

//记录

public int record() {

return mMilliseconds;

}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值