DrawerLayout使用心得、踩坑经验以及定制化

基础使用

这里要介绍的是google的DrawerLayout,行为可见google官方应用如gmail;而手Q的抽屉则是根据android-undergarment项目来定制的一个控件。

Google Desgin GuildLines内的介绍

官方教程:Creating a Navigation Drawer

DrawerLayout添加在主内容区的上层,作为parent,下面的第一个child是主内容区域,第二个child则可以是其他任何东西,需要作为抽屉的view则需要声明android:layout_gravity。

DrawerLayout的setScrimColor可以设置抽屉拉出时右侧主内容剩余区域上面盖的颜色(默认0x99000000)。

高级应用

DrawerLayout默认只有在边缘的一个edge能够触发抽屉拉取的动作,而这个是通过ViewDragHelper这个类来实现的。

1
2
3
4
private static final int EDGE_SIZE = 20; // dp  

private static final int BASE_SETTLE_DURATION = 256; // ms
private static final int MAX_SETTLE_DURATION = 600; // ms

EDGE_SIZE是触发区域,默认20dp,而BASE_SETTLE_DURATION和MAX_SETTLE_DURATION则是控制抽屉从打开到关闭之间的这个间隔。由于是私有静态常量,可以通过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static void setDrawerLeftEdgeSize(DrawerLayout drawerLayout, float dp) {  
if (drawerLayout == null) {
return;
}

try {
// find ViewDragHelper and set it accessible
Field leftDraggerField = drawerLayout.getClass().getDeclaredField("mLeftDragger");
leftDraggerField.setAccessible(true);
ViewDragHelper leftDragger = (ViewDragHelper) leftDraggerField.get(drawerLayout);
// find edgesize and set is accessible
Field edgeSizeField = leftDragger.getClass().getDeclaredField("mEdgeSize");
edgeSizeField.setAccessible(true);
int edgeSize = edgeSizeField.getInt(leftDragger);
edgeSizeField.setInt(leftDragger, Math.max(edgeSize, ViewUtils.dpToPx(dp)));
} catch (NoSuchFieldException e) {
// ignore
} catch (IllegalArgumentException e) {
// ignore
} catch (IllegalAccessException e) {
// ignore
}
}

来反射设置左侧的触发区域,类似地可以修改右侧触发区域以及打开动画的间隔(当然你也可以直接去ViewDragHelper里面修改)。

不建议自己处理onTouch,会导致抽屉不能平滑跟手,比如stackoverflow上有给出以下这种方案的,简直坑爹:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// ======================== 触摸事件处理 ===================================  
private float startX, startY;

public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
startX = ev.getX();
startY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
case MotionEvent.ACTION_UP:
float endX = ev.getX();
float endY = ev.getY();

if (startX > HOT_FIELD || Math.abs(endY - startY) > SENSIBILITY_Y) {
break;
}
// From left to right
if (endX - startX >= SENSIBILITY_X) {
handled = openDrawer();
}
// From right to left
if (startX - endX >= SENSIBILITY_X) {
handled = closeDrawer();
}
break;
}
if (handled) {
mDrawerLayout.cancelChildViewTouch();
}
return handled;
}


## 坑爹的bug们
### 初始化LayoutParam时可能出错

```java
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
LayoutParams layoutParams = null;
try {
// 出现异常时,用默认值
layoutParams = new LayoutParams(getContext(), attrs);
} catch (Throwable e) {
layoutParams = null;
}
if (layoutParams == null) {
layoutParams = new LayoutParams(-1, -1);
layoutParams.gravity = Gravity.NO_GRAVITY;
}
return layoutParams;
}

多点触摸的时候DrawerLayout抛出ArrayIndexOutOfBoundsException

这是由于多点触摸时候requestDisallowInterceptTouchEvent和DrawerLayout的innerViews问题。自己在外面继承DrawerLayout然后改一下行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class SafeDrawerLayout extends DrawerLayout {

public SafeDrawerLayout(Context context) {
super(context);
}

public SafeDrawerLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}

public SafeDrawerLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}

private boolean mIsDisallowIntercept = false;

@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
// keep the info about if the innerViews do requestDisallowInterceptTouchEvent
mIsDisallowIntercept = disallowIntercept;
super.requestDisallowInterceptTouchEvent(disallowIntercept);
}

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// the incorrect array size will only happen in the multi-touch scenario.
if (ev.getPointerCount() > 1 && mIsDisallowIntercept) {
requestDisallowInterceptTouchEvent(false);
boolean handled = super.dispatchTouchEvent(ev);
requestDisallowInterceptTouchEvent(true);
return handled;
} else {
return super.dispatchTouchEvent(ev);
}
}
}

有时候手动拉出抽屉时候,抽屉会卡在那里,拉不出来

这也是极其坑爹的一个bug,原因是触摸EDGE的时候,事件触发到抽屉出现有一个延时:

1
2
3
4
5
6
7
8
/**
* Length of time to delay before peeking the drawer.
*/
private static final int PEEK_DELAY = 160; // ms
@Override
public void onEdgeTouched(int edgeFlags, int pointerId) {
postDelayed(mPeekRunnable, PEEK_DELAY);
}

抽屉有STATE_IDLE, STATE_DRAGGING和STATE_SETTLING三种状态,而这个偶然状况下,已经处于STATE_DRAGGING,而这个动作打开了抽屉20dp并试图再次置回STATE_DRAGGING,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private boolean checkNewEdgeDrag(float delta, float odelta, int pointerId, int edge) {
final float absDelta = Math.abs(delta);
final float absODelta = Math.abs(odelta);

if ((mInitialEdgesTouched[pointerId] & edge) != edge || (mTrackingEdges & edge) == 0 ||
(mEdgeDragsLocked[pointerId] & edge) == edge ||
(mEdgeDragsInProgress[pointerId] & edge) == edge ||
(absDelta <= mTouchSlop && absODelta <= mTouchSlop)) {
return false;
}
if (absDelta < absODelta * 0.5f && mCallback.onEdgeLock(edge)) {
mEdgeDragsLocked[pointerId] |= edge;
return false;
}
return (mEdgeDragsInProgress[pointerId] & edge) == 0 && absDelta > mTouchSlop;
}

但这里由于mEdgeDragsInProgress[pointerId] & edge) == edge所以阻止了DrawerLayout回到STATE_DRAGGING。

解决方案是把DrawerLayout的ViewDragCallback中的mPeekRunnable进行修改,简单粗暴。

1
2
3
4
5
private final Runnable mPeekRunnable = new Runnable() {  
@Override public void run() {
//peekDrawer();
}
};
坚持原创技术分享,您的支持将鼓励我继续创作!