Android 菜單欄DIY實現(xiàn)效果詳解
前言
個人打算開發(fā)個視頻編輯的APP,然后把一些用上的技術(shù)總結(jié)一下,這次主要是APP的底部菜單欄用到了一個自定義View去繪制實現(xiàn)的,所以這次主要想講講自定義View的一些用到的點(diǎn)和自己如何去DIY一個不一樣的自定義布局。
實現(xiàn)的效果和思路
可以先看看實現(xiàn)的效果
兩個頁面的內(nèi)容還沒做,當(dāng)前就是一個Demo,可以看到底部的菜單欄是一個繪制出來的不規(guī)則的一個布局,那要如何實現(xiàn)呢??梢韵葋砜纯此囊粋€繪制區(qū)域:
就是一個底部的布局和3個子view,底部的區(qū)域當(dāng)然也是個規(guī)則的區(qū)域,只不過我們是在這塊區(qū)域上去進(jìn)行繪制。
可以把整個過程分為幾個步驟:
1. 繪制底部布局
- (1) 繪制矩形區(qū)域
- (2) 繪制外圓形區(qū)域
- (3) 繪制內(nèi)圓形區(qū)域
2. 添加子view進(jìn)行布局
3. 處理事件分發(fā)的區(qū)域 (底部菜單上邊的白色區(qū)域不觸發(fā)菜單的事件)
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 // 繪制藍(lán)色區(qū)域的畫筆 private var paint2 : Paint ?= null // 繪制白色內(nèi)圓的畫筆 private var allHeight : Int = 0 // 總高度,就是繪制的范圍 private var bgHeight : Int = 0 // 背景的高度,就是藍(lán)色矩陣的范圍 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) }
上邊是先把一些尺寸給定義好(我這邊是沒有設(shè)計圖,自己去直接調(diào)整的,所以可能有些視覺效果不太好,如果有設(shè)計師幫忙的話效果肯定會好些),繪制流程就是繪制3個形狀,然后代碼里也加了些注釋哪個變量有什么用,這步應(yīng)該不難,沒什么可以多解釋的。
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { super.onMeasure(widthMeasureSpec, heightMeasureSpec) val wSize = MeasureSpec.getSize(widthMeasureSpec) // 拿到子view做操作的,和這步無關(guān),可以先不看 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) }
這步其實也很簡單,就是說給當(dāng)前自定義view設(shè)置高度為allHeight
override fun onDraw(canvas: Canvas?) { super.onDraw(canvas) // 繪制長方形區(qū)域 canvas?.drawRect(left.toFloat(), ((allHeight - bgHeight).toFloat()), right.toFloat(), bottom.toFloat(), paint!!) // 繪制圓形區(qū)域 paint?.let { canvas?.drawCircle( (width/2).toFloat(), mRadius.toFloat(), mRadius.toFloat(), it ) } // 繪制內(nèi)圓區(qū)域 paint2?.let { canvas?.drawCircle( (width/2).toFloat(), mRadius.toFloat(), (mRadius - 28).toFloat(), it ) } }
最后進(jìn)行繪制, 就是上面說的繪制3個圖形,代碼里的注釋也說得很清楚。
2. 添加子view
我這里是外面布局去加子view的,想弄得靈活點(diǎn)(但感覺也不太好,后面還是想改成里面定義一套規(guī)范來弄會好些,如果自由度太高的話去做自定義就很麻煩,而且實際開發(fā)中這種需求也沒必要把擴(kuò)展性做到這種地步,基本就是整個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進(jìn)行一個管理,做一些初始化的操作,主要是設(shè)點(diǎn)擊事件這些,這里不是很重要。
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { if (mChildCentre == 0){ mChildCentre = width / 6 } // 輔助事件分發(fā)區(qū)域 if (mWidthZone1 == 0 || mWidthZone2 == 0) { mWidthZone1 = width / 2 - mRadius / 2 mWidthZone2 = width / 2 + mRadius / 2 } // 設(shè)置每個子view的顯示區(qū)域 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) } } }
進(jìn)行布局,這里比較重要,因為能看出,中間的圖標(biāo)會更大一些,所以要做一些適配。其實這里就是把寬度分為6塊,然后3個view分別在1,3,5這三個左邊點(diǎn),y的話就是除中間那個,其它兩個都是bgHeight繪制高度的的一半,中間那個是allHeight總高度的一半,這樣3個view的x和y坐標(biāo)都能拿到了,再根據(jù)寬高就能算出l,t,r,b四個點(diǎn),然后布局。
3. 處理事件分發(fā)
可以看出我們的區(qū)域是一個不規(guī)則的區(qū)域,按照我們用抽象的角度去思考,我們希望這個菜單欄的區(qū)域只是顯示藍(lán)色的那個區(qū)域,所以藍(lán)色區(qū)域上面的白色區(qū)域就算是我們自定義view的范圍,他觸發(fā)的事件也應(yīng)該是后面的view的事件(Demo中后面的View是一個ViewPager),而不是菜單欄。
// 輔助事件分發(fā)區(qū)域 if (mWidthZone1 == 0 || mWidthZone2 == 0) { mWidthZone1 = width / 2 - mRadius / 2 mWidthZone2 = width / 2 + mRadius / 2 }
這兩塊是圓外的x的區(qū)域。
/** * 判斷點(diǎn)擊事件是否在點(diǎn)擊區(qū)域中 */ private fun isShowZone(x : Float, y : Float) : Boolean{ if (y >= allHeight - bgHeight){ return true } if (x >= mWidthZone1 && x <= mWidthZone2){ // 在圓內(nèi) 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分成矩陣,外圓,內(nèi)圓),那肯定是菜單的區(qū)域。如果不在,那就要判斷y在不在圓內(nèi),這里就必須用勾股定理去判斷。
override fun onTouchEvent(event: MotionEvent?): Boolean { // 點(diǎn)擊區(qū)域進(jìn)行攔截 if (event?.action == MotionEvent.ACTION_DOWN && isShowZone(event.x, event.y)){ return true } return super.onTouchEvent(event) }
最后做一個事件分發(fā)的攔截。除了計算區(qū)域那可能需要去想想,其它地方我覺得都挺好理解的吧。
4. 做個動畫
給子view設(shè)點(diǎn)擊事件讓外部處理,然后給中間的按鈕做個動畫效果。
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. 小結(jié)
其實代碼都挺簡單的,關(guān)鍵是你要去想出一個方法來實現(xiàn)這個場景,然后感覺這個自定義viewgroup也是比較經(jīng)典的,涉及到measure、layout、draw,涉及到動畫,涉及到點(diǎn)擊沖突。
這個Demo表示你要實現(xiàn)怎樣的效果都可以,只要是draw能畫出來的,你都能實現(xiàn),我這個是中間凸出來,你可以實現(xiàn)凹進(jìn)去,你可以實現(xiàn)波浪的樣子,可以實現(xiàn)復(fù)雜的曲線,都行,你用各種基礎(chǔ)圖形去做拼接,或者畫貝塞爾等等,其實都不難,主要是要有個計算和調(diào)試的過程。但是你的形狀要和點(diǎn)擊區(qū)域關(guān)聯(lián)起來,你設(shè)計的圖案越復(fù)雜,你要適配的點(diǎn)擊區(qū)域計算量就越大。
甚至我還能做得效果更屌的是,那3個子view的圖標(biāo),我都能畫出來,就不用ImagerView,直接手動畫出來,這樣做的好處是什么呢?我對子view的圖標(biāo)能做各種炫酷的屬性動畫,我在切換viewpager時對圖標(biāo)做屬性動畫,那不得逼格再上一層。 為什么我沒做呢,因為沒有設(shè)計,我自己做的話要花大量的時間去調(diào),要是有設(shè)計的話他告訴我尺寸啊位置啊這些信息,做起來就很快。我的APP主要是打算實現(xiàn)視頻的編輯為主,所以這些支線就沒打算花太多時間去處理。
以上就是Android 菜單欄DIY實現(xiàn)效果詳解的詳細(xì)內(nèi)容,更多關(guān)于Android 菜單欄DIY的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android 調(diào)用系統(tǒng)聯(lián)系人界面(添加聯(lián)系人,添加已有聯(lián)系人,編輯和修改)
這篇文章主要介紹了Android 調(diào)用系統(tǒng)聯(lián)系人界面(添加聯(lián)系人,添加已有聯(lián)系人,編輯和修改),非常不錯,具有參考借鑒價值,需要的朋友可以參考下2017-03-03Android編程實現(xiàn)點(diǎn)擊鏈接打開APP功能示例
這篇文章主要介紹了Android編程實現(xiàn)點(diǎn)擊鏈接打開APP功能,結(jié)合實例形式較為詳細(xì)的分析了Android實現(xiàn)點(diǎn)擊鏈接打開APP功能的具體步驟與相關(guān)注意事項,需要的朋友可以參考下2017-01-01Android中執(zhí)行java命令的方法及java代碼執(zhí)行并解析shell命令
這篇文章給大家介紹Android中執(zhí)行java命令的方法及java代碼執(zhí)行并解析shell命令,需要的朋友一起學(xué)習(xí)2015-11-11Android中的ViewPager視圖滑動切換類的入門實例教程
Android中ViewPager通常與Fragments組件共同使用來實現(xiàn)視圖切換功能,本文就帶大家一起來學(xué)習(xí)Android中的ViewPager視圖滑動切換類的入門實例教程:2016-06-06Android 實現(xiàn)無網(wǎng)絡(luò)頁面切換的示例代碼
本篇文章主要介紹了Android 實現(xiàn)無網(wǎng)絡(luò)頁面切換的示例代碼,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-09-09基于Fedora14下自帶jdk1.6版本 安裝jdk1.7不識別的解決方法
本篇文章是對Fedora14下自帶jdk1.6版本,安裝jdk1.7不識別的解決方法進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-05-05