DrawerLayout是android support包新增的側(cè)滑菜單控件,在Android Studio中可以很方便的創(chuàng)建一個(gè)帶有側(cè)滑菜單的頁(yè)面。今天,我們來(lái)分析DrawerLayout它的實(shí)現(xiàn)原理,來(lái)加深對(duì)它的了解。為了能讓讀者有一個(gè)清晰的認(rèn)識(shí)和選擇性的了解,我在這里先列出本次分析的內(nèi)容概要,讀者可以按需了解。分析內(nèi)容為:
- 1.分析整體結(jié)構(gòu),實(shí)現(xiàn)的功能性。
- 2.分析包含的重點(diǎn)屬性,構(gòu)造方法初始化等。
- 3.分析布局實(shí)現(xiàn),包括measure,layout,draw等。
- 4.分析觸摸事件,onTouchEvent,onInteceptTouchEvent等。
- 5.分析LayoutParams的使用
- 6.分析SavedState,用于備份還原狀態(tài),備忘錄模式
- 7.由此總結(jié),自定義一個(gè)View可能需要考慮實(shí)現(xiàn)哪些內(nèi)容。
1.整體結(jié)構(gòu),功能性分析
DrawerLayout相關(guān)的類及接口有如下:
- 1.類ViewDragHelper,與DrawerLayout最緊密關(guān)系的類。作為一個(gè)輔助類,它主要用于幫助DrawerLayout進(jìn)行觸摸開(kāi)啟,關(guān)閉,拖動(dòng),釋放滑動(dòng)等邏輯的判斷和處理,同時(shí),還通過(guò)ViewDragHelper.Callback通知DrawerLayout狀態(tài)的一些變化。
- 2.類ViewDragCallback,ViewDragHelper.Callback接口的實(shí)現(xiàn),通過(guò)它可以使DrawerLayout和ViewDragHelper進(jìn)行一些拖動(dòng)等邏輯上的交互。
- 3.接口DrawerListener,提供對(duì)外回調(diào)的接口,用于監(jiān)聽(tīng)onDrawerSlide(抽屜滑動(dòng)),onDrawerOpened(抽屜打開(kāi)),onDrawerClosed(抽屜關(guān)閉),onDrawerStateChanged(抽屜狀態(tài)變化)等事件,以便外部能做出一些響應(yīng)。例如配合ToolBar,實(shí)現(xiàn)側(cè)滑菜單時(shí),更新ToolBar左側(cè)按鈕旋轉(zhuǎn)效果。SimpleDrawerListener,接口DrawerListener的空實(shí)現(xiàn),目的是可以通過(guò)它選擇性實(shí)現(xiàn)接口方法,不會(huì)一次彈出那么多方法。
- 4.接口DrawerLayoutCompatImpl,定義DrawerLayout需要根據(jù)版本進(jìn)行適配的接口。實(shí)現(xiàn)類分別有DrawerLayoutCompatImplBase和DrawerLayoutCompatImplApi21。版本21及以上,做的是布局內(nèi)容區(qū)域是否要填充到狀態(tài)欄,導(dǎo)航欄上,實(shí)現(xiàn)沉浸式效果。版本21以下空實(shí)現(xiàn),因?yàn)橄到y(tǒng)不支持,所以不做處理。順便提下,這里采用了策略模式。
- 5.類SavedState,用于保存和恢復(fù)當(dāng)前DrawerLayout狀態(tài)的類,實(shí)現(xiàn)Parcelable接口,可實(shí)現(xiàn)數(shù)據(jù)序列化。配合onSaveInstanceState保存狀態(tài)數(shù)據(jù),onRestoreInstanceState恢復(fù)狀態(tài)數(shù)據(jù)。這里采用了備忘錄模式,SavedState作為備忘者,DrawerLayout是備忘錄管理者,Activity是備忘錄使用者。
- 6.類LayoutParams,自定義的ViewGroup.MarginLayoutParams,通過(guò)它可以增加一些額外屬性的處理,這里有onScreen(劃出屏幕百分比),openState(開(kāi)啟狀態(tài))等。
- 7.類AccessibilityDelegate,輔助功能邏輯處理類,這里不做詳談。
2.重點(diǎn)屬性,構(gòu)造方法初始化分析
- 1.包含三種狀態(tài),STATE_IDLE(已打開(kāi)或已關(guān)閉), STATE_DRAGGING(正在拖動(dòng)), STATE_SETTLING(執(zhí)行打開(kāi)或關(guān)閉的動(dòng)畫(huà)過(guò)程中)。
- 2.包含四種鎖定模式,LOCK_MODE_UNLOCKED(未鎖定,用戶可以活動(dòng)側(cè)滑), LOCK_MODE_LOCKED_CLOSED(鎖定并關(guān)閉菜單,用戶無(wú)法側(cè)滑,但是程序調(diào)用可以實(shí)現(xiàn)側(cè)滑), LOCK_MODE_LOCKED_OPEN(鎖定并打開(kāi)菜單,用戶無(wú)法側(cè)滑,但是程序調(diào)用可以實(shí)現(xiàn)側(cè)滑), LOCK_MODE_UNDEFINED(空白狀態(tài),初始狀態(tài))。
- 3.mLeftDragger,mRightDragger,用于處理左側(cè)和右側(cè)側(cè)滑的輔助類ViewDragHelper對(duì)象。
- 4.mLeftCallback,mRightCallback,左側(cè)和右側(cè)側(cè)滑處理的回調(diào)接口。
- 5.mShadowStart等各個(gè)方向側(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;
//初始化左右拖動(dòng)回調(diào)接口
mLeftCallback = new ViewDragCallback(Gravity.LEFT);
mRightCallback = new ViewDragCallback(Gravity.RIGHT);
//初始化左右拖動(dòng)輔助類,并與拖動(dòng)回調(diào)接口綁定,設(shè)置當(dāng)前方向拖動(dòng)輔助對(duì)象可以觸發(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è)置可獲取焦點(diǎn),以便能捕獲返回鍵事件
setFocusableInTouchMode(true);
ViewCompat.setImportantForAccessibility(this,
ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
ViewCompat.setAccessibilityDelegate(this, new AccessibilityDelegate());
//設(shè)置不支持多點(diǎn)觸摸
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.布局實(shí)現(xiàn)分析
對(duì)于一個(gè)自定義View,它的布局實(shí)現(xiàn)和觸摸事件實(shí)現(xiàn)是它的核心功能。布局上一般需要實(shí)現(xiàn)測(cè)量,布局,繪制三個(gè)模塊,在DrawerLayout中,實(shí)現(xiàn)了以下方法:
- onMeasure
- onLayout
- onDraw
- drawChild
onMeasure,根據(jù)父View傳遞過(guò)來(lái)的測(cè)量參數(shù),解析得到高度和寬度的測(cè)量模式,測(cè)量大小,這是父View提供的一個(gè)參考標(biāo)準(zhǔn),在DrawerLayout中,測(cè)量模式只接受MeasureSpec.EXACTLY,也就是只接受確定的值,所以DrawerLayout的布局高度寬度屬性一般要設(shè)置為match_parent或者固定值,而不能是wrap_conent,當(dāng)然在編輯模式下除外。所以DrawerLayout的測(cè)量大小設(shè)置了和父View一樣大小。然后針對(duì)所有子View,確定是否要適應(yīng)狀態(tài)欄區(qū)域。然后區(qū)分內(nèi)容區(qū)域和側(cè)滑區(qū)域,內(nèi)容區(qū)域完整填充DrawerLayout區(qū)域,側(cè)滑區(qū)域根據(jù)相應(yīng)的規(guī)則測(cè)量,目的使使側(cè)滑能占據(jù)DrawerLayout的一部分區(qū)域,既不能完全填充,也不能完全沒(méi)顯示區(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);
//默認(rèn),測(cè)量模式必須為EXACTLY
if (widthMode != MeasureSpec.EXACTLY || heightMode != MeasureSpec.EXACTLY) {
if (isInEditMode()) {
//編輯模式下,針對(duì)非EXACTLY 模式做的一些適配
}
} else {
throw new IllegalArgumentException(
"DrawerLayout must be measured with MeasureSpec.EXACTLY.");
}
}
//設(shè)置最終DrawerLayout的測(cè)量大小
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();
//對(duì)所有子View進(jìn)行測(cè)量
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;
}
//計(jì)算側(cè)滑的寬高的測(cè)量值,并對(duì)側(cè)滑區(qū)域進(jìn)行測(cè)量
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é)測(cè)量的結(jié)果就是,DrawerLayout的大小完整填充父View,內(nèi)容區(qū)域完整填充DrawerLayout,側(cè)滑區(qū)域?qū)挾壬喜糠痔畛洌叨壬峡赏暾畛浠虿糠痔畛洹?/p>
onLayout,對(duì)所有子View,如果是內(nèi)容區(qū)域,根據(jù)測(cè)量結(jié)果進(jìn)行布局,如果是側(cè)滑區(qū)域,那就要區(qū)分是左側(cè)側(cè)滑還是右側(cè)側(cè)滑,這里分析左側(cè)側(cè)滑,根據(jù)當(dāng)前子View的LayoutParams參數(shù)的gravity屬性,在高度上分為頂部對(duì)齊,底部對(duì)齊,居中顯示三種,在寬度上,根據(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;
//計(jì)算側(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ū)分頂部對(duì)齊,底部對(duì)齊,居中對(duì)齊布局
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è)滑過(guò)程中,通知更新布局參數(shù)的onScreen屬性,并通知監(jiān)聽(tīng),側(cè)滑滑動(dòng)中
setDrawerViewOffset(child, newOffset);
}
//側(cè)滑沒(méi)有劃出屏幕時(shí),設(shè)置為不可見(jiàn),這樣后面就避免無(wú)效繪制了
final int newVisibility = lp.onScreen > 0 ? VISIBLE : INVISIBLE;
if (child.getVisibility() != newVisibility) {
child.setVisibility(newVisibility);
}
}
}
mInLayout = false;
mFirstLayout = false;
}
onDraw,接下來(lái)開(kāi)始繪制,這個(gè)很簡(jiǎn)單,因?yàn)樽鳛橐粋€(gè)容器,本身不需要繪制什么內(nèi)容,這里根據(jù)版本適配,做了繪制狀態(tài)欄顏色的工作。
@Override
public void onDraw(Canvas c) {
super.onDraw(c);
//如果需要繪制狀態(tài)欄,并且狀態(tài)欄背景drawable不為空即21以上版本,就進(jìn)行狀態(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,接下來(lái)是繪制具體的某個(gè)子View,首先繪制內(nèi)容區(qū)域,為了提高繪制效率,如果側(cè)滑劃出時(shí),那么被側(cè)滑遮擋的區(qū)域就不需要繪制了,只裁剪繪制需要顯示出來(lái)的那部分。然后判斷是否繪制覆蓋在內(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ū)域,一般劃出了就會(huì)顯示
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ù)滑動(dòng)距離調(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ù)滑動(dòng)距離調(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實(shí)現(xiàn)了onInterceptTouchEvent和onTouchEvent方法,onInterceptTouchEvent處理TouchEvent事件的攔截,如果左側(cè)或者右側(cè)ViewDragHelper對(duì)象要攔截,或者是側(cè)滑菜單顯示時(shí),點(diǎn)擊位置在內(nèi)容區(qū)域,或者側(cè)滑欄正在執(zhí)行移動(dòng)動(dòng)畫(huà),或者取消子View的Touch操作,就會(huì)攔截,這樣子View就無(wú)法接收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方法,會(huì)將Touch事件交給左,右ViewDragHelper對(duì)象幫助處理,然后自己還實(shí)現(xiàn)了發(fā)生ACTION_UP和ACTION_CANCEL時(shí),關(guān)閉側(cè)滑欄的操作。
@Override
public boolean onTouchEvent(MotionEvent ev) {
//將Touch事件交給ViewDragHelper對(duì)象處理
mLeftDragger.processTouchEvent(ev);
mRightDragger.processTouchEvent(ev);
final int action = ev.getAction();
boolean wantTouchEvents = true;
//后面處理ACTION_UP和ACTION_CANCEL時(shí),關(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) {
//取得當(dāng)前Touch的action 和action 序號(hào)
final int action = MotionEventCompat.getActionMasked(ev);
final int actionIndex = MotionEventCompat.getActionIndex(ev);
//down事件的話,執(zhí)行cancel,重置一些記錄Touch事件的對(duì)象數(shù)據(jù),為后面處理Touch事件做初始化準(zhǔn)備
if (action == MotionEvent.ACTION_DOWN) {
// Reset things for a new event stream, just in case we didn't get
// the whole previous stream.
cancel();
}
//添加觸摸力度跟蹤對(duì)象,為后期計(jì)算滑動(dòng)速度檢測(cè)做準(zhǔn)備,這里這個(gè)對(duì)象的獲取采用享元模式,避免頻繁創(chuàng)建銷毀對(duì)象
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);
//這里找到當(dāng)前觸摸點(diǎn)的最頂層的子View,作為需要操作的View
final View toCapture = findTopChildUnder((int) x, (int) y);
//保存當(dāng)前Touch點(diǎn)發(fā)生的初始狀態(tài)
saveInitialMotion(x, y, pointerId);
//這里是點(diǎn)在一個(gè)正在滑動(dòng)的側(cè)滑欄上,使側(cè)滑欄的狀態(tài)由正在滑動(dòng)狀態(tài)變?yōu)檎谕蟿?dòng)狀態(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里就會(huì)處理它,執(zhí)行一個(gè)側(cè)滑微彈的操作,也就是稍微彈出一點(diǎn),表示觸發(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);
//保存當(dāng)前Touch點(diǎn)發(fā)生的初始狀態(tài)
saveInitialMotion(x, y, pointerId);
//嘗試去觸發(fā)拖動(dòng)操作
// 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]);
//正在拖動(dòng)時(shí),更新側(cè)滑欄拖動(dòng)的位置
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è)滑邊緣移動(dòng),以嘗試去觸發(fā)側(cè)滑欄拖動(dòng)操作
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;
}
//在拖動(dòng)狀態(tài)下,嘗試去尋找當(dāng)前的新的Touch點(diǎn)是否觸發(fā)側(cè)滑拖動(dòng)操作
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;
}
}
//如果當(dāng)前這個(gè)Touch點(diǎn)沒(méi)有成功觸發(fā)側(cè)滑拖動(dòng)操作,就去釋放這個(gè)正在拖動(dòng)的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ā)生時(shí),釋放這個(gè)正在拖動(dòng)的View
if (mDragState == STATE_DRAGGING) {
releaseViewForPointerUp();
}
cancel();
break;
}
case MotionEvent.ACTION_CANCEL: {
if (mDragState == STATE_DRAGGING) {
dispatchViewReleased(0, 0);
}
cancel();
break;
}
}
}
此外還有shouldInterceptTouchEvent這個(gè)輔助攔截事件,實(shí)現(xiàn)上和processTouchEvent差不多,大家可以自行分析。
總結(jié)觸摸事件的處理,判斷是否觸摸在可觸發(fā)側(cè)滑欄的區(qū)域,未彈出時(shí),根據(jù)滑動(dòng)的力度判斷是否彈出側(cè)滑,在側(cè)滑彈出的過(guò)程中,正在拖動(dòng)側(cè)滑的過(guò)程,已經(jīng)滑出后等狀態(tài)時(shí),的一些觸摸事件的處理。
5.自定義LayoutParams分析,通過(guò)自定義LayoutParams,可以為子View提供一些額外的布局參數(shù)。實(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(打開(kāi)狀態(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ù)寫(xiě)了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實(shí)現(xiàn)Parcelable接口,可實(shí)現(xiàn)數(shù)據(jù)的序列化。這里是一種備忘錄模式,SavedState作為備忘者,DrawerLayout是備忘錄管理者,Activity是備忘錄使用者。那么我們看看使用SavedState的實(shí)現(xiàn)
@Override
protected Parcelable onSaveInstanceState() {
//這里是保存狀態(tài),系統(tǒng)在需要保存該狀態(tài)時(shí)會(huì)調(diào)用該方法,在這里初始化SavedState,將要保存的數(shù)據(jù)集合起來(lái)
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 存儲(chǔ)的數(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.實(shí)現(xiàn)總結(jié)
分析完DrawerLayout之后,我們總結(jié)自定義一個(gè)View可能需要的實(shí)現(xiàn)有,測(cè)量,布局,繪制,事件分發(fā)處理,事件攔截處理,自身事件處理,自定義LayoutParams,考慮更多的話,有狀態(tài)的存儲(chǔ)恢復(fù),輔助功能狀態(tài)下的事件處理,當(dāng)然,還有重要的自身的邏輯處理。
我們也看到DrawerLayout這個(gè)View本身只是一個(gè)控制側(cè)滑顯示的容器,一般我們會(huì)有如下的方式使用它。
<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會(huì)認(rèn)定它就是側(cè)滑部分。
顯然DrawerLayout并沒(méi)有完全實(shí)現(xiàn)我們想要的側(cè)滑菜單,因?yàn)槔锩嫖覀儾](méi)有看到側(cè)滑的內(nèi)容。后面我將分析NavigationView的實(shí)現(xiàn)。