[UI]抽屜菜單DrawerLayout分析(一)

本文轉載於:http://www.cnblogs.com/avenwu/archive/2014/04/16/3669367.htmlhtml

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

Device 2014 04 16 191818

    官方介紹

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.git

Drawer positioning and layout is controlled using the android:layout_gravity attribute 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.)github

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 the layout_gravity appropriately. Drawers commonly use match_parent for height with a fixed width.app

DrawerLayout.DrawerListener can 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 the STATE_IDLE state. DrawerLayout.SimpleDrawerListener offers 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 elsewhereide

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的使用很是簡單,和不少容器類佈局同樣,它自己也繼承自ViewGroup,只是在內部實現中會默認將第一個子節點做爲內容區,第二個做爲抽屜菜單,因此寫佈局的過後必須牢記,好在如今的IDE已經很是智能,經過引導來建立Drawerlayout時,會自動生成Activity和xml layout佈局,好比使用AndroidStudio就很是方便。
 

源碼分析

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.

 */

 
如今來看看這個helper究竟是怎麼封裝的滑動操做,從上面的實例化咱們知道這個helper經過工廠方法來構造實例,工廠方法有兩個以下:

/**

 * 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

 

 

做者: 小文字
本文版權歸做者和博客園共有,歡迎轉載,但未經做者贊成必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接,不然保留追究法律責任的權利.

繼續分析DrawerLayout的手勢分發部分

談到手勢分發,這自己就是個好話題,DrawerLayout做爲繼承自ViewGroup得佈局他能夠攔截手勢也能夠分發給子view,也就是在onInterceptTouchEvent中作的操做,可是他的下面還有一個onTouchEvent方法,先看哪一個呢?追溯代碼咱們能夠知道ViewGroup繼承自View,而onTouchEvent是View的方法

NewImage

咱們仍是先花點時間把二者的關係先確認再繼續。

onInterceptTouchEvent和onTouchEvent---雞和蛋?

定位到ViewGroup,能夠發現onInterceptTouchEvent分定義以下,從它前面一段很是長的註釋就能夠看出其重要性和複雜,默認的返回是false

/**

 * Implement this method to intercept all touch screen motion events.  This

 * allows you to watch events as they are dispatched to your children, and

 * take ownership of the current gesture at any point.

 *

 * <p>Using this function takes some care, as it has a fairly complicated

 * interaction with {@link View#onTouchEvent(MotionEvent)

 * View.onTouchEvent(MotionEvent)}, and using it requires implementing

 * that method as well as this one in the correct way.  Events will be

 * received in the following order:

 *

 * <ol>

 * <li> You will receive the down event here.

 * <li> The down event will be handled either by a child of this view

 * group, or given to your own onTouchEvent() method to handle; this means

 * you should implement onTouchEvent() to return true, so you will

 * continue to see the rest of the gesture (instead of looking for

 * a parent view to handle it).  Also, by returning true from

 * onTouchEvent(), you will not receive any following

 * events in onInterceptTouchEvent() and all touch processing must

 * happen in onTouchEvent() like normal.

 * <li> For as long as you return false from this function, each following

 * event (up to and including the final up) will be delivered first here

 * and then to the target's onTouchEvent().

 * <li> If you return true from here, you will not receive any

 * following events: the target view will receive the same event but

 * with the action {@link MotionEvent#ACTION_CANCEL}, and all further

 * events will be delivered to your onTouchEvent() method and no longer

 * appear here.

 * </ol>

 *

 * @param ev The motion event being dispatched down the hierarchy.

 * @return Return true to steal motion events from the children and have

 * them dispatched to this ViewGroup through onTouchEvent().

 * The current target will receive an ACTION_CANCEL event, and no further

 * messages will be delivered here.

 */

public boolean onInterceptTouchEvent(MotionEvent ev) {

    return false;

}

 

前兩段告訴咱們,複寫onInterceptTouchEvent方法,能夠實現監聽全部的動做事件MotionEvent,在向子view傳遞事件前作咱們須要的操做,固然這指的是和這個viewgroup相關的事件;同時咱們須要慎重處理該函數,由於他和onTouchEvent關係很是緊密,下面是事件接收的順序:

