自定义功能球按钮群View

本文由gxp1182893781(泥巴城)原创,转载请注明!!

实现效果预览

效果预览图

功能球分析

  1. 默认状态下,功能球会显示一个不断旋转的扫描样式,当点击后会显示出功能球的四个按钮模块。
  2. 预览图中我们可以看到,每个功能模块包含功能名称和一个“列表”按钮,且部分功能模块包含数个子功能,当点击功能模块除去“列表”按钮的部分,子功能列表将会弹出,同时点击的部分会变色。再次点击会收起子列表,并恢复颜色。
  3. 选中的功能模块会旋转到左上角(这个设计是因为在我的需求中这个功能球在屏幕的右下角),请注意旋转过程,文字和“列表”按钮是不会相对旋转的,永远是正常方向,且如果子列表正在显示,会自动隐藏,目标功能模块如果有子列表,也会自动显示。
  4. 关于旋转方向:会自动选择旋转角度绝对值最小的方向进行旋转,如果对角线,则逆时针旋转。
  5. 功能球中间还有个“设置”按钮,在项目中并未用到,只是UI给的切图是这样的,就当是一个预留功能也照UI切图画上了。

动图看不清?来几张静图吧!

默认状态:
默认状态
旋转过程:
旋转过程
子列表完全展开:
在这里插入图片描述

实现思路

  1. 子列表
    我这里直接使用了多个TextView+动画来实现,展开过程的起始坐标是功能球中“设置”按钮的中心。这部分就不详细展开了,相信稍微懂得Android动画的人都能自己完成。

子列表选项之间的角度计算方法
子列表两端两个按钮中心点之间的角度 / (子列表选项个数 - 1)
两个按钮中心点之间的角度是多少?我这里30°+90°+30°=150°(也就是说两端的选项中心点分别跟水平方向、竖直方向的夹角都是30°,再加上第二象限的90°)。来看下图就明白了:
辅助线

  1. 功能球
    这个View是自己写的,名为FunctionGlobalView,下面介绍。

FunctionGlobalView(功能球)

FunctionGlobalView主要通过Paint在Canvas上进行绘制来完成的,使用Region来判断点击位置,从而响应各个按钮的点击事件。

关于尺寸测量

我们需要包裹内容来完成View测量,且应以按钮群状态来计算,扫描状态的大小与按钮群状态应保持一致。建议参照预览图阅读此部分内容,以便更好理解测量的过程。

  1. 功能球整体从外到内分别是外圆、内圆、按钮模块。下面给出各变量的含义,请参考示例图:
    注解

其中还用到的变量:
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地址点此跳转

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值