侧滑菜单(抽屉效果)DrawerLayout实现原理
2017-10-15 16:37
507 查看
DrawerLayout是android support包新增的侧滑菜单控件,在Android Studio中可以很方便的创建一个带有侧滑菜单的页面。今天,我们来分析DrawerLayout它的实现原理,来加深对它的了解。
为了能让读者有一个清晰的认识和选择性的了解,我在这里先列出本次分析的内容概要,读者可以按需了解。分析内容为:
1.分析整体结构,实现的功能性。
2.分析包含的重点属性,构造方法初始化等。
3.分析布局实现,包括measure,layout,draw等。
4.分析触摸事件,onTouchEvent,onInteceptTouchEvent等。
5.分析LayoutParams的使用
6.分析SavedState,用于备份还原状态,备忘录模式
7.由此总结,自定义一个View可能需要考虑实现哪些内容。
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.包含四种锁定模式,LOCK_MODE_UNLOCKED(未锁定,用户可以活动侧滑), LOCK_MODE_LOCKED_CLOSED(锁定并关闭菜单,用户无法侧滑,但是程序调用可以实现侧滑), LOCK_MODE_LOCKED_OPEN(锁定并打开菜单,用户无法侧滑,但是程序调用可以实现侧滑), LOCK_MODE_UNDEFINED(空白状态,初始状态)。
3.mLeftDragger,mRightDragger,用于处理左侧和右侧侧滑的辅助类ViewDragHelper对象。
4.mLeftCallback,mRightCallback,左侧和右侧侧滑处理的回调接口。
5.mShadowStart等各个方向侧滑菜单阴影部分Drawable。
构造方法分析
onMeasure
onLayout
onDraw
drawChild
我们来依次分析。
onMeasure,根据父View传递过来的测量参数,解析得到高度和宽度的测量模式,测量大小,这是父View提供的一个参考标准,在DrawerLayout中,测量模式只接受MeasureSpec.EXACTLY,也就是只接受确定的值,所以DrawerLayout的布局高度宽度属性一般要设置为match_parent或者固定值,而不能是wrap_conent,当然在编辑模式下除外。所以DrawerLayout的测量大小设置了和父View一样大小。然后针对所有子View,确定是否要适应状态栏区域。然后区分内容区域和侧滑区域,内容区域完整填充DrawerLayout区域,侧滑区域根据相应的规则测量,目的使使侧滑能占据DrawerLayout的一部分区域,既不能完全填充,也不能完全没显示区域。
所以总结测量的结果就是,DrawerLayout的大小完整填充父View,内容区域完整填充DrawerLayout,侧滑区域宽度上部分填充,高度上可完整填充或部分填充。
onLayout,对所有子View,如果是内容区域,根据测量结果进行布局,如果是侧滑区域,那就要区分是左侧侧滑还是右侧侧滑,这里分析左侧侧滑,根据当前子View的LayoutParams参数的gravity属性,在高度上分为顶部对齐,底部对齐,居中显示三种,在宽度上,根据LayoutParams参数的onScreen(侧滑显示在屏幕上的百分比),将侧滑布局到完全收起到完全划出之间。
onDraw,接下来开始绘制,这个很简单,因为作为一个容器,本身不需要绘制什么内容,这里根据版本适配,做了绘制状态栏颜色的工作。
drawChild,接下来是绘制具体的某个子View,首先绘制内容区域,为了提高绘制效率,如果侧滑划出时,那么被侧滑遮挡的区域就不需要绘制了,只裁剪绘制需要显示出来的那部分。然后判断是否绘制覆盖在内容区域上阴影区域,如果不显示内容上层阴影,则判断是否绘制左侧或者右侧的侧边阴影。
onTouchEvent方法,会将Touch事件交给左,右ViewDragHelper对象帮助处理,然后自己还实现了发生ACTION_UP和ACTION_CANCEL时,关闭侧滑栏的操作。
DrawerLayout把绝大部分的触摸事件交给ViewDragHelper去处理,那么在ViewDragHelper中是怎么处理的呢?我们看看processTouchEvent
此外还有shouldInterceptTouchEvent这个辅助拦截事件,实现上和processTouchEvent差不多,大家可以自行分析。
总结触摸事件的处理,判断是否触摸在可触发侧滑栏的区域,未弹出时,根据滑动的力度判断是否弹出侧滑,在侧滑弹出的过程中,正在拖动侧滑的过程,已经滑出后等状态时,的一些触摸事件的处理。
那么它是在哪里生效的呢?是DrawerLayout复写了ViewGroup的generateLayoutParams方法,在这里提供了自己的LayoutParams
而generateLayoutParams是在ViewGroup的addView方法中调用的
看到这里,我们就明白我们自定义的LayoutParams是怎么生效的了。
下面看看恢复数据的地方
我们也看到DrawerLayout这个View本身只是一个控制侧滑显示的容器,一般我们会有如下的方式使用它。
include的部分就是内容部分,而侧滑部分就是NavigationView了,为什么判断它是侧滑部分,是看其中定义的 android:layout_gravity=”start”,DrawerLayout会认定它就是侧滑部分。
显然DrawerLayout并没有完全实现我们想要的侧滑菜单,因为里面我们并没有看到侧滑的内容。后面我将分析NavigationView的实现。
为了能让读者有一个清晰的认识和选择性的了解,我在这里先列出本次分析的内容概要,读者可以按需了解。分析内容为:
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的实现。
相关文章推荐
- DrawerLayout实现侧滑菜单效果
- Android Drawerlayout实现侧滑菜单效果
- DrawerLayout实现侧滑菜单效果
- android组件之DrawerLayout(抽屉导航)-- 侧滑菜单效果
- 使用DrawerLayout与listview实现抽屉菜单效果
- Android DrawerLayout抽屉效果的导航菜单实现
- Toolbar+DrawerLayout实现知乎app抽屉菜单侧边拉出并带有箭头动画效果
- Android使用DrawerLayout实现侧滑菜单效果
- Kotlin实现侧滑抽屉菜单(DrawerLayout+NavigationView+Toolbar)
- android组件之DrawerLayout(抽屉导航)-- 侧滑菜单效果
- 利用DrawerLayout和触摸事件分发实现抽屉侧滑效果
- DrawerLayout实现多样侧滑菜单效果
- android组件之DrawerLayout(抽屉导航)-- 侧滑菜单效果
- (转)android组件之DrawerLayout(抽屉导航)-- 侧滑菜单效果
- Android DrawerLayout+ToolBar+NavigationView实现侧滑菜单效果,沉浸式状态栏
- android组件之DrawerLayout(抽屉导航)-- 侧滑菜单效果
- Android 使用DrawerLayout实现抽屉效果的导航菜单
- android 侧滑菜单效果----DrawerLayout(抽屉导航)
- [Android基础知识] 之二十: 侧滑菜单DrawerLayout(抽屉布局)实现
- android开发游记:DrawerLayout 实现抽屉效果的导航菜单