側(cè)滑菜單(抽屜效果)DrawerLayout實現(xiàn)原理

DrawerLayout是android support包新增的側(cè)滑菜單控件感昼,在Android Studio中可以很方便的創(chuàng)建一個帶有側(cè)滑菜單的頁面澈缺。今天郎仆,我們來分析DrawerLayout它的實現(xiàn)原理蜀涨,來加深對它的了解瞎嬉。為了能讓讀者有一個清晰的認識和選擇性的了解,我在這里先列出本次分析的內(nèi)容概要厚柳,讀者可以按需了解氧枣。分析內(nèi)容為:

  • 1.分析整體結(jié)構(gòu),實現(xiàn)的功能性草娜。
  • 2.分析包含的重點屬性挑胸,構(gòu)造方法初始化等。
  • 3.分析布局實現(xiàn)宰闰,包括measure,layout,draw等茬贵。
  • 4.分析觸摸事件,onTouchEvent,onInteceptTouchEvent等移袍。
  • 5.分析LayoutParams的使用
  • 6.分析SavedState解藻,用于備份還原狀態(tài),備忘錄模式
  • 7.由此總結(jié)葡盗,自定義一個View可能需要考慮實現(xiàn)哪些內(nèi)容螟左。

1.整體結(jié)構(gòu),功能性分析

DrawerLayout相關(guān)的類及接口有如下:

  • 1.類ViewDragHelper觅够,與DrawerLayout最緊密關(guān)系的類胶背。作為一個輔助類,它主要用于幫助DrawerLayout進行觸摸開啟喘先,關(guān)閉钳吟,拖動,釋放滑動等邏輯的判斷和處理窘拯,同時红且,還通過ViewDragHelper.Callback通知DrawerLayout狀態(tài)的一些變化坝茎。
  • 2.類ViewDragCallback,ViewDragHelper.Callback接口的實現(xiàn)暇番,通過它可以使DrawerLayout和ViewDragHelper進行一些拖動等邏輯上的交互嗤放。
  • 3.接口DrawerListener,提供對外回調(diào)的接口壁酬,用于監(jiān)聽onDrawerSlide(抽屜滑動)次酌,onDrawerOpened(抽屜打開),onDrawerClosed(抽屜關(guān)閉)厨喂,onDrawerStateChanged(抽屜狀態(tài)變化)等事件和措,以便外部能做出一些響應(yīng)庄呈。例如配合ToolBar蜕煌,實現(xiàn)側(cè)滑菜單時,更新ToolBar左側(cè)按鈕旋轉(zhuǎn)效果诬留。SimpleDrawerListener斜纪,接口DrawerListener的空實現(xiàn),目的是可以通過它選擇性實現(xiàn)接口方法文兑,不會一次彈出那么多方法盒刚。
  • 4.接口DrawerLayoutCompatImpl,定義DrawerLayout需要根據(jù)版本進行適配的接口绿贞。實現(xiàn)類分別有DrawerLayoutCompatImplBase和DrawerLayoutCompatImplApi21因块。版本21及以上,做的是布局內(nèi)容區(qū)域是否要填充到狀態(tài)欄籍铁,導(dǎo)航欄上涡上,實現(xiàn)沉浸式效果。版本21以下空實現(xiàn)拒名,因為系統(tǒng)不支持吩愧,所以不做處理。順便提下增显,這里采用了策略模式雁佳。
  • 5.類SavedState,用于保存和恢復(fù)當前DrawerLayout狀態(tài)的類同云,實現(xiàn)Parcelable接口糖权,可實現(xiàn)數(shù)據(jù)序列化。配合onSaveInstanceState保存狀態(tài)數(shù)據(jù)炸站,onRestoreInstanceState恢復(fù)狀態(tài)數(shù)據(jù)星澳。這里采用了備忘錄模式,SavedState作為備忘者武契,DrawerLayout是備忘錄管理者募判,Activity是備忘錄使用者荡含。
  • 6.類LayoutParams,自定義的ViewGroup.MarginLayoutParams届垫,通過它可以增加一些額外屬性的處理释液,這里有onScreen(劃出屏幕百分比),openState(開啟狀態(tài))等装处。
  • 7.類AccessibilityDelegate误债,輔助功能邏輯處理類,這里不做詳談妄迁。

2.重點屬性寝蹈,構(gòu)造方法初始化分析

  • 1.包含三種狀態(tài),STATE_IDLE(已打開或已關(guān)閉)登淘, STATE_DRAGGING(正在拖動)箫老, STATE_SETTLING(執(zhí)行打開或關(guān)閉的動畫過程中)。
  • 2.包含四種鎖定模式黔州,LOCK_MODE_UNLOCKED(未鎖定耍鬓,用戶可以活動側(cè)滑), LOCK_MODE_LOCKED_CLOSED(鎖定并關(guān)閉菜單流妻,用戶無法側(cè)滑牲蜀,但是程序調(diào)用可以實現(xiàn)側(cè)滑), LOCK_MODE_LOCKED_OPEN(鎖定并打開菜單绅这,用戶無法側(cè)滑涣达,但是程序調(diào)用可以實現(xiàn)側(cè)滑), LOCK_MODE_UNDEFINED(空白狀態(tài)证薇,初始狀態(tài))度苔。
  • 3.mLeftDragger,mRightDragger棕叫,用于處理左側(cè)和右側(cè)側(cè)滑的輔助類ViewDragHelper對象林螃。
  • 4.mLeftCallback,mRightCallback俺泣,左側(cè)和右側(cè)側(cè)滑處理的回調(diào)接口疗认。
  • 5.mShadowStart等各個方向側(cè)滑菜單陰影部分Drawable。

構(gòu)造方法分析

public DrawerLayout(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
    final float density = getResources().getDisplayMetrics().density;
    mMinDrawerMargin = (int) (MIN_DRAWER_MARGIN * density + 0.5f);
    final float minVel = MIN_FLING_VELOCITY * density;
    //初始化左右拖動回調(diào)接口
    mLeftCallback = new ViewDragCallback(Gravity.LEFT);
    mRightCallback = new ViewDragCallback(Gravity.RIGHT);
    //初始化左右拖動輔助類伏钠,并與拖動回調(diào)接口綁定横漏,設(shè)置當前方向拖動輔助對象可以觸發(fā)側(cè)滑的邊緣方向
    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);

    // 設(shè)置可獲取焦點,以便能捕獲返回鍵事件
    setFocusableInTouchMode(true);

    ViewCompat.setImportantForAccessibility(this,
            ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);

    ViewCompat.setAccessibilityDelegate(this, new AccessibilityDelegate());
    //設(shè)置不支持多點觸摸
    ViewGroupCompat.setMotionEventSplittingEnabled(this, false);
    //適配狀態(tài)欄區(qū)域顯示
    if (ViewCompat.getFitsSystemWindows(this)) {
        IMPL.configureApplyInsets(this);
        mStatusBarBackground = IMPL.getDefaultStatusBarBackground(context);
    }

    mDrawerElevation = DRAWER_ELEVATION * density;
    //里面非抽屜的子View列表
    mNonDrawerViews = new ArrayList<View>();
}

3.布局實現(xiàn)分析

對于一個自定義View熟掂,它的布局實現(xiàn)和觸摸事件實現(xiàn)是它的核心功能缎浇。布局上一般需要實現(xiàn)測量,布局赴肚,繪制三個模塊素跺,在DrawerLayout中二蓝,實現(xiàn)了以下方法:

  • onMeasure
  • onLayout
  • onDraw
  • drawChild

onMeasure,根據(jù)父View傳遞過來的測量參數(shù)指厌,解析得到高度和寬度的測量模式刊愚,測量大小,這是父View提供的一個參考標準踩验,在DrawerLayout中鸥诽,測量模式只接受MeasureSpec.EXACTLY,也就是只接受確定的值箕憾,所以DrawerLayout的布局高度寬度屬性一般要設(shè)置為match_parent或者固定值牡借,而不能是wrap_conent,當然在編輯模式下除外袭异。所以DrawerLayout的測量大小設(shè)置了和父View一樣大小钠龙。然后針對所有子View,確定是否要適應(yīng)狀態(tài)欄區(qū)域扁远。然后區(qū)分內(nèi)容區(qū)域和側(cè)滑區(qū)域俊鱼,內(nèi)容區(qū)域完整填充DrawerLayout區(qū)域刻像,側(cè)滑區(qū)域根據(jù)相應(yīng)的規(guī)則測量畅买,目的使使側(cè)滑能占據(jù)DrawerLayout的一部分區(qū)域,既不能完全填充细睡,也不能完全沒顯示區(qū)域谷羞。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    //默認,測量模式必須為EXACTLY 
    if (widthMode != MeasureSpec.EXACTLY || heightMode != MeasureSpec.EXACTLY) {
        if (isInEditMode()) {
            //編輯模式下溜徙,針對非EXACTLY 模式做的一些適配
            }
        } else {
            throw new IllegalArgumentException(
                    "DrawerLayout must be measured with MeasureSpec.EXACTLY.");
        }
    }
    //設(shè)置最終DrawerLayout的測量大小
    setMeasuredDimension(widthSize, heightSize);

    final boolean applyInsets = mLastInsets != null && ViewCompat.getFitsSystemWindows(this);
    final int layoutDirection = ViewCompat.getLayoutDirection(this);

    // Only one drawer is permitted along each vertical edge (left / right). These two booleans
    // are tracking the presence of the edge drawers.
    boolean hasDrawerOnLeftEdge = false;
    boolean hasDrawerOnRightEdge = false;
    final int childCount = getChildCount();
    //對所有子View進行測量
    for (int i = 0; i < childCount; i++) {
        final View child = getChildAt(i);

        if (child.getVisibility() == GONE) {
            continue;
        }

        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        //是否適配狀態(tài)欄區(qū)域
        if (applyInsets) {
            final int cgrav = GravityCompat.getAbsoluteGravity(lp.gravity, layoutDirection);
            if (ViewCompat.getFitsSystemWindows(child)) {
                IMPL.dispatchChildInsets(child, mLastInsets, cgrav);
            } else {
                IMPL.applyMarginInsets(lp, mLastInsets, cgrav);
            }
        }

        if (isContentView(child)) {
            //內(nèi)容區(qū)域湃缎,完整填充DrawerLayout
            // Content views get measured at exactly the layout's size.
            final int contentWidthSpec = MeasureSpec.makeMeasureSpec(
                    widthSize - lp.leftMargin - lp.rightMargin, MeasureSpec.EXACTLY);
            final int contentHeightSpec = MeasureSpec.makeMeasureSpec(
                    heightSize - lp.topMargin - lp.bottomMargin, MeasureSpec.EXACTLY);
            child.measure(contentWidthSpec, contentHeightSpec);
        } else if (isDrawerView(child)) {
            //側(cè)滑區(qū)域,設(shè)置陰影效果
            if (SET_DRAWER_SHADOW_FROM_ELEVATION) {
                if (ViewCompat.getElevation(child) != mDrawerElevation) {
                    ViewCompat.setElevation(child, mDrawerElevation);
                }
            }
            final @EdgeGravity int childGravity =
                    getDrawerViewAbsoluteGravity(child) & Gravity.HORIZONTAL_GRAVITY_MASK;
            // Note that the isDrawerView check guarantees that childGravity here is either
            // LEFT or RIGHT
            boolean isLeftEdgeDrawer = (childGravity == Gravity.LEFT);
            if ((isLeftEdgeDrawer && hasDrawerOnLeftEdge)
                    || (!isLeftEdgeDrawer && hasDrawerOnRightEdge)) {
                throw new IllegalStateException("Child drawer has absolute gravity "
                        + gravityToString(childGravity) + " but this " + TAG + " already has a "
                        + "drawer view along that edge");
            }
            if (isLeftEdgeDrawer) {
                hasDrawerOnLeftEdge = true;
            } else {
                hasDrawerOnRightEdge = true;
            }
            //計算側(cè)滑的寬高的測量值蠢壹,并對側(cè)滑區(qū)域進行測量
            final int drawerWidthSpec = getChildMeasureSpec(widthMeasureSpec,
                    mMinDrawerMargin + lp.leftMargin + lp.rightMargin,
                    lp.width);
            final int drawerHeightSpec = getChildMeasureSpec(heightMeasureSpec,
                    lp.topMargin + lp.bottomMargin,
                    lp.height);
            child.measure(drawerWidthSpec, drawerHeightSpec);
        } else {
            throw new IllegalStateException("Child " + child + " at index " + i
                    + " does not have a valid layout_gravity - must be Gravity.LEFT, "
                    + "Gravity.RIGHT or Gravity.NO_GRAVITY");
        }
    }
}

所以總結(jié)測量的結(jié)果就是嗓违,DrawerLayout的大小完整填充父View,內(nèi)容區(qū)域完整填充DrawerLayout图贸,側(cè)滑區(qū)域?qū)挾壬喜糠痔畛漉寮荆叨壬峡赏暾畛浠虿糠痔畛洹?/p>

onLayout,對所有子View疏日,如果是內(nèi)容區(qū)域偿洁,根據(jù)測量結(jié)果進行布局,如果是側(cè)滑區(qū)域沟优,那就要區(qū)分是左側(cè)側(cè)滑還是右側(cè)側(cè)滑涕滋,這里分析左側(cè)側(cè)滑,根據(jù)當前子View的LayoutParams參數(shù)的gravity屬性挠阁,在高度上分為頂部對齊宾肺,底部對齊溯饵,居中顯示三種,在寬度上锨用,根據(jù)LayoutParams參數(shù)的onScreen(側(cè)滑顯示在屏幕上的百分比)瓣喊,將側(cè)滑布局到完全收起到完全劃出之間。

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    mInLayout = true;
    final int width = r - l;
    final int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        final View child = getChildAt(i);
        //隱藏的子View不考慮布局
        if (child.getVisibility() == GONE) {
            continue;
        }

        final LayoutParams lp = (LayoutParams) child.getLayoutParams();

        if (isContentView(child)) {
            //內(nèi)容區(qū)域布局
            child.layout(lp.leftMargin, lp.topMargin,
                    lp.leftMargin + child.getMeasuredWidth(),
                    lp.topMargin + child.getMeasuredHeight());
        } else { // Drawer, if it wasn't onMeasure would have thrown an exception.
            final int childWidth = child.getMeasuredWidth();
            final int childHeight = child.getMeasuredHeight();
            int childLeft;
            //計算側(cè)滑顯示到屏幕的寬度百分比
            final float newOffset;
            if (checkDrawerViewAbsoluteGravity(child, Gravity.LEFT)) {
                childLeft = -childWidth + (int) (childWidth * lp.onScreen);
                newOffset = (float) (childWidth + childLeft) / childWidth;
            } else { // Right; onMeasure checked for us.
                childLeft = width - (int) (childWidth * lp.onScreen);
                newOffset = (float) (width - childLeft) / childWidth;
            }

            final boolean changeOffset = newOffset != lp.onScreen;

            final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
            //區(qū)分頂部對齊黔酥,底部對齊藻三,居中對齊布局
            switch (vgrav) {
                default:
                case Gravity.TOP: {
                    child.layout(childLeft, lp.topMargin, childLeft + childWidth,
                            lp.topMargin + childHeight);
                    break;
                }

                case Gravity.BOTTOM: {
                    final int height = b - t;
                    child.layout(childLeft,
                            height - lp.bottomMargin - child.getMeasuredHeight(),
                            childLeft + childWidth,
                            height - lp.bottomMargin);
                    break;
                }

                case Gravity.CENTER_VERTICAL: {
                    final int height = b - t;
                    int childTop = (height - childHeight) / 2;

                    // Offset for margins. If things don't fit right because of
                    // bad measurement before, oh well.
                    if (childTop < lp.topMargin) {
                        childTop = lp.topMargin;
                    } else if (childTop + childHeight > height - lp.bottomMargin) {
                        childTop = height - lp.bottomMargin - childHeight;
                    }
                    child.layout(childLeft, childTop, childLeft + childWidth,
                            childTop + childHeight);
                    break;
                }
            }

            if (changeOffset) {
                //側(cè)滑過程中统捶,通知更新布局參數(shù)的onScreen屬性乾蓬,并通知監(jiān)聽葛作,側(cè)滑滑動中
                setDrawerViewOffset(child, newOffset);
            }
            //側(cè)滑沒有劃出屏幕時活烙,設(shè)置為不可見帽芽,這樣后面就避免無效繪制了
            final int newVisibility = lp.onScreen > 0 ? VISIBLE : INVISIBLE;
            if (child.getVisibility() != newVisibility) {
                child.setVisibility(newVisibility);
            }
        }
    }
    mInLayout = false;
    mFirstLayout = false;
}

onDraw稿茉,接下來開始繪制颅夺,這個很簡單箕宙,因為作為一個容器忘衍,本身不需要繪制什么內(nèi)容逾苫,這里根據(jù)版本適配,做了繪制狀態(tài)欄顏色的工作枚钓。

@Override
public void onDraw(Canvas c) {
    super.onDraw(c);
     //如果需要繪制狀態(tài)欄铅搓,并且狀態(tài)欄背景drawable不為空即21以上版本,就進行狀態(tài)欄區(qū)域的繪制
    if (mDrawStatusBarBackground && mStatusBarBackground != null) {
        final int inset = IMPL.getTopInset(mLastInsets);
        if (inset > 0) {
            mStatusBarBackground.setBounds(0, 0, getWidth(), inset);
            mStatusBarBackground.draw(c);
        }
    }
}

drawChild搀捷,接下來是繪制具體的某個子View星掰,首先繪制內(nèi)容區(qū)域,為了提高繪制效率嫩舟,如果側(cè)滑劃出時氢烘,那么被側(cè)滑遮擋的區(qū)域就不需要繪制了,只裁剪繪制需要顯示出來的那部分家厌。然后判斷是否繪制覆蓋在內(nèi)容區(qū)域上陰影區(qū)域播玖,如果不顯示內(nèi)容上層陰影,則判斷是否繪制左側(cè)或者右側(cè)的側(cè)邊陰影饭于。

@Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    final int height = getHeight();
    final boolean drawingContent = isContentView(child);
    int clipLeft = 0, clipRight = getWidth();
    //裁剪區(qū)域繪制內(nèi)容區(qū)域
    final int restoreCount = canvas.save();
    if (drawingContent) {
        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View v = getChildAt(i);
            if (v == child || v.getVisibility() != VISIBLE
                    || !hasOpaqueBackground(v) || !isDrawerView(v)
                    || v.getHeight() < height) {
                continue;
            }

            if (checkDrawerViewAbsoluteGravity(v, Gravity.LEFT)) {
                final int vright = v.getRight();
                if (vright > clipLeft) clipLeft = vright;
            } else {
                final int vleft = v.getLeft();
                if (vleft < clipRight) clipRight = vleft;
            }
        }
        canvas.clipRect(clipLeft, 0, clipRight, getHeight());
    }
    final boolean result = super.drawChild(canvas, child, drawingTime);
    canvas.restoreToCount(restoreCount);

    if (mScrimOpacity > 0 && drawingContent) {
        //繪制內(nèi)容區(qū)域上層的陰影區(qū)域蜀踏,一般劃出了就會顯示
        final int baseAlpha = (mScrimColor & 0xff000000) >>> 24;
        final int imag = (int) (baseAlpha * mScrimOpacity);
        final int color = imag << 24 | (mScrimColor & 0xffffff);
        mScrimPaint.setColor(color);

        canvas.drawRect(clipLeft, 0, clipRight, getHeight(), mScrimPaint);
    } else if (mShadowLeftResolved != null
            &&  checkDrawerViewAbsoluteGravity(child, Gravity.LEFT)) {
        //繪制左側(cè)側(cè)滑欄的陰影部分,根據(jù)滑動距離調(diào)整陰影透明度
        final int shadowWidth = mShadowLeftResolved.getIntrinsicWidth();
        final int childRight = child.getRight();
        final int drawerPeekDistance = mLeftDragger.getEdgeSize();
        final float alpha =
                Math.max(0, Math.min((float) childRight / drawerPeekDistance, 1.f));
        mShadowLeftResolved.setBounds(childRight, child.getTop(),
                childRight + shadowWidth, child.getBottom());
        mShadowLeftResolved.setAlpha((int) (0xff * alpha));
        mShadowLeftResolved.draw(canvas);
    } else if (mShadowRightResolved != null
            &&  checkDrawerViewAbsoluteGravity(child, Gravity.RIGHT)) {
        //繪制右側(cè)側(cè)滑欄的陰影部分镰绎,根據(jù)滑動距離調(diào)整陰影透明度
        final int shadowWidth = mShadowRightResolved.getIntrinsicWidth();
        final int childLeft = child.getLeft();
        final int showing = getWidth() - childLeft;
        final int drawerPeekDistance = mRightDragger.getEdgeSize();
        final float alpha =
                Math.max(0, Math.min((float) showing / drawerPeekDistance, 1.f));
        mShadowRightResolved.setBounds(childLeft - shadowWidth, child.getTop(),
                childLeft, child.getBottom());
        mShadowRightResolved.setAlpha((int) (0xff * alpha));
        mShadowRightResolved.draw(canvas);
    }
    return result;
}

4.觸摸事件分析

DrawerLayout實現(xiàn)了onInterceptTouchEvent和onTouchEvent方法脓斩,onInterceptTouchEvent處理TouchEvent事件的攔截,如果左側(cè)或者右側(cè)ViewDragHelper對象要攔截畴栖,或者是側(cè)滑菜單顯示時随静,點擊位置在內(nèi)容區(qū)域,或者側(cè)滑欄正在執(zhí)行移動動畫,或者取消子View的Touch操作燎猛,就會攔截恋捆,這樣子View就無法接收Touch事件了。

@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) {
                final View child = mLeftDragger.findTopChildUnder((int) x, (int) y);
                if (child != null && isContentView(child)) {
                    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;
}

onTouchEvent方法重绷,會將Touch事件交給左沸停,右ViewDragHelper對象幫助處理,然后自己還實現(xiàn)了發(fā)生ACTION_UP和ACTION_CANCEL時昭卓,關(guān)閉側(cè)滑欄的操作愤钾。

@Override
public boolean onTouchEvent(MotionEvent ev) {
    //將Touch事件交給ViewDragHelper對象處理
    mLeftDragger.processTouchEvent(ev);
    mRightDragger.processTouchEvent(ev);

    final int action = ev.getAction();
    boolean wantTouchEvents = true;
    //后面處理ACTION_UP和ACTION_CANCEL時,關(guān)閉側(cè)滑欄的操作
    switch (action & MotionEventCompat.ACTION_MASK) {
        case MotionEvent.ACTION_DOWN: {
            final float x = ev.getX();
            final float y = ev.getY();
            mInitialMotionX = x;
            mInitialMotionY = y;
            mDisallowInterceptRequested = false;
            mChildrenCanceledTouch = false;
            break;
        }

        case MotionEvent.ACTION_UP: {
            final float x = ev.getX();
            final float y = ev.getY();
            boolean peekingOnly = true;
            final View touchedView = mLeftDragger.findTopChildUnder((int) x, (int) y);
            if (touchedView != null && isContentView(touchedView)) {
                final float dx = x - mInitialMotionX;
                final float dy = y - mInitialMotionY;
                final int slop = mLeftDragger.getTouchSlop();
                if (dx * dx + dy * dy < slop * slop) {
                    // Taps close a dimmed open drawer but only if it isn't locked open.
                    final View openDrawer = findOpenDrawer();
                    if (openDrawer != null) {
                        peekingOnly = getDrawerLockMode(openDrawer) == LOCK_MODE_LOCKED_OPEN;
                    }
                }
            }
            closeDrawers(peekingOnly);
            mDisallowInterceptRequested = false;
            break;
        }

        case MotionEvent.ACTION_CANCEL: {
            closeDrawers(true);
            mDisallowInterceptRequested = false;
            mChildrenCanceledTouch = false;
            break;
        }
    }

    return wantTouchEvents;
}

DrawerLayout把絕大部分的觸摸事件交給ViewDragHelper去處理候醒,那么在ViewDragHelper中是怎么處理的呢能颁?我們看看processTouchEvent

public void processTouchEvent(MotionEvent ev) {
    //取得當前Touch的action 和action 序號
    final int action = MotionEventCompat.getActionMasked(ev);
    final int actionIndex = MotionEventCompat.getActionIndex(ev);
    //down事件的話,執(zhí)行cancel倒淫,重置一些記錄Touch事件的對象數(shù)據(jù)伙菊,為后面處理Touch事件做初始化準備
    if (action == MotionEvent.ACTION_DOWN) {
        // Reset things for a new event stream, just in case we didn't get
        // the whole previous stream.
        cancel();
    }
    //添加觸摸力度跟蹤對象,為后期計算滑動速度檢測做準備敌土,這里這個對象的獲取采用享元模式镜硕,避免頻繁創(chuàng)建銷毀對象
    if (mVelocityTracker == null) {
        mVelocityTracker = VelocityTracker.obtain();
    }
    mVelocityTracker.addMovement(ev);

    switch (action) {
        case MotionEvent.ACTION_DOWN: {
            final float x = ev.getX();
            final float y = ev.getY();
            final int pointerId = ev.getPointerId(0);
            //這里找到當前觸摸點的最頂層的子View,作為需要操作的View
            final View toCapture = findTopChildUnder((int) x, (int) y);
           //保存當前Touch點發(fā)生的初始狀態(tài)
            saveInitialMotion(x, y, pointerId);
            //這里是點在一個正在滑動的側(cè)滑欄上,使側(cè)滑欄的狀態(tài)由正在滑動狀態(tài)變?yōu)檎谕蟿訝顟B(tài)
            // Since the parent is already directly processing this touch event,
            // there is no reason to delay for a slop before dragging.
            // Start immediately if possible.
            tryCaptureViewForDrag(toCapture, pointerId);
            //這里處理側(cè)滑欄的觸摸觸發(fā)區(qū)域是否觸摸了返干,如果側(cè)滑欄邊緣觸摸了兴枯,則通知回調(diào),那么DrawerLayout里就會處理它犬金,執(zhí)行一個側(cè)滑微彈的操作念恍,也就是稍微彈出一點,表示觸發(fā)了側(cè)滑操作
            final int edgesTouched = mInitialEdgesTouched[pointerId];
            if ((edgesTouched & mTrackingEdges) != 0) {
                mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
            }
            break;
        }

        case MotionEventCompat.ACTION_POINTER_DOWN: {
            final int pointerId = ev.getPointerId(actionIndex);
            final float x = ev.getX(actionIndex);
            final float y = ev.getY(actionIndex);
            //保存當前Touch點發(fā)生的初始狀態(tài)
            saveInitialMotion(x, y, pointerId);
            //嘗試去觸發(fā)拖動操作
            // A ViewDragHelper can only manipulate one view at a time.
            if (mDragState == STATE_IDLE) {
                // If we're idle we can do anything! Treat it like a normal down event.

                final View toCapture = findTopChildUnder((int) x, (int) y);
                tryCaptureViewForDrag(toCapture, pointerId);

                final int edgesTouched = mInitialEdgesTouched[pointerId];
                if ((edgesTouched & mTrackingEdges) != 0) {
                    mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
                }
            } else if (isCapturedViewUnder((int) x, (int) y)) {
                // We're still tracking a captured view. If the same view is under this
                // point, we'll swap to controlling it with this pointer instead.
                // (This will still work if we're "catching" a settling view.)

                tryCaptureViewForDrag(mCapturedView, pointerId);
            }
            break;
        }

        case MotionEvent.ACTION_MOVE: {
            if (mDragState == STATE_DRAGGING) {
                // If pointer is invalid then skip the ACTION_MOVE.
                if (!isValidPointerForActionMove(mActivePointerId)) break;

                final int index = ev.findPointerIndex(mActivePointerId);
                final float x = ev.getX(index);
                final float y = ev.getY(index);
                final int idx = (int) (x - mLastMotionX[mActivePointerId]);
                final int idy = (int) (y - mLastMotionY[mActivePointerId]);
                //正在拖動時晚顷,更新側(cè)滑欄拖動的位置
                dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy);

                saveLastMotion(ev);
            } else {
                // Check to see if any pointer is now over a draggable view.
                final int pointerCount = ev.getPointerCount();
                for (int i = 0; i < pointerCount; i++) {
                    final int pointerId = ev.getPointerId(i);

                    // If pointer is invalid then skip the ACTION_MOVE.
                    if (!isValidPointerForActionMove(pointerId)) continue;
                    //否則,判斷事件是否正在側(cè)滑邊緣移動疗疟,以嘗試去觸發(fā)側(cè)滑欄拖動操作
                    final float x = ev.getX(i);
                    final float y = ev.getY(i);
                    final float dx = x - mInitialMotionX[pointerId];
                    final float dy = y - mInitialMotionY[pointerId];

                    reportNewEdgeDrags(dx, dy, pointerId);
                    if (mDragState == STATE_DRAGGING) {
                        // Callback might have started an edge drag.
                        break;
                    }

                    final View toCapture = findTopChildUnder((int) x, (int) y);
                    if (checkTouchSlop(toCapture, dx, dy)
                            && tryCaptureViewForDrag(toCapture, pointerId)) {
                        break;
                    }
                }
                saveLastMotion(ev);
            }
            break;
        }

        case MotionEventCompat.ACTION_POINTER_UP: {
            final int pointerId = ev.getPointerId(actionIndex);
            if (mDragState == STATE_DRAGGING && pointerId == mActivePointerId) {
                // Try to find another pointer that's still holding on to the captured view.
                int newActivePointer = INVALID_POINTER;
                final int pointerCount = ev.getPointerCount();
                for (int i = 0; i < pointerCount; i++) {
                    final int id = ev.getPointerId(i);
                    if (id == mActivePointerId) {
                        // This one's going away, skip.
                        continue;
                    }
                    //在拖動狀態(tài)下该默,嘗試去尋找當前的新的Touch點是否觸發(fā)側(cè)滑拖動操作
                    final float x = ev.getX(i);
                    final float y = ev.getY(i);
                    if (findTopChildUnder((int) x, (int) y) == mCapturedView
                            && tryCaptureViewForDrag(mCapturedView, id)) {
                        newActivePointer = mActivePointerId;
                        break;
                    }
                }
                //如果當前這個Touch點沒有成功觸發(fā)側(cè)滑拖動操作,就去釋放這個正在拖動的View
                if (newActivePointer == INVALID_POINTER) {
                    // We didn't find another pointer still touching the view, release it.
                    releaseViewForPointerUp();
                }
            }
            clearMotionHistory(pointerId);
            break;
        }

        case MotionEvent.ACTION_UP: {
            //up和cancel事件發(fā)生時策彤,釋放這個正在拖動的View
            if (mDragState == STATE_DRAGGING) {
                releaseViewForPointerUp();
            }
            cancel();
            break;
        }

        case MotionEvent.ACTION_CANCEL: {
            if (mDragState == STATE_DRAGGING) {
                dispatchViewReleased(0, 0);
            }
            cancel();
            break;
        }
    }
}

此外還有shouldInterceptTouchEvent這個輔助攔截事件栓袖,實現(xiàn)上和processTouchEvent差不多,大家可以自行分析店诗。
總結(jié)觸摸事件的處理裹刮,判斷是否觸摸在可觸發(fā)側(cè)滑欄的區(qū)域,未彈出時庞瘸,根據(jù)滑動的力度判斷是否彈出側(cè)滑捧弃,在側(cè)滑彈出的過程中,正在拖動側(cè)滑的過程,已經(jīng)滑出后等狀態(tài)時违霞,的一些觸摸事件的處理嘴办。

5.自定義LayoutParams分析,通過自定義LayoutParams买鸽,可以為子View提供一些額外的布局參數(shù)涧郊。實現(xiàn)如下。

public static class LayoutParams extends ViewGroup.MarginLayoutParams {
    private static final int FLAG_IS_OPENED = 0x1;
    private static final int FLAG_IS_OPENING = 0x2;
    private static final int FLAG_IS_CLOSING = 0x4;
    //額外處理了眼五,gravity(靠邊方向)妆艘,onScreen(顯示出屏幕的百分比),isPeeking(是否正在微彈)看幼,openState(打開狀態(tài))
    public int gravity = Gravity.NO_GRAVITY;
    float onScreen;
    boolean isPeeking;
    int openState;

    public LayoutParams(Context c, AttributeSet attrs) {
        super(c, attrs);

        final TypedArray a = c.obtainStyledAttributes(attrs, LAYOUT_ATTRS);
        this.gravity = a.getInt(0, Gravity.NO_GRAVITY);
        a.recycle();
    }

    public LayoutParams(int width, int height) {
        super(width, height);
    }

    public LayoutParams(int width, int height, int gravity) {
        this(width, height);
        this.gravity = gravity;
    }

    public LayoutParams(LayoutParams source) {
        super(source);
        this.gravity = source.gravity;
    }

    public LayoutParams(ViewGroup.LayoutParams source) {
        super(source);
    }

    public LayoutParams(ViewGroup.MarginLayoutParams source) {
        super(source);
    }
}

那么它是在哪里生效的呢?是DrawerLayout復(fù)寫了ViewGroup的generateLayoutParams方法双仍,在這里提供了自己的LayoutParams

@Override
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
    return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
}

@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
    return p instanceof LayoutParams
            ? new LayoutParams((LayoutParams) p)
            : p instanceof ViewGroup.MarginLayoutParams
            ? new LayoutParams((MarginLayoutParams) p)
            : new LayoutParams(p);
}

而generateLayoutParams是在ViewGroup的addView方法中調(diào)用的

public void addView(View child, int index) {
    if (child == null) {
        throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
    }
    LayoutParams params = child.getLayoutParams();
    if (params == null) {
        //此處調(diào)用了generateDefaultLayoutParams
        params = generateDefaultLayoutParams();
        if (params == null) {
            throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
        }
    }
    addView(child, index, params);
}

public void addView(View child, int width, int height) {
    //此處調(diào)用了generateDefaultLayoutParams
    final LayoutParams params = generateDefaultLayoutParams();
    params.width = width;
    params.height = height;
    addView(child, -1, params);
}

看到這里,我們就明白我們自定義的LayoutParams是怎么生效的了桌吃。

6.SaveState分析

SavedState用于保存和恢復(fù)DrawerLayout的狀態(tài)朱沃,SavedState實現(xiàn)Parcelable接口,可實現(xiàn)數(shù)據(jù)的序列化。這里是一種備忘錄模式茅诱,SavedState作為備忘者逗物,DrawerLayout是備忘錄管理者,Activity是備忘錄使用者瑟俭。那么我們看看使用SavedState的實現(xiàn)

@Override
protected Parcelable onSaveInstanceState() {
//這里是保存狀態(tài)翎卓,系統(tǒng)在需要保存該狀態(tài)時會調(diào)用該方法,在這里初始化SavedState摆寄,將要保存的數(shù)據(jù)集合起來
    final Parcelable superState = super.onSaveInstanceState();
    final SavedState ss = new SavedState(superState);

    final int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        final View child = getChildAt(i);
        LayoutParams lp = (LayoutParams) child.getLayoutParams();
        // Is the current child fully opened (that is, not closing)?
        boolean isOpenedAndNotClosing = (lp.openState == LayoutParams.FLAG_IS_OPENED);
        // Is the current child opening?
        boolean isClosedAndOpening = (lp.openState == LayoutParams.FLAG_IS_OPENING);
        if (isOpenedAndNotClosing || isClosedAndOpening) {
            // If one of the conditions above holds, save the child's gravity
            // so that we open that child during state restore.
            ss.openDrawerGravity = lp.gravity;
            break;
        }
    }

    ss.lockModeLeft = mLockModeLeft;
    ss.lockModeRight = mLockModeRight;
    ss.lockModeStart = mLockModeStart;
    ss.lockModeEnd = mLockModeEnd;

    return ss;
}

下面看看恢復(fù)數(shù)據(jù)的地方

@Override
protected void onRestoreInstanceState(Parcelable state) {
    if (!(state instanceof SavedState)) {
        super.onRestoreInstanceState(state);
        return;
    }
    //先恢復(fù)非SavedState 的數(shù)據(jù)
    final SavedState ss = (SavedState) state;
    super.onRestoreInstanceState(ss.getSuperState());
    //后面再根據(jù)SavedState 存儲的數(shù)據(jù)失暴,恢復(fù)相應(yīng)的狀態(tài)
    if (ss.openDrawerGravity != Gravity.NO_GRAVITY) {
        final View toOpen = findDrawerWithGravity(ss.openDrawerGravity);
        if (toOpen != null) {
            openDrawer(toOpen);
        }
    }

    if (ss.lockModeLeft != LOCK_MODE_UNDEFINED) {
        setDrawerLockMode(ss.lockModeLeft, Gravity.LEFT);
    }
    if (ss.lockModeRight != LOCK_MODE_UNDEFINED) {
        setDrawerLockMode(ss.lockModeRight, Gravity.RIGHT);
    }
    if (ss.lockModeStart != LOCK_MODE_UNDEFINED) {
        setDrawerLockMode(ss.lockModeStart, GravityCompat.START);
    }
    if (ss.lockModeEnd != LOCK_MODE_UNDEFINED) {
        setDrawerLockMode(ss.lockModeEnd, GravityCompat.END);
    }
}

7.實現(xiàn)總結(jié)

分析完DrawerLayout之后,我們總結(jié)自定義一個View可能需要的實現(xiàn)有微饥,測量逗扒,布局,繪制欠橘,事件分發(fā)處理矩肩,事件攔截處理,自身事件處理肃续,自定義LayoutParams黍檩,考慮更多的話,有狀態(tài)的存儲恢復(fù)始锚,輔助功能狀態(tài)下的事件處理刽酱,當然,還有重要的自身的邏輯處理瞧捌。

我們也看到DrawerLayout這個View本身只是一個控制側(cè)滑顯示的容器棵里,一般我們會有如下的方式使用它。

<android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:openDrawer="start"
    >

  <include
      layout="@layout/app_bar_main2"
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      />

  <android.support.design.widget.NavigationView
      android:id="@+id/nav_view"
      android:layout_width="wrap_content"
      android:layout_height="match_parent"
      android:layout_gravity="start"
      android:fitsSystemWindows="true"
      app:headerLayout="@layout/nav_header_main2"
      app:menu="@menu/activity_main2_drawer"
      />

</android.support.v4.widget.DrawerLayout>

include的部分就是內(nèi)容部分,而側(cè)滑部分就是NavigationView了衍慎,為什么判斷它是側(cè)滑部分转唉,是看其中定義的 android:layout_gravity="start",DrawerLayout會認定它就是側(cè)滑部分稳捆。

顯然DrawerLayout并沒有完全實現(xiàn)我們想要的側(cè)滑菜單赠法,因為里面我們并沒有看到側(cè)滑的內(nèi)容。后面我將分析NavigationView的實現(xiàn)乔夯。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末砖织,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子末荐,更是在濱河造成了極大的恐慌侧纯,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,013評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件甲脏,死亡現(xiàn)場離奇詭異眶熬,居然都是意外死亡,警方通過查閱死者的電腦和手機块请,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,205評論 2 382
  • 文/潘曉璐 我一進店門娜氏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人墩新,你說我怎么就攤上這事贸弥。” “怎么了海渊?”我有些...
    開封第一講書人閱讀 152,370評論 0 342
  • 文/不壞的土叔 我叫張陵绵疲,是天一觀的道長。 經(jīng)常有香客問我臣疑,道長盔憨,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,168評論 1 278
  • 正文 為了忘掉前任朝捆,我火速辦了婚禮般渡,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘芙盘。我一直安慰自己,他們只是感情好脸秽,可當我...
    茶點故事閱讀 64,153評論 5 371
  • 文/花漫 我一把揭開白布儒老。 她就那樣靜靜地躺著,像睡著了一般记餐。 火紅的嫁衣襯著肌膚如雪驮樊。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 48,954評論 1 283
  • 那天,我揣著相機與錄音囚衔,去河邊找鬼挖腰。 笑死,一個胖子當著我的面吹牛练湿,可吹牛的內(nèi)容都是我干的猴仑。 我是一名探鬼主播,決...
    沈念sama閱讀 38,271評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼肥哎,長吁一口氣:“原來是場噩夢啊……” “哼辽俗!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起篡诽,我...
    開封第一講書人閱讀 36,916評論 0 259
  • 序言:老撾萬榮一對情侶失蹤崖飘,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后杈女,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體朱浴,經(jīng)...
    沈念sama閱讀 43,382評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,877評論 2 323
  • 正文 我和宋清朗相戀三年达椰,在試婚紗的時候發(fā)現(xiàn)自己被綠了翰蠢。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 37,989評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡砰碴,死狀恐怖躏筏,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情呈枉,我是刑警寧澤趁尼,帶...
    沈念sama閱讀 33,624評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站猖辫,受9級特大地震影響酥泞,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜啃憎,卻給世界環(huán)境...
    茶點故事閱讀 39,209評論 3 307
  • 文/蒙蒙 一芝囤、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧辛萍,春花似錦悯姊、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,199評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至辉阶,卻和暖如春先壕,著一層夾襖步出監(jiān)牢的瞬間瘩扼,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,418評論 1 260
  • 我被黑心中介騙來泰國打工垃僚, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留集绰,地道東北人。 一個月前我還...
    沈念sama閱讀 45,401評論 2 352
  • 正文 我出身青樓谆棺,卻偏偏與公主長得像栽燕,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子包券,可洞房花燭夜當晚...
    茶點故事閱讀 42,700評論 2 345

推薦閱讀更多精彩內(nèi)容