目录
前言
个人打算开发个视频编辑的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的资料请关注其它相关文章!