Java tutorial
/* Copyright (c) 2011-2013 Tang Ke * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package me.tangke.slidemenu; import me.tangke.slidemenu.utils.ScrollDetectors; import me.tangke.slidemenu.utils.ScrollDetectors.ScrollDetector; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.GradientDrawable.Orientation; import android.os.Parcel; import android.os.Parcelable; import android.support.v4.view.ViewCompat; import android.util.AttributeSet; import android.util.TypedValue; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewDebug.ExportedProperty; import android.view.ViewGroup; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; import android.widget.Scroller; /** * Swipe left/right to show the hidden menu behind the content view, Use * {@link ScrollDetector} to custom the rule of MotionEvent intercept * * @author Tank * */ public class SlideMenu extends ViewGroup { private final static int MAX_DURATION = 500; private static int STATUS_BAR_HEIGHT; public final static int FLAG_DIRECTION_LEFT = 1 << 0; public final static int FLAG_DIRECTION_RIGHT = 1 << 1; public final static int MODE_SLIDE_WINDOW = 1; public final static int MODE_SLIDE_CONTENT = 2; public final static int STATE_CLOSE = 1 << 0; public final static int STATE_OPEN_LEFT = 1 << 1; public final static int STATE_OPEN_RIGHT = 1 << 2; public final static int STATE_DRAG = 1 << 3; public final static int STATE_SCROLL = 1 << 4; public final static int STATE_OPEN_MASK = 6; private final static int POSITION_LEFT = -1; private final static int POSITION_MIDDLE = 0; private final static int POSITION_RIGHT = 1; private boolean mCanSlide = true; private int mCurrentContentPosition; private int mCurrentState; private View mContent; private View mPrimaryMenu; private View mSecondaryMenu; private int mTouchSlop; private float mPressedX; private float mPressedY; private float mLastMotionX; private volatile int mCurrentContentOffset; private int mContentBoundsLeft; private int mContentBoundsRight; private boolean mIsTapInContent; private Rect mContentHitRect; @ExportedProperty private Drawable mPrimaryShadowDrawable; @ExportedProperty private Drawable mSecondaryShadowDrawable; @ExportedProperty private float mPrimaryShadowWidth; @ExportedProperty private float mSecondaryShadowWidth; private int mSlideDirectionFlag; private boolean mIsPendingResolveSlideMode; private int mSlideMode = MODE_SLIDE_CONTENT; private boolean mIsEdgeSlideEnable = true; private int mEdgeSlideWidth; private Rect mEdgeSlideDetectRect; private boolean mIsTapInEdgeSlide; private int mWidth; private int mHeight; private OnSlideStateChangeListener mSlideStateChangeListener; private OnContentTapListener mContentTapListener; private VelocityTracker mVelocityTracker; private Scroller mScroller; private Interpolator mInterpolator; public final static Interpolator DEFAULT_INTERPOLATOR = new Interpolator() { @Override public float getInterpolation(float t) { t -= 1.0f; return t * t * t * t * t + 1.0f; } }; public SlideMenu(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); // we want to draw drop shadow of content mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); mVelocityTracker = VelocityTracker.obtain(); mContentHitRect = new Rect(); mEdgeSlideDetectRect = new Rect(); STATUS_BAR_HEIGHT = (int) getStatusBarHeight(context); setWillNotDraw(false); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SlideMenu, defStyle, 0); // Set the shadow attributes setPrimaryShadowWidth(a.getDimension(R.styleable.SlideMenu_primaryShadowWidth, 30)); setSecondaryShadowWidth(a.getDimension(R.styleable.SlideMenu_secondaryShadowWidth, 30)); Drawable primaryShadowDrawable = a.getDrawable(R.styleable.SlideMenu_primaryShadowDrawable); if (null == primaryShadowDrawable) { primaryShadowDrawable = new GradientDrawable(Orientation.LEFT_RIGHT, new int[] { Color.TRANSPARENT, Color.argb(99, 0, 0, 0) }); } setPrimaryShadowDrawable(primaryShadowDrawable); Drawable secondaryShadowDrawable = a.getDrawable(R.styleable.SlideMenu_secondaryShadowDrawable); if (null == secondaryShadowDrawable) { secondaryShadowDrawable = new GradientDrawable(Orientation.LEFT_RIGHT, new int[] { Color.argb(99, 0, 0, 0), Color.TRANSPARENT }); } setSecondaryShadowDrawable(secondaryShadowDrawable); int interpolatorResId = a.getResourceId(R.styleable.SlideMenu_interpolator, -1); setInterpolator(-1 == interpolatorResId ? DEFAULT_INTERPOLATOR : AnimationUtils.loadInterpolator(context, interpolatorResId)); mSlideDirectionFlag = a.getInt(R.styleable.SlideMenu_slideDirection, FLAG_DIRECTION_LEFT | FLAG_DIRECTION_RIGHT); setEdgeSlideEnable(a.getBoolean(R.styleable.SlideMenu_edgeSlide, false)); setEdgetSlideWidth(a.getDimensionPixelSize(R.styleable.SlideMenu_edgeSlideWidth, 100)); a.recycle(); setFocusable(true); setFocusableInTouchMode(true); } public SlideMenu(Context context, AttributeSet attrs) { this(context, attrs, R.attr.slideMenuStyle); } public SlideMenu(Context context) { this(context, null); } public void setCanSlide(boolean canSlide) { mCanSlide = canSlide; } protected Drawable getDefaultContentBackground(Context context) { TypedValue value = new TypedValue(); context.getTheme().resolveAttribute(android.R.attr.windowBackground, value, true); return context.getResources().getDrawable(value.resourceId); } /** * Retrieve the height of status bar that defined in system * * @param context * @return */ public static float getStatusBarHeight(Context context) { Resources resources = context.getResources(); int statusBarIdentifier = resources.getIdentifier("status_bar_height", "dimen", "android"); if (0 != statusBarIdentifier) { return resources.getDimension(statusBarIdentifier); } return 0; } /** * Resolve the attribute slideMode */ protected void resolveSlideMode() { final ViewGroup decorView = (ViewGroup) getRootView(); final ViewGroup contentContainer = (ViewGroup) decorView.findViewById(android.R.id.content); final View content = mContent; if (null == decorView || null == content || 0 == getChildCount()) { return; } TypedValue value = new TypedValue(); getContext().getTheme().resolveAttribute(android.R.attr.windowBackground, value, true); switch (mSlideMode) { case MODE_SLIDE_WINDOW: { // remove this view from parent removeViewFromParent(this); // copy the layoutparams of content LayoutParams contentLayoutParams = new LayoutParams(content.getLayoutParams()); // remove content view from this view removeViewFromParent(content); // add content to layout root view contentContainer.addView(content); // get window with ActionBar View decorChild = decorView.getChildAt(0); decorChild.setBackgroundResource(0); removeViewFromParent(decorChild); addView(decorChild, contentLayoutParams); // add this view to root view decorView.addView(this); setBackgroundResource(value.resourceId); } break; case MODE_SLIDE_CONTENT: { // remove this view from decor view setBackgroundResource(0); removeViewFromParent(this); // get the origin content view from the content wrapper View originContent = contentContainer.getChildAt(0); // this is the decor child remove from decor view View decorChild = mContent; LayoutParams layoutParams = (LayoutParams) decorChild.getLayoutParams(); // remove the origin content from content wrapper removeViewFromParent(originContent); // remove decor child from this view removeViewFromParent(decorChild); // restore the decor child to decor view decorChild.setBackgroundResource(value.resourceId); decorView.addView(decorChild); // add this view to content wrapper contentContainer.addView(this); // add the origin content to this view addView(originContent, layoutParams); } break; } } @Override public void addView(View child, int index, android.view.ViewGroup.LayoutParams params) { if (!(params instanceof LayoutParams)) { throw new IllegalArgumentException( "The parameter params must a instance of com.aretha.slidemenu.SlideMenu$LayoutParams"); } if (null == params) { // Skip the view without LayoutParams return; } LayoutParams layoutParams = (LayoutParams) params; switch (layoutParams.role) { case LayoutParams.ROLE_CONTENT: removeView(mContent); mContent = child; break; case LayoutParams.ROLE_PRIMARY_MENU: removeView(mPrimaryMenu); mPrimaryMenu = child; break; case LayoutParams.ROLE_SECONDARY_MENU: removeView(mSecondaryMenu); mSecondaryMenu = child; break; default: // We will ignore the view without attribute layout_role return; } invalidateMenuState(); super.addView(child, index, params); } /** * Remove view child it's parent node, if the view does not have parent. * ignore * * @param view */ public static void removeViewFromParent(View view) { if (null == view) { return; } ViewGroup parent = (ViewGroup) view.getParent(); if (null == parent) { return; } parent.removeView(view); } /** * Set animation interpolator when SlideMenu scroll * * @param interpolator */ public void setInterpolator(Interpolator interpolator) { mInterpolator = interpolator; mScroller = new Scroller(getContext(), interpolator); } /** * Get the animation interpolator * * @return */ public Interpolator getInterpolator() { return mInterpolator; } /** * Set the shadow drawable of left side * * @param shadowDrawable */ public void setPrimaryShadowDrawable(Drawable shadowDrawable) { mPrimaryShadowDrawable = shadowDrawable; } /** * Get the shadow drawable of left side * * @return */ public Drawable getPrimaryShadowDrawable() { return mPrimaryShadowDrawable; } /** * Get the shadow drawable of right side * * @return */ public Drawable getSecondaryShadowDrawable() { return mSecondaryShadowDrawable; } /** * Set the shadow drawable of right side * * @param secondaryShadowDrawable */ public void setSecondaryShadowDrawable(Drawable secondaryShadowDrawable) { this.mSecondaryShadowDrawable = secondaryShadowDrawable; } /** * Get the slide mode current specified * * @return */ public int getSlideMode() { return mSlideMode; } /** * Set the slide mode:<br/> * {@link #MODE_SLIDE_CONTENT} {@link #MODE_SLIDE_WINDOW} * * @param slideMode */ public void setSlideMode(int slideMode) { if (isAttacthedInContentView()) { throw new IllegalStateException("SlidingMenu must be the root of layout"); } if (mSlideMode == slideMode) { return; } mSlideMode = slideMode; if (0 == getChildCount()) { mIsPendingResolveSlideMode = true; } else { resolveSlideMode(); } } /** * Toggle the edge slide * * @param enable */ public void setEdgeSlideEnable(boolean enable) { mIsEdgeSlideEnable = enable; } /** * Indicate user can only open SlideMenu from the edge * * @return */ public boolean isEdgeSlideEnable() { return mIsEdgeSlideEnable; } /** * Set edge slide width next left and right side of SlideMenu * * @param width */ public void setEdgetSlideWidth(int width) { if (width < 0) { throw new IllegalArgumentException("Edge slide width must above 0"); } mEdgeSlideWidth = width; } /** * Get the edge slide width * * @return */ public float getEdgeSlideWidth() { return mEdgeSlideWidth; } /** * Indicate this SlideMenu is open * * @return true open, otherwise false */ public boolean isOpen() { return (STATE_OPEN_MASK & mCurrentState) != 0; } /** * Open the SlideMenu * * @param isSlideLeft * @param isAnimated */ public void open(boolean isSlideLeft, boolean isAnimated) { if (isOpen()) { return; } int targetOffset = isSlideLeft ? mContentBoundsLeft : mContentBoundsRight; if (isAnimated) { smoothScrollContentTo(targetOffset); } else { mScroller.abortAnimation(); setCurrentOffset(targetOffset); setCurrentState(isSlideLeft ? STATE_OPEN_LEFT : STATE_OPEN_RIGHT); } } /** * Close the SlideMenu * * @param isAnimated */ public void close(boolean isAnimated) { if (STATE_CLOSE == mCurrentState) { return; } if (isAnimated) { smoothScrollContentTo(0); } else { mScroller.abortAnimation(); setCurrentOffset(0); setCurrentState(STATE_CLOSE); } } /** * Get current slide direction, {@link #FLAG_DIRECTION_LEFT}, * {@link #FLAG_DIRECTION_RIGHT} or {@link #FLAG_DIRECTION_LEFT}| * {@link #FLAG_DIRECTION_RIGHT} * * @return */ public int getSlideDirection() { return mSlideDirectionFlag; } /** * Set slide direction * * @param slideDirectionFlag */ public void setSlideDirection(int slideDirectionFlag) { this.mSlideDirectionFlag = slideDirectionFlag; } /** * Set the listener to listen the state change and offset change * * @return */ public OnSlideStateChangeListener getOnSlideStateChangeListener() { return mSlideStateChangeListener; } /** * Get the current listener * * @param slideStateChangeListener */ public void setOnSlideStateChangeListener(OnSlideStateChangeListener slideStateChangeListener) { this.mSlideStateChangeListener = slideStateChangeListener; } public void setOnContentTapListener(OnContentTapListener contentTapListener) { this.mContentTapListener = contentTapListener; } public OnContentTapListener getOnContentTapListener() { return this.mContentTapListener; } /** * Get current state * * @return */ public int getCurrentState() { return mCurrentState; } /** * Set current state * * @param currentState */ protected void setCurrentState(int currentState) { if (null != mSlideStateChangeListener && currentState != mCurrentState) { mSlideStateChangeListener.onSlideStateChange(currentState); } this.mCurrentState = currentState; } /** * Equals invoke {@link #smoothScrollContentTo(int, float)} with 0 velocity * * @param targetOffset */ public void smoothScrollContentTo(int targetOffset) { smoothScrollContentTo(targetOffset, 0); } /** * Perform a smooth slide of content, the offset of content will limited to * menu width * * @param targetOffset * @param velocity */ public void smoothScrollContentTo(int targetOffset, float velocity) { if (!mCanSlide) return; setCurrentState(STATE_SCROLL); int distance = targetOffset - mCurrentContentOffset; velocity = Math.abs(velocity); int duration = 400; if (velocity > 0) { duration = 3 * Math.round(1000 * Math.abs(distance / velocity)); } duration = Math.min(duration, MAX_DURATION); mScroller.abortAnimation(); mScroller.startScroll(mCurrentContentOffset, 0, distance, 0, duration); invalidate(); } private boolean isTapInContent(float x, float y) { final View content = mContent; if (null != content) { content.getHitRect(mContentHitRect); return mContentHitRect.contains((int) x, (int) y); } return false; } private boolean isTapInEdgeSlide(float x, float y) { final Rect rect = mEdgeSlideDetectRect; boolean result = false; if (null != mPrimaryMenu) { getHitRect(rect); rect.right = mEdgeSlideWidth; result |= rect.contains((int) x, (int) y); } if (null != mSecondaryMenu) { getHitRect(rect); rect.left = rect.right - mEdgeSlideWidth; result |= rect.contains((int) x, (int) y); } return result; } @Override public boolean dispatchTouchEvent(MotionEvent ev) { if (MotionEvent.ACTION_UP == ev.getAction()) { requestDisallowInterceptTouchEvent(false); } return super.dispatchTouchEvent(ev); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { final float x = ev.getX(); final float y = ev.getY(); final int currentState = mCurrentState; if (STATE_DRAG == currentState || STATE_SCROLL == currentState) { return true; } switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: mPressedX = mLastMotionX = x; mPressedY = y; mIsTapInContent = isTapInContent(x, y); mIsTapInEdgeSlide = isTapInEdgeSlide(x, y); return isOpen() && mIsTapInContent; case MotionEvent.ACTION_MOVE: float dx = x - mPressedX; float dy = y - mPressedY; if (mIsEdgeSlideEnable && !mIsTapInEdgeSlide && mCurrentState == STATE_CLOSE) { return false; } // Detect the vertical scroll if (Math.abs(dy) >= mTouchSlop && mIsTapInContent && canScrollVertically(this, (int) dy, (int) x, (int) y)) { // if the child can response the vertical scroll, we will not to // steal the MotionEvent any more requestDisallowInterceptTouchEvent(true); return false; } if (Math.abs(dx) >= mTouchSlop && mIsTapInContent) { if (!canScrollHorizontally(this, (int) dx, (int) x, (int) y)) { setCurrentState(STATE_DRAG); return true; } } } return false; } @Override public boolean onTouchEvent(MotionEvent event) { if (!mCanSlide) return false; final float x = event.getX(); final float y = event.getY(); final int currentState = mCurrentState; final int action = event.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: mPressedX = mLastMotionX = x; mPressedY = y; mIsTapInContent = isTapInContent(x, y); mIsTapInEdgeSlide = isTapInEdgeSlide(x, y); if (mIsTapInContent) { mScroller.abortAnimation(); } break; case MotionEvent.ACTION_MOVE: mVelocityTracker.addMovement(event); if (mIsEdgeSlideEnable && !mIsTapInEdgeSlide && mCurrentState == STATE_CLOSE) { return false; } if (Math.abs(x - mPressedX) >= mTouchSlop && mIsTapInContent && currentState != STATE_DRAG) { getParent().requestDisallowInterceptTouchEvent(true); setCurrentState(STATE_DRAG); } if (STATE_DRAG != currentState) { mLastMotionX = x; return false; } drag(mLastMotionX, x); mLastMotionX = x; break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_OUTSIDE: if (STATE_DRAG == currentState) { mVelocityTracker.computeCurrentVelocity(1000); endDrag(x, mVelocityTracker.getXVelocity()); } else if (mIsTapInContent && MotionEvent.ACTION_UP == action) { performContentTap(); } mVelocityTracker.clear(); getParent().requestDisallowInterceptTouchEvent(false); mIsTapInContent = mIsTapInEdgeSlide = false; break; } return true; } @Override public boolean dispatchKeyEvent(KeyEvent event) { if (KeyEvent.ACTION_UP == event.getAction()) { final boolean isOpen = isOpen(); switch (event.getKeyCode()) { case KeyEvent.KEYCODE_BACK: if (isOpen) { close(true); return true; } break; case KeyEvent.KEYCODE_DPAD_LEFT: if (STATE_OPEN_LEFT == mCurrentState) { close(true); return true; } else if (!isOpen) { open(true, true); return true; } break; case KeyEvent.KEYCODE_DPAD_RIGHT: if (STATE_OPEN_RIGHT == mCurrentState) { close(true); return true; } else if (!isOpen) { open(false, true); return true; } break; } } return super.dispatchKeyEvent(event); } /** * Get current primary menu * * @return */ public View getPrimaryMenu() { return mPrimaryMenu; } /** * Get current secondary menu * * @return */ public View getSecondaryMenu() { return mSecondaryMenu; } /** * Perform click on the content */ public void performContentTap() { if (isOpen()) { smoothScrollContentTo(0); return; } if (null != mContentTapListener) { mContentTapListener.onContentTap(this); } } protected void drag(float lastX, float x) { mCurrentContentOffset += (int) (x - lastX); setCurrentOffset(mCurrentContentOffset); } private void invalidateMenuState() { mCurrentContentPosition = mCurrentContentOffset < 0 ? POSITION_LEFT : (mCurrentContentOffset == 0 ? POSITION_MIDDLE : POSITION_RIGHT); switch (mCurrentContentPosition) { case POSITION_LEFT: invalidateViewVisibility(mPrimaryMenu, View.INVISIBLE); invalidateViewVisibility(mSecondaryMenu, View.VISIBLE); break; case POSITION_MIDDLE: invalidateViewVisibility(mPrimaryMenu, View.INVISIBLE); invalidateViewVisibility(mSecondaryMenu, View.INVISIBLE); break; case POSITION_RIGHT: invalidateViewVisibility(mPrimaryMenu, View.VISIBLE); invalidateViewVisibility(mSecondaryMenu, View.INVISIBLE); break; } } @Override public boolean shouldDelayChildPressedState() { return false; } private void invalidateViewVisibility(View view, int visibility) { if (null != view && view.getVisibility() != visibility) { view.setVisibility(visibility); } } protected void endDrag(float x, float velocity) { final int currentContentOffset = mCurrentContentOffset; final int currentContentPosition = mCurrentContentPosition; boolean velocityMatched = Math.abs(velocity) > 400; switch (currentContentPosition) { case POSITION_LEFT: if ((velocity < 0 && velocityMatched) || (velocity >= 0 && !velocityMatched)) { smoothScrollContentTo(mContentBoundsLeft, velocity); } else if ((velocity > 0 && velocityMatched) || (velocity <= 0 && !velocityMatched)) { smoothScrollContentTo(0, velocity); } break; case POSITION_MIDDLE: setCurrentState(STATE_CLOSE); break; case POSITION_RIGHT: if ((velocity > 0 && velocityMatched) || (velocity <= 0 && !velocityMatched)) { smoothScrollContentTo(mContentBoundsRight, velocity); } else if ((velocity < 0 && velocityMatched) || (velocity >= 0 && !velocityMatched)) { smoothScrollContentTo(0, velocity); } break; } } private void setCurrentOffset(int currentOffset) { final int slideDirectionFlag = mSlideDirectionFlag; final int currentContentOffset = mCurrentContentOffset = Math .min((slideDirectionFlag & FLAG_DIRECTION_RIGHT) == FLAG_DIRECTION_RIGHT ? mContentBoundsRight : 0, Math.max(currentOffset, (slideDirectionFlag & FLAG_DIRECTION_LEFT) == FLAG_DIRECTION_LEFT ? mContentBoundsLeft : 0)); if (null != mSlideStateChangeListener) { float slideOffsetPercent = 0; if (0 < currentContentOffset) { slideOffsetPercent = currentContentOffset * 1.0f / mContentBoundsRight; } else if (0 > currentContentOffset) { slideOffsetPercent = -currentContentOffset * 1.0f / mContentBoundsLeft; } mSlideStateChangeListener.onSlideOffsetChange(slideOffsetPercent); } invalidateMenuState(); invalidate(); requestLayout(); } @Override public void computeScroll() { if (STATE_SCROLL == mCurrentState || isOpen()) { if (mScroller.computeScrollOffset()) { setCurrentOffset(mScroller.getCurrX()); } else { setCurrentState(mCurrentContentOffset == 0 ? STATE_CLOSE : (mCurrentContentOffset > 0 ? STATE_OPEN_LEFT : STATE_OPEN_RIGHT)); } } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final int count = getChildCount(); final int slideMode = mSlideMode; final int statusBarHeight = STATUS_BAR_HEIGHT; final int measureHeight = MeasureSpec.getSize(heightMeasureSpec); final int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec); boolean needRemeasure = false; int maxChildWidth = 0, maxChildHeight = 0; for (int index = 0; index < count; index++) { View child = getChildAt(index); LayoutParams layoutParams = (LayoutParams) child.getLayoutParams(); needRemeasure |= (MeasureSpec.EXACTLY != measureHeightMode && LayoutParams.MATCH_PARENT == layoutParams.height); switch (layoutParams.role) { case LayoutParams.ROLE_CONTENT: measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); break; case LayoutParams.ROLE_PRIMARY_MENU: case LayoutParams.ROLE_SECONDARY_MENU: measureChildWithMargins(child, widthMeasureSpec, 0, slideMode == MODE_SLIDE_WINDOW ? MeasureSpec.makeMeasureSpec(measureHeight - statusBarHeight, MeasureSpec.getMode(heightMeasureSpec)) : heightMeasureSpec, 0); break; } maxChildWidth = Math.max(maxChildWidth, child.getMeasuredWidth()); maxChildHeight = Math.max(maxChildHeight, child.getMeasuredHeight()); } maxChildWidth += getPaddingLeft() + getPaddingRight(); maxChildHeight += getPaddingTop() + getPaddingBottom(); setMeasuredDimension(resolveSize(maxChildWidth, widthMeasureSpec), resolveSize(maxChildHeight, heightMeasureSpec)); // we know the exactly size of SlideMenu, so we should use the new size // to remeasure the child if (needRemeasure) { for (int index = 0; index < count; index++) { View child = getChildAt(index); if (View.GONE != child.getVisibility() && LayoutParams.MATCH_PARENT == child.getLayoutParams().height) { measureChildWithMargins(child, widthMeasureSpec, 0, MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY), 0); } } } } private boolean isAttacthedInContentView() { View parent = (View) getParent(); return null != parent && (android.R.id.content == parent.getId() && MODE_SLIDE_CONTENT == mSlideMode) && (getRootView() == parent && MODE_SLIDE_WINDOW == mSlideMode); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { final int count = getChildCount(); final int paddingLeft = getPaddingLeft(); final int paddingRight = getPaddingRight(); final int paddingTop = getPaddingTop(); final int statusBarHeight = mSlideMode == MODE_SLIDE_WINDOW ? STATUS_BAR_HEIGHT : 0; for (int index = 0; index < count; index++) { View child = getChildAt(index); final int measureWidth = child.getMeasuredWidth(); final int measureHeight = child.getMeasuredHeight(); LayoutParams layoutParams = (LayoutParams) child.getLayoutParams(); switch (layoutParams.role) { case LayoutParams.ROLE_CONTENT: // we should display the content in front of all other views child.bringToFront(); child.layout(mCurrentContentOffset + paddingLeft, paddingTop, paddingLeft + measureWidth + mCurrentContentOffset, paddingTop + measureHeight); break; case LayoutParams.ROLE_PRIMARY_MENU: mContentBoundsRight = measureWidth; child.layout(paddingLeft, statusBarHeight + paddingTop, paddingLeft + measureWidth, statusBarHeight + paddingTop + measureHeight); break; case LayoutParams.ROLE_SECONDARY_MENU: mContentBoundsLeft = -measureWidth; child.layout(r - l - paddingRight - measureWidth, statusBarHeight + paddingTop, r - l - paddingRight, statusBarHeight + paddingTop + measureHeight); break; default: continue; } } } /** * Detect whether the views inside content are slidable */ protected final boolean canScrollHorizontally(View v, int dx, int x, int y) { if (v instanceof ViewGroup) { final ViewGroup viewGroup = (ViewGroup) v; final int scrollX = v.getScrollX(); final int scrollY = v.getScrollY(); final int childCount = viewGroup.getChildCount(); for (int index = 0; index < childCount; index++) { View child = viewGroup.getChildAt(index); final int left = child.getLeft(); final int top = child.getTop(); if (x + scrollX >= left && x + scrollX < child.getRight() && y + scrollY >= top && y + scrollY < child.getBottom() && View.VISIBLE == child.getVisibility() && (ScrollDetectors.canScrollHorizontal(child, dx) || canScrollHorizontally(child, dx, x + scrollX - left, y + scrollY - top))) { return true; } } } return ViewCompat.canScrollHorizontally(v, -dx); } protected final boolean canScrollVertically(View v, int dy, int x, int y) { if (v instanceof ViewGroup) { final ViewGroup viewGroup = (ViewGroup) v; final int scrollX = v.getScrollX(); final int scrollY = v.getScrollY(); final int childCount = viewGroup.getChildCount(); for (int index = 0; index < childCount; index++) { View child = viewGroup.getChildAt(index); final int left = child.getLeft(); final int top = child.getTop(); if (x + scrollX >= left && x + scrollX < child.getRight() && y + scrollY >= top && y + scrollY < child.getBottom() && View.VISIBLE == child.getVisibility() && (ScrollDetectors.canScrollVertical(child, dy) || canScrollVertically(child, dy, x + scrollX - left, y + scrollY - top))) { return true; } } } return ViewCompat.canScrollVertically(v, -dy); } public float getPrimaryShadowWidth() { return mPrimaryShadowWidth; } public void setPrimaryShadowWidth(float primaryShadowWidth) { this.mPrimaryShadowWidth = primaryShadowWidth; invalidate(); } public float getSecondaryShadowWidth() { return mSecondaryShadowWidth; } public void setSecondaryShadowWidth(float secondaryShadowWidth) { this.mSecondaryShadowWidth = secondaryShadowWidth; invalidate(); } @Override public void draw(Canvas canvas) { super.draw(canvas); drawShadow(canvas); } private void drawShadow(Canvas canvas) { if (null == mContent) { return; } final int left = mContent.getLeft(); final int width = mWidth; final int height = mHeight; if (null != mPrimaryShadowDrawable) { mPrimaryShadowDrawable.setBounds((int) (left - mPrimaryShadowWidth), 0, left, height); mPrimaryShadowDrawable.draw(canvas); } if (null != mSecondaryShadowDrawable) { mSecondaryShadowDrawable.setBounds(left + width, 0, (int) (width + left + mSecondaryShadowWidth), height); mSecondaryShadowDrawable.draw(canvas); } } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mWidth = w; mHeight = h; if (mIsPendingResolveSlideMode) { resolveSlideMode(); } } @Override public android.view.ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { LayoutParams layoutParams = new LayoutParams(getContext(), attrs); return layoutParams; } @Override protected Parcelable onSaveInstanceState() { SavedState savedState = new SavedState(super.onSaveInstanceState()); savedState.primaryShadowWidth = mPrimaryShadowWidth; savedState.secondaryShadaryWidth = mSecondaryShadowWidth; savedState.slideDirectionFlag = mSlideDirectionFlag; savedState.slideMode = mSlideMode; savedState.currentState = mCurrentState; savedState.currentContentOffset = mCurrentContentOffset; return savedState; } @Override protected void onRestoreInstanceState(Parcelable state) { SavedState savedState = (SavedState) state; super.onRestoreInstanceState(savedState.getSuperState()); mPrimaryShadowWidth = savedState.primaryShadowWidth; mSecondaryShadowWidth = savedState.secondaryShadaryWidth; mSlideDirectionFlag = savedState.slideDirectionFlag; setSlideMode(savedState.slideMode); mCurrentState = savedState.currentState; mCurrentContentOffset = savedState.currentContentOffset; invalidateMenuState(); requestLayout(); invalidate(); } public static class SavedState extends BaseSavedState { public float primaryShadowWidth; public float secondaryShadaryWidth; public int slideDirectionFlag; public int slideMode; public int currentState; public int currentContentOffset; SavedState(Parcelable superState) { super(superState); } private SavedState(Parcel in) { super(in); primaryShadowWidth = in.readFloat(); secondaryShadaryWidth = in.readFloat(); slideDirectionFlag = in.readInt(); slideMode = in.readInt(); currentState = in.readInt(); currentContentOffset = in.readInt(); } @Override public void writeToParcel(Parcel out, int flags) { super.writeToParcel(out, flags); out.writeFloat(primaryShadowWidth); out.writeFloat(secondaryShadaryWidth); out.writeInt(slideDirectionFlag); out.writeInt(slideMode); out.writeInt(currentState); out.writeInt(currentContentOffset); } public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() { @Override public SavedState createFromParcel(Parcel in) { return new SavedState(in); } @Override public SavedState[] newArray(int size) { return new SavedState[size]; } }; } /** * Add view role for {@link #SlidingMenu} * * @author Tank * */ public static class LayoutParams extends MarginLayoutParams { public final static int ROLE_CONTENT = 0; public final static int ROLE_PRIMARY_MENU = 1; public final static int ROLE_SECONDARY_MENU = 2; public int role; public LayoutParams(Context context, AttributeSet attrs) { super(context, attrs); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SlideMenu_Layout, 0, 0); final int indexCount = a.getIndexCount(); for (int index = 0; index < indexCount; index++) { int i = a.getIndex(index); if (i == R.styleable.SlideMenu_Layout_layout_role) { role = a.getInt(R.styleable.SlideMenu_Layout_layout_role, -1); } } switch (role) { // content should match whole SlidingMenu case ROLE_CONTENT: width = MATCH_PARENT; height = MATCH_PARENT; break; case ROLE_SECONDARY_MENU: case ROLE_PRIMARY_MENU: // add your custom layout rule here for menu break; default: throw new IllegalArgumentException("You must specified a layout_role for this view"); } a.recycle(); } public LayoutParams(int width, int height) { super(width, height); } public LayoutParams(int width, int height, int role) { super(width, height); this.role = role; } public LayoutParams(android.view.ViewGroup.LayoutParams layoutParams) { super(layoutParams); if (layoutParams instanceof LayoutParams) { role = ((LayoutParams) layoutParams).role; } } } public interface OnSlideStateChangeListener { /** * Invoked when slide state change * * @param slideState * {@link SlideMenu#STATE_CLOSE},{@link SlideMenu#STATE_OPEN} * ,{@link SlideMenu#STATE_DRAG}, * {@link SlideMenu#STATE_SCROLL} */ public void onSlideStateChange(int slideState); /** * Invoked when slide offset change * * @param offsetPercent * negative means slide left, otherwise slide right */ public void onSlideOffsetChange(float offsetPercent); } public interface OnContentTapListener { public void onContentTap(SlideMenu slideMenu); } }