高级UI具体自定义控件

自定义View-圆形菜单控件

2019-12-11  本文已影响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的流程,我觉得只要捋顺逻辑,然后一步步画,缺什么补什么,这种简单的控件还是手到擒来的。


上一篇下一篇

猜你喜欢

热点阅读