首先接收的的事按下事件,down事件,他能夠被view處理也能夠在自身的onTouchEvent裏處理,因此實現onTouchEvent而且返回true,這樣onTouchEvent繼續才能收到down以後的其餘事件,同時onInterceptTouchEvent不會在收到後續事件,由於已經轉移到onTouchEvent處理了。

那麼何時onInterceptTouchEvent會把後續事件轉移到他的onTouchEvent呢?這取決於onInterceptTouchEvent的返回值,若是返回false,全部事件都會先分發到這裏,而後再到目標view的onTouchEvent;相反若是返回true,那麼onInterceptTouchEvent將再也不收到後續事件,而且目標view會收到cancel事件,接着自身的onTouchEvent幾首後續的事件。

這其實從名字來看是比較好理解的onInterceptTouchEvent表示在截取觸摸事件的被調用的方法,既然是截取就能夠直接吧事件截下來後再也不日後傳遞,這是就是上面的第二種狀況,返回true,即咱們本身消耗了觸摸事件,子view將沒有機會獲得喚醒。

OnInterceptTouchEvent

大體意思就是若是但願自身消耗掉改事件就能夠直接返回true,這一點和onTouchEvent的返回相似目的。

博客園有篇文章對這些事件分發作了很好的分析:http://www.cnblogs.com/sunzn/archive/2013/05/10/3064129.html

詳細的闡述了了dispatchTouchEvent,onInterceptTouchEvent以及onTouchEvent之間的關係

如今咱們回過頭來看DrawerLayout裏的分發是如何寫的:

重寫了後面兩個方法,先看onInterceptTouchEvent:

@Override

public boolean onInterceptTouchEvent(MotionEvent ev) {

    final int action = MotionEventCompat.getActionMasked(ev);

 

    // "|" used deliberately here; both methods should be invoked.

    final boolean interceptForDrag = mLeftDragger.shouldInterceptTouchEvent(ev) |

            mRightDragger.shouldInterceptTouchEvent(ev);

 

    boolean interceptForTap = false;

 

    switch (action) {

        case MotionEvent.ACTION_DOWN: {

            final float x = ev.getX();

            final float y = ev.getY();

            mInitialMotionX = x;

            mInitialMotionY = y;

            if (mScrimOpacity > 0 &&

                    isContentView(mLeftDragger.findTopChildUnder((int) x, (int) y))) {

                interceptForTap = true;

            }

            mDisallowInterceptRequested = false;

            mChildrenCanceledTouch = false;

            break;

        }

 

        case MotionEvent.ACTION_MOVE: {

            // If we cross the touch slop, don't perform the delayed peek for an edge touch.

            if (mLeftDragger.checkTouchSlop(ViewDragHelper.DIRECTION_ALL)) {

                mLeftCallback.removeCallbacks();

                mRightCallback.removeCallbacks();

            }

            break;

        }

 

        case MotionEvent.ACTION_CANCEL:

        case MotionEvent.ACTION_UP: {

            closeDrawers(true);

            mDisallowInterceptRequested = false;

            mChildrenCanceledTouch = false;

        }

    }

 

    return interceptForDrag || interceptForTap || hasPeekingDrawer() || mChildrenCanceledTouch;

}

 

1.首先從touch event裏面獲取當前具體的action動做,MotionEventCompat.getActionMasked(ev),內部實際上作了一次按位於操做event.getAction() & ACTION_MASK;

2.檢查當前是否知足截取drag狀態,用於決定onInterceptTouchEvent返回值,這裏有個註解說是故意用了|或,而不是||或,二者區別在於||只要第一個條件知足就不在執行第二個檢查,二|不一樣,不管如何都會將兩個條件檢查一遍;

3.接下來是幾個case,根據當前的action作處理;

