我报名参与金石计划1期应战——分割10万奖池,这是我的第3篇文章,点击检查活动详情

前语

个人计划开发个视频修改的APP,然后把一些用上的技术总结一下,这次主要是APP的底部菜单栏用到了一个自定义View去制作完成的,所以这次主要想讲讲自定义View的一些用到的点和自己如何去DIY一个不一样的自定义布局。

完成的作用和思路

能够先看看完成的作用

Android DIY你的菜单栏

两个页面的内容还没做,当前便是一个Demo,能够看到底部的菜单栏是一个制作出来的不规则的一个布局,那要如何完成呢。能够先来看看它的一个制作区域:

Android DIY你的菜单栏

便是一个底部的布局和3个子view,底部的区域当然也是个规则的区域,只不过我们是在这块区域上去进行制作。

能够把整个进程分为几个步骤:

1. 制作底部布局
    (1) 制作矩形区域
    (2) 制作外圆形区域
    (3) 制作内圆形区域
2. 增加子view进行布局
3. 处理事情分发的区域 (底部菜单上边的白色区域不触发菜单的事情)
4. 写个动画意思意思

1. 制作底部布局

这儿做的话就没必要手动去增加view这些了,直接全部手动制作就行。

companion object{
    const val DIMENS_64 = 64.0
    const val DIMENS_96 = 96.0
    const val DIMENS_50 = 50.0
    const val DIMENS_48 = 48.0
    interface OnChildClickListener{
        fun onClick(index : Int)
    }
}
private var paint : Paint ?= null  // 制作蓝色区域的画笔
private var paint2 : Paint ?= null  // 制作白色内圆的画笔
private var allHeight : Int = 0  // 总高度,便是制作的规模
private var bgHeight : Int = 0  // 布景的高度,便是蓝色矩阵的规模
private var mRadius : Int = 0  // 外圆的高度
private var mChildSize : Int = 0
private var mChildCenterSize : Int = 0
private var mWidthZone1 : Int = 0
private var mWidthZone2 : Int = 0
private var mChildCentre : Int = 0
private var childViews : MutableList<View> = mutableListOf()
private var objectAnimation : ObjectAnimator ?= null
var onChildClickListener : OnChildClickListener ?= null
init {
    initView()
}
private fun initView(){
    val lp = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
        DimensionUtils.dp2px(context, DIMENS_64).toInt())
    layoutParams = lp
    allHeight = DimensionUtils.dp2px(context, DIMENS_96).toInt()
    bgHeight = DimensionUtils.dp2px(context, DIMENS_64).toInt()
    mRadius = DimensionUtils.dp2px(context, DIMENS_50).toInt()
    mChildSize = DimensionUtils.dp2px(context, DIMENS_48).toInt()
    mChildCenterSize = DimensionUtils.dp2px(context, DIMENS_64).toInt()
    setWillNotDraw(false)
    initPaint()
}
private fun initPaint(){
    paint = Paint()
    paint?.isAntiAlias = true
    paint?.color = context.resources.getColor(R.color.kylin_main_color)
    paint2 = Paint()
    paint2?.isAntiAlias = true
    paint2?.color = context.resources.getColor(R.color.kylin_third_color)
}

上边是先把一些尺度给定义好(我这边是没有规划图,自己去直接调整的,所以或许有些视觉作用不太好,假如有规划师帮忙的话作用肯定会好些),制作流程便是制作3个形状,然后代码里也加了些注释哪个变量有什么用,这步应该不难,没什么能够多解释的。

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    val wSize = MeasureSpec.getSize(widthMeasureSpec)
    // 拿到子view做操作的,和这步无关,能够先不看
    if (childViews.size <= 0) {
        for (i in 0 until childCount) {
            val cView = getChildAt(i)
            initChildView(cView, i)
            childViews.add(cView)
            if (i == childCount/2){
                val ms: Int = MeasureSpec.makeMeasureSpec(mRadius, MeasureSpec.AT_MOST)
                measureChild(cView, ms, ms)
            }else {
                val ms: Int = MeasureSpec.makeMeasureSpec(mChildSize, MeasureSpec.AT_MOST)
                measureChild(cView, ms, ms)
            }
        }
    }
    setMeasuredDimension(wSize, allHeight)
}

