測滑菜單MenuDrawer的使用以及解析
來自: http://www.jcodecraeer.com//a/anzhuokaifa/androidkaifa/2014/0310/1589.html
在安卓中左右側滑菜單的使用用的比ios多得多,可能是谷歌帶的頭吧,幾乎所有的谷歌應用都有側滑菜單。谷歌沒有開放這個源碼,在一個成熟的開源代碼出現之前,大家都是各自為戰,偶爾能看到一個勉強實現了的。MenuDrawer和其他的側滑代碼不同,他是一個性能高效且成熟的庫。
在menuDraer出現之前我還用過slidemenu,效果差不多,但感覺沒有MenuDrawer流暢,后來看了MenuDrawer和slidemenu的源碼之后,才知道其實他們的實現思路是基本一致的,不過MenuDrawer的代碼寫的更好些。
MenuDrawer支持左、右、上、下四種側滑方式,同時支持Overlay和Sliding兩種模式。如下圖:
但是MenuDrawer在github上的項目是gradle的android studio做的,直接將library導入到項目中會出現資源文件找不到的錯誤。因此要學會在eclipse中使用的話需要點耐心。官方已經有了其使用的方法,我這里就暫時不多說。其實要學會用這個庫很簡單,但是我的目的是要學會他實現滑動的基本方法,學會修改這個庫,運用在更多的場景中。
我想弄明白下面的疑問:
在一個界面中如何自如的通過手勢控制布局,想隱藏多少隱藏多少。
MenuDrawer的代碼很多,但是其實我并不需要里面的很多功能,比如和actionbar的關聯,甚至是上下滑動我也不需要,我只需要一個實現左滑菜單的范例,然后其他的我自己來,我不想一個項目下來用了很多別人的庫,包含著一大堆跟業務無關的代碼,而且我還不知道是拿來干嘛的。
經過研究,發現只需反復研究三個幾個文件就能知道MenuDrawer的實現過程。
這三個類以及他們的的繼承關系如下:
MenuDrawer.java -> DraggableDrawer.java -> SlidingDrawer.java
MenuDrawer.java 這個是基本的類,繼承自ViewGroup,他定義了許多公共方法和屬性變量,MenuDrawer初始化也是在這個類中。其中包含如何加載菜單和主內容的layout,如何根據xml中的屬性以及樣式來獲取一些初始化值,比如菜單的寬度,是否要陰影以及當前菜單的箭頭指示資源文件等,繪制箭頭和陰影的實現也在這個文件中。
DraggableDrawer.java實現了menu滑動的一些動畫過度效果,其實感覺這里面的東西并不多。
SlidingDrawer.java這個是實現滑動的主要類,要知道如何通過手勢滑動布局,這里就可以找到答案。
下面是SlidingDrawer的代碼,不過是我修改過了的。
package net.simonvt.menudrawer; import android.app.Activity; import android.content.Context; import android.graphics.Canvas; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.VelocityTracker; public class SlidingDrawer extends DraggableDrawer { private static final String TAG = "OverlayDrawer"; SlidingDrawer(Activity activity, int dragMode) { super(activity, dragMode); } public SlidingDrawer(Context context) { super(context); } public SlidingDrawer(Context context, AttributeSet attrs) { super(context, attrs); } public SlidingDrawer(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } @Override protected void initDrawer(Context context, AttributeSet attrs, int defStyle) { super.initDrawer(context, attrs, defStyle); super.addView(mMenuContainer, -1, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); super.addView(mContentContainer, -1, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); } @Override public void openMenu(boolean animate) { int animateTo = 0; switch (getPosition()) { case LEFT: case TOP: animateTo = mMenuSize; break; case RIGHT: case BOTTOM: animateTo = -mMenuSize; break; } animateOffsetTo(animateTo, 0, animate); } @Override public void closeMenu(boolean animate) { animateOffsetTo(mMenuCloseSize, 0, animate); } @Override protected void onOffsetPixelsChanged(int offsetPixels) { if (USE_TRANSLATIONS) { switch (getPosition()) { case TOP: case BOTTOM: mContentContainer.setTranslationY(offsetPixels); break; default: mContentContainer.setTranslationX(offsetPixels); break; } } else { switch (getPosition()) { case TOP: case BOTTOM: mContentContainer.offsetTopAndBottom(offsetPixels - mContentContainer.getTop()); break; default: mContentContainer.offsetLeftAndRight(offsetPixels - mContentContainer.getLeft()); break; } } offsetMenu(offsetPixels); invalidate(); } @Override protected void initPeekScroller() { switch (getPosition()) { case RIGHT: case BOTTOM: { final int dx = -mMenuSize / 3; mPeekScroller.startScroll(0, 0, dx, 0, PEEK_DURATION); break; } default: { final int dx = mMenuSize / 3; mPeekScroller.startScroll(0, 0, dx, 0, PEEK_DURATION); break; } } } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); onOffsetPixelsChanged((int) mOffsetPixels); } @Override protected void drawOverlay(Canvas canvas) { final int width = getWidth(); final int height = getHeight(); final int offsetPixels = (int) mOffsetPixels; final float openRatio = Math.abs(mOffsetPixels) / mMenuSize; switch (getPosition()) { case LEFT: mMenuOverlay.setBounds(0, 0, offsetPixels, height); break; case RIGHT: mMenuOverlay.setBounds(width + offsetPixels, 0, width, height); break; case TOP: mMenuOverlay.setBounds(0, 0, width, offsetPixels); break; case BOTTOM: mMenuOverlay.setBounds(0, height + offsetPixels, width, height); break; } mMenuOverlay.setAlpha((int) (MAX_MENU_OVERLAY_ALPHA * (1.f - openRatio))); mMenuOverlay.draw(canvas); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { final int width = r - l; final int height = b - t; if (USE_TRANSLATIONS) { mContentContainer.layout(0, 0, width, height); } else { final int offsetPixels = (int) mOffsetPixels; if (getPosition() == Position.LEFT || getPosition() == Position.RIGHT) { mContentContainer.layout(offsetPixels, 0, width + offsetPixels, height); } else { mContentContainer.layout(0, offsetPixels, width, height + offsetPixels); } } switch (getPosition()) { case LEFT: mMenuContainer.layout(0, 0, mMenuSize, height); break; case RIGHT: mMenuContainer.layout(width - mMenuSize, 0, width, height); break; case TOP: mMenuContainer.layout(0, 0, width, mMenuSize); break; case BOTTOM: mMenuContainer.layout(0, height - mMenuSize, width, height); break; } } /** * Offsets the menu relative to its original position based on the position of the content. * * @param offsetPixels The number of pixels the content if offset. */ private void offsetMenu(int offsetPixels) { if (!mOffsetMenu || mMenuSize == 0) { return; } final int width = getWidth(); final int height = getHeight(); final int menuSize = mMenuSize; final int sign = (int) (mOffsetPixels / Math.abs(mOffsetPixels)); final float openRatio = Math.abs(mOffsetPixels) / menuSize; final int offset = (int) (-0.25f * ((1.0f - openRatio) * menuSize) * sign); switch (getPosition()) { case LEFT: { if (USE_TRANSLATIONS) { if (offsetPixels > 0) { mMenuContainer.setTranslationX(offset); } else { mMenuContainer.setTranslationX(-menuSize); } } else { mMenuContainer.offsetLeftAndRight(offset - mMenuContainer.getLeft()); mMenuContainer.setVisibility(offsetPixels == 0 ? INVISIBLE : VISIBLE); } break; } case RIGHT: { if (USE_TRANSLATIONS) { if (offsetPixels != 0) { mMenuContainer.setTranslationX(offset); } else { mMenuContainer.setTranslationX(menuSize); } } else { final int oldOffset = mMenuContainer.getRight() - width; final int offsetBy = offset - oldOffset; mMenuContainer.offsetLeftAndRight(offsetBy); mMenuContainer.setVisibility(offsetPixels == 0 ? INVISIBLE : VISIBLE); } break; } case TOP: { if (USE_TRANSLATIONS) { if (offsetPixels > 0) { mMenuContainer.setTranslationY(offset); } else { mMenuContainer.setTranslationY(-menuSize); } } else { mMenuContainer.offsetTopAndBottom(offset - mMenuContainer.getTop()); mMenuContainer.setVisibility(offsetPixels == 0 ? INVISIBLE : VISIBLE); } break; } case BOTTOM: { if (USE_TRANSLATIONS) { if (offsetPixels != 0) { mMenuContainer.setTranslationY(offset); } else { mMenuContainer.setTranslationY(menuSize); } } else { final int oldOffset = mMenuContainer.getBottom() - height; final int offsetBy = offset - oldOffset; mMenuContainer.offsetTopAndBottom(offsetBy); mMenuContainer.setVisibility(offsetPixels == 0 ? INVISIBLE : VISIBLE); } break; } } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final int widthMode = MeasureSpec.getMode(widthMeasureSpec); final int heightMode = MeasureSpec.getMode(heightMeasureSpec); if (widthMode == MeasureSpec.UNSPECIFIED || heightMode == MeasureSpec.UNSPECIFIED) { throw new IllegalStateException("Must measure with an exact size"); } final int width = MeasureSpec.getSize(widthMeasureSpec); final int height = MeasureSpec.getSize(heightMeasureSpec); /** by hejie */ mMenuSize = width; mMenuCloseSize = (int) (mMenuSize * 0.3f);; //mMenuCloseSize = (int) (mMenuSize * 0.4f);; // if (mOffsetPixels == -1) openMenu(false); openMenu(false); /** */ int menuWidthMeasureSpec; int menuHeightMeasureSpec; switch (getPosition()) { case TOP: case BOTTOM: menuWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, width); menuHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, 0, mMenuSize); break; default: // LEFT/RIGHT menuWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, mMenuSize); menuHeightMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, height); } mMenuContainer.measure(menuWidthMeasureSpec, menuHeightMeasureSpec); final int contentWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, width); final int contentHeightMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, height); mContentContainer.measure(contentWidthMeasureSpec, contentHeightMeasureSpec); setMeasuredDimension(width, height); updateTouchAreaSize(); } private boolean isContentTouch(int x, int y) { boolean contentTouch = false; switch (getPosition()) { case LEFT: contentTouch = ViewHelper.getLeft(mContentContainer) < x; break; case RIGHT: contentTouch = ViewHelper.getRight(mContentContainer) > x; break; case TOP: contentTouch = ViewHelper.getTop(mContentContainer) < y; break; case BOTTOM: contentTouch = ViewHelper.getBottom(mContentContainer) > y; break; } return contentTouch; } protected boolean onDownAllowDrag(int x, int y) { switch (getPosition()) { case LEFT: return (!mMenuVisible && mInitialMotionX <= mTouchSize) || (mMenuVisible && mInitialMotionX >= mOffsetPixels); case RIGHT: final int width = getWidth(); final int initialMotionX = (int) mInitialMotionX; return (!mMenuVisible && initialMotionX >= width - mTouchSize) || (mMenuVisible && initialMotionX <= width + mOffsetPixels); case TOP: return (!mMenuVisible && mInitialMotionY <= mTouchSize) || (mMenuVisible && mInitialMotionY >= mOffsetPixels); case BOTTOM: final int height = getHeight(); return (!mMenuVisible && mInitialMotionY >= height - mTouchSize) || (mMenuVisible && mInitialMotionY <= height + mOffsetPixels); } return false; } protected boolean onMoveAllowDrag(int x, int y, float dx, float dy) { switch (getPosition()) { case LEFT: return (!mMenuVisible && mInitialMotionX <= mTouchSize && (dx > 0)) || (mMenuVisible && x >= mOffsetPixels); case RIGHT: final int width = getWidth(); return (!mMenuVisible && mInitialMotionX >= width - mTouchSize && (dx < 0)) || (mMenuVisible && x <= width + mOffsetPixels); case TOP: return (!mMenuVisible && mInitialMotionY <= mTouchSize && (dy > 0)) || (mMenuVisible && y >= mOffsetPixels); case BOTTOM: final int height = getHeight(); return (!mMenuVisible && mInitialMotionY >= height - mTouchSize && (dy < 0)) || (mMenuVisible && y <= height + mOffsetPixels); } return false; } protected void onMoveEvent(float dx, float dy) { switch (getPosition()) { case LEFT: setOffsetPixels(Math.min(Math.max(mOffsetPixels + dx, mMenuCloseSize), mMenuSize)); break; case RIGHT: setOffsetPixels(Math.max(Math.min(mOffsetPixels + dx, 0), -mMenuSize)); break; case TOP: setOffsetPixels(Math.min(Math.max(mOffsetPixels + dy, 0), mMenuSize)); break; case BOTTOM: setOffsetPixels(Math.max(Math.min(mOffsetPixels + dy, 0), -mMenuSize)); break; } } protected void onUpEvent(int x, int y) { final int offsetPixels = (int) mOffsetPixels; switch (getPosition()) { case LEFT: { if (mIsDragging) { mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity); final int initialVelocity = (int) getXVelocity(mVelocityTracker); mLastMotionX = x; animateOffsetTo(initialVelocity > 0 ? mMenuSize : mMenuCloseSize, initialVelocity, true); // Close the menu when content is clicked while the menu is visible. } else if (mMenuVisible && x > offsetPixels) { closeMenu(); } break; } case TOP: { if (mIsDragging) { mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity); final int initialVelocity = (int) getYVelocity(mVelocityTracker); mLastMotionY = y; animateOffsetTo(initialVelocity > 0 ? mMenuSize : 0, initialVelocity, true); // Close the menu when content is clicked while the menu is visible. } else if (mMenuVisible && y > offsetPixels) { closeMenu(); } break; } case RIGHT: { final int width = getWidth(); if (mIsDragging) { mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity); final int initialVelocity = (int) getXVelocity(mVelocityTracker); mLastMotionX = x; animateOffsetTo(initialVelocity > 0 ? 0 : -mMenuSize, initialVelocity, true); // Close the menu when content is clicked while the menu is visible. } else if (mMenuVisible && x < width + offsetPixels) { closeMenu(); } break; } case BOTTOM: { if (mIsDragging) { mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity); final int initialVelocity = (int) getYVelocity(mVelocityTracker); mLastMotionY = y; animateOffsetTo(initialVelocity < 0 ? -mMenuSize : 0, initialVelocity, true); // Close the menu when content is clicked while the menu is visible. } else if (mMenuVisible && y < getHeight() + offsetPixels) { closeMenu(); } break; } } } protected boolean checkTouchSlop(float dx, float dy) { switch (getPosition()) { case TOP: case BOTTOM: return Math.abs(dy) > mTouchSlop && Math.abs(dy) > Math.abs(dx); default: return Math.abs(dx) > mTouchSlop && Math.abs(dx) > Math.abs(dy); } } public boolean onInterceptTouchEvent(MotionEvent ev) { final int action = ev.getAction() & MotionEvent.ACTION_MASK; if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { mActivePointerId = INVALID_POINTER; mIsDragging = false; if (mVelocityTracker != null) { mVelocityTracker.recycle(); mVelocityTracker = null; } if (Math.abs(mOffsetPixels) > mMenuSize / 2) { openMenu(); } else { closeMenu(); } return false; } if (action == MotionEvent.ACTION_DOWN && mMenuVisible && isCloseEnough()) { setOffsetPixels(0); stopAnimation(); endPeek(); setDrawerState(STATE_CLOSED); mIsDragging = false; } // Always intercept events over the content while menu is visible. if (mMenuVisible) { int index = 0; if (mActivePointerId != INVALID_POINTER) { index = ev.findPointerIndex(mActivePointerId); index = index == -1 ? 0 : index; } final int x = (int) ev.getX(index); final int y = (int) ev.getY(index); if (isContentTouch(x, y)) { return true; } } if (!mMenuVisible && !mIsDragging && mTouchMode == TOUCH_MODE_NONE) { return false; } if (action != MotionEvent.ACTION_DOWN && mIsDragging) { return true; } switch (action) { case MotionEvent.ACTION_DOWN: { mLastMotionX = mInitialMotionX = ev.getX(); mLastMotionY = mInitialMotionY = ev.getY(); final boolean allowDrag = onDownAllowDrag((int) mLastMotionX, (int) mLastMotionY); mActivePointerId = ev.getPointerId(0); if (allowDrag) { setDrawerState(mMenuVisible ? STATE_OPEN : STATE_CLOSED); stopAnimation(); endPeek(); mIsDragging = false; } break; } case MotionEvent.ACTION_MOVE: { final int activePointerId = mActivePointerId; if (activePointerId == INVALID_POINTER) { // If we don't have a valid id, the touch down wasn't on content. break; } final int pointerIndex = ev.findPointerIndex(activePointerId); if (pointerIndex == -1) { mIsDragging = false; mActivePointerId = INVALID_POINTER; endDrag(); closeMenu(true); return false; } final float x = ev.getX(pointerIndex); final float dx = x - mLastMotionX; final float y = ev.getY(pointerIndex); final float dy = y - mLastMotionY; if (checkTouchSlop(dx, dy)) { if (mOnInterceptMoveEventListener != null && (mTouchMode == TOUCH_MODE_FULLSCREEN || mMenuVisible) && canChildrenScroll((int) dx, (int) dy, (int) x, (int) y)) { endDrag(); // Release the velocity tracker requestDisallowInterceptTouchEvent(true); return false; } final boolean allowDrag = onMoveAllowDrag((int) x, (int) y, dx, dy); if (allowDrag) { setDrawerState(STATE_DRAGGING); mIsDragging = true; mLastMotionX = x; mLastMotionY = y; } } break; } case MotionEvent.ACTION_POINTER_UP: onPointerUp(ev); mLastMotionX = ev.getX(ev.findPointerIndex(mActivePointerId)); mLastMotionY = ev.getY(ev.findPointerIndex(mActivePointerId)); break; } if (mVelocityTracker == null) mVelocityTracker = VelocityTracker.obtain(); mVelocityTracker.addMovement(ev); return mIsDragging; } @Override public boolean onTouchEvent(MotionEvent ev) { if (!mMenuVisible && !mIsDragging && mTouchMode == TOUCH_MODE_NONE) { return false; } final int action = ev.getAction() & MotionEvent.ACTION_MASK; if (mVelocityTracker == null) mVelocityTracker = VelocityTracker.obtain(); mVelocityTracker.addMovement(ev); switch (action) { case MotionEvent.ACTION_DOWN: { mLastMotionX = mInitialMotionX = ev.getX(); mLastMotionY = mInitialMotionY = ev.getY(); final boolean allowDrag = onDownAllowDrag((int) mLastMotionX, (int) mLastMotionY); mActivePointerId = ev.getPointerId(0); if (allowDrag) { stopAnimation(); endPeek(); startLayerTranslation(); } break; } case MotionEvent.ACTION_MOVE: { final int pointerIndex = ev.findPointerIndex(mActivePointerId); if (pointerIndex == -1) { mIsDragging = false; mActivePointerId = INVALID_POINTER; endDrag(); closeMenu(true); return false; } if (!mIsDragging) { final float x = ev.getX(pointerIndex); final float dx = x - mLastMotionX; final float y = ev.getY(pointerIndex); final float dy = y - mLastMotionY; if (checkTouchSlop(dx, dy)) { final boolean allowDrag = onMoveAllowDrag((int) x, (int) y, dx, dy); if (allowDrag) { setDrawerState(STATE_DRAGGING); mIsDragging = true; mLastMotionX = x; mLastMotionY = y; } else { mInitialMotionX = x; mInitialMotionY = y; } } } if (mIsDragging) { startLayerTranslation(); final float x = ev.getX(pointerIndex); final float dx = x - mLastMotionX; final float y = ev.getY(pointerIndex); final float dy = y - mLastMotionY; mLastMotionX = x; mLastMotionY = y; onMoveEvent(dx, dy); } break; } case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: { int index = ev.findPointerIndex(mActivePointerId); index = index == -1 ? 0 : index; final int x = (int) ev.getX(index); final int y = (int) ev.getY(index); onUpEvent(x, y); mActivePointerId = INVALID_POINTER; mIsDragging = false; break; } case MotionEvent.ACTION_POINTER_DOWN: final int index = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; mLastMotionX = ev.getX(index); mLastMotionY = ev.getY(index); mActivePointerId = ev.getPointerId(index); break; case MotionEvent.ACTION_POINTER_UP: onPointerUp(ev); mLastMotionX = ev.getX(ev.findPointerIndex(mActivePointerId)); mLastMotionY = ev.getY(ev.findPointerIndex(mActivePointerId)); break; } return true; } private void onPointerUp(MotionEvent ev) { final int pointerIndex = ev.getActionIndex(); final int pointerId = ev.getPointerId(pointerIndex); if (pointerId == mActivePointerId) { final int newPointerIndex = pointerIndex == 0 ? 1 : 0; mLastMotionX = ev.getX(newPointerIndex); mActivePointerId = ev.getPointerId(newPointerIndex); if (mVelocityTracker != null) { mVelocityTracker.clear(); } } } }
onInterceptTouchEvent和onTouchEvent是捕捉用戶滑動事件的方法,其中onInterceptTouchEvent的用途是在onTouchEvent之前截獲事件,并提供一次改變事件傳遞方向的機會。當用戶滑動屏幕,onTouchEvent會被調用并執行case MotionEvent.ACTION_MOVE:下面的代碼,通過比較當前觸摸點和上次觸摸點的位置記錄x和y方向上的偏移量,然后根據這個偏移量移動繼承自ViewGroup的MenuDrawer。其實MenuDrawer的這個過程和ViewPager非常相似。
final float x = ev.getX(pointerIndex); final float dx = x - mLastMotionX; final float y = ev.getY(pointerIndex); final float dy = y - mLastMotionY; mLastMotionX = x; mLastMotionY = y; onMoveEvent(dx, dy)
onMoveEvent就是移動ViewGroup的方法。
protected void onMoveEvent(float dx, float dy) { switch (getPosition()) { case LEFT: setOffsetPixels(Math.min(Math.max(mOffsetPixels + dx, mMenuCloseSize), mMenuSize)); break; case RIGHT: setOffsetPixels(Math.max(Math.min(mOffsetPixels + dx, 0), -mMenuSize)); break; case TOP: setOffsetPixels(Math.min(Math.max(mOffsetPixels + dy, 0), mMenuSize)); break; case BOTTOM: setOffsetPixels(Math.max(Math.min(mOffsetPixels + dy, 0), -mMenuSize)); break; } }
根據上面的代碼我們知道手指滑動屏幕的過程中onTouchEvent onMoveEvent
和setOffsetPixels
相繼被調用。setOffsetPixels
之后最終會在offsetMenu中用offsetLeftAndRight()來移動ViewGroup。
github地址: https://github.com/SimonVT/android-menudrawer