质感十足,适合用在一些需要对物联网智能设备进行控制的场景,比如调节某个智能音箱的音量
稍加改造,也可用在一些类似交互的控件上
一、设计思路
有时候,用户并不需要关心控件的具体读数,在一些具有读数的音量控制控件中,我们会发现一个有意思的现象:用户会特意将数值调至整数或偶数,当用户无法调至这个数时,则会焦虑感骤增,原地螺旋爆炸。如同吃完重庆火锅后往菊花里塞了颗薄荷糖——微辣中带一丝清凉。
那么,如果隐藏掉具体数值会不会更好呢?在一些老式音箱硬件上,我们会经常看到音量旋钮但不具备具体的读数,音量调至多少合适全凭入耳的感觉,感觉对了音量就对了。全程的交互中没有具体的读数概念,感觉是唯一的驱动力。
二、实现方案
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循环生成刻度形状。
三、后记
如何减少用户在交互时的焦虑感?这是设计上可以探索的方向。
但是用户需要减少焦虑吗?现实中的应用都巴不得促进用户越来越焦虑。
看看吧
可以无限往下刷的信息列表;
可以无限刷的短视频;
可以无限下翻的回答、帖子;
无限次下拉刷新;
文字社区强行添加视频流;
腹泻式不停地兴趣推荐......
什么时候才是个头啊?
那种调节音箱音量旋钮时感觉对了就好了的体验不复存在了。
这样真的好吗?
我说,不好。
但我算个屁。
啊,既然看到了这里,是时候亮出我的个人微信和公众号了......才怪!