側拉菜單作為常見的導航交互控件,最開始在沒有沒有android官方控件時,很多時候都是使用開源的SlidingMenu,一直沒機會分析側拉菜單的實現機理,本文將分析android.support.v4.widget.DrawerLayout的使用及實現。

官方介紹
DrawerLayout acts as a top-level container for window content that allows for interactive "drawer" views to be pulled out from the edge of the window.
Drawer positioning and layout is controlled using the
android:layout_gravityattribute on child views corresponding to which side of the view you want the drawer to emerge from: left or right. (Or start/end on platform versions that support layout direction.)To use a DrawerLayout, position your primary content view as the first child with a width and height of
match_parent. Add drawers as child views after the main content view and set thelayout_gravityappropriately. Drawers commonly usematch_parentfor height with a fixed width.
DrawerLayout.DrawerListenercan be used to monitor the state and motion of drawer views. Avoid performing expensive operations such as layout during animation as it can cause stuttering; try to perform expensive operations during theSTATE_IDLEstate.DrawerLayout.SimpleDrawerListeneroffers default/no-op implementations of each callback method.As per the Android Design guide, any drawers positioned to the left/start should always contain content for navigating around the application, whereas any drawers positioned to the right/end should always contain actions to take on the current content. This preserves the same navigation left, actions right structure present in the Action Bar and elsewhere
DrawerLayout直譯的事抽屜布局的意思,作為視窗內的頂層容器,它允許用戶通過抽屜式的推拉操作,從而把視圖視窗外邊緣拉到屏幕內,如右圖:
抽屜菜單的擺放和布局通過android:layout_gravity屬性來控制,可選值為left、right或start、end。通過xml來布局的話,需要把DrawerLayout作為父容器,組界面布局作為其第一個子節點,抽屜布局則緊隨其后作為第二個子節點,這樣就做就已經把內容展示區和抽屜菜單區獨立開來,只需要分別非兩個區域設置內容即可。android提供了一些實用的監聽器,重載相關的回調方法可以在菜單的交互過程中書寫邏輯業務。下面是一個demo布局:
<android.support.v4.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.aven.myapplication2.app.MainActivity">
<FrameLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<fragmentandroid:id="@+id/navigation_drawer"
android:layout_width="@dimen/navigation_drawer_width"
android:layout_height="match_parent"
android:layout_gravity="start"
android:name="com.aven.myapplication2.app.NavigationDrawerFragment"
tools:layout="@layout/fragment_navigation_drawer"/>
</android.support.v4.widget.DrawerLayout>
源碼分析
DrawerLayout實例化相關輔助類
既然DrawerLayout使用是作為頂層布局layout,那先看看他的構造函數:
public DrawerLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
//根據屏幕分辨率密度計算最小的邊距
final float density = getResources().getDisplayMetrics().density;
mMinDrawerMargin = (int) (MIN_DRAWER_MARGIN * density + 0.5f);
final float minVel = MIN_FLING_VELOCITY * density;
//實例化視圖滑動的回調接口,包括左右兩邊
mLeftCallback = new ViewDragCallback(Gravity.LEFT);
mRightCallback = new ViewDragCallback(Gravity.RIGHT);
//創建滑動手勢的的輔助類,負責具體的滑動監聽實現
mLeftDragger = ViewDragHelper.create(this, TOUCH_SLOP_SENSITIVITY, mLeftCallback);
mLeftDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);
mLeftDragger.setMinVelocity(minVel);
mLeftCallback.setDragger(mLeftDragger);
mRightDragger = ViewDragHelper.create(this, TOUCH_SLOP_SENSITIVITY, mRightCallback);
mRightDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_RIGHT);
mRightDragger.setMinVelocity(minVel);
mRightCallback.setDragger(mRightDragger);
// So that we can catch the back button
setFocusableInTouchMode(true);
ViewCompat.setAccessibilityDelegate(this, new AccessibilityDelegate());
ViewGroupCompat.setMotionEventSplittingEnabled(this,false);
}
從構造函數中,我們發現有兩個關鍵的類ViewDragCallback, ViewDragHelper,命名上來看前者和滑動的回調相關,后者和view的滑動操作實現有關,所以先看ViewDragHelper。
ViewDragHelper負責實現drag操作
從它的類注釋信息中可以看到,這個helper是個輔助類,里面封裝了一些便於用戶拖動ViewGroup內子view的操作及狀態記錄方法。
/**
* ViewDragHelper is a utility class for writing custom ViewGroups. It offers a number
* of useful operations and state tracking for allowing a user to drag and reposition
* views within their parent ViewGroup.
*/
/**
* Factory method to create a new ViewDragHelper.
*
* @param forParent Parent view to monitor
* @param cb Callback to provide information and receive events
* @return a new ViewDragHelper instance
*/
public static ViewDragHelper create(ViewGroup forParent, Callback cb) {
return new ViewDragHelper(forParent.getContext(), forParent, cb);
}
/**
* Factory method to create a new ViewDragHelper.
*
* @param forParent Parent view to monitor
* @param sensitivity Multiplier for how sensitive the helper should be about detecting
* the start of a drag. Larger values are more sensitive. 1.0f is normal.
* @param cb Callback to provide information and receive events
* @return a new ViewDragHelper instance
*/
public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb) {
final ViewDragHelper helper = create(forParent, cb);
helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity));
return helper;
}
這第二個工廠方法create就是剛才看到的上層調用來創建helper實例的,我們傳入了一個viewgroup,也就是說helper將持有我們的DrawerLayout實例引用,第二是一個浮點數,和drag操作的敏感性相關,數值越大表示drag操作更易被監聽,最后是一個Callback,即ViewDragCallback實例,它本身繼承自ViewDragHelper.Callback,現在來看helper的構造方法:
/**
* Apps should use ViewDragHelper.create() to get a new instance.
* This will allow VDH to use internal compatibility implementations for different
* platform versions.
*
* @param context Context to initialize config-dependent params from
* @param forParent Parent view to monitor
*/
private ViewDragHelper(Context context, ViewGroup forParent, Callback cb) {
if (forParent == null) {
throw new IllegalArgumentException("Parent view may not be null");
}
if (cb == null) {
throw new IllegalArgumentException("Callback may not be null");
}
mParentView = forParent;
mCallback = cb;
final ViewConfiguration vc = ViewConfiguration.get(context);
finalfloat density = context.getResources().getDisplayMetrics().density;
mEdgeSize = (int) (EDGE_SIZE * density + 0.5f);
mTouchSlop = vc.getScaledTouchSlop();
mMaxVelocity = vc.getScaledMaximumFlingVelocity();
mMinVelocity = vc.getScaledMinimumFlingVelocity();
mScroller = ScrollerCompat.create(context, sInterpolator);
}
首先需要檢測我們傳入的DrawerLayout和回調Callback,不允許為空。接下來從ViewConfiguration中獲取一些view的默認配置,
vc.getScaledTouchSlop是獲取一個pix為單位的距離,代表view在滑動的值;
vc.getScaledMaximumFlingVelocity獲取觸發view fling的最大每秒滾動的距離,也是pix為單位;
獲取view fling的最小每秒滾動距離,同樣pix為單位;
這里有scroll和fling,我的理解是scroll表示手沒有離開屏幕產生的滑動效果,二fling則是用力一划,然后view自己開始滾動的效果。
最后實例化一個Scroller,這是專門用來處理滾動的一個類,這里用的是擴展包里的campact類做版本兼容。
到此DrawerLayout已經准備好所有資源,接下來就是手勢分發時候的各種調用,這一部分留到下一篇文章在做分析
Source:
git clone https://github.com/avenwu/DrawerDemo.git
