我们先来看下示意图:
质感十足,适合用在一些需要对物联网智能设备进行控制的场景,比如调节某个智能音箱的音量。稍加改造,也可用在一些类似带有角度旋转属性的交互的控件上
一、设计思路
有时候,用户并不需要关心控件的具体读数,在一些具有读数的音量控制控件中,我们会发现一个有意思的现象:用户会特意将数值调至整数或偶数,当用户无法调至这个数时,则会焦虑感骤增,原地螺旋爆炸。
那么,如果隐藏掉具体数值会不会更好呢?在一些老式音箱硬件上,我们会经常看到音量旋钮但不具备具体的读数,音量调至多少合适全凭入耳的感觉,感觉对了音量就对了。全程的交互中没有具体的读数概念,感觉是唯一的驱动力。
二、实现方案
2.1 UI拆解
老套路,首先分析形状,不难发现,由旋钮、旋钮上的指示器、外围刻度构成
2.2 UI绘制
UI整体绘制难度非常简单,主要在ondraw中
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
setLayerType(LAYER_TYPE_SOFTWARE, null)
canvas?.let {
//刻度
drawScale(it)
//旋钮&阴影
drawShadow(it)
//指示器
drawIndicator(it)
}
}
2.2.1 绘制旋钮
绘制圆形的同时绘制阴影,增加质感
代码如下
private fun drawShadow(canvas: Canvas) {
paint.color = Color.WHITE
paint.setShadowLayer(shadowSize, 0F, 15F, Color.GRAY)
canvas.drawCircle(centerX, centerY, radius, paint)
paint.clearShadowLayer()
}
2.2.2 绘制指示器
代码如下,需要注意的是,指示器带有角度属性,需要旋转画布
private fun drawIndicator(canvas: Canvas) {
canvas.save()
canvas.rotate(circularOpUtils.curDegree, centerX, centerY)
paint.color = Color.RED
canvas.drawRect(RectF(centerX + radius / 4, centerY - 4, centerX + radius * 3 / 4, centerY + 4), paint)
canvas.restore()
}
2.2.3 绘制刻度
代码如下:
private fun drawScale(canvas: Canvas) {
Log.i(TAG, "curDegree==>" + circularOpUtils.curDegree)
canvas.save()
paint.color = Color.GRAY
var scaleCount = 360 / scaleSpace.toInt()
for (i in 0 until scaleCount) {
//绘制当前指示
if ((i * scaleSpace <= circularOpUtils.curDegree) && ((i + 1) * scaleSpace > circularOpUtils.curDegree)) {
Log.i(TAG, "i*scaleSpace==>" + i * scaleSpace)
paint.color = curSelScaleColor
canvas.drawRect(width - scaleWidth, centerY - 4F, width - scaleWidth + curSelScaleWith, centerY + 4F, paint)
paint.color = Color.GRAY
} else {
canvas.drawRect(width - scaleWidth, centerY - 4F, width.toFloat(), centerY + 4F, paint)
}
canvas.rotate(scaleSpace, centerX, centerY)
}
canvas.restore()
}
2.3 交互实现
按交互抽象出计算旋转角度的工具类CircularOpUtils,类似圆盘旋转的控件都可通用
2.3.1 旋转角度计算
思路是这样的,在手指移动中如何计算移动点和起始按压点的角度呢?可以采用几何公式,利用反tan或反cos;也可以采用两点分别与x轴的夹角的差进行计算。这里采用后者
/**
* 计算坐标点与x轴的夹角
*/
fun calculateAngle(x: Float, y: Float): Float {
val distance = sqrt(((x - centerX) * (x - centerX) + (y - centerY) * (y - centerY)))
if (distance == 0F) {
return 0F
}
var degree = acos((x - centerX) / distance) * 180 / PI.toFloat()
if (y < centerY) {
degree = 360 - degree
}
return degree
}
/**
* 计算两点的夹角
*/
fun calculateAngle(x1: Float, y1: Float, x2: Float, y2: Float): Float {
val angle1 = calculateAngle(x1, y1)
val angle2 = calculateAngle(x2, y2)
return angle2 - angle1
}
2.3.2 刻度动画
主要是当前选中刻度的长短变化
private fun shrinkScaleAnim() {
shrinkScaleAnim?.cancel()
if (curSelScaleWith > 10F) {
shrinkScaleAnim = ValueAnimator.ofFloat(curSelScaleWith, 10F)
with(shrinkScaleAnim!!) {
duration = 300L
addUpdateListener {
curSelScaleWith = it.animatedValue as Float
postInvalidate()
}
start()
}
}
}
2.3.3 一点细节
可以观察到,用户手指按压和抬起时,旋钮的阴影变化
private fun startUpShadowAnim() {
shadowAnim?.cancel()
shadowAnim = ValueAnimator.ofFloat(shadowSize, 30F)
with(shadowAnim!!) {
duration = 300L
addUpdateListener {
shadowSize = animatedValue as Float
postInvalidate()
}
start()
}
}
private fun startDownShadowAnim() {
shadowAnim?.cancel()
shadowAnim = ValueAnimator.ofFloat(shadowSize, 20F)
with(shadowAnim!!) {
duration = 300L
addUpdateListener {
shadowSize = animatedValue as Float
postInvalidate()
}
start()
}
}
2.3.4 优化思路
这个控件涉及了刻度绘制,当没有动画在运行时,可以将刻度的形状使用path保存下来,用户交互时,旋转path即可,而不需要每次在ondraw中for循环生成刻度形状。
最后福利:学习资料赠送
福利:由本人亲自撰写 & 整理的「Android学习方法资料」 数量:10名 参与方式:「点击文章右下角”在看“ -> 回复截图到公众号 即可,我将从中随机抽取」 点击“在看”就能升职 & 加薪水哦!