我报名参与金石计划1期应战——分割10万奖池,这是我的第3篇文章,点击检查活动详情
前语
个人计划开发个视频修改的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主要是计划完成视频的修改为主,所以这些支线就没计划花太多时刻去处理。