最近在项目开发上有一个侧拉菜单的需求,之前想到的会是android官方提供的DrawerLayout,DrawerLayout 为android 2.3 及以上系统版本提供侧拉的功能实现,但在项目中有露出拖拽标签的需求,入下图这样:
这点官方提供的控件似乎并不能满足需求,于是我自己写了个侧拉控件,实现原理与逻辑都十分简单,今天就大家分享一下我基于FrameLayout及ObjectAnimation的侧拉菜单,也算给想要做自订义控件的小伙伴入一下门。 先来看一下侧拉布局的原理图:
控件的使用方法
<DrwaerPlusLayout>
<!—中间主体布局-->
<FrameLayout/>
<!—侧拉菜单布局-->
<FrameLayout/>
DrwaerPlusLayout>
可以看出来,整个侧拉控件由两个帧布局(FrameLayout)及一个MaskView(就是View, 用于菜单打开时的蒙板效果)组成,实现逻辑(onFinishInflate-> onMeasure-> onLayout->OnTouch()):
下面是我实现侧拉控件的思路
发现:在对侧拉菜单的使用中发现,一般的侧拉是视图在横轴方向上的的平移
思考: 1. 平移怎么实现?->实现平移->2. 侧拉与平移有什么细节上的不同? ->实现侧拉 ->细节调整
接下来就跟着这个思路一步步实现自定义的侧拉控件:
解决第一个问题 : 平移怎么实现?
在android基础中关于touch触摸事件的介绍中大多都有跟随手指的小球的例子,大概的意思就是,视图设置了触摸监听之后,当手指触摸视图,即会响应触摸监听器中对应的方法。在OnTouch()函数的MotionEvent 携带了事件信息(事件类型和触摸的坐标X,Y),在拖动(MOVE)时将触摸的坐标信息赋值给小球图片;
回顾一下“拖动小球” MOVE(拖动)代码
case MotionEvent.ACTION_MOVE:
ball.x = event.getRawX; //getRawX获取触摸屏幕位置的X坐标,getX获取的是触摸在父容器的坐标位置
ball.y = event.getRawY;
那怎么实现平移?
如果需要实现横轴方向上的平移,只需将
ball.y = event.getRawY;
这句代码删掉;
现在就有了一个跟随手指左右平移的小球。
问题2:侧拉和平移有什么不同?
侧拉和平移的不相同点有以下几点,
1. 侧拉需要从界面外面进入;
2. 侧拉菜单不能拉出超过菜单自身的宽度;
3. 当拉出位置超过菜单宽度一半时,菜单自动展开,反之自动收回;
4. 有蒙板遮罩
思考: 现在有这么一个需求,让菜单从界面外进来,那么如何去实现这个过程呢?第一个大前提必须是菜单在界面外(总感觉想废话),需要视图要从屏幕外进入,就在给各个布局摆放位置时(onLayout),让视图摆在屏幕之外。
那现在要做的就是把视图放到界面里,然后把它全部移出界面。
把视图放到界面的方法:
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
//设置布局视图位置,从左上开始,铺满屏幕
view.layout(left, top, right, bottom);
}
接下来就是把视图放到界面外,计算当视图完全在界面外边时,他的起点坐标(左上坐标)是(-view.宽, 0),终点坐标(右上坐标)是 (0,0);
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
//设置布局视图位置,从左上开始,铺满屏幕
view.layout(-view.getWidth, top, 0(或是left), bottom);
}
这样,左边的菜单视图就被放到了界面外面,接下来要做的事就是让它跟着手指滑进来;
要让菜单跟着手指滑进来,得让它知道当什么时候,需要做什么,因此首先我们得与菜单达成协议:
那到底什么时候让他滑进来呢,有如下几种解决方案
让菜单露出一些到界面,直接将菜单拖出来。(本文例子以拖拽标签侧拉为例,因此是这中方法)
还有其他方法比如点击一个按钮就弹出来,喊一声就弹出来等等方法。这都是跟菜单之间定的协议,可以根据各种需求,去制定各种协议,这里不做深究。(对于协议的解释,就是双方或多方约定好什么时候,做什么,在编程的时候也是,只要自己定好各种协议,无论是界面控件的进入方式,底层数据的封装,都能够很清晰的解决)。
最终跟我们的菜单达成的协议是这样的,你露一点进来,我一拽你(什么时候)你就进来(做什么);
达成了协议,制定好让菜单什么时候进入的方法,接下来解决下一个问题。
现在菜单进来了,但是又规定,菜单一条边必须贴在左边的边界上,那菜单能绘制到的最大范围就是从左边的边界开始到菜单宽度的位置。
同样,当菜单被拖出,需要告诉它,终点在哪,到哪你就不能动了,如果我们不想菜单被脱离屏幕左边界,那么这个终点位置就是菜单本身的宽度。(菜单宽200, 能移动覆盖的范围就是0-200)
让菜单的一部分露出到屏幕,只需重新调整位置摆放。在其本身的位置向右偏移leftDistance个单位dip
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
//设置布局视图位置,从左上开始,铺满屏幕
view.layout(-view.getWidth + leftDistance, top, 0(或是left)+ leftDistance, bottom);
}
这样我们就达到了露出菜单的目的,接下来我们需要跟菜单达成协议,我往右拖你的时候你就出来啊,这时候就要给视图添加触摸Touch监听。
View.setOnTouchListener(this);
当触发了协议的内容(往右拖动了,什么时候),就执行OnTouch函数(做什么)。
在OnTouch中判断View的位置是否超过了View的宽度。
case MotionEvent.ACTION_MOVE:
float distanceX = lastX - motionEvent.getRawX();
//下一次的位置 用于判断是否越界
float nextX = view.getX() - distanceX;
if (nextX > 0){
nextX = 0;
}
View.setX(nextX);
lastX = motionEvent.getRawX();
break;
解释一下以上代码,distanceX是这次MOVE事件触发所移动的距离,nextX是这次MOVE事件执行后View的X坐标,当执行后的X坐标大于0(即视图离开左边框时),将nextX一直置为0 视图便不再移动。
思考:如果手指往右上方移动,抽屉还出来么?如果需要确定手指移动方向,比较一下手指位移的x y的大小即可。
需求: 侧拉超过一半长度释放菜单时,自动展开,反之自动收回;
处理这个逻辑时,首先的弄明白什么时候判断当前侧拉的长度,即释放侧拉菜单时,在UP手指抬起动作做这个处理,当手指抬起时,判断已经侧拉出的长度与视图的宽度做比较。
当侧拉距离超过一般时,则以水平平移动画的方式,将菜单完全打开。
水平动画示例:
if (lastX > 0.5f * View.getWidth){
ObjectAnimation.ofFloat(View, “x”, startX, endX).setDuration(100).start();
}
此刻当侧拉距离大于视图一半时,就能执行平移动画并完全展开了。
需求: 蒙板遮罩
蒙板遮罩的作用主要还是突出菜单的内容,减少底层界面视觉上的影响,隐藏遮罩层的存在还是十分有必要的,我们这使用的遮罩层本身就是一个View,初始化属性时,将其背景色设为透黑色(0x88000000),通过监听侧拉的动作来设置遮罩层的透明度。
什么时候改变遮罩层的透明度?
当用户拖动菜单时,或是菜单被突然关闭或打开时,需要改变遮罩层透明度。
因此设计一下函数,根据菜单的x轴位置设置遮罩透明度。
public void serMaskAlpha(int x , View view){
maskView.setAlpha(x/(float)(x / (view.getWidth*0.25)));
}
将这个方法放入MOVE事件中,传入当前的X值以及view即可;
思考:在菜单突然关闭时,将maskView的alpha值设为0,这段逻辑放哪实现?
接下来制作侧拉按钮的图片,并放置在菜单右侧露出来的部分。
当完成以上操作我们就有了一个带侧拉按钮的侧拉菜单了,效果如下图:
下一次将给自定义侧拉控件加上自定义的属性,为控件布局增加更多可变性,使控件能适应更多的应用场景。