这步其实也很简单,便是说给当前自定义view设置高度为allHeight

override fun onDraw(canvas: Canvas?) {
    super.onDraw(canvas)
    // 制作长方形区域
    canvas?.drawRect(left.toFloat(), ((allHeight - bgHeight).toFloat()),
        right.toFloat(), bottom.toFloat(), paint!!)
    // 制作圆形区域
    paint?.let {
        canvas?.drawCircle(
            (width/2).toFloat(), mRadius.toFloat(),
            mRadius.toFloat(),
            it
        )
    }
    // 制作内圆区域
    paint2?.let {
        canvas?.drawCircle(
            (width/2).toFloat(), mRadius.toFloat(),
            (mRadius - 28).toFloat(),
            it
        )
    }
}

最后进行制作, 便是上面说的制作3个图形,代码里的注释也说得很清楚。

2. 增加子view

我这儿是外面布局去加子view的,想弄得灵活点(但感觉也不太好,后边仍是想改成里面定义一套规范来弄会好些,假如自由度太高的话去做自定义就很麻烦,并且实践开发中这种需求也没必要把扩展性做到这种境地,根本便是整个APP只要一个当地运用)

但是这边也仅仅一个Demo先做个演示。

<com.kylin.libkcommons.widget.BottomMenuBar
    android:id="@+id/bv_content"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_alignParentBottom="true"
    >
    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/home"
        />
    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/video"
        />
    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/more"
        />
</com.kylin.libkcommons.widget.BottomMenuBar>
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    val wSize = MeasureSpec.getSize(widthMeasureSpec)
    if (childViews.size <= 0) {
        for (i in 0 until childCount) {
            val cView = getChildAt(i)
            initChildView(cView, i)
            childViews.add(cView)
            if (i == childCount/2){
                val ms: Int = MeasureSpec.makeMeasureSpec(mRadius, MeasureSpec.AT_MOST)
                measureChild(cView, ms, ms)
            }else {
                val ms: Int = MeasureSpec.makeMeasureSpec(mChildSize, MeasureSpec.AT_MOST)
                measureChild(cView, ms, ms)
            }
        }
    }
    setMeasuredDimension(wSize, allHeight)
}

拿到子view进行一个管理,做一些初始化的操作,主要是设点击事情这些,这儿不是很重要。

override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
    if (mChildCentre == 0){
        mChildCentre = width / 6
    }
    // 辅佐事情分发区域
    if (mWidthZone1 == 0 || mWidthZone2 == 0) {
        mWidthZone1 = width / 2 - mRadius / 2
        mWidthZone2 = width / 2 + mRadius / 2
    }
    // 设置每个子view的显示区域
    for (i in 0 until childViews.size) {
        if (i == childCount/2){
            childViews[i].layout(mChildCentre*(2*i+1) - mChildCenterSize/2 ,
                allHeight/2 - mChildCenterSize/2,
                mChildCentre*(2*i+1) + mChildCenterSize/2 ,
                allHeight/2 + mChildCenterSize/2)
        }else {
            childViews[i].layout(mChildCentre*(2*i+1) - mChildSize/2 ,
                allHeight - bgHeight/2 - mChildSize/2,
                mChildCentre*(2*i+1) + mChildSize/2 ,
                allHeight - bgHeight/2 + mChildSize/2)
        }
    }
}

进行布局,这儿比较重要,因为能看出,中心的图标会更大一些,所以要做一些适配。其实这儿便是把宽度分为6块,然后3个view分别在1,3,5这三个左边点,y的话便是除中心那个,其它两个都是bgHeight制作高度的的一半,中心那个是allHeight总高度的一半,这样3个view的x和y坐标都能拿到了,再依据宽高就能算出l,t,r,b四个点,然后布局。

3. 处理事情分发

能够看出我们的区域是一个不规则的区域,按照我们用抽象的视点去考虑,我们期望这个菜单栏的区域仅仅显示蓝色的那个区域,所以蓝色区域上面的白色区域就算是我们自定义view的规模,他触发的事情也应该是后边的view的事情(Demo中后边的View是一个ViewPager),而不是菜单栏。

