一、需求
对QQ抽屉导航栏效果进行复现。
二、实现方式
- DrawerLayout
- ViewDragHelper
- 自定义ViewGroup
三、调研
-
DrawerLayout
DrawerLayout包含两个子布局,内容布局以及菜单栏布局。实现 addDrawerListener接口以及在回调方法里进行逻辑处理。
-
ViewDragHelper
View拖拽帮助类,创建实例并重写callback接口回调方法处理逻辑。 -
自定义ViewGroup
重写事件有关方法,针对事件进行布局处理。 -
确定实现方式
- DrawerLayout实现简单方便,但是由于封装的原因,很难了解到原理。
- ViewDragHelper的tryCaptureView()方法只能捕获需要移动的View,而我们需要对菜单栏布局进行位移,需要在onViewCaptured()方法中调用captureChildView()传入需要进行操作的布局。
public void captureChildView(@NonNull View childView, int activePointerId) {
if (childView.getParent() != mParentView) {
throw new IllegalArgumentException("captureChildView: parameter must be a descendant "
+ "of the ViewDragHelper's tracked parent view (" + mParentView + ")");
}
mCapturedView = childView;
mActivePointerId = activePointerId;
mCallback.onViewCaptured(childView, activePointerId);
setDragState(STATE_DRAGGING);
}
在captureChildView()中mCapturedView表示捕获的View,我们需要在这里把菜单栏布局View传 入,但是此方法还会调用onViewCaptured()方法,形成死循环。可以在onViewCaptured()方法中加一个变量判断是否是第一次调用避免死循环。但是在ViewDragHelper事件方法回调中还是会调用onViewCaptured()方法将包含事件的View传入,所以即使进行了View调换但是最后还是会捕获包含事件的View,可实现较难。
- 自定义ViewGroup 只需将dispatchTouchEvent()方法进行重写,针对事件进行逻辑处理即可。
四、分析
1.当在主页面进行滑动时,菜单栏跟着滑动,即菜单栏滑动距离依赖于滑动距离。
2.当手指松开时,滑动距离大于等于屏幕宽度1/2,菜单栏布局自动铺满屏幕,否则显示内容布局。
3.当菜单栏滑动时,主页面做缩放及透明度动画。
当事件结束时,菜单栏始终拥有两种状态:OUT(屏幕外)和FULL(填充屏幕)
右滑时,事件结束时事件位置始终大于等于事件开始时位置(当等于时,菜单栏已经全部在屏幕外,所以小于时看不到效果直接不做判断)。左滑一样。
五、代码实现
因为是自定义ViewGroup,所以实现FrameLayout布局。
class FirstFrameLayout(context: Context, attributeSet: AttributeSet? = null) : FrameLayout(context, attributeSet){}
在onFinishInflate()中获取子布局对象
override fun onFinishInflate() {
super.onFinishInflate()
menuLayout = getChildAt(1) as RelativeLayout
contentLayout = getChildAt(0) as FrameLayout
}
重写onLayout()布局,确定子布局位置
override fun onLayout(p0: Boolean, p1: Int, p2: Int, p3: Int, p4: Int) {
secondLayout.layout(-right, top, left, bottom)
firstLayout.layout(left,top,right,bottom)
}
设置菜单栏状态
enum class State {
OUT,
FULL,
}
重写dispatchTouchEvent()方法进行事件拦截
var first = 0f
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
when (ev.action) {
MotionEvent.ACTION_DOWN -> {
first = ev.x //记录第一次DOWN事件,X轴坐标位置
}
MotionEvent.ACTION_MOVE -> {
val dif = ev.x - first //每次MOVE事件移动距离
if (flag == State.OUT) { //菜单栏从左往右滑动
if (dif > 0) {
menuLayout.left = -right
menuLayout.right = 0
menuLayout.offsetLeftAndRight(dif.toInt()) //菜单栏布局进行位移
scaleSecond(dif) //内容布局动画
}
} else if (flag == State.FULL) { //菜单栏从右往左滑动
if (dif < 0) {
menuLayout.left = 0
menuLayout.right = right
menuLayout.offsetLeftAndRight(dif.toInt())
scaleSecond1(dif)
}
}
}
MotionEvent.ACTION_UP -> {
val dif = ev.x - first//UP事件移动距离
if (flag == State.OUT) { //从外往里滑动停止
flag = if (dif >= right / 2) { //距离大于1/2 菜单栏全屏 状态置为FULL
menuLayout.layout(left, top, right, bottom)
State.FULL
} else {//距离小于1/2 菜单栏左挨屏幕 状态不变
menuLayout.layout(-right, top, left, bottom)
release()
State.OUT
}
} else if (flag == State.FULL) {
flag = if (dif < 0 && abs(dif) >= right / 2) {
menuLayout.layout(-right, top, left, bottom)
State.OUT
} else {
menuLayout.layout(left, top, right, bottom)
release1()
State.FULL
}
}
}
}
return true //必须返回true,否则只会拦截DOWN事件后,对后续其他UP、MOVE事件不做处理。
}
//对contentLayout动画清空
private fun release() {
contentLayout.scaleX = 1f
contentLayout.scaleX = 1f
contentLayout.alpha = 1f
}
//contentLayout缩放及透明度动画
private fun scaleSecond(dif: Float) {
val scale = 1 - ((dif / right) * 0.05f)
val alpha = 1 - ((dif / right) * 0.6f)
contentLayout.scaleX = scale
contentLayout.scaleY = scale
contentLayout.alpha = alpha
}
六、小结
实现前应该针对抽屉效果进行分类,针对不同状态做不同处理,可提高效率。