自定义View-圆形菜单控件
美好的一天开始啦!昨天看到UI上面有一个界面是这样:
条目是根据后台动态获取的。本片文章重点不在如何自定义View而是从梳理绘制上去记录我画自定义控件的步骤。
我的理解中,画自定义View首先要明确你的思路:比如先画什么再画什么。其次再想清楚这个自定义View会有哪些属性,哪些属性是可以通过xml设置的,哪些属性是需要计算的,并且要明确哪些是动态需要暴露出去的。最后才开始着手写代码去画。
那么我按着步骤来写一下:
先画什么 再画什么
我对绘制顺序的理解是,先底层后顶层,先静态后动态,那么我们按顺序看。
需要绘制的控件大家看到了。这个控件比较简单,绘制的顺序一眼就能看出来:圆---->线------>单位菜单按钮-------->人
画人都顺序可以才在按钮前也可以在按钮后。
属性
本菜鸡的理解中,考虑属性的逻辑顺序是:控件的宽高->使能的布尔值->数据源->暴露的接口/方法
首先看控件的宽高:
整个控件的宽高=(大圆半径+小圆半径)*2
private var mRadius=100f//大圆半径
private var smallRadius=20f//小圆半径
自然而然的 那么onMeasure如下:
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
var mWidth=0
var mHeight=0
mWidth=((mRadius+smallRadius)*2).toInt()
mHeight=mWidth
setMeasuredDimension(mWidth,mHeight)
}
使能的布尔值其实就是看看是否需要做一些开关,比如可以设置是否绘制背景圆的开关,线的开关,根据这些布尔值来判断是否需要画这些部分(这里只是举例,并不是必须的)
数据源:
这里的数据源是一个List<T>,考虑数据源的时候大家要考虑清楚界面是否跟数据源有关联,这里的关联就是菜单的个数会根据数据源的size改变,关联的另一个属性就是每个菜单之间的间距角度
private var mDevide=10f//间距角度
暴露的接口/方法:
因为控件比较简单,所以这里同样比较明显。 需要暴露的方法有你需要设置的使能布尔值,另外一个当然就是我们数据源的set方法
/**
* 设置数据源
*/
public fun setDatas(datas:ArrayList<TypesBean>){
this.datas.clear()
this.datas.addAll(datas)
invalidate()
}
接口当然是菜单的点击事件(后面会讲到对应的onTouchEvent):
public interface OnItemClickListener{
fun click(p:Int)
}
我觉得捋的差不多了,开始画了,我们直接在Ondraw当中来看:
第一个当然是画圆:
//先移动基准点
canvas?.translate(mRadius+smallRadius,mRadius+smallRadius)
//画背景圆形
mPaint?.let {
it.style=Paint.Style.STROKE
it.strokeWidth=3f
if (isCicleEnable)//是否画圆的布尔值
canvas?.drawCircle(0f, 0f,mRadius,it)
}
那么第二个就是线了,线的位置 方向和数量都是跟数据源有关的 所以此时需要我们开始根据数据源来算坐标了。
/**
* 计算坐标点
*/
private fun getPoint(): ArrayList<PointBean> {
val size = datas.size
mDevide = 360f / size //间距角的弧度∠
for (i in 0 until datas.size){
//人为的把第一个点定到上方正中央的位置
if (i==0){
points.add(PointBean(0f,-mRadius))
}else{
val x=Math.sin((mDevide*i).toDouble()*2*Math.PI/360)*mRadius
val y=-1*Math.cos((mDevide*i).toDouble()*2*Math.PI/360)*mRadius
points.add(PointBean(x.toFloat(),y.toFloat()))
}
}
return points
}
这里我们就根据数据源计算出了所有节点Point的坐标了,那么再根据point画线:
val points = getPoint()
touchRects.clear()//响应区域
for (i in 0 until points.size){
val point = points[i]
//画线条
if (isStrokeEnable){
mPaint?.color=resources.getColor(R.color.text_999)
canvas?.drawLine(0f,0f,point.x,point.y,mPaint)
}
}
下面就要画各个菜单的区块了,代码如下:
//画区块
//这里要注意区块的起始和结束坐标、区域等
//todo 判断状态决定画哪个图
val bm = BitmapFactory.decodeResource(resources, R.drawable.ic_launcher)
val bitmap = setImgSize(bm, (smallRadius * 2).toInt(), (smallRadius * 2).toInt())
val rect = RectF(
point.x - smallRadius,
point.y - smallRadius,
point.x + smallRadius,
point.y + smallRadius
)
touchRects.add(rect)
bitmap?.let {
canvas?.drawBitmap(it,rect.left,rect.top,mPaint)
}
//画字
mPaint?.let {
it.color=Color.WHITE
it.textAlign=Paint.Align.CENTER;
val fontMetrics = it.getFontMetrics()
val top = fontMetrics.top//为基线到字体上边框的距离,即上图中的top
val bottom = fontMetrics.bottom//为基线到字体下边框的距离,即上图中的bottom
val baseLineY = (rect.centerY() - top / 2 - bottom / 2) //基线中间点的y轴计算公式
canvas?.drawText(datas[i].name,rect.centerX(), baseLineY,mPaint)
}
//画状态勾勾√
//todo 先判断状态
// canvas?.drawBitmap(gouBit,rct,disRect,mPaint)
if (isStatusEnable)
canvas?.drawBitmap(gouBit,rect.right-gouBit.width,rect.bottom-gouBit.height,mPaint)
现在画出来就长这个样子了, 偷了个懒,中间的人我是直接放在上级布局里放了个居中位置。
是不是很棒???
下面再贴下点击事件的代码:
override fun onTouchEvent(event: MotionEvent?): Boolean {
val off = mRadius + smallRadius
event?.let {
if (it.action==MotionEvent.ACTION_UP){
"有反应x=${it.x}---y=${it.y}".logIt()
for (i in 0 until touchRects.size){
val rectF = touchRects[i]
"对应的区域位置遍历 left=${rectF.left+off} right=${rectF.right+off} top=${rectF.top+off} bottom=${rectF.bottom+off}".logIt()
if (it.x>=rectF.left+off&&it.x<=rectF.right+off&&it.y>=rectF.top+off&&it.y<=rectF.bottom+off){
//区域内
"点击了${i}".logIt()
onItemClickListener?.click(i)
break
}
}
}
}
return true
}
touchRects是在绘制是记录的所有菜单的区域,为了方便响应ACTION_UP.
这个控件比较简单,自定义的流程大概就是上面。记录本文的目的在于自己再理一下自定义View的流程,我觉得只要捋顺逻辑,然后一步步画,缺什么补什么,这种简单的控件还是手到擒来的。