ACTION_DOWN,當按下時記錄按下點的x,y座標值,根據條件設置當前是否知足tap狀態,具體條件有兩個,一是mScrimOpacity,表示子view中在屏幕上佔據的最大寬度(0-1),二時根據座標點的位置取得改點對應的最上層view對象,若是是預約義的content view即DrawerLayout裏的主內容展現view,也就是同時知足view在屏幕上且點擊的位置直接落在了content view上。

ACTION_MOVE,當手按下後開始在屏幕上移動時,若是垂直和水平上的位移差量達到了drag helper的閥值則一處左右兩邊的回調接口

ACTION_CANCLE和ACTION_UP,手勢結束後,關閉菜單

最後結合幾個狀態來那個來決定onInterceptTouchEvent返回true仍是false,

未完待續

在[UI]抽屜菜單DrawerLayout分析(一)和[UI]抽屜菜單DrawerLayout分析(二)中分別介紹了DrawerLayout得基本框架結構和ViewDragerHelper的做用以及手勢分發,本文一塊兒來分析其中的Scroller的使用狀況。

      在ViewDragerHelper中能夠發現private ScrollerCompat mScroller;說明抽屜菜單的具體滑動也是依賴於Scroller的使用,檢索一下mScroller的引用,定位到forceSettleCapturedViewAt,這個方法回調用Scroller的startScroll來計算位移,它自己適用於計算和保存位移在特定時間的變化狀況,最終的在繪製view時我能夠獲取其保存的x,y座標值。

/**

 * Settle the captured view at the given (left, top) position.

 *

 * @param finalLeft Target left position for the captured view

 * @param finalTop Target top position for the captured view

 * @param xvel Horizontal velocity

 * @param yvel Vertical velocity

 * @return true if animation should continue through {@link #continueSettling(boolean)} calls

 */

private boolean forceSettleCapturedViewAt(int finalLeft, int finalTop, int xvel, int yvel) {

    final int startLeft = mCapturedView.getLeft();

    final int startTop = mCapturedView.getTop();

    final int dx = finalLeft - startLeft;

    final int dy = finalTop - startTop;

 

    if (dx == 0 && dy == 0) {

        // Nothing to do. Send callbacks, be done.

        mScroller.abortAnimation();

        setDragState(STATE_IDLE);

        returnfalse;

    }

 

    final int duration = computeSettleDuration(mCapturedView, dx, dy, xvel, yvel);

    mScroller.startScroll(startLeft, startTop, dx, dy, duration);

 

    setDragState(STATE_SETTLING);

    returntrue;

}

      這裏用的是v4擴展包裏的ScrollerCompat用於低版本兼容,它繼承自ScrollerCompatImpl,能夠看到裏面主要的方法聲明:

interface ScrollerCompatImpl{

    Object createScroller(Context context, Interpolator interpolator);

    boolean isFinished(Object scroller);

    int getCurrX(Object scroller);

    int getCurrY(Object scroller);

    float getCurrVelocity(Object scroller);

    boolean computeScrollOffset(Object scroller);

    void startScroll(Object scroller, int startX, int startY, int dx, int dy);

    void startScroll(Object scroller, int startX, int startY, int dx, int dy, int duration);

    void fling(Object scroller, int startX, int startY, int velX, int velY,

            int minX, int maxX, int minY, int maxY);

    void fling(Object scroller, int startX, int startY, int velX, int velY,

            int minX, int maxX, int minY, int maxY, int overX, int overY);

    void abortAnimation(Object scroller);

    void notifyHorizontalEdgeReached(Object scroller, int startX, int finalX, int overX);

    void notifyVerticalEdgeReached(Object scroller, int startY, int finalY, int overY);

    boolean isOverScrolled(Object scroller);

    int getFinalX(Object scroller);

    int getFinalY(Object scroller);

}

DragActionMethodFlow

從Scroller一直往上追溯,能夠獲得如圖的調用流程。

當滑動屏幕時,DrawerLayout中的手勢分發被觸發,先執行onInterceptTouchEvent根據返回結果肯定是否執行onTouchEvent,以後就是一些和ViewDragHelper之間的回調接口處理。

接下來追蹤一下何時從Scroller中取出x,y來使用:

Scroller

