这里要介绍的是google的DrawerLayout,行为可见google官方应用如gmail,看手Q的抽屉,应该是根据android-undergarment项目来定制的一个控件。
Google Desgin GuildLines里面有介绍:http://www.google.com/design/spec/layout/structure.html#structure-system-bars
官方教程:Creating a Navigation Drawer
DrawerLayout添加在主内容区的上层,作为parent,下面的第一个child是主内容区域,第二个child则可以是其他任何东西,需要作为抽屉的view则需要声明android:layout_gravity。
DrawerLayout的setScrimColor可以设置抽屉拉出时右侧主内容剩余区域上面盖的颜色(默认0x99000000)。
DrawerLayout默认只有在边缘的一个edge能够触发抽屉拉取的动作,而这个是通过ViewDragHelper这个类来实现的。
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则是控制抽屉从打开到关闭之间的这个间隔。由于是私有静态常量,可以通过
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上有给出以下这种方案的,简直坑爹:
// ======================== 触摸事件处理 ===================================
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;
}
@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;
}
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的时候,事件触发到抽屉出现有一个延时
/**
* 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,
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进行修改,简单粗暴。
private final Runnable mPeekRunnable = new Runnable() {
@Override public void run() {
//peekDrawer();
}
};