From 8731a5db99cc055b7fc40ecbb967511202dce22c Mon Sep 17 00:00:00 2001 From: Vitaly Date: Sun, 23 Sep 2012 16:02:07 +0400 Subject: [PATCH] Bug fix: when you touch host panel when it is moving to the right (when the menu is opening) then host panel flies to the right side and then host panel can be seen only by half of width. --- .../src/org/kidfolk/flyinmenu/RootView.java | 1443 +++++++++-------- 1 file changed, 727 insertions(+), 716 deletions(-) diff --git a/library/src/org/kidfolk/flyinmenu/RootView.java b/library/src/org/kidfolk/flyinmenu/RootView.java index 13e0b16..8961daf 100644 --- a/library/src/org/kidfolk/flyinmenu/RootView.java +++ b/library/src/org/kidfolk/flyinmenu/RootView.java @@ -1,716 +1,727 @@ -package org.kidfolk.flyinmenu; - -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.Paint; -import android.graphics.drawable.Drawable; -import android.os.Handler; -import android.util.AttributeSet; -import android.util.Log; -import android.util.TypedValue; -import android.view.MotionEvent; -import android.view.VelocityTracker; -import android.view.View; -import android.view.ViewConfiguration; -import android.view.ViewGroup; -import android.view.animation.Interpolator; -import android.widget.OverScroller; - -/** - * 左侧菜单视图 - * - * @author 徐武进 - * - */ -public class RootView extends ViewGroup { - - private View mMenu;// 菜单 - private View mHost;// 内容 - private int mMenuId;// 菜单id - private int mHostId;// 内容id - private int mHostRemainWidth;// 菜单打开后host还保留的宽度 - private int mBezelSwipeWidth; - private int mScreenWidth;// 屏幕宽度 - private int mShadowWidth;// 屏幕高度 - - private OverScroller mScroller; - private VelocityTracker mVelocityTracker; - private int mState = MENU_CLOSED; - private static final int MENU_CLOSED = 1; - private static final int MENU_OPENED = 2; - private static final int MENU_DRAGGING = 4; - private static final int MENU_FLINGING = 8; - private boolean mGestureToRight = false;// 记录是否是向右的打开菜单手势 - - private Drawable mShadowDrawable; - private Paint mMenuOverlayPaint; - - private float mLastX; - private float mLastY; - private ViewConfiguration mViewConfig; - /** - * ID of the active pointer. This is used to retain consistency during - * drags/flings if multiple pointers are used. - */ - private int mActivePointerId = INVALID_POINTER_ID; - private float mMaximumVelocity; - private float mMinimumVelocity; - private int mMenuHeight; - private int mMenuWidth; - private int mHostWidth; - private int mHostHeight; - /** - * Sentinel value for no current active pointer. Used by - * {@link #mActivePointerId}. - */ - private static final int INVALID_POINTER_ID = -1; - - private static final float PARALLAX_SPEED_RATIO = 0.25f; - private static final int MAXIMUM_MENU_ALPHA_OVERLAY = 170; - - private static final int ANIMATION_FRAME_DURATION = 1000 / 60; - private static final int ANIMATION_DURATION = 500; - private static final int HOST_REMAIN_WIDTH = 44;// dp - private static final int BEZEL_SWIPE_WIDTH = 30;// dp - private static final int SHADOW_WIDTH = 3;// dp - private static final Interpolator sInterpolator = new Interpolator() { - - @Override - public float getInterpolation(float input) { - input -= 1.0f; - return input * input * input * input * input + 1.0f; - } - - }; - private static final String TAG = "RootView"; - - public RootView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - TypedArray a = context.obtainStyledAttributes(attrs, - R.styleable.RootView, defStyle, 0); - int menuId = a.getResourceId(R.styleable.RootView_menu, 0); - if (menuId == 0) { - throw new IllegalArgumentException( - "The menu attribute is required and must refer to a valid child."); - } - int hostId = a.getResourceId(R.styleable.RootView_host, 0); - if (hostId == 0) { - throw new IllegalArgumentException( - "The host attribute is required and must refer to a valid child."); - } - mMenuId = menuId; - mHostId = hostId; - mHostRemainWidth = (int) a.getDimension( - R.styleable.RootView_host_remain_width, HOST_REMAIN_WIDTH); - boolean open = a.getBoolean(R.styleable.RootView_open, false); - if (open) { - mState = MENU_OPENED; - } else { - mState = MENU_CLOSED; - } - a.recycle(); - - init(context); - } - - /** - * 初始化参数信息 - * - * @param context - */ - private void init(Context context) { - mBezelSwipeWidth = (int) TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, BEZEL_SWIPE_WIDTH, getResources() - .getDisplayMetrics()); - mShadowWidth = (int) TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, SHADOW_WIDTH, getResources() - .getDisplayMetrics()); - mViewConfig = ViewConfiguration.get(context); - mMaximumVelocity = mViewConfig.getScaledMaximumFlingVelocity(); - mMinimumVelocity = mViewConfig.getScaledMinimumFlingVelocity(); - Resources res = getResources(); - mScreenWidth = res.getDisplayMetrics().widthPixels; - mScroller = new OverScroller(context, sInterpolator); - mShadowDrawable = res.getDrawable(R.drawable.host_shadow); - mShadowDrawable.setBounds(0, 0, mShadowWidth, - res.getDisplayMetrics().heightPixels); - mMenuOverlayPaint = new Paint(); - } - - public RootView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public RootView(Context context) { - super(context); - init(context); - } - - @Override - protected void onFinishInflate() { - setMenu(mMenuId); - setHost(mHostId); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - int maxHeight = 0; - int maxWidth = 0; - - final View menu = mMenu; - final View host = mHost; - measureChildWithMargins(menu, widthMeasureSpec, mHostRemainWidth, - heightMeasureSpec, 0); - if (mMenuWidth * mMenuHeight == 0) { - mMenuWidth = menu.getMeasuredWidth(); - mMenuHeight = menu.getMeasuredHeight(); - } - maxWidth = Math.max(mMenuWidth, maxWidth); - maxHeight = Math.max(mMenuHeight, maxHeight); - - measureChildWithMargins(host, widthMeasureSpec, 0, heightMeasureSpec, 0); - if (mHostWidth * mHostHeight == 0) { - mHostWidth = host.getMeasuredWidth(); - mHostHeight = host.getMeasuredHeight(); - } - maxWidth = Math.max(mHostWidth, maxWidth); - maxHeight = Math.max(mHostHeight, maxHeight); - - maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight()); - maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth()); - - setMeasuredDimension( - MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.EXACTLY)); - - } - - @Override - protected void onLayout(boolean changed, int l, int t, int r, int b) { - final View menu = mMenu; - final int menuWidth = mMenuWidth; - final int menuHeight = mMenuHeight; - menu.layout(0, 0, menuWidth, menuHeight); - - final View host = mHost; - final int hostWidth = mHostWidth; - final int hostHeight = mHostHeight; - if (mState == MENU_OPENED) { - host.layout(menuWidth, 0, menuWidth + hostWidth, hostHeight); - } else { - host.layout(0, 0, hostWidth, hostHeight); - } - } - - @Override - protected void dispatchDraw(Canvas canvas) { - super.dispatchDraw(canvas); - - if (mState != MENU_CLOSED) { - // the menu is not closed.that means we can potentially see the host - // overlapping it.let's add a tiny gradient to indicate the host is - // sliding over the menu - canvas.save(); - canvas.translate(mHost.getLeft() - - mShadowDrawable.getBounds().right, 0); - mShadowDrawable.draw(canvas); - canvas.restore(); - - final int menuWidth = mMenuWidth; - if (menuWidth != 0) { - final float opennessRatio = (menuWidth - mHost.getLeft()) - / (float) menuWidth; - - // we also draw an overlay over the menu indicating the menu is - // in the process of being visible or invisible - drawMenuOverlay(canvas, opennessRatio); - - // finally we draw an arrow indicating the feature we are - // currently int - drawMenuArrow(canvas, opennessRatio); - - // offset menu to implement the parallax effect - mMenu.offsetLeftAndRight((int) (-opennessRatio * menuWidth * PARALLAX_SPEED_RATIO) - - mMenu.getLeft()); - } - } - } - - public interface OnDrawMenuArrowListener { - public void onDrawMenuArrow(Canvas canvas, float opennessRatio); - } - - private OnDrawMenuArrowListener mOnDrawMenuArrowListener; - - public void setOnDrawMenuArrowListener(OnDrawMenuArrowListener listener) { - this.mOnDrawMenuArrowListener = listener; - } - - private void drawMenuArrow(Canvas canvas, float opennessRatio) { - if (mOnDrawMenuArrowListener != null) { - mOnDrawMenuArrowListener.onDrawMenuArrow(canvas, opennessRatio); - } - } - - private void drawMenuOverlay(Canvas canvas, float opennessRatio) { - final Paint menuOverlayPaint = mMenuOverlayPaint; - final int alpha = (int) (MAXIMUM_MENU_ALPHA_OVERLAY * opennessRatio); - if (alpha > 0) { - menuOverlayPaint.setColor(Color.argb(alpha, 0, 0, 0)); - canvas.drawRect(0, 0, mHost.getLeft(), getHeight(), - menuOverlayPaint); - } - } - - public interface OnMenuOpenListener { - public boolean ignoreOpen(MotionEvent event); - } - - public interface OnMenuCloseListener { - - } - - public OnMenuOpenListener mMenuOpenListener; - - public void setOnMenuOpenListener(OnMenuOpenListener listener) { - this.mMenuOpenListener = listener; - } - - /** - * when can a gesture be considered as a swipe - */ - @Override - public boolean onInterceptTouchEvent(MotionEvent ev) { - /** - * This method just determines whether we want to intercept the motion. - * If we return true.onTouchEvent will be called and we do the actual - * scrolling there - */ - if (!isEnabled()) { - return false; - } - if (isFlinging()) { - return false; - } - - final int action = ev.getAction() & MotionEvent.ACTION_MASK; - - switch (action) { - case MotionEvent.ACTION_DOWN: - // Log.d(TAG, "onInterceptTouchEvent: ACTION_DOWN"); - mLastX = ev.getX(); - mLastY = ev.getY(); - mActivePointerId = ev.getPointerId(0); - break; - case MotionEvent.ACTION_MOVE: - // Log.d(TAG, "onInterceptTouchEvent: ACTION_MOVE"); - if (mActivePointerId != INVALID_POINTER_ID) { - final int pointerIndex = ev.findPointerIndex(mActivePointerId); - final float x = ev.getX(pointerIndex); - final float y = ev.getY(pointerIndex); - - float xDiff = Math.abs(x - mLastX); - float yDiff = Math.abs(y - mLastY); - double distance = Math.hypot(xDiff, yDiff); - - if (distance > mViewConfig.getScaledTouchSlop() - && (x - mLastX) > 0 - && distance > mViewConfig.getScaledPagingTouchSlop() - && mState == MENU_CLOSED && xDiff > yDiff) { - // open gesture - if (this.mMenuOpenListener != null) { - if (this.mMenuOpenListener.ignoreOpen(ev)) { - - } else { - mState |= MENU_DRAGGING; - mLastX = x; - mLastY = y; - } - } else { - mState |= MENU_DRAGGING; - mLastX = x; - mLastY = y; - } - } else if (mLastX >= mScreenWidth - mHostRemainWidth - && (mLastX - x) > 0 - && distance > mViewConfig.getScaledTouchSlop() - && distance > mViewConfig.getScaledPagingTouchSlop() - && mState == MENU_OPENED && xDiff > yDiff) { - // close gesture - mState |= MENU_DRAGGING; - mLastX = x; - mLastY = y; - } - } - break; - case MotionEvent.ACTION_CANCEL: - // Log.d(TAG, "onInterceptTouchEvent: ACTION_CANCEL"); - break; - - case MotionEvent.ACTION_POINTER_UP: - // Log.d(TAG, "onInterceptTouchEvent: ACTION_POINTER_UP"); - onSecondaryPointerUp(ev); - break; - } - - if (mVelocityTracker == null) { - mVelocityTracker = VelocityTracker.obtain(); - } - mVelocityTracker.addMovement(ev); - - return isDragging(); - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - if (!isEnabled()) { - return false; - } - if (isFlinging()) { - return true; - } - if (mVelocityTracker == null) { - mVelocityTracker = VelocityTracker.obtain(); - } - mVelocityTracker.addMovement(event); - final int action = event.getAction() & MotionEvent.ACTION_MASK; - switch (action) { - case MotionEvent.ACTION_MOVE: { - if (mActivePointerId != INVALID_POINTER_ID) { - final int pointerIndex = event - .findPointerIndex(mActivePointerId); - final float x = event.getX(pointerIndex); - final float y = event.getY(pointerIndex); - if (isDragging()) { - float distance = x - mLastX; - int right = mHost.getRight(); - if (right + distance < mHostWidth) { - // 修正host左边界移出屏幕范围 - distance = mHostWidth - right; - } - int left = mHost.getLeft(); - if (left + distance > mMenuWidth) { - // 修正menu右边界超过menu的宽度 - distance = mMenuWidth - left; - } - - // 记录最终手势方向 - if (distance > 0) { - // user want open - mGestureToRight = true; - } else { - // user want close - mGestureToRight = false; - } - - mHost.offsetLeftAndRight((int) distance); - postInvalidate(); - } - mLastX = x; - mLastY = y; - } - break; - } - case MotionEvent.ACTION_UP: { - // Log.d(TAG, "onTouchEvent: ACTION_UP"); - doActionUpJustWithGesutureDirection(event); - mActivePointerId = INVALID_POINTER_ID; - break; - } - case MotionEvent.ACTION_DOWN: - // Log.d(TAG, "onTouchEvent: ACTION_DOWN"); - mLastX = event.getX(); - mLastY = event.getY(); - mActivePointerId = event.getPointerId(0); - return true; - case MotionEvent.ACTION_CANCEL: - // Log.d(TAG, "onTouchEvent: ACTION_CANCEL"); - mActivePointerId = INVALID_POINTER_ID; - break; - case MotionEvent.ACTION_POINTER_DOWN: { - // Log.d(TAG, "onTouchEvent: ACTION_POINTER_DOWN"); - break; - } - case MotionEvent.ACTION_POINTER_UP: { - // Log.d(TAG, "onTouchEvent: ACTION_POINTER_UP"); - onSecondaryPointerUp(event); - break; - } - } - return super.onTouchEvent(event); - } - - private void onSecondaryPointerUp(MotionEvent event) { - final int pointerIndex = event.getActionIndex(); - final int pointerId = event.getPointerId(pointerIndex); - if (pointerId == mActivePointerId) { - // this is our active pointer going up.choose a new - // active pointer an adjust accordingly. - // Log.d(TAG, "active pointer going up"); - final int newPointerIndex = pointerIndex == 0 ? 1 : 0; - mLastX = event.getX(newPointerIndex); - mLastY = event.getY(newPointerIndex); - mActivePointerId = event.getPointerId(newPointerIndex); - if (mVelocityTracker != null) { - mVelocityTracker.clear(); - } - } - } - - @SuppressWarnings("unused") - private void doActionUpWithVelocityAndThreshold(MotionEvent event) { - if ((mState & MENU_DRAGGING) != 0) { - final VelocityTracker velocityTracker = mVelocityTracker; - velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); - float xVelocity = Math.abs(velocityTracker.getXVelocity()); - if (xVelocity > mMinimumVelocity) { - // velocity is greater than the minimum velocity - if ((mState & MENU_CLOSED) != 0) { - // open - startScroll(0, 0, - -(mMenu.getMeasuredWidth() - mHost.getLeft()), 0); - } else if ((mState & MENU_OPENED) != 0) { - // close - startScroll(0, 0, mHost.getLeft(), 0); - } - } else { - final int pointerIndex = event - .findPointerIndex(mActivePointerId); - final float x = event.getX(pointerIndex); - float diff = Math.abs(x - mLastX); - if (diff > mMenu.getMeasuredWidth() / 2) { - if ((mState & MENU_CLOSED) != 0) { - // open - startScroll(0, 0, - -(mMenu.getMeasuredWidth() - mHost.getLeft()), - 0); - } else if ((mState & MENU_OPENED) != 0) { - // close - startScroll(0, 0, mHost.getLeft(), 0); - } - } else { - if ((mState & MENU_CLOSED) != 0) { - // close - startScroll(0, 0, mHost.getLeft(), 0); - } else if ((mState & MENU_OPENED) != 0) { - // open - startScroll(0, 0, - -(mMenu.getMeasuredWidth() - mHost.getLeft()), - 0); - } - } - } - } - mActivePointerId = INVALID_POINTER_ID; - } - - private void doActionUpJustWithGesutureDirection(MotionEvent event) { - if (mGestureToRight) { - startScroll(0, 0, -(mMenuWidth - mHost.getLeft()), 0); - } else { - startScroll(0, 0, mHost.getLeft(), 0); - } - } - - private void startScroll(int startX, int startY, int dx, int dy) { - mScroller.startScroll(startX, startY, dx, dy, ANIMATION_DURATION); - mHandler.post(new Scrolling(dx > 0 ? false : true)); - } - - public void animateClose() { - if ((mState & MENU_CLOSED) != 0 || (mState & MENU_FLINGING) != 0) - return; - this.post(new Runnable() { - - @Override - public void run() { - startScroll(0, 0, mMenuWidth, 0); - } - }); - - } - - public void animateOpen() { - if ((mState & MENU_OPENED) != 0 || (mState & MENU_FLINGING) != 0) - return; - this.post(new Runnable() { - - @Override - public void run() { - startScroll(0, 0, -mMenuWidth, 0); - } - }); - - } - - public void animateToggle() { - if ((mState & MENU_OPENED) != 0) { - animateClose(); - } else if ((mState & MENU_CLOSED) != 0) { - animateOpen(); - } - } - - public void close() { - if (mState == MENU_CLOSED) - return; - mHost.offsetLeftAndRight(-mMenuWidth); - mState = MENU_CLOSED; - } - - public void open() { - if (mState == MENU_OPENED) - return; - mHost.offsetLeftAndRight(mMenuWidth); - mState = MENU_OPENED; - } - - public void toggle() { - if (mState == MENU_OPENED) { - close(); - } else if (mState == MENU_CLOSED) { - open(); - } - } - - public boolean isOpened() { - return mState == MENU_OPENED; - } - - public boolean isDragging() { - return (mState & MENU_DRAGGING) != 0; - } - - public boolean isFlinging() { - return (mState & MENU_FLINGING) != 0; - } - - private Handler mHandler = new Handler(); - - class Scrolling implements Runnable { - private int lastX; - boolean open; - - Scrolling(boolean open) { - this.open = open; - } - - @Override - public void run() { - boolean more = mScroller.computeScrollOffset(); - if (more) { - int x = mScroller.getCurrX(); - int diff = x - lastX; - Log.d(TAG, "diff: " + diff + ",lastX: " + lastX); - if (diff != 0) { - if (isDragging()) { - // 如果当前视图正在拖动,则当用户松手之后重置拖动状态 - mState ^= MENU_DRAGGING; - } - if (!isFlinging()) { - mState |= MENU_FLINGING; - } - if (isFlinging()) { - mHost.offsetLeftAndRight(-diff); - lastX = x; - postInvalidate(); - } - } - mHandler.postDelayed(this, ANIMATION_FRAME_DURATION); - } else { - mHandler.removeCallbacks(this); - if (open) { - mState = MENU_OPENED; - } else { - mState = MENU_CLOSED; - } - } - - } - - } - - @Override - public LayoutParams generateLayoutParams(AttributeSet attrs) { - return new RootView.LayoutParams(getContext(), attrs); - } - - @Override - protected boolean checkLayoutParams(android.view.ViewGroup.LayoutParams p) { - return p instanceof LayoutParams; - } - - @Override - protected LayoutParams generateDefaultLayoutParams() { - return new LayoutParams(LayoutParams.MATCH_PARENT, - LayoutParams.MATCH_PARENT); - } - - public static class LayoutParams extends MarginLayoutParams { - - public LayoutParams(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public LayoutParams(android.view.ViewGroup.LayoutParams source) { - super(source); - } - - public LayoutParams(int width, int height) { - super(width, height); - } - - } - - /** - * 设置菜单视图 - * - * @param menuId - * 菜单视图的id - */ - public void setMenu(int menuId) { - if (menuId == 0) { - throw new IllegalArgumentException( - "The menu attribute is required and must refer to a valid child."); - } - mMenuId = menuId; - mMenu = findViewById(menuId); - if (mMenu == null) { - throw new IllegalArgumentException( - "The menu attribute is must refer to an existing child."); - } - } - - /** - * 设置内容视图 - * - * @param hostId - * 内容视图的id - */ - public void setHost(int hostId) { - if (hostId == 0) { - throw new IllegalArgumentException( - "The host attribute is required and must refer to a valid child."); - } - mHostId = hostId; - mHost = findViewById(hostId); - if (mHost == null) { - throw new IllegalArgumentException( - "The host attribute is must refer to an existing child."); - } - } - - public void setMenu(View menu) { - mMenuId = menu.getId(); - mMenu = menu; - } - - public void setHost(View host) { - mHostId = host.getId(); - mHost = host; - } - -} +package org.kidfolk.flyinmenu; + +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.Paint; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.util.AttributeSet; +import android.util.Log; +import android.util.TypedValue; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.animation.Interpolator; +import android.widget.OverScroller; + +/** + * 左侧菜单视图 + * + * @author 徐武进 + * + */ +public class RootView extends ViewGroup { + + private View mMenu;// 菜单 + private View mHost;// 内容 + private int mMenuId;// 菜单id + private int mHostId;// 内容id + private int mHostRemainWidth;// 菜单打开后host还保留的宽度 + private int mBezelSwipeWidth; + private int mScreenWidth;// 屏幕宽度 + private int mShadowWidth;// 屏幕高度 + + private OverScroller mScroller; + private VelocityTracker mVelocityTracker; + private int mState = MENU_CLOSED; + private static final int MENU_CLOSED = 1; + private static final int MENU_OPENED = 2; + private static final int MENU_DRAGGING = 4; + private static final int MENU_FLINGING = 8; + private boolean mGestureToRight = false;// 记录是否是向右的打开菜单手势 + + private Drawable mShadowDrawable; + private Paint mMenuOverlayPaint; + + private float mLastX; + private float mLastY; + private ViewConfiguration mViewConfig; + /** + * ID of the active pointer. This is used to retain consistency during + * drags/flings if multiple pointers are used. + */ + private int mActivePointerId = INVALID_POINTER_ID; + private float mMaximumVelocity; + private float mMinimumVelocity; + private int mMenuHeight; + private int mMenuWidth; + private int mHostWidth; + private int mHostHeight; + /** + * Sentinel value for no current active pointer. Used by + * {@link #mActivePointerId}. + */ + private static final int INVALID_POINTER_ID = -1; + + private static final float PARALLAX_SPEED_RATIO = 0.25f; + private static final int MAXIMUM_MENU_ALPHA_OVERLAY = 170; + + private static final int ANIMATION_FRAME_DURATION = 1000 / 60; + private static final int ANIMATION_DURATION = 500; + private static final int HOST_REMAIN_WIDTH = 44;// dp + private static final int BEZEL_SWIPE_WIDTH = 30;// dp + private static final int SHADOW_WIDTH = 3;// dp + private static final Interpolator sInterpolator = new Interpolator() { + + @Override + public float getInterpolation(float input) { + input -= 1.0f; + return input * input * input * input * input + 1.0f; + } + + }; + private static final String TAG = "RootView"; + + public RootView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + TypedArray a = context.obtainStyledAttributes(attrs, + R.styleable.RootView, defStyle, 0); + int menuId = a.getResourceId(R.styleable.RootView_menu, 0); + if (menuId == 0) { + throw new IllegalArgumentException( + "The menu attribute is required and must refer to a valid child."); + } + int hostId = a.getResourceId(R.styleable.RootView_host, 0); + if (hostId == 0) { + throw new IllegalArgumentException( + "The host attribute is required and must refer to a valid child."); + } + mMenuId = menuId; + mHostId = hostId; + mHostRemainWidth = (int) a.getDimension( + R.styleable.RootView_host_remain_width, HOST_REMAIN_WIDTH); + boolean open = a.getBoolean(R.styleable.RootView_open, false); + if (open) { + mState = MENU_OPENED; + } else { + mState = MENU_CLOSED; + } + a.recycle(); + + init(context); + } + + /** + * 初始化参数信息 + * + * @param context + */ + private void init(Context context) { + mBezelSwipeWidth = (int) TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, BEZEL_SWIPE_WIDTH, getResources() + .getDisplayMetrics()); + mShadowWidth = (int) TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, SHADOW_WIDTH, getResources() + .getDisplayMetrics()); + mViewConfig = ViewConfiguration.get(context); + mMaximumVelocity = mViewConfig.getScaledMaximumFlingVelocity(); + mMinimumVelocity = mViewConfig.getScaledMinimumFlingVelocity(); + Resources res = getResources(); + mScreenWidth = res.getDisplayMetrics().widthPixels; + mScroller = new OverScroller(context, sInterpolator); + mShadowDrawable = res.getDrawable(R.drawable.host_shadow); + mShadowDrawable.setBounds(0, 0, mShadowWidth, + res.getDisplayMetrics().heightPixels); + mMenuOverlayPaint = new Paint(); + } + + public RootView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public RootView(Context context) { + super(context); + init(context); + } + + @Override + protected void onFinishInflate() { + setMenu(mMenuId); + setHost(mHostId); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int maxHeight = 0; + int maxWidth = 0; + + final View menu = mMenu; + final View host = mHost; + measureChildWithMargins(menu, widthMeasureSpec, mHostRemainWidth, + heightMeasureSpec, 0); + if (mMenuWidth * mMenuHeight == 0) { + mMenuWidth = menu.getMeasuredWidth(); + mMenuHeight = menu.getMeasuredHeight(); + } + maxWidth = Math.max(mMenuWidth, maxWidth); + maxHeight = Math.max(mMenuHeight, maxHeight); + + measureChildWithMargins(host, widthMeasureSpec, 0, heightMeasureSpec, 0); + if (mHostWidth * mHostHeight == 0) { + mHostWidth = host.getMeasuredWidth(); + mHostHeight = host.getMeasuredHeight(); + } + maxWidth = Math.max(mHostWidth, maxWidth); + maxHeight = Math.max(mHostHeight, maxHeight); + + maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight()); + maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth()); + + setMeasuredDimension( + MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.EXACTLY)); + + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + final View menu = mMenu; + final int menuWidth = mMenuWidth; + final int menuHeight = mMenuHeight; + menu.layout(0, 0, menuWidth, menuHeight); + + final View host = mHost; + final int hostWidth = mHostWidth; + final int hostHeight = mHostHeight; + if (mState == MENU_OPENED) { + host.layout(menuWidth, 0, menuWidth + hostWidth, hostHeight); + } else { + host.layout(0, 0, hostWidth, hostHeight); + } + } + + @Override + protected void dispatchDraw(Canvas canvas) { + super.dispatchDraw(canvas); + + if (mState != MENU_CLOSED) { + // the menu is not closed.that means we can potentially see the host + // overlapping it.let's add a tiny gradient to indicate the host is + // sliding over the menu + canvas.save(); + canvas.translate(mHost.getLeft() + - mShadowDrawable.getBounds().right, 0); + mShadowDrawable.draw(canvas); + canvas.restore(); + + final int menuWidth = mMenuWidth; + if (menuWidth != 0) { + final float opennessRatio = (menuWidth - mHost.getLeft()) + / (float) menuWidth; + + // we also draw an overlay over the menu indicating the menu is + // in the process of being visible or invisible + drawMenuOverlay(canvas, opennessRatio); + + // finally we draw an arrow indicating the feature we are + // currently int + drawMenuArrow(canvas, opennessRatio); + + // offset menu to implement the parallax effect + mMenu.offsetLeftAndRight((int) (-opennessRatio * menuWidth * PARALLAX_SPEED_RATIO) + - mMenu.getLeft()); + } + } + } + + public interface OnDrawMenuArrowListener { + public void onDrawMenuArrow(Canvas canvas, float opennessRatio); + } + + private OnDrawMenuArrowListener mOnDrawMenuArrowListener; + + public void setOnDrawMenuArrowListener(OnDrawMenuArrowListener listener) { + this.mOnDrawMenuArrowListener = listener; + } + + private void drawMenuArrow(Canvas canvas, float opennessRatio) { + if (mOnDrawMenuArrowListener != null) { + mOnDrawMenuArrowListener.onDrawMenuArrow(canvas, opennessRatio); + } + } + + private void drawMenuOverlay(Canvas canvas, float opennessRatio) { + final Paint menuOverlayPaint = mMenuOverlayPaint; + final int alpha = (int) (MAXIMUM_MENU_ALPHA_OVERLAY * opennessRatio); + if (alpha > 0) { + menuOverlayPaint.setColor(Color.argb(alpha, 0, 0, 0)); + canvas.drawRect(0, 0, mHost.getLeft(), getHeight(), + menuOverlayPaint); + } + } + + public interface OnMenuOpenListener { + public boolean ignoreOpen(MotionEvent event); + } + + public interface OnMenuCloseListener { + + } + + public OnMenuOpenListener mMenuOpenListener; + + public void setOnMenuOpenListener(OnMenuOpenListener listener) { + this.mMenuOpenListener = listener; + } + + /** + * when can a gesture be considered as a swipe + */ + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + /** + * This method just determines whether we want to intercept the motion. + * If we return true.onTouchEvent will be called and we do the actual + * scrolling there + */ + if (!isEnabled()) { + return false; + } + if (isFlinging()) { + return false; + } + + final int action = ev.getAction() & MotionEvent.ACTION_MASK; + + switch (action) { + case MotionEvent.ACTION_DOWN: + // Log.d(TAG, "onInterceptTouchEvent: ACTION_DOWN"); + mLastX = ev.getX(); + mLastY = ev.getY(); + mActivePointerId = ev.getPointerId(0); + break; + case MotionEvent.ACTION_MOVE: + // Log.d(TAG, "onInterceptTouchEvent: ACTION_MOVE"); + if (mActivePointerId != INVALID_POINTER_ID) { + final int pointerIndex = ev.findPointerIndex(mActivePointerId); + final float x = ev.getX(pointerIndex); + final float y = ev.getY(pointerIndex); + + float xDiff = Math.abs(x - mLastX); + float yDiff = Math.abs(y - mLastY); + double distance = Math.hypot(xDiff, yDiff); + + if (distance > mViewConfig.getScaledTouchSlop() + && (x - mLastX) > 0 + && distance > mViewConfig.getScaledPagingTouchSlop() + && mState == MENU_CLOSED && xDiff > yDiff) { + // open gesture + if (this.mMenuOpenListener != null) { + if (this.mMenuOpenListener.ignoreOpen(ev)) { + + } else { + mState |= MENU_DRAGGING; + mLastX = x; + mLastY = y; + } + } else { + mState |= MENU_DRAGGING; + mLastX = x; + mLastY = y; + } + } else if (mLastX >= mScreenWidth - mHostRemainWidth + && (mLastX - x) > 0 + && distance > mViewConfig.getScaledTouchSlop() + && distance > mViewConfig.getScaledPagingTouchSlop() + && mState == MENU_OPENED && xDiff > yDiff) { + // close gesture + mState |= MENU_DRAGGING; + mLastX = x; + mLastY = y; + } + } + break; + case MotionEvent.ACTION_CANCEL: + // Log.d(TAG, "onInterceptTouchEvent: ACTION_CANCEL"); + break; + + case MotionEvent.ACTION_POINTER_UP: + // Log.d(TAG, "onInterceptTouchEvent: ACTION_POINTER_UP"); + onSecondaryPointerUp(ev); + break; + } + + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(ev); + + return isDragging(); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (!isEnabled()) { + return false; + } + if (isFlinging()) { + return true; + } + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(event); + final int action = event.getAction() & MotionEvent.ACTION_MASK; + switch (action) { + case MotionEvent.ACTION_MOVE: { + if (mActivePointerId != INVALID_POINTER_ID) { + final int pointerIndex = event + .findPointerIndex(mActivePointerId); + final float x = event.getX(pointerIndex); + final float y = event.getY(pointerIndex); + if (isDragging()) { + float distance = x - mLastX; + int right = mHost.getRight(); + if (right + distance < mHostWidth) { + // 修正host左边界移出屏幕范围 + distance = mHostWidth - right; + } + int left = mHost.getLeft(); + if (left + distance > mMenuWidth) { + // 修正menu右边界超过menu的宽度 + distance = mMenuWidth - left; + } + + // 记录最终手势方向 + if (distance > 0) { + // user want open + mGestureToRight = true; + } else { + // user want close + mGestureToRight = false; + } + + mHost.offsetLeftAndRight((int) distance); + postInvalidate(); + } + mLastX = x; + mLastY = y; + } + break; + } + case MotionEvent.ACTION_UP: { + // Log.d(TAG, "onTouchEvent: ACTION_UP"); + doActionUpJustWithGesutureDirection(event); + mActivePointerId = INVALID_POINTER_ID; + break; + } + case MotionEvent.ACTION_DOWN: + // Log.d(TAG, "onTouchEvent: ACTION_DOWN"); + mLastX = event.getX(); + mLastY = event.getY(); + mActivePointerId = event.getPointerId(0); + return true; + case MotionEvent.ACTION_CANCEL: + // Log.d(TAG, "onTouchEvent: ACTION_CANCEL"); + mActivePointerId = INVALID_POINTER_ID; + break; + case MotionEvent.ACTION_POINTER_DOWN: { + // Log.d(TAG, "onTouchEvent: ACTION_POINTER_DOWN"); + break; + } + case MotionEvent.ACTION_POINTER_UP: { + // Log.d(TAG, "onTouchEvent: ACTION_POINTER_UP"); + onSecondaryPointerUp(event); + break; + } + } + return super.onTouchEvent(event); + } + + private void onSecondaryPointerUp(MotionEvent event) { + final int pointerIndex = event.getActionIndex(); + final int pointerId = event.getPointerId(pointerIndex); + if (pointerId == mActivePointerId) { + // this is our active pointer going up.choose a new + // active pointer an adjust accordingly. + // Log.d(TAG, "active pointer going up"); + final int newPointerIndex = pointerIndex == 0 ? 1 : 0; + mLastX = event.getX(newPointerIndex); + mLastY = event.getY(newPointerIndex); + mActivePointerId = event.getPointerId(newPointerIndex); + if (mVelocityTracker != null) { + mVelocityTracker.clear(); + } + } + } + + @SuppressWarnings("unused") + private void doActionUpWithVelocityAndThreshold(MotionEvent event) { + if ((mState & MENU_DRAGGING) != 0) { + final VelocityTracker velocityTracker = mVelocityTracker; + velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + float xVelocity = Math.abs(velocityTracker.getXVelocity()); + if (xVelocity > mMinimumVelocity) { + // velocity is greater than the minimum velocity + if ((mState & MENU_CLOSED) != 0) { + // open + startScroll(0, 0, + -(mMenu.getMeasuredWidth() - mHost.getLeft()), 0); + } else if ((mState & MENU_OPENED) != 0) { + // close + startScroll(0, 0, mHost.getLeft(), 0); + } + } else { + final int pointerIndex = event + .findPointerIndex(mActivePointerId); + final float x = event.getX(pointerIndex); + float diff = Math.abs(x - mLastX); + if (diff > mMenu.getMeasuredWidth() / 2) { + if ((mState & MENU_CLOSED) != 0) { + // open + startScroll(0, 0, + -(mMenu.getMeasuredWidth() - mHost.getLeft()), + 0); + } else if ((mState & MENU_OPENED) != 0) { + // close + startScroll(0, 0, mHost.getLeft(), 0); + } + } else { + if ((mState & MENU_CLOSED) != 0) { + // close + startScroll(0, 0, mHost.getLeft(), 0); + } else if ((mState & MENU_OPENED) != 0) { + // open + startScroll(0, 0, + -(mMenu.getMeasuredWidth() - mHost.getLeft()), + 0); + } + } + } + } + mActivePointerId = INVALID_POINTER_ID; + } + + private void doActionUpJustWithGesutureDirection(MotionEvent event) { + if (mGestureToRight) { + startScroll(0, 0, -(mMenuWidth - mHost.getLeft()), 0); + } else { + startScroll(0, 0, mHost.getLeft(), 0); + } + } + + private void startScroll(int startX, int startY, int dx, int dy) { + mScroller.startScroll(startX, startY, dx, dy, ANIMATION_DURATION); + mHandler.post(new Scrolling(dx > 0 ? false : true)); + } + + public void animateClose() { + if ((mState & MENU_CLOSED) != 0 || (mState & MENU_FLINGING) != 0) + return; + this.post(new Runnable() { + + @Override + public void run() { + startScroll(0, 0, mMenuWidth, 0); + } + }); + + } + + public void animateOpen() { + if ((mState & MENU_OPENED) != 0 || (mState & MENU_FLINGING) != 0) + return; + this.post(new Runnable() { + + @Override + public void run() { + startScroll(0, 0, -mMenuWidth, 0); + } + }); + + } + + public void animateToggle() { + if ((mState & MENU_OPENED) != 0) { + animateClose(); + } else if ((mState & MENU_CLOSED) != 0) { + animateOpen(); + } + } + + public void close() { + if (mState == MENU_CLOSED) + return; + mHost.offsetLeftAndRight(-mMenuWidth); + mState = MENU_CLOSED; + } + + public void open() { + if (mState == MENU_OPENED) + return; + mHost.offsetLeftAndRight(mMenuWidth); + mState = MENU_OPENED; + } + + public void toggle() { + if (mState == MENU_OPENED) { + close(); + } else if (mState == MENU_CLOSED) { + open(); + } + } + + public boolean isOpened() { + return mState == MENU_OPENED; + } + + public boolean isDragging() { + return (mState & MENU_DRAGGING) != 0; + } + + public boolean isFlinging() { + return (mState & MENU_FLINGING) != 0; + } + + private Handler mHandler = new Handler(); + + class Scrolling implements Runnable { + private int lastX; + boolean open; + + Scrolling(boolean open) { + this.open = open; + } + + @Override + public void run() { + boolean more = mScroller.computeScrollOffset(); + if (more) { + int x = mScroller.getCurrX(); + int diff = x - lastX; + Log.d(TAG, "diff: " + diff + ",lastX: " + lastX); + + if (mHost.getLeft() > mMenuWidth || mHost.getLeft() < 0){ + mHandler.removeCallbacks(this); + if (open) { + mState = MENU_OPENED; + } else { + mState = MENU_CLOSED; + } + return; + } + + if (diff != 0) { + if (isDragging()) { + // 如果当前视图正在拖动,则当用户松手之后重置拖动状态 + mState ^= MENU_DRAGGING; + } + if (!isFlinging()) { + mState |= MENU_FLINGING; + } + if (isFlinging()) { + mHost.offsetLeftAndRight(-diff); + lastX = x; + postInvalidate(); + } + } + mHandler.postDelayed(this, ANIMATION_FRAME_DURATION); + } else { + mHandler.removeCallbacks(this); + if (open) { + mState = MENU_OPENED; + } else { + mState = MENU_CLOSED; + } + } + + } + + } + + @Override + public LayoutParams generateLayoutParams(AttributeSet attrs) { + return new RootView.LayoutParams(getContext(), attrs); + } + + @Override + protected boolean checkLayoutParams(android.view.ViewGroup.LayoutParams p) { + return p instanceof LayoutParams; + } + + @Override + protected LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(LayoutParams.MATCH_PARENT, + LayoutParams.MATCH_PARENT); + } + + public static class LayoutParams extends MarginLayoutParams { + + public LayoutParams(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public LayoutParams(android.view.ViewGroup.LayoutParams source) { + super(source); + } + + public LayoutParams(int width, int height) { + super(width, height); + } + + } + + /** + * 设置菜单视图 + * + * @param menuId + * 菜单视图的id + */ + public void setMenu(int menuId) { + if (menuId == 0) { + throw new IllegalArgumentException( + "The menu attribute is required and must refer to a valid child."); + } + mMenuId = menuId; + mMenu = findViewById(menuId); + if (mMenu == null) { + throw new IllegalArgumentException( + "The menu attribute is must refer to an existing child."); + } + } + + /** + * 设置内容视图 + * + * @param hostId + * 内容视图的id + */ + public void setHost(int hostId) { + if (hostId == 0) { + throw new IllegalArgumentException( + "The host attribute is required and must refer to a valid child."); + } + mHostId = hostId; + mHost = findViewById(hostId); + if (mHost == null) { + throw new IllegalArgumentException( + "The host attribute is must refer to an existing child."); + } + } + + public void setMenu(View menu) { + mMenuId = menu.getId(); + mMenu = menu; + } + + public void setHost(View host) { + mHostId = host.getId(); + mHost = host; + } + +}