首頁 > 軟體

Android 選單欄DIY實現效果詳解

2022-09-14 22:02:47

前言

個人打算開發個視訊編輯的APP,然後把一些用上的技術總結一下,這次主要是APP的底部選單欄用到了一個自定義View去繪製實現的,所以這次主要想講講自定義View的一些用到的點和自己如何去DIY一個不一樣的自定義佈局。

實現的效果和思路

可以先看看實現的效果

兩個頁面的內容還沒做,當前就是一個Demo,可以看到底部的選單欄是一個繪製出來的不規則的一個佈局,那要如何實現呢。可以先來看看它的一個繪製區域:

就是一個底部的佈局和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主要是打算實現視訊的編輯為主,所以這些支線就沒打算花太多時間去處理。

以上就是Android 選單欄DIY實現效果詳解的詳細內容,更多關於Android 選單欄DIY的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com