// 辅佐事情分发区域
if (mWidthZone1 == 0 || mWidthZone2 == 0) {
    mWidthZone1 = width / 2 - mRadius / 2
    mWidthZone2 = width / 2 + mRadius / 2
}

这两块是圆外的x的区域。

/**
 *  判别点击事情是否在点击区域中
 */
private fun isShowZone(x : Float, y : Float) : Boolean{
    if (y >= allHeight - bgHeight){
        return true
    }
    if (x >= mWidthZone1 && x <= mWidthZone2){
        // 在圆内
        val relativeX = abs(x - width/2)
        val squareYZone = mRadius.toDouble().pow(2.0) - relativeX.toDouble().pow(2.0)
        return y >= mRadius - sqrt(squareYZone)
    }
    return false
}

先判别y假如在布景的矩阵中(上面说了自定义view分成矩阵,外圆,内圆),那肯定是菜单的区域。假如不在,那就要判别y在不在圆内,这儿就必须用勾股定理去判别。

override fun onTouchEvent(event: MotionEvent?): Boolean {
    // 点击区域进行阻拦
    if (event?.action == MotionEvent.ACTION_DOWN && isShowZone(event.x, event.y)){
        return true
    }
    return super.onTouchEvent(event)
}

最后做一个事情分发的阻拦。除了计算区域那或许需要去想想,其它当地我觉得都挺好理解的吧。

4. 做个动画

给子view设点击事情让外部处理,然后给中心的按钮做个动画作用。

private fun initChildView(cView : View?, index : Int) {
    cView?.setOnClickListener {
        if (index == childViews.size/2) {
            startAnim(cView)
        }else {
            onChildClickListener?.onClick(index)
        }
    }
}
private fun startAnim(view : View){
    if (objectAnimation == null) {
        objectAnimation = ObjectAnimator.ofFloat(view,
            "rotation", 0f, -15f, 180f, 0f)
        objectAnimation?.addListener(object : Animator.AnimatorListener {
            override fun onAnimationStart(p0: Animator) {
            }
            override fun onAnimationEnd(p0: Animator) {
                onChildClickListener?.onClick(childViews.size / 2)
            }
            override fun onAnimationCancel(p0: Animator) {
                onChildClickListener?.onClick(childViews.size / 2)
            }
            override fun onAnimationRepeat(p0: Animator) {
            }
        })
        objectAnimation?.duration = 1000
        objectAnimation?.interpolator = AccelerateDecelerateInterpolator()
    }
    objectAnimation?.start()
}

留意做开释操作。

fun onDestroy(){
    try {
        objectAnimation?.cancel()
        objectAnimation?.removeAllListeners()
    }catch (e : Exception){
        e.printStackTrace()
    }finally {
        objectAnimation = null
    }
}

5. 小结

其实代码都挺简单的,关键是你要去想出一个方法来完成这个场景,然后感觉这个自定义viewgroup也是比较经典的,涉及到measure、layout、draw,涉及到动画,涉及到点击抵触。

这个Demo表示你要完成怎样的作用都能够,只要是draw能画出来的,你都能完成,我这个是中心凸出来,你能够完成凹进去,你能够完成波浪的样子,能够完成杂乱的曲线,都行,你用各种基础图形去做拼接,或许画贝塞尔等等,其实都不难,主要是要有个计算和调试的进程。但是你的形状要和点击区域相关起来,你规划的图案越杂乱,你要适配的点击区域计算量就越大。

乃至我还能做得作用更屌的是,那3个子view的图标,我都能画出来,就不用ImagerView,直接手动画出来,这样做的好处是什么呢?我对子view的图标能做各种炫酷的特点动画,我在切换viewpager时对图标做特点动画,那不得逼格再上一层。 为什么我没做呢,因为没有规划,我自己做的话要花大量的时刻去调,要是有规划的话他告诉我尺度啊位置啊这些信息,做起来就很快。我的APP主要是计划完成视频的修改为主,所以这些支线就没计划花太多时刻去处理。