侧滑菜单(抽屉效果)DrawerLayout实现原理

DrawerLayout是android support包新增的侧滑菜单控件,在Android Studio中可以很方便的创建一个带有侧滑菜单的页面。今天,我们来分析DrawerLayout它的实现原理,来加深对它的了解。为了能让读者有一个清晰的认识和选择性的了解,我在这里先列出本次分析的内容概要,读者可以按需了解。分析内容为:

  • 1.分析整体结构,实现的功能性。
  • 2.分析包含的重点属性,构造方法初始化等。
  • 3.分析布局实现,包括measure,layout,draw等。
  • 4.分析触摸事件,onTouchEvent,onInteceptTouchEvent等。
  • 5.分析LayoutParams的使用
  • 6.分析SavedState,用于备份还原状态,备忘录模式
  • 7.由此总结,自定义一个View可能需要考虑实现哪些内容。

1.整体结构,功能性分析

DrawerLayout相关的类及接口有如下:

  • 1.类ViewDragHelper,与DrawerLayout最紧密关系的类。作为一个辅助类,它主要用于帮助DrawerLayout进行触摸开启,关闭,拖动,释放滑动等逻辑的判断和处理,同时,还通过ViewDragHelper.Callback通知DrawerLayout状态的一些变化。
  • 2.类ViewDragCallback,ViewDragHelper.Callback接口的实现,通过它可以使DrawerLayout和ViewDragHelper进行一些拖动等逻辑上的交互。
  • 3.接口DrawerListener,提供对外回调的接口,用于监听onDrawerSlide(抽屉滑动),onDrawerOpened(抽屉打开),onDrawerClosed(抽屉关闭),onDrawerStateChanged(抽屉状态变化)等事件,以便外部能做出一些响应。例如配合ToolBar,实现侧滑菜单时,更新ToolBar左侧按钮旋转效果。SimpleDrawerListener,接口DrawerListener的空实现,目的是可以通过它选择性实现接口方法,不会一次弹出那么多方法。
  • 4.接口DrawerLayoutCompatImpl,定义DrawerLayout需要根据版本进行适配的接口。实现类分别有DrawerLayoutCompatImplBase和DrawerLayoutCompatImplApi21。版本21及以上,做的是布局内容区域是否要填充到状态栏,导航栏上,实现沉浸式效果。版本21以下空实现,因为系统不支持,所以不做处理。顺便提下,这里采用了策略模式。
  • 5.类SavedState,用于保存和恢复当前DrawerLayout状态的类,实现Parcelable接口,可实现数据序列化。配合onSaveInstanceState保存状态数据,onRestoreInstanceState恢复状态数据。这里采用了备忘录模式,SavedState作为备忘者,DrawerLayout是备忘录管理者,Activity是备忘录使用者。
  • 6.类LayoutParams,自定义的ViewGroup.MarginLayoutParams,通过它可以增加一些额外属性的处理,这里有onScreen(划出屏幕百分比),openState(开启状态)等。
  • 7.类AccessibilityDelegate,辅助功能逻辑处理类,这里不做详谈。

2.重点属性,构造方法初始化分析

  • 1.包含三种状态,STATE_IDLE(已打开或已关闭), STATE_DRAGGING(正在拖动), STATE_SETTLING(执行打开或关闭的动画过程中)。
  • 2.包含四种锁定模式,LOCK_MODE_UNLOCKED(未锁定,用户可以活动侧滑), LOCK_MODE_LOCKED_CLOSED(锁定并关闭菜单,用户无法侧滑,但是程序调用可以实现侧滑), LOCK_MODE_LOCKED_OPEN(锁定并打开菜单,用户无法侧滑,但是程序调用可以实现侧滑), LOCK_MODE_UNDEFINED(空白状态,初始状态)。
  • 3.mLeftDragger,mRightDragger,用于处理左侧和右侧侧滑的辅助类ViewDragHelper对象。
  • 4.mLeftCallback,mRightCallback,左侧和右侧侧滑处理的回调接口。
  • 5.mShadowStart等各个方向侧滑菜单阴影部分Drawable。

构造方法分析

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;
    //初始化左右拖动回调接口
    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);

    // 设置可获取焦点,以便能捕获返回键事件
    setFocusableInTouchMode(true);

    ViewCompat.setImportantForAccessibility(this,
            ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);

    ViewCompat.setAccessibilityDelegate(this, new AccessibilityDelegate());
    //设置不支持多点触摸
    ViewGroupCompat.setMotionEventSplittingEnabled(this, false);
    //适配状态栏区域显示
    if (ViewCompat.getFitsSystemWindows(this)) {
        IMPL.configureApplyInsets(this);
        mStatusBarBackground = IMPL.getDefaultStatusBarBackground(context);
    }

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

3.布局实现分析

对于一个自定义View,它的布局实现和触摸事件实现是它的核心功能。布局上一般需要实现测量,布局,绘制三个模块,在DrawerLayout中,实现了以下方法:

  • onMeasure
  • onLayout
  • onDraw
  • drawChild

onMeasure,根据父View传递过来的测量参数,解析得到高度和宽度的测量模式,测量大小,这是父View提供的一个参考标准,在DrawerLayout中,测量模式只接受MeasureSpec.EXACTLY,也就是只接受确定的值,所以DrawerLayout的布局高度宽度属性一般要设置为match_parent或者固定值,而不能是wrap_conent,当然在编辑模式下除外。所以DrawerLayout的测量大小设置了和父View一样大小。然后针对所有子View,确定是否要适应状态栏区域。然后区分内容区域和侧滑区域,内容区域完整填充DrawerLayout区域,侧滑区域根据相应的规则测量,目的使使侧滑能占据DrawerLayout的一部分区域,既不能完全填充,也不能完全没显示区域。

@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.");
        }
    }
    //设置最终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();
        //是否适配状态栏区域
        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)) {
            //内容区域,完整填充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)) {
            //侧滑区域,设置阴影效果
            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;
            }
            //计算侧滑的宽高的测量值,并对侧滑区域进行测量
            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");
        }
    }
}

所以总结测量的结果就是,DrawerLayout的大小完整填充父View,内容区域完整填充DrawerLayout,侧滑区域宽度上部分填充,高度上可完整填充或部分填充。

onLayout,对所有子View,如果是内容区域,根据测量结果进行布局,如果是侧滑区域,那就要区分是左侧侧滑还是右侧侧滑,这里分析左侧侧滑,根据当前子View的LayoutParams参数的gravity属性,在高度上分为顶部对齐,底部对齐,居中显示三种,在宽度上,根据LayoutParams参数的onScreen(侧滑显示在屏幕上的百分比),将侧滑布局到完全收起到完全划出之间。

@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)) {
            //内容区域布局
            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;
            //计算侧滑显示到屏幕的宽度百分比
            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;
            //区分顶部对齐,底部对齐,居中对齐布局
            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) {
                //侧滑过程中,通知更新布局参数的onScreen属性,并通知监听,侧滑滑动中
                setDrawerViewOffset(child, newOffset);
            }
            //侧滑没有划出屏幕时,设置为不可见,这样后面就避免无效绘制了
            final int newVisibility = lp.onScreen > 0 ? VISIBLE : INVISIBLE;
            if (child.getVisibility() != newVisibility) {
                child.setVisibility(newVisibility);
            }
        }
    }
    mInLayout = false;
    mFirstLayout = false;
}

onDraw,接下来开始绘制,这个很简单,因为作为一个容器,本身不需要绘制什么内容,这里根据版本适配,做了绘制状态栏颜色的工作。

@Override
public void onDraw(Canvas c) {
    super.onDraw(c);
     //如果需要绘制状态栏,并且状态栏背景drawable不为空即21以上版本,就进行状态栏区域的绘制
    if (mDrawStatusBarBackground && mStatusBarBackground != null) {
        final int inset = IMPL.getTopInset(mLastInsets);
        if (inset > 0) {
            mStatusBarBackground.setBounds(0, 0, getWidth(), inset);
            mStatusBarBackground.draw(c);
        }
    }
}

drawChild,接下来是绘制具体的某个子View,首先绘制内容区域,为了提高绘制效率,如果侧滑划出时,那么被侧滑遮挡的区域就不需要绘制了,只裁剪绘制需要显示出来的那部分。然后判断是否绘制覆盖在内容区域上阴影区域,如果不显示内容上层阴影,则判断是否绘制左侧或者右侧的侧边阴影。

@Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    final int height = getHeight();
    final boolean drawingContent = isContentView(child);
    int clipLeft = 0, clipRight = getWidth();
    //裁剪区域绘制内容区域
    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) {
        //绘制内容区域上层的阴影区域,一般划出了就会显示
        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)) {
        //绘制左侧侧滑栏的阴影部分,根据滑动距离调整阴影透明度
        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)) {
        //绘制右侧侧滑栏的阴影部分,根据滑动距离调整阴影透明度
        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实现了onInterceptTouchEvent和onTouchEvent方法,onInterceptTouchEvent处理TouchEvent事件的拦截,如果左侧或者右侧ViewDragHelper对象要拦截,或者是侧滑菜单显示时,点击位置在内容区域,或者侧滑栏正在执行移动动画,或者取消子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对象帮助处理,然后自己还实现了发生ACTION_UP和ACTION_CANCEL时,关闭侧滑栏的操作。

@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时,关闭侧滑栏的操作
    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事件的话,执行cancel,重置一些记录Touch事件的对象数据,为后面处理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();
    }
    //添加触摸力度跟踪对象,为后期计算滑动速度检测做准备,这里这个对象的获取采用享元模式,避免频繁创建销毁对象
    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点发生的初始状态
            saveInitialMotion(x, y, pointerId);
            //这里是点在一个正在滑动的侧滑栏上,使侧滑栏的状态由正在滑动状态变为正在拖动状态
            // 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);
            //这里处理侧滑栏的触摸触发区域是否触摸了,如果侧滑栏边缘触摸了,则通知回调,那么DrawerLayout里就会处理它,执行一个侧滑微弹的操作,也就是稍微弹出一点,表示触发了侧滑操作
            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点发生的初始状态
            saveInitialMotion(x, y, pointerId);
            //尝试去触发拖动操作
            // 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]);
                //正在拖动时,更新侧滑栏拖动的位置
                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;
                    //否则,判断事件是否正在侧滑边缘移动,以尝试去触发侧滑栏拖动操作
                    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;
                    }
                    //在拖动状态下,尝试去寻找当前的新的Touch点是否触发侧滑拖动操作
                    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点没有成功触发侧滑拖动操作,就去释放这个正在拖动的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事件发生时,释放这个正在拖动的View
            if (mDragState == STATE_DRAGGING) {
                releaseViewForPointerUp();
            }
            cancel();
            break;
        }

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

此外还有shouldInterceptTouchEvent这个辅助拦截事件,实现上和processTouchEvent差不多,大家可以自行分析。
总结触摸事件的处理,判断是否触摸在可触发侧滑栏的区域,未弹出时,根据滑动的力度判断是否弹出侧滑,在侧滑弹出的过程中,正在拖动侧滑的过程,已经滑出后等状态时,的一些触摸事件的处理。

5.自定义LayoutParams分析,通过自定义LayoutParams,可以为子View提供一些额外的布局参数。实现如下。

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(打开状态)
    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复写了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方法中调用的

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) {
        //此处调用了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) {
    //此处调用了generateDefaultLayoutParams
    final LayoutParams params = generateDefaultLayoutParams();
    params.width = width;
    params.height = height;
    addView(child, -1, params);
}

看到这里,我们就明白我们自定义的LayoutParams是怎么生效的了。

6.SaveState分析

SavedState用于保存和恢复DrawerLayout的状态,SavedState实现Parcelable接口,可实现数据的序列化。这里是一种备忘录模式,SavedState作为备忘者,DrawerLayout是备忘录管理者,Activity是备忘录使用者。那么我们看看使用SavedState的实现

@Override
protected Parcelable onSaveInstanceState() {
//这里是保存状态,系统在需要保存该状态时会调用该方法,在这里初始化SavedState,将要保存的数据集合起来
    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;
}

下面看看恢复数据的地方

@Override
protected void onRestoreInstanceState(Parcelable state) {
    if (!(state instanceof SavedState)) {
        super.onRestoreInstanceState(state);
        return;
    }
    //先恢复非SavedState 的数据
    final SavedState ss = (SavedState) state;
    super.onRestoreInstanceState(ss.getSuperState());
    //后面再根据SavedState 存储的数据,恢复相应的状态
    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.实现总结

分析完DrawerLayout之后,我们总结自定义一个View可能需要的实现有,测量,布局,绘制,事件分发处理,事件拦截处理,自身事件处理,自定义LayoutParams,考虑更多的话,有状态的存储恢复,辅助功能状态下的事件处理,当然,还有重要的自身的逻辑处理。

我们也看到DrawerLayout这个View本身只是一个控制侧滑显示的容器,一般我们会有如下的方式使用它。

<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的部分就是内容部分,而侧滑部分就是NavigationView了,为什么判断它是侧滑部分,是看其中定义的 android:layout_gravity="start",DrawerLayout会认定它就是侧滑部分。

显然DrawerLayout并没有完全实现我们想要的侧滑菜单,因为里面我们并没有看到侧滑的内容。后面我将分析NavigationView的实现。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容