本文由gxp1182893781(泥巴城)原创,转载请注明!!
实现效果预览
功能球分析
- 默认状态下,功能球会显示一个不断旋转的扫描样式,当点击后会显示出功能球的四个按钮模块。
- 预览图中我们可以看到,每个功能模块包含功能名称和一个“列表”按钮,且部分功能模块包含数个子功能,当点击功能模块除去“列表”按钮的部分,子功能列表将会弹出,同时点击的部分会变色。再次点击会收起子列表,并恢复颜色。
- 选中的功能模块会旋转到左上角(这个设计是因为在我的需求中这个功能球在屏幕的右下角),请注意旋转过程,文字和“列表”按钮是不会相对旋转的,永远是正常方向,且如果子列表正在显示,会自动隐藏,目标功能模块如果有子列表,也会自动显示。
- 关于旋转方向:会自动选择旋转角度绝对值最小的方向进行旋转,如果对角线,则逆时针旋转。
- 功能球中间还有个“设置”按钮,在项目中并未用到,只是UI给的切图是这样的,就当是一个预留功能也照UI切图画上了。
动图看不清?来几张静图吧!
默认状态:
旋转过程:
子列表完全展开:
实现思路
- 子列表
我这里直接使用了多个TextView+动画来实现,展开过程的起始坐标是功能球中“设置”按钮的中心。这部分就不详细展开了,相信稍微懂得Android动画的人都能自己完成。
子列表选项之间的角度计算方法:
子列表两端两个按钮中心点之间的角度 / (子列表选项个数 - 1)
两个按钮中心点之间的角度是多少?我这里30°+90°+30°=150°(也就是说两端的选项中心点分别跟水平方向、竖直方向的夹角都是30°,再加上第二象限的90°)。来看下图就明白了:
- 功能球
这个View是自己写的,名为FunctionGlobalView,下面介绍。
FunctionGlobalView(功能球)
FunctionGlobalView主要通过Paint在Canvas上进行绘制来完成的,使用Region来判断点击位置,从而响应各个按钮的点击事件。
关于尺寸测量
我们需要包裹内容来完成View测量,且应以按钮群状态来计算,扫描状态的大小与按钮群状态应保持一致。建议参照预览图阅读此部分内容,以便更好理解测量的过程。
- 功能球整体从外到内分别是外圆、内圆、按钮模块。下面给出各变量的含义,请参考示例图:
其中还用到的变量:
mInnerCircleRadius:内圆半径
mInnerButtonRadius:内部设置按钮半径
mMenuMarginDiagonal:两个menu模块之间的间距
mCircleMargin:内圆和外圆之间的间距
我们需要计算的FunctionGlobalView的width和height自然就是外圆的宽和高,且两个值相等。计算方式如下:
width = height =
(mCircleMargin + mInnerCircleRadius) * 2 + hypot(mMenuMarginDiagonal,mMenuMarginDiagonal)
mInnerCircleRadius的值如何计算呢?
既然要包裹内容,那么就要能容得下最长的文字。我这里用最长文字的width * 1.5来赋值的。重点就是计算menu中最长的文字。
var menuMaxWidth = 0
//mMenus是存放menu模块相关的列表,text字段是menu内容
mMenus.forEach {
mPaint.getTextBounds(it.text, 0, it.text.length, mRect)
menuMaxWidth = max(menuMaxWidth, mRect.right - mRect.left)
//其他无关内容
}
关于绘制
FunctionGlobalView分两种状态:默认扫描效果、按钮群。
定义一个枚举类,标识两种状态:
enum class ShowMode {
GLOBAL, //功能球旋转动画模式
BUTTON //按钮模式
}
绘制扫描状态
仔细看这张图
需要绘制的主要由以下内容:
- 蓝色的背景圆
- 几个白色小点
- 功能球文字和图标
- 白色锐角扫描条
- 浅蓝色半透明扫描条
其中,除了蓝色背景圆,其余都是图片。
将drawbale转换为bitmap的方法:
private fun drawableToBitmap(drawable: Drawable, @ColorInt color: Int?): Bitmap {
val drawableWrap = DrawableCompat.wrap(drawable).mutate()
// 获取 drawable 长宽
val width = drawableWrap.intrinsicWidth
val heigh = drawableWrap.intrinsicHeight
drawableWrap.setBounds(0, 0, width, heigh)
if (color != null) {
drawableWrap.setColorFilter(color, PorterDuff.Mode.SRC_IN)
}
// 获取drawable的颜色格式
val config = if (drawableWrap.opacity != PixelFormat.OPAQUE)
Bitmap.Config.ARGB_8888
else
Bitmap.Config.RGB_565
// 创建bitmap
val bitmap = Bitmap.createBitmap(width, heigh, config)
// 创建bitmap画布
val canvas = Canvas(bitmap)
// 将drawable 内容画到画布中
drawableWrap.draw(canvas)
return bitmap
}
关于旋转效果,我用canvas.rotate方法将画布旋转了一个角度然后绘制。
绘制按钮群
我们来参考下面的图进行分析
按钮群状态下,我们需要绘制以下内容:
- 蓝色背景圆(需要外圆描边)
- menu模块的1/4圆弧
- menu名称
- “列表”按钮的圆角矩形背景
- “列表”按钮的“列表”二字
- 中间的设置按钮(图片)
其实绘制以上内容都不是难点,主要是需要进行一些计算和调整,以便让View中的文字显得更好看一些。
我将menu文字和“列表”按钮看做高度宽高分别相等,同时将两者看做是在同一个矩形(以下称该矩形为文字矩形)中上下排列。在我的项目里,经过调整,文字矩形的中心点与View的矩形中心点的距离 和 mInnerCircleRadius 的比例是7:5,在这个比例下刚好能让文字不遮蔽边线。
当旋转时,需要通过当前旋转的角度来计算文字矩形的中心,保持上面的比例不变,以此来绘制文字,这样文字就能确定旋转过程中的位置。
关于点击事件
我们首先需要判断View当前的状态,如果是扫描状态,则修改状态,绘制按钮群。如果是按钮群状态,则判断点击到的哪个path,然后执行对应的点击事件。
Region判断是否点击在了path内:
val region: Region = Region()
//构造一个区域对象。
val r = RectF()
mExternCirclePath.computeBounds(r, true)
region.setPath(
mExternCirclePath, Region(
r.left.toInt(), r.top.toInt(),
r.right.toInt(), r.bottom.toInt()
)
)
if (region.contains(event.x.toInt(),event.y.toInt())) {
//点击到了path内
return true
}
写在最后
本文并没有太多逻辑代码,只是一些基础的工具方法,着重讲述
View的分析和实现思路。想看源代码?
如果您账户积分富裕,可以前往CSDN资源下载直接下载CSDN资源地址
如果您也不富裕,源代码已上传至Github,点这☞Github地址点此跳转