在一个项目中需要用到 DrawerLayout,但是其默认实现为边缘滑动打开侧滑界面,只能指定左边缘或者右边缘。想要实现全屏滑动,思路是通过反射的方式修改 DrawerLayout 的相应属性,涉及到枯燥的源码阅读。在完成全屏滑动之后,又发现其默认实现了长按弹出侧滑界面,在全屏滑动下,用户长按任何地方都会跳出侧滑菜单,而且还会出现留白问题。研究半天,还是利用反射的思路一并解决,特此记录。
DrawerLayout 侧滑
在 DrawerLayout 中定义了两个变量,分别对应 Gravity 为 Left 和 Right 的滑动情景,两者并无实质分别,本文只分析 Left 的情况。此外,DrawerLayout 包含三种状态,STATE_IDLE(已打开或已关闭),STATE_DRAGGING(正在拖动),STATE_SETTLING(执行打开或关闭的动画过程中)。
private final ViewDragHelper mLeftDragger; |
构造函数对一些变量做了初始化
mLeftCallback = new ViewDragCallback(Gravity.LEFT); |
ViewDraghelper 是官方提供的专门为自定义 ViewGroup 处理拖拽的手势类。此处用到的构造方法为
public static ViewDragHelper create(@NonNull ViewGroup forParent, float sensitivity, |
DrawerLayout 中侧滑打开界面正是通过 ViewDragHelper 实现的,查看 DrawerLayout 的onTouchEvent
方法
|
其明显调用了 ViewDragHelper 的processTouchEvent
方法处理 Touch 事件
public void processTouchEvent(MotionEvent ev) { |
重点在mInitialEdgesTouched[pointerId]
,其为一个保存边缘滑动值的 int 数组。在saveInitialMotion
方法中发现其赋值过程
private void saveInitialMotion(float x, float y, int pointerId) { |
原来是调用了getEdgesTouched
方法
private int getEdgesTouched(int x, int y) { |
可以看到,该方法将判断x < mParentView.get*() + mEdgeSize
,然后将对应的 result 返回。mEdgeSize
即为边缘滑动的临界值,其初始化值为
final float density = context.getResources().getDisplayMetrics().density; |
因此,要让 DrawerLayout 支持全屏滑动打开侧滑菜单而不是边缘滑动,重点便是要修改该值,将其设为屏幕宽度。
具体的反射代码(kotlin)
//获取 ViewDragHelper,更改 edgeSizeField |
DrawerLayout 长按弹出
在 DrawerLayout 中,用户在非侧滑界面的 mEdgeSize 范围内长按,侧滑界面将弹出。当我们修改 mEdgeSize 为屏幕宽度之后,用户所有的长按动作都将触发原来的弹出逻辑,而且触发范围为屏幕宽度,侧滑菜单将过度右移,造成左侧边缘有空白。
原来是 DrawerLayout 的私有内部类 ViewDragCallback 重写了onEdgeTouched
方法
private class ViewDragCallback extends ViewDragHelper.Callback |
|
该方法会执行一个 mPeekRunnable,其为内部类的私有 Runnable 类型的属性,其run
方法执行了peekDrawer
方法
void peekDrawer() { |
注意mDragger.smoothSlideViewTo(toCapture, childLeft, toCapture.getTop())
就是长按屏幕时,侧滑菜单会自动滑出来的原因。
解决这个问题着实费了一番脑筋,因为 ViewDragCallback 为私有内部类,外部无法直接得到其引用。幸好观察之后发现其实现了 ViewDragHelper.Callback 接口,从而让我们可以利用多态的方式,获取其反射实例
//获取 Layout 的 ViewDragCallBack 实例“mLeftCallback” |
完美解决问题!
最后便是构建一个工具类
object DrawerLayoutHelper { |