<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
個人打算開發個視訊編輯的APP,然後把一些用上的技術總結一下,這次主要是APP的底部選單欄用到了一個自定義View去繪製實現的,所以這次主要想講講自定義View的一些用到的點和自己如何去DIY一個不一樣的自定義佈局。
可以先看看實現的效果
兩個頁面的內容還沒做,當前就是一個Demo,可以看到底部的選單欄是一個繪製出來的不規則的一個佈局,那要如何實現呢。可以先來看看它的一個繪製區域:
就是一個底部的佈局和3個子view,底部的區域當然也是個規則的區域,只不過我們是在這塊區域上去進行繪製。
可以把整個過程分為幾個步驟:
1. 繪製底部佈局
2. 新增子view進行佈局
3. 處理事件分發的區域 (底部選單上邊的白色區域不觸發選單的事件)
4. 寫個動畫意思意思
這裡做的話就沒必要手動去新增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個圖形,程式碼裡的註釋也說得很清楚。
我這裡是外面佈局去加子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四個點,然後佈局。
可以看出我們的區域是一個不規則的區域,按照我們用抽象的角度去思考,我們希望這個選單欄的區域只是顯示藍色的那個區域,所以藍色區域上面的白色區域就算是我們自定義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) }
最後做一個事件分發的攔截。除了計算區域那可能需要去想想,其它地方我覺得都挺好理解的吧。
給子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 } }
其實程式碼都挺簡單的,關鍵是你要去想出一個方法來實現這個場景,然後感覺這個自定義viewgroup也是比較經典的,涉及到measure、layout、draw,涉及到動畫,涉及到點選衝突。
這個Demo表示你要實現怎樣的效果都可以,只要是draw能畫出來的,你都能實現,我這個是中間凸出來,你可以實現凹進去,你可以實現波浪的樣子,可以實現複雜的曲線,都行,你用各種基礎圖形去做拼接,或者畫貝塞爾等等,其實都不難,主要是要有個計算和偵錯的過程。但是你的形狀要和點選區域關聯起來,你設計的圖案越複雜,你要適配的點選區域計算量就越大。
甚至我還能做得效果更屌的是,那3個子view的圖示,我都能畫出來,就不用ImagerView,直接手動畫出來,這樣做的好處是什麼呢?我對子view的圖示能做各種炫酷的屬性動畫,我在切換viewpager時對圖示做屬性動畫,那不得逼格再上一層。 為什麼我沒做呢,因為沒有設計,我自己做的話要花大量的時間去調,要是有設計的話他告訴我尺寸啊位置啊這些資訊,做起來就很快。我的APP主要是打算實現視訊的編輯為主,所以這些支線就沒打算花太多時間去處理。
以上就是Android 選單欄DIY實現效果詳解的詳細內容,更多關於Android 選單欄DIY的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45