美文网首页高级UI具体自定义控件
自定义View-圆形菜单控件

自定义View-圆形菜单控件

作者: 宇智波_佐星 | 来源:发表于2019-12-11 10:47 被阅读0次

    美好的一天开始啦!昨天看到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的流程,我觉得只要捋顺逻辑,然后一步步画,缺什么补什么,这种简单的控件还是手到擒来的。


    相关文章

      网友评论

        本文标题:自定义View-圆形菜单控件

        本文链接:https://www.haomeiwen.com/subject/oxuggctx.html