android自定义起止时间的时间刻度尺,Android 自定义View篇(六)实现时钟表盘效果...

09942e06512a0aff373a86287cd1c4aa.png

前言

Android 自定义 View 是高级进阶不可或缺的内容,日常工作中,经常会遇到产品、UI 设计出花里胡哨的界面。当系统自带的控件不能满足开发需求时,就只能自己动手撸一个效果。

本文就带自定义 View 初学者手动撸一个效果,通过自定义 View 实现钟表功能,每行代码都有注释,保证易懂,看不懂你留言打我!!!

实现效果

1、先看效果图

d6a625d79bed09e034f6aa1e7d9f2f53.png

在这里插入图片描述

2、下载地址

3、步骤分析

实现以上效果,主要分为四个步骤:

绘制外层表盘

绘制刻度线

绘制刻度数字

绘制指针

测量View宽高

代码实现

1、绘制外层表盘

外层表盘就是一个空心圆,只要获取圆的 x、y 轴位置、圆的半径,使用 Canvas.drawCircle()方法即可完成。

/**

* 绘制表盘

*/

private fun drawClock(canvas: Canvas, centerX: Float, centerY: Float) {

// 设置外层圆画笔宽度

mPaint.strokeWidth = mCircleWidth

// 设置画笔颜色

mPaint.color = Color.BLACK

// 设置画笔空心风格

mPaint.style = Paint.Style.STROKE

// 绘制圆方法

canvas.drawCircle(centerX, centerY, radius, mPaint)

}

c36efe36aec43ceb94350e9d6866511a.png

在这里插入图片描述

2、绘制刻度线

绘制思路分析

看到效果图上密密麻麻刻度线后,先不要着急上手,屡清楚绘制思路。绘制刻度线一定要结合 Canvas 几何变换思路完成,千万不要局限于效果图的表面(如果对 Canvas 相关 API 不熟悉的朋友,建议先了解下)。

假设以 12 点钟为例,那么刻度线就是一条笔直的竖线,调用 Canvas.drawLine()方法完成绘制。

如果每绘制完成一个刻度,把表盘逆时针/顺时针旋转一定角度,将下次需要绘制刻度线的位置旋转到 12 点钟位置,那么每次绘制刻度线的 startX、startY、stopX、stopY 一致(一致仅代表所有长刻度一致,所有短刻度一致)。

观察表盘共有 60 个刻度线(12 长,48 短),那么每次旋转角度degrees=360/60

听完以上分析,是否觉得绘制刻度线很简单,只要在 60 个刻度遍历判断长短,即可轻松出效果。

/**

* 绘制表盘刻度

*/

private fun drawClockScale(canvas: Canvas, centerX: Float, centerY: Float) {

for (index in 1..60) {

// 刻度绘制以12点钟为准,每次将表盘旋转6°,后续绘制都以12点钟为基准绘制

canvas.rotate(6F, centerX, centerY)

// 绘制长刻度线

if (index % 5 == 0) {

// 设置长刻度画笔宽度

mPaint.strokeWidth = 4.0F

// 绘制刻度线

canvas.drawLine(

centerX,

centerY - radius,

centerX,

centerY - radius + scaleMax,

mPaint

)

}

// 绘制短刻度线

else {

// 设置短刻度画笔宽度

mPaint.strokeWidth = 2.0F

canvas.drawLine(

centerX,

centerY - radius,

centerX,

centerY - radius + scaleMin,

mPaint

)

}

}

}

以上代码就完成了绘制刻度线的效果,下面插个题外话,第一次尝试在绘制刻度线的时候,表盘数字一并完成,后来发现数字如下图所示:

050e3db4c2d036ceea314008ffd4a491.png

// 测量绘制数字

mPaint.strokeWidth = 1.0F

mPaint.style = Paint.Style.FILL

mPaint.getTextBounds((index / 5).toString(), 0, (index / 5).toString().length, mRect)

val width = mRect.width()

canvas.drawText(

(index / 5).toString(),

centerX - mRect.width() / 2,

(centerY - radius + scaleMax + mRect.height() + 8),

mPaint

)

3、绘制数字方案

热心网友指导我绘制数字新方案,真的是高手如云阿。

首先将坐标位置(0,0)设置到圆心位置,这步是在绘制外层圆的时候,已经设置了。这样的好处是后期减少很多计算的步骤,新方案已经在代码中更改!

canvas.translate(centerX, centerY)

主要是通过 canvas 几何变换方式,先将圆点平移到 12 点钟位置,然后逆时针旋转数字对应的角度,然后开始绘制数字文本。这样的话,绘制数字文本就和绘制刻度线可以一并完成,使得代码清晰很多。需要注意的是,记得在使用几何变换前后分别调用canvas.restore()和 canvas.restore()方法。

其中相关坐标计算方式:

1、平移 y 轴距离 = - 半径 + 刻度线长度 + 刻度与文本间距 + 文本高度 / 2

(因为坐标原点在圆心,需要平移到 12 点钟位置,所以半径为负数)

2、旋转角度 = - 6 * 数字大小

3、文本 x 轴距离 = 文本宽度 / 2 ;

4、文本 y 轴距离 = 文本高度 / 2 ;

附上绘制刻度线和文本的完整代码:

/**

* 绘制表盘刻度线和数字文本

*/

private fun drawClockScale(canvas: Canvas) {

for (index in 1..60) {

// 刻度绘制以12点钟为准,每次将表盘旋转6°,后续绘制都以12点钟为基准绘制

canvas.rotate(6F, 0F, 0F)

// 绘制长刻度线

if (index % 5 == 0) {

// 设置长刻度画笔宽度

mPaint.strokeWidth = 4.0F

// 绘制刻度线

canvas.drawLine(0F, -radius, 0F, -radius + scaleMax, mPaint)

/** 绘制文本 **/

canvas.save()

// 设置画笔宽度

mPaint.strokeWidth = 1.0F

// 设置画笔实心风格

mPaint.style = Paint.Style.FILL

mPaint.getTextBounds(

(index / 5).toString(),

0,

(index / 5).toString().length,

mRect

)

canvas.translate(0F, -radius + mNumberSpace + scaleMax + (mRect.height() / 2))

canvas.rotate((index * -6).toFloat())

canvas.drawText(

(index / 5).toString(), -mRect.width() / 2.toFloat(),

mRect.height().toFloat() / 2, mPaint

)

canvas.restore()

}

// 绘制短刻度线

else {

// 设置短刻度画笔宽度

mPaint.strokeWidth = 2.0F

canvas.drawLine(0F, -radius, 0F, -radius + scaleMin, mPaint)

}

}

}

4、绘制指针

指针绘制具体分以下步骤:

首先获取当前时间

根据当前时间计算指针旋转过的角度

利用 Canvas.rotate()旋转画布

使用 Canvas.drawRoundRect()绘制指针矩形

绘制圆点

/**

* 第四步:绘制指针

*/

private fun drawPointer(canvas: Canvas, centerX: Float, centerY: Float) {

// 获取当前时间:时分秒

val calendar = Calendar.getInstance()

val hour = calendar[Calendar.HOUR]

val minute = calendar[Calendar.MINUTE]

val second = calendar[Calendar.SECOND]

// 计算时分秒转过的角度

val angleHour = (hour + minute.toFloat() / 60) * 360 / 12

val angleMinute = (minute + second.toFloat() / 60) * 360 / 60

val angleSecond = second * 360 / 60

// 绘制时针

canvas.save()

// 旋转到时针的角度

canvas.rotate(angleHour, centerX, centerY)

val rectHour = RectF(

centerX - mHourPointWidth / 2,

centerY - radius / 2,

centerX + mHourPointWidth / 2,

centerY + radius / 6

)

// 设置时针画笔属性

mPaint.color = Color.BLUE

mPaint.style = Paint.Style.STROKE

mPaint.strokeWidth = mHourPointWidth

canvas.drawRoundRect(rectHour, mPointRange, mPointRange, mPaint)

canvas.restore()

// 绘制分针

canvas.save()

// 旋转到分针的角度

canvas.rotate(angleMinute, centerX, centerY)

val rectMinute = RectF(

centerX - mMinutePointWidth / 2,

centerY - radius * 3.5f / 5,

centerX + mMinutePointWidth / 2,

centerY + radius / 6

)

// 设置分针画笔属性

mPaint.color = Color.BLACK

mPaint.strokeWidth = mMinutePointWidth

canvas.drawRoundRect(rectMinute, mPointRange, mPointRange, mPaint)

canvas.restore()

// 绘制秒针

canvas.save()

// 旋转到分针的角度

canvas.rotate(angleSecond.toFloat(), centerX, centerY)

val rectSecond = RectF(

centerX - mSecondPointWidth / 2,

centerY - radius + 10,

centerX + mSecondPointWidth / 2,

centerY + radius / 6

)

// 设置秒针画笔属性

mPaint.strokeWidth = mSecondPointWidth

mPaint.color = Color.RED

canvas.drawRoundRect(rectSecond, mPointRange, mPointRange, mPaint)

canvas.restore()

// 绘制原点

mPaint.style = Paint.Style.FILL

canvas.drawCircle(

(centerX).toFloat(),

(centerY).toFloat(), mSecondPointWidth * 4, mPaint

)

}

以上就已经完成表盘指针绘制工作,效果图如下:

4addc0dd9ea936893d692ebdc0d57aa9.png

在这里插入图片描述

5、onMeasure 测量 View 宽高

MeasureSpecMode 有三个属性:EXACTLY、AT_MOST、UNSPECIFIED

根据 View 在 xml 设置宽高类型不同会触发相应的方法,这里对 onMeasure()测量不做具体讲解,如果对自定义 View 测量宽高 onMeasure()方法不熟悉的,请自己补习。onMeasure()方法也是自定义 View 学习过程中非常重要的一个环节!

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {

super.onMeasure(widthMeasureSpec, heightMeasureSpec)

// 根据表盘半径 + 表盘圆环宽度计算View宽度和高度

mWidth = onMeasuredSpec(widthMeasureSpec) + (mCircleWidth * 2).toInt()

mHeight = onMeasuredSpec(heightMeasureSpec) + (mCircleWidth * 2).toInt()

// 计算最终View表盘的半径

radius = (mWidth - mCircleWidth * 2) / 2

// 设置View最终宽高

setMeasuredDimension(mWidth, mHeight)

}

private fun onMeasuredSpec(measureSpec: Int): Int {

// 临时值,用于计算后返回

var specViewSize = 0

val specMode = MeasureSpec.getMode(measureSpec)

val specSize = MeasureSpec.getSize(measureSpec)

when (specMode) {

MeasureSpec.EXACTLY -> {

// 一般为固定尺寸或者match_parent

specViewSize = specSize

}

MeasureSpec.AT_MOST -> {

// 计算半径以宽高最小值为准

specViewSize = min((radius * 2).toInt(), specSize)

}

}

return specViewSize

}

到此为止,如果想让表盘和指针动起来,还需要在 onDraw()方法里调用postInvalidateDelayed(1000)方法,或者启动一个线程,使每秒钟刷新重绘一次布局,这样就可以让指针实时更新时间。最后贴上 onDraw()方法里面的代码:

override fun onDraw(canvas: Canvas) {

super.onDraw(canvas)

// 设置圆心X轴位置

val centerX: Float = (mWidth / 2).toFloat()

// 设置圆心Y轴位置

val centerY: Float = (mHeight / 2).toFloat()

/** 第一步:绘制最外层的圆 **/

drawClock(canvas, centerX, centerY)

/** 第二步:表盘一共60个刻度,1到12点整数属于长刻度,其余属于短刻度 **/

drawClockScale(canvas, centerX, centerY)

/** 第三步:绘制表盘数字 **/

drawClockNumber(canvas, centerX, centerY)

/** 第四步:绘制指针 **/

drawPointer(canvas, centerX, centerY)

// 刷新表盘指针

postInvalidateDelayed(1000)

}

总结

自定 view 在 Android 开发过程中应用极其广泛,为了更好的掌握,建议从自定义 View 绘制流程、Canvas、Paint、Path、onLayout()、onMeasure()、onDraw() 系统化学习,然后上手多做练习,这样势必会对自定义 View 有很好的提升!

看完文章,是不是觉得这个效果其实也很简单,案例中相关属性可以使用自定义属性,因为练习案例,所以这里在 View 中直接写死了,感兴趣的朋友可以使用自定义属性尝试实现。这个案例基本上将自定义 View 中 Canvas、Paint 常见的 API 方法以及 onMeasure()测量方法都应用到了,算是一个上手练习自定义 View 的好案例,希望看完文章对你学习有所帮助!

前文说过,保证每个自定义 View 初学者都能看懂,因为每行代码都会添加注释,如果没看懂的留言打我!!!

d2bdc505027e5164889e7013cbd2b3bd.png

Android 自定义View篇(一)View绘制流程

Android 自定义View篇(二)Canvas详解

Android 自定义View篇(三)Paint详解

Android 自定义View篇(四)自定义属性详解

Android 自定义View篇(五)事件分发机制

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。
以下是一个简单的布局文件,用于在主页编写搜索模块并选择预定起止时间: ``` <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="16dp"> <!-- Search Box --> <EditText android:id="@+id/search_box" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="Search" android:inputType="text" android:maxLines="1" /> <!-- Date Range Selector --> <LinearLayout android:id="@+id/date_range_selector" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="16dp" android:orientation="horizontal"> <TextView android:id="@+id/start_date_label" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Start Date:" /> <Button android:id="@+id/start_date_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Select" android:textColor="@color/colorAccent" /> <TextView android:id="@+id/end_date_label" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="End Date:" /> <Button android:id="@+id/end_date_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Select" android:textColor="@color/colorAccent" /> </LinearLayout> </LinearLayout> ``` 这个布局文件包含两个主要部分:搜索框和日期范围选择器。搜索框使用EditText实现,而日期范围选择器使用LinearLayout和Button实现。 在这个布局中,我们使用了android:layout_marginTop属性来为日期范围选择器添加了一些顶部的空白间距。我们还使用了android:textColor属性来设置日期选择器按钮的颜色,以使其与应用程序的色调保持一致。 您可以使用此布局文件作为起点,并根据您的应用程序需求进行自定义

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值