Android 菜單欄DIY實現(xiàn)效果詳解
前言
個人打算開發(fā)個視頻編輯的APP,然后把一些用上的技術(shù)總結(jié)一下,這次主要是APP的底部菜單欄用到了一個自定義View去繪制實現(xiàn)的,所以這次主要想講講自定義View的一些用到的點和自己如何去DIY一個不一樣的自定義布局。
實現(xiàn)的效果和思路
可以先看看實現(xiàn)的效果

兩個頁面的內(nèi)容還沒做,當前就是一個Demo,可以看到底部的菜單欄是一個繪制出來的不規(guī)則的一個布局,那要如何實現(xiàn)呢??梢韵葋砜纯此囊粋€繪制區(qū)域:

就是一個底部的布局和3個子view,底部的區(qū)域當然也是個規(guī)則的區(qū)域,只不過我們是在這塊區(qū)域上去進行繪制。
可以把整個過程分為幾個步驟:
1. 繪制底部布局
- (1) 繪制矩形區(qū)域
- (2) 繪制外圓形區(qū)域
- (3) 繪制內(nèi)圓形區(qū)域
2. 添加子view進行布局
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 // 繪制藍色區(qū)域的畫筆
private var paint2 : Paint ?= null // 繪制白色內(nèi)圓的畫筆
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)
}
上邊是先把一些尺寸給定義好(我這邊是沒有設(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)
}
這步其實也很簡單,就是說給當前自定義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
)
}
}
最后進行繪制, 就是上面說的繪制3個圖形,代碼里的注釋也說得很清楚。
2. 添加子view
我這里是外面布局去加子view的,想弄得靈活點(但感覺也不太好,后面還是想改成里面定義一套規(guī)范來弄會好些,如果自由度太高的話去做自定義就很麻煩,而且實際開發(fā)中這種需求也沒必要把擴展性做到這種地步,基本就是整個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進行一個管理,做一些初始化的操作,主要是設(shè)點擊事件這些,這里不是很重要。
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)
}
}
}
進行布局,這里比較重要,因為能看出,中間的圖標會更大一些,所以要做一些適配。其實這里就是把寬度分為6塊,然后3個view分別在1,3,5這三個左邊點,y的話就是除中間那個,其它兩個都是bgHeight繪制高度的的一半,中間那個是allHeight總高度的一半,這樣3個view的x和y坐標都能拿到了,再根據(jù)寬高就能算出l,t,r,b四個點,然后布局。
3. 處理事件分發(fā)
可以看出我們的區(qū)域是一個不規(guī)則的區(qū)域,按照我們用抽象的角度去思考,我們希望這個菜單欄的區(qū)域只是顯示藍色的那個區(qū)域,所以藍色區(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ū)域。
/**
* 判斷點擊事件是否在點擊區(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 {
// 點擊區(qū)域進行攔截
if (event?.action == MotionEvent.ACTION_DOWN && isShowZone(event.x, event.y)){
return true
}
return super.onTouchEvent(event)
}
最后做一個事件分發(fā)的攔截。除了計算區(qū)域那可能需要去想想,其它地方我覺得都挺好理解的吧。
4. 做個動畫
給子view設(shè)點擊事件讓外部處理,然后給中間的按鈕做個動畫效果。
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,涉及到動畫,涉及到點擊沖突。
這個Demo表示你要實現(xiàn)怎樣的效果都可以,只要是draw能畫出來的,你都能實現(xiàn),我這個是中間凸出來,你可以實現(xiàn)凹進去,你可以實現(xiàn)波浪的樣子,可以實現(xiàn)復(fù)雜的曲線,都行,你用各種基礎(chǔ)圖形去做拼接,或者畫貝塞爾等等,其實都不難,主要是要有個計算和調(diào)試的過程。但是你的形狀要和點擊區(qū)域關(guān)聯(lián)起來,你設(shè)計的圖案越復(fù)雜,你要適配的點擊區(qū)域計算量就越大。
甚至我還能做得效果更屌的是,那3個子view的圖標,我都能畫出來,就不用ImagerView,直接手動畫出來,這樣做的好處是什么呢?我對子view的圖標能做各種炫酷的屬性動畫,我在切換viewpager時對圖標做屬性動畫,那不得逼格再上一層。 為什么我沒做呢,因為沒有設(shè)計,我自己做的話要花大量的時間去調(diào),要是有設(shè)計的話他告訴我尺寸啊位置啊這些信息,做起來就很快。我的APP主要是打算實現(xiàn)視頻的編輯為主,所以這些支線就沒打算花太多時間去處理。
以上就是Android 菜單欄DIY實現(xiàn)效果詳解的詳細內(nèi)容,更多關(guān)于Android 菜單欄DIY的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android開發(fā)實現(xiàn)ListView點擊item改變顏色功能示例
這篇文章主要介紹了Android開發(fā)實現(xiàn)ListView點擊item改變顏色功能,涉及Android布局及響應(yīng)事件動態(tài)變換元素屬性相關(guān)操作技巧,需要的朋友可以參考下2017-11-11
Android 網(wǎng)絡(luò)html源碼查看器詳解及實例
這篇文章主要介紹了Android 網(wǎng)絡(luò)html源碼查看器詳解及實例的相關(guān)資料,需要的朋友可以參考下2017-03-03
Android中監(jiān)聽軟鍵盤顯示狀態(tài)實現(xiàn)代碼
這篇文章主要介紹了Android中監(jiān)聽軟鍵盤顯示狀態(tài)實現(xiàn)代碼,本文直接給出核心實現(xiàn)代碼,需要的朋友可以參考下2015-04-04
Android 解決build path errors的問題
這篇文章主要介紹了Android 解決build path errors的問題的相關(guān)資料,需要的朋友可以參考下2016-09-09
Activity透明/半透明效果的設(shè)置transparent(兩種實現(xiàn)方法)
兩種方法實現(xiàn)Activity透明/半透明效果的設(shè)置,代碼思路很有調(diào)理,感興趣的朋友可以參考下,希望本文可以幫助到你2013-02-02
Android 利用ViewPager實現(xiàn)圖片可以左右循環(huán)滑動效果附代碼下載
本文通過一個小demo給大家展示一段代碼實現(xiàn)viewpage圖片左右循環(huán)滑動效果,對viewgager循環(huán)滑動相關(guān)知識感興趣的朋友一起學習吧2015-11-11
Android Canvas之drawBitmap方法案例詳解
這篇文章主要介紹了Android Canvas之drawBitmap方法案例詳解,本篇文章通過簡要的案例,講解了該項技術(shù)的了解與使用,以下就是詳細內(nèi)容,需要的朋友可以參考下2021-08-08