在View裏面有一個實現爲空的computeScroll,DrawerLayout對它進行重寫,這個方法應該是在view自動重繪是會被調用,回到continueSettling:

/**

 * Move the captured settling view by the appropriate amount for the current time.

 * If <code>continueSettling</code> returns true, the caller should call it again

 * on the next frame to continue.

 *

 * @param deferCallbacks true if state callbacks should be deferred via posted message.

 *                       Set this to true if you are calling this method from

 *                       {@link android.view.View#computeScroll()} or similar methods

 *                       invoked as part of layout or drawing.

 * @return true if settle is still in progress

 */

public boolean continueSettling(boolean deferCallbacks) {

    if (mDragState == STATE_SETTLING) {

        boolean keepGoing = mScroller.computeScrollOffset();

        final int x = mScroller.getCurrX();

        final int y = mScroller.getCurrY();

        final int dx = x - mCapturedView.getLeft();

        final int dy = y - mCapturedView.getTop();

 

        if (dx != 0) {

            mCapturedView.offsetLeftAndRight(dx);

        }

        if (dy != 0) {

            mCapturedView.offsetTopAndBottom(dy);

        }

 

        if (dx != 0 || dy != 0) {

            mCallback.onViewPositionChanged(mCapturedView, x, y, dx, dy);

        }

 

        if (keepGoing && x == mScroller.getFinalX() && y == mScroller.getFinalY()) {

            // Close enough. The interpolator/scroller might think we're still moving

            // but the user sure doesn't.

            mScroller.abortAnimation();

            keepGoing = mScroller.isFinished();

        }

 

        if (!keepGoing) {

            if (deferCallbacks) {

                mParentView.post(mSetIdleRunnable);

            } else {

                setDragState(STATE_IDLE);

            }

        }

    }

 

    return mDragState == STATE_SETTLING;

}

 

當狀態處於STATE_SETTLING時開始獲取Scroller中的x,y值,結合當前運動view的left,top位置,計算出偏移量,經過offsetLeftAndRight設置,裏面是一些具體的位置改變,挺複雜的。

/**

 * Offset this view's horizontal location by the specified amount of pixels.

 *

 * @param offset the number of pixels to offset the view by

 */

public void offsetLeftAndRight(int offset) {

    if (offset != 0) {

        updateMatrix();

        final boolean matrixIsIdentity = mTransformationInfo == null

                || mTransformationInfo.mMatrixIsIdentity;

        if (matrixIsIdentity) {

            if (mDisplayList != null) {

                invalidateViewProperty(false, false);

            } else {

                final ViewParent p = mParent;

                if (!= null && mAttachInfo != null) {

                    final Rect r = mAttachInfo.mTmpInvalRect;

                    int minLeft;

                    int maxRight;

                    if (offset < 0) {

                        minLeft = mLeft + offset;

                        maxRight = mRight;

                    } else {

                        minLeft = mLeft;

                        maxRight = mRight + offset;

                    }

                    r.set(0, 0, maxRight - minLeft, mBottom - mTop);

                    p.invalidateChild(this, r);

                }

            }

        } else {

            invalidateViewProperty(false, false);

        }

 

        mLeft += offset;

        mRight += offset;

        if (mDisplayList != null) {

            mDisplayList.offsetLeftAndRight(offset);

            invalidateViewProperty(false, false);

        } else {

            if (!matrixIsIdentity) {

                invalidateViewProperty(false, true);

            }

            invalidateParentIfNeeded();

        }

    }

}

小結

至此DrawerLayout的基本工做流程分析完畢,簡單作一個總結,v4包提供了ViewDragHelper類,裏面封裝了對scroller合view的位移操做,和Callback接口,經過DrawerLayout內的onInterceptTouchEvent和onTouchEvent的重載,觸發ViewDragHelper內的相關方法,同時在DrawerLayout內實現ViewDragHelp.Callback.

 

做者: 小文字
本文版權歸做者和博客園共有,歡迎轉載,但未經做者贊成必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接,不然保留追究法律責任的權利.
相關文章
相關標籤/搜索