Created
March 25, 2019 10:35
-
-
Save mohamed-khaled-hsn/a1f9be232e4e1836b6bfae5584ba8011 to your computer and use it in GitHub Desktop.
Swipe to reveal layout, to be used as an item viewType in RecyclerView or ListView
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class SwipeRevealLayout extends ViewGroup { | |
// These states are used only for ViewBindHelper | |
protected static final int STATE_CLOSE = 0; | |
protected static final int STATE_CLOSING = 1; | |
protected static final int STATE_OPEN = 2; | |
protected static final int STATE_OPENING = 3; | |
protected static final int STATE_DRAGGING = 4; | |
private static final int DEFAULT_MIN_FLING_VELOCITY = 300; // dp per second | |
private static final int DEFAULT_MIN_DIST_REQUEST_DISALLOW_PARENT = 1; // dp | |
public static final int DRAG_EDGE_LEFT = 0x1; | |
public static final int DRAG_EDGE_RIGHT = 0x1 << 1; | |
public static final int DRAG_EDGE_TOP = 0x1 << 2; | |
public static final int DRAG_EDGE_BOTTOM = 0x1 << 3; | |
/** | |
* The secondary view will be under the main view. | |
*/ | |
public static final int MODE_NORMAL = 0; | |
/** | |
* The secondary view will stick the edge of the main view. | |
*/ | |
public static final int MODE_SAME_LEVEL = 1; | |
/** | |
* Main view is the view which is shown when the layout is closed. | |
*/ | |
private View mMainView; | |
/** | |
* Secondary view is the view which is shown when the layout is opened. | |
*/ | |
private View mSecondaryView; | |
/** | |
* The rectangle position of the main view when the layout is closed. | |
*/ | |
private Rect mRectMainClose = new Rect(); | |
/** | |
* The rectangle position of the main view when the layout is opened. | |
*/ | |
private Rect mRectMainOpen = new Rect(); | |
/** | |
* The rectangle position of the secondary view when the layout is closed. | |
*/ | |
private Rect mRectSecClose = new Rect(); | |
/** | |
* The rectangle position of the secondary view when the layout is opened. | |
*/ | |
private Rect mRectSecOpen = new Rect(); | |
/** | |
* The minimum distance (px) to the closest drag edge that the SwipeRevealLayout | |
* will disallow the parent to intercept touch event. | |
*/ | |
private int mMinDistRequestDisallowParent = 0; | |
private boolean mIsOpenBeforeInit = false; | |
private volatile boolean mAborted = false; | |
private volatile boolean mIsScrolling = false; | |
private volatile boolean mLockDrag = false; | |
private int mMinFlingVelocity = DEFAULT_MIN_FLING_VELOCITY; | |
private int mState = STATE_CLOSE; | |
private int mMode = MODE_NORMAL; | |
private int mLastMainLeft = 0; | |
private int mLastMainTop = 0; | |
private int mDragEdge = DRAG_EDGE_LEFT; | |
private float mDragDist = 0; | |
private float mPrevX = -1; | |
private float mPrevY = -1; | |
private ViewDragHelper mDragHelper; | |
private GestureDetectorCompat mGestureDetector; | |
private DragStateChangeListener mDragStateChangeListener; // only used for ViewBindHelper | |
private SwipeListener mSwipeListener; | |
private int mOnLayoutCount = 0; | |
interface DragStateChangeListener { | |
void onDragStateChanged(int state); | |
} | |
/** | |
* Listener for monitoring events about swipe layout. | |
*/ | |
public interface SwipeListener { | |
/** | |
* Called when the main view becomes completely closed. | |
*/ | |
void onClosed(SwipeRevealLayout view); | |
/** | |
* Called when the main view becomes completely opened. | |
*/ | |
void onOpened(SwipeRevealLayout view); | |
/** | |
* Called when the main view's position changes. | |
* @param slideOffset The new offset of the main view within its range, from 0-1 | |
*/ | |
void onSlide(SwipeRevealLayout view, float slideOffset); | |
} | |
/** | |
* No-op stub for {@link SwipeListener}. If you only want ot implement a subset | |
* of the listener methods, you can extend this instead of implement the full interface. | |
*/ | |
public static class SimpleSwipeListener implements SwipeListener { | |
@Override | |
public void onClosed(SwipeRevealLayout view) {} | |
@Override | |
public void onOpened(SwipeRevealLayout view) {} | |
@Override | |
public void onSlide(SwipeRevealLayout view, float slideOffset) {} | |
} | |
public SwipeRevealLayout(Context context) { | |
super(context); | |
init(context, null); | |
} | |
public SwipeRevealLayout(Context context, AttributeSet attrs) { | |
super(context, attrs); | |
init(context, attrs); | |
} | |
public SwipeRevealLayout(Context context, AttributeSet attrs, int defStyleAttr) { | |
super(context, attrs, defStyleAttr); | |
} | |
@Override | |
public boolean onTouchEvent(MotionEvent event) { | |
mGestureDetector.onTouchEvent(event); | |
mDragHelper.processTouchEvent(event); | |
return true; | |
} | |
@Override | |
public boolean onInterceptTouchEvent(MotionEvent ev) { | |
if (isDragLocked()) { | |
return super.onInterceptTouchEvent(ev); | |
} | |
mDragHelper.processTouchEvent(ev); | |
mGestureDetector.onTouchEvent(ev); | |
accumulateDragDist(ev); | |
boolean couldBecomeClick = couldBecomeClick(ev); | |
boolean settling = mDragHelper.getViewDragState() == ViewDragHelper.STATE_SETTLING; | |
boolean idleAfterScrolled = mDragHelper.getViewDragState() == ViewDragHelper.STATE_IDLE | |
&& mIsScrolling; | |
// must be placed as the last statement | |
mPrevX = ev.getX(); | |
mPrevY = ev.getY(); | |
// return true => intercept, cannot trigger onClick event | |
return !couldBecomeClick && (settling || idleAfterScrolled); | |
} | |
@Override | |
protected void onFinishInflate() { | |
super.onFinishInflate(); | |
// get views | |
if (getChildCount() >= 2) { | |
mSecondaryView = getChildAt(0); | |
mMainView = getChildAt(1); | |
} | |
else if (getChildCount() == 1) { | |
mMainView = getChildAt(0); | |
} | |
} | |
/** | |
* {@inheritDoc} | |
*/ | |
@SuppressWarnings("ConstantConditions") | |
@Override | |
protected void onLayout(boolean changed, int l, int t, int r, int b) { | |
mAborted = false; | |
for (int index = 0; index < getChildCount(); index++) { | |
final View child = getChildAt(index); | |
int left, right, top, bottom; | |
left = right = top = bottom = 0; | |
final int minLeft = getPaddingLeft(); | |
final int maxRight = Math.max(r - getPaddingRight() - l, 0); | |
final int minTop = getPaddingTop(); | |
final int maxBottom = Math.max(b - getPaddingBottom() - t, 0); | |
int measuredChildHeight = child.getMeasuredHeight(); | |
int measuredChildWidth = child.getMeasuredWidth(); | |
// need to take account if child size is match_parent | |
final LayoutParams childParams = child.getLayoutParams(); | |
boolean matchParentHeight = false; | |
boolean matchParentWidth = false; | |
if (childParams != null) { | |
matchParentHeight = (childParams.height == LayoutParams.MATCH_PARENT) || | |
(childParams.height == LayoutParams.FILL_PARENT); | |
matchParentWidth = (childParams.width == LayoutParams.MATCH_PARENT) || | |
(childParams.width == LayoutParams.FILL_PARENT); | |
} | |
if (matchParentHeight) { | |
measuredChildHeight = maxBottom - minTop; | |
childParams.height = measuredChildHeight; | |
} | |
if (matchParentWidth) { | |
measuredChildWidth = maxRight - minLeft; | |
childParams.width = measuredChildWidth; | |
} | |
switch (mDragEdge) { | |
case DRAG_EDGE_RIGHT: | |
left = Math.max(r - measuredChildWidth - getPaddingRight() - l, minLeft); | |
top = Math.min(getPaddingTop(), maxBottom); | |
right = Math.max(r - getPaddingRight() - l, minLeft); | |
bottom = Math.min(measuredChildHeight + getPaddingTop(), maxBottom); | |
break; | |
case DRAG_EDGE_LEFT: | |
left = Math.min(getPaddingLeft(), maxRight); | |
top = Math.min(getPaddingTop(), maxBottom); | |
right = Math.min(measuredChildWidth + getPaddingLeft(), maxRight); | |
bottom = Math.min(measuredChildHeight + getPaddingTop(), maxBottom); | |
break; | |
case DRAG_EDGE_TOP: | |
left = Math.min(getPaddingLeft(), maxRight); | |
top = Math.min(getPaddingTop(), maxBottom); | |
right = Math.min(measuredChildWidth + getPaddingLeft(), maxRight); | |
bottom = Math.min(measuredChildHeight + getPaddingTop(), maxBottom); | |
break; | |
case DRAG_EDGE_BOTTOM: | |
left = Math.min(getPaddingLeft(), maxRight); | |
top = Math.max(b - measuredChildHeight - getPaddingBottom() - t, minTop); | |
right = Math.min(measuredChildWidth + getPaddingLeft(), maxRight); | |
bottom = Math.max(b - getPaddingBottom() - t, minTop); | |
break; | |
} | |
child.layout(left, top, right, bottom); | |
} | |
// taking account offset when mode is SAME_LEVEL | |
if (mMode == MODE_SAME_LEVEL) { | |
switch (mDragEdge) { | |
case DRAG_EDGE_LEFT: | |
mSecondaryView.offsetLeftAndRight(-mSecondaryView.getWidth()); | |
break; | |
case DRAG_EDGE_RIGHT: | |
mSecondaryView.offsetLeftAndRight(mSecondaryView.getWidth()); | |
break; | |
case DRAG_EDGE_TOP: | |
mSecondaryView.offsetTopAndBottom(-mSecondaryView.getHeight()); | |
break; | |
case DRAG_EDGE_BOTTOM: | |
mSecondaryView.offsetTopAndBottom(mSecondaryView.getHeight()); | |
} | |
} | |
initRects(); | |
if (mIsOpenBeforeInit) { | |
open(false); | |
} else { | |
close(false); | |
} | |
mLastMainLeft = mMainView.getLeft(); | |
mLastMainTop = mMainView.getTop(); | |
mOnLayoutCount++; | |
} | |
/** | |
* {@inheritDoc} | |
*/ | |
@Override | |
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { | |
if (getChildCount() < 2) { | |
throw new RuntimeException("Layout must have two children"); | |
} | |
final LayoutParams params = getLayoutParams(); | |
final int widthMode = MeasureSpec.getMode(widthMeasureSpec); | |
final int heightMode = MeasureSpec.getMode(heightMeasureSpec); | |
int desiredWidth = 0; | |
int desiredHeight = 0; | |
// first find the largest child | |
for (int i = 0; i < getChildCount(); i++) { | |
final View child = getChildAt(i); | |
measureChild(child, widthMeasureSpec, heightMeasureSpec); | |
desiredWidth = Math.max(child.getMeasuredWidth(), desiredWidth); | |
desiredHeight = Math.max(child.getMeasuredHeight(), desiredHeight); | |
} | |
// create new measure spec using the largest child width | |
widthMeasureSpec = MeasureSpec.makeMeasureSpec(desiredWidth, widthMode); | |
heightMeasureSpec = MeasureSpec.makeMeasureSpec(desiredHeight, heightMode); | |
final int measuredWidth = MeasureSpec.getSize(widthMeasureSpec); | |
final int measuredHeight = MeasureSpec.getSize(heightMeasureSpec); | |
for (int i = 0; i < getChildCount(); i++) { | |
final View child = getChildAt(i); | |
final LayoutParams childParams = child.getLayoutParams(); | |
if (childParams != null) { | |
if (childParams.height == LayoutParams.MATCH_PARENT) { | |
child.setMinimumHeight(measuredHeight); | |
} | |
if (childParams.width == LayoutParams.MATCH_PARENT) { | |
child.setMinimumWidth(measuredWidth); | |
} | |
} | |
measureChild(child, widthMeasureSpec, heightMeasureSpec); | |
desiredWidth = Math.max(child.getMeasuredWidth(), desiredWidth); | |
desiredHeight = Math.max(child.getMeasuredHeight(), desiredHeight); | |
} | |
// taking accounts of padding | |
desiredWidth += getPaddingLeft() + getPaddingRight(); | |
desiredHeight += getPaddingTop() + getPaddingBottom(); | |
// adjust desired width | |
if (widthMode == MeasureSpec.EXACTLY) { | |
desiredWidth = measuredWidth; | |
} else { | |
if (params.width == LayoutParams.MATCH_PARENT) { | |
desiredWidth = measuredWidth; | |
} | |
if (widthMode == MeasureSpec.AT_MOST) { | |
desiredWidth = (desiredWidth > measuredWidth)? measuredWidth : desiredWidth; | |
} | |
} | |
// adjust desired height | |
if (heightMode == MeasureSpec.EXACTLY) { | |
desiredHeight = measuredHeight; | |
} else { | |
if (params.height == LayoutParams.MATCH_PARENT) { | |
desiredHeight = measuredHeight; | |
} | |
if (heightMode == MeasureSpec.AT_MOST) { | |
desiredHeight = (desiredHeight > measuredHeight)? measuredHeight : desiredHeight; | |
} | |
} | |
setMeasuredDimension(desiredWidth, desiredHeight); | |
} | |
@Override | |
public void computeScroll() { | |
if (mDragHelper.continueSettling(true)) { | |
ViewCompat.postInvalidateOnAnimation(this); | |
} | |
} | |
/** | |
* Open the panel to show the secondary view | |
* @param animation true to animate the open motion. {@link SwipeListener} won't be | |
* called if is animation is false. | |
*/ | |
public void open(boolean animation) { | |
mIsOpenBeforeInit = true; | |
mAborted = false; | |
if (animation) { | |
mState = STATE_OPENING; | |
mDragHelper.smoothSlideViewTo(mMainView, mRectMainOpen.left, mRectMainOpen.top); | |
if (mDragStateChangeListener != null) { | |
mDragStateChangeListener.onDragStateChanged(mState); | |
} | |
} else { | |
mState = STATE_OPEN; | |
mDragHelper.abort(); | |
mMainView.layout( | |
mRectMainOpen.left, | |
mRectMainOpen.top, | |
mRectMainOpen.right, | |
mRectMainOpen.bottom | |
); | |
mSecondaryView.layout( | |
mRectSecOpen.left, | |
mRectSecOpen.top, | |
mRectSecOpen.right, | |
mRectSecOpen.bottom | |
); | |
} | |
ViewCompat.postInvalidateOnAnimation(SwipeRevealLayout.this); | |
} | |
/** | |
* Close the panel to hide the secondary view | |
* @param animation true to animate the close motion. {@link SwipeListener} won't be | |
* called if is animation is false. | |
*/ | |
public void close(boolean animation) { | |
mIsOpenBeforeInit = false; | |
mAborted = false; | |
if (animation) { | |
mState = STATE_CLOSING; | |
mDragHelper.smoothSlideViewTo(mMainView, mRectMainClose.left, mRectMainClose.top); | |
if (mDragStateChangeListener != null) { | |
mDragStateChangeListener.onDragStateChanged(mState); | |
} | |
} else { | |
mState = STATE_CLOSE; | |
mDragHelper.abort(); | |
mMainView.layout( | |
mRectMainClose.left, | |
mRectMainClose.top, | |
mRectMainClose.right, | |
mRectMainClose.bottom | |
); | |
mSecondaryView.layout( | |
mRectSecClose.left, | |
mRectSecClose.top, | |
mRectSecClose.right, | |
mRectSecClose.bottom | |
); | |
} | |
ViewCompat.postInvalidateOnAnimation(SwipeRevealLayout.this); | |
} | |
/** | |
* Set the minimum fling velocity to cause the layout to open/close. | |
* @param velocity dp per second | |
*/ | |
public void setMinFlingVelocity(int velocity) { | |
mMinFlingVelocity = velocity; | |
} | |
/** | |
* Get the minimum fling velocity to cause the layout to open/close. | |
* @return dp per second | |
*/ | |
public int getMinFlingVelocity() { | |
return mMinFlingVelocity; | |
} | |
/** | |
* Set the edge where the layout can be dragged from. | |
* @param dragEdge Can be one of these | |
* <ul> | |
* <li>{@link #DRAG_EDGE_LEFT}</li> | |
* <li>{@link #DRAG_EDGE_TOP}</li> | |
* <li>{@link #DRAG_EDGE_RIGHT}</li> | |
* <li>{@link #DRAG_EDGE_BOTTOM}</li> | |
* </ul> | |
*/ | |
public void setDragEdge(int dragEdge) { | |
mDragEdge = dragEdge; | |
} | |
/** | |
* Get the edge where the layout can be dragged from. | |
* @return Can be one of these | |
* <ul> | |
* <li>{@link #DRAG_EDGE_LEFT}</li> | |
* <li>{@link #DRAG_EDGE_TOP}</li> | |
* <li>{@link #DRAG_EDGE_RIGHT}</li> | |
* <li>{@link #DRAG_EDGE_BOTTOM}</li> | |
* </ul> | |
*/ | |
public int getDragEdge() { | |
return mDragEdge; | |
} | |
public void setSwipeListener(SwipeListener listener) { | |
mSwipeListener = listener; | |
} | |
/** | |
* @param lock if set to true, the user cannot drag/swipe the layout. | |
*/ | |
public void setLockDrag(boolean lock) { | |
mLockDrag = lock; | |
} | |
/** | |
* @return true if the drag/swipe motion is currently locked. | |
*/ | |
public boolean isDragLocked() { | |
return mLockDrag; | |
} | |
/** | |
* @return true if layout is fully opened, false otherwise. | |
*/ | |
public boolean isOpened() { | |
return (mState == STATE_OPEN); | |
} | |
/** | |
* @return true if layout is fully closed, false otherwise. | |
*/ | |
public boolean isClosed() { | |
return (mState == STATE_CLOSE); | |
} | |
/** Only used for {@link ViewBinderHelper} */ | |
void setDragStateChangeListener(DragStateChangeListener listener) { | |
mDragStateChangeListener = listener; | |
} | |
/** Abort current motion in progress. Only used for {@link ViewBinderHelper} */ | |
protected void abort() { | |
mAborted = true; | |
mDragHelper.abort(); | |
} | |
/** | |
* In RecyclerView/ListView, onLayout should be called 2 times to display children views correctly. | |
* This method check if it've already called onLayout two times. | |
* @return true if you should call {@link #requestLayout()}. | |
*/ | |
protected boolean shouldRequestLayout() { | |
return mOnLayoutCount < 2; | |
} | |
private int getMainOpenLeft() { | |
switch (mDragEdge) { | |
case DRAG_EDGE_LEFT: | |
return mRectMainClose.left + mSecondaryView.getWidth(); | |
case DRAG_EDGE_RIGHT: | |
return mRectMainClose.left - mSecondaryView.getWidth(); | |
case DRAG_EDGE_TOP: | |
return mRectMainClose.left; | |
case DRAG_EDGE_BOTTOM: | |
return mRectMainClose.left; | |
default: | |
return 0; | |
} | |
} | |
private int getMainOpenTop() { | |
switch (mDragEdge) { | |
case DRAG_EDGE_LEFT: | |
return mRectMainClose.top; | |
case DRAG_EDGE_RIGHT: | |
return mRectMainClose.top; | |
case DRAG_EDGE_TOP: | |
return mRectMainClose.top + mSecondaryView.getHeight(); | |
case DRAG_EDGE_BOTTOM: | |
return mRectMainClose.top - mSecondaryView.getHeight(); | |
default: | |
return 0; | |
} | |
} | |
private int getSecOpenLeft() { | |
if (mMode == MODE_NORMAL || mDragEdge == DRAG_EDGE_BOTTOM || mDragEdge == DRAG_EDGE_TOP) { | |
return mRectSecClose.left; | |
} | |
if (mDragEdge == DRAG_EDGE_LEFT) { | |
return mRectSecClose.left + mSecondaryView.getWidth(); | |
} else { | |
return mRectSecClose.left - mSecondaryView.getWidth(); | |
} | |
} | |
private int getSecOpenTop() { | |
if (mMode == MODE_NORMAL || mDragEdge == DRAG_EDGE_LEFT || mDragEdge == DRAG_EDGE_RIGHT) { | |
return mRectSecClose.top; | |
} | |
if (mDragEdge == DRAG_EDGE_TOP) { | |
return mRectSecClose.top + mSecondaryView.getHeight(); | |
} else { | |
return mRectSecClose.top - mSecondaryView.getHeight(); | |
} | |
} | |
private void initRects() { | |
// close position of main view | |
mRectMainClose.set( | |
mMainView.getLeft(), | |
mMainView.getTop(), | |
mMainView.getRight(), | |
mMainView.getBottom() | |
); | |
// close position of secondary view | |
mRectSecClose.set( | |
mSecondaryView.getLeft(), | |
mSecondaryView.getTop(), | |
mSecondaryView.getRight(), | |
mSecondaryView.getBottom() | |
); | |
// open position of the main view | |
mRectMainOpen.set( | |
getMainOpenLeft(), | |
getMainOpenTop(), | |
getMainOpenLeft() + mMainView.getWidth(), | |
getMainOpenTop() + mMainView.getHeight() | |
); | |
// open position of the secondary view | |
mRectSecOpen.set( | |
getSecOpenLeft(), | |
getSecOpenTop(), | |
getSecOpenLeft() + mSecondaryView.getWidth(), | |
getSecOpenTop() + mSecondaryView.getHeight() | |
); | |
} | |
private boolean couldBecomeClick(MotionEvent ev) { | |
return isInMainView(ev) && !shouldInitiateADrag(); | |
} | |
private boolean isInMainView(MotionEvent ev) { | |
float x = ev.getX(); | |
float y = ev.getY(); | |
boolean withinVertical = mMainView.getTop() <= y && y <= mMainView.getBottom(); | |
boolean withinHorizontal = mMainView.getLeft() <= x && x <= mMainView.getRight(); | |
return withinVertical && withinHorizontal; | |
} | |
private boolean shouldInitiateADrag() { | |
float minDistToInitiateDrag = mDragHelper.getTouchSlop(); | |
return mDragDist >= minDistToInitiateDrag; | |
} | |
private void accumulateDragDist(MotionEvent ev) { | |
final int action = ev.getAction(); | |
if (action == MotionEvent.ACTION_DOWN) { | |
mDragDist = 0; | |
return; | |
} | |
boolean dragHorizontally = getDragEdge() == DRAG_EDGE_LEFT || | |
getDragEdge() == DRAG_EDGE_RIGHT; | |
float dragged; | |
if (dragHorizontally) { | |
dragged = Math.abs(ev.getX() - mPrevX); | |
} else { | |
dragged = Math.abs(ev.getY() - mPrevY); | |
} | |
mDragDist += dragged; | |
} | |
private void init(Context context, AttributeSet attrs) { | |
if (attrs != null && context != null) { | |
TypedArray a = context.getTheme().obtainStyledAttributes( | |
attrs, | |
R.styleable.SwipeRevealLayout, | |
0, 0 | |
); | |
mDragEdge = a.getInteger(R.styleable.SwipeRevealLayout_dragEdge, DRAG_EDGE_LEFT); | |
mMinFlingVelocity = a.getInteger(R.styleable.SwipeRevealLayout_flingVelocity, DEFAULT_MIN_FLING_VELOCITY); | |
mMode = a.getInteger(R.styleable.SwipeRevealLayout_mode, MODE_NORMAL); | |
mMinDistRequestDisallowParent = a.getDimensionPixelSize( | |
R.styleable.SwipeRevealLayout_minDistRequestDisallowParent, | |
dpToPx(DEFAULT_MIN_DIST_REQUEST_DISALLOW_PARENT) | |
); | |
} | |
mDragHelper = ViewDragHelper.create(this, 1.0f, mDragHelperCallback); | |
mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_ALL); | |
mGestureDetector = new GestureDetectorCompat(context, mGestureListener); | |
} | |
private final GestureDetector.OnGestureListener mGestureListener = new GestureDetector.SimpleOnGestureListener() { | |
boolean hasDisallowed = false; | |
@Override | |
public boolean onDown(MotionEvent e) { | |
mIsScrolling = false; | |
hasDisallowed = false; | |
return true; | |
} | |
@Override | |
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { | |
mIsScrolling = true; | |
return false; | |
} | |
@Override | |
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { | |
mIsScrolling = true; | |
if (getParent() != null) { | |
boolean shouldDisallow; | |
if (!hasDisallowed) { | |
shouldDisallow = getDistToClosestEdge() >= mMinDistRequestDisallowParent; | |
if (shouldDisallow) { | |
hasDisallowed = true; | |
} | |
} else { | |
shouldDisallow = true; | |
} | |
// disallow parent to intercept touch event so that the layout will work | |
// properly on RecyclerView or view that handles scroll gesture. | |
getParent().requestDisallowInterceptTouchEvent(shouldDisallow); | |
} | |
return false; | |
} | |
}; | |
private int getDistToClosestEdge() { | |
switch (mDragEdge) { | |
case DRAG_EDGE_LEFT: | |
final int pivotRight = mRectMainClose.left + mSecondaryView.getWidth(); | |
return Math.min( | |
mMainView.getLeft() - mRectMainClose.left, | |
pivotRight - mMainView.getLeft() | |
); | |
case DRAG_EDGE_RIGHT: | |
final int pivotLeft = mRectMainClose.right - mSecondaryView.getWidth(); | |
return Math.min( | |
mMainView.getRight() - pivotLeft, | |
mRectMainClose.right - mMainView.getRight() | |
); | |
case DRAG_EDGE_TOP: | |
final int pivotBottom = mRectMainClose.top + mSecondaryView.getHeight(); | |
return Math.min( | |
mMainView.getBottom() - pivotBottom, | |
pivotBottom - mMainView.getTop() | |
); | |
case DRAG_EDGE_BOTTOM: | |
final int pivotTop = mRectMainClose.bottom - mSecondaryView.getHeight(); | |
return Math.min( | |
mRectMainClose.bottom - mMainView.getBottom(), | |
mMainView.getBottom() - pivotTop | |
); | |
} | |
return 0; | |
} | |
private int getHalfwayPivotHorizontal() { | |
if (mDragEdge == DRAG_EDGE_LEFT) { | |
return mRectMainClose.left + mSecondaryView.getWidth() / 2; | |
} else { | |
return mRectMainClose.right - mSecondaryView.getWidth() / 2; | |
} | |
} | |
private int getHalfwayPivotVertical() { | |
if (mDragEdge == DRAG_EDGE_TOP) { | |
return mRectMainClose.top + mSecondaryView.getHeight() / 2; | |
} else { | |
return mRectMainClose.bottom - mSecondaryView.getHeight() / 2; | |
} | |
} | |
private final ViewDragHelper.Callback mDragHelperCallback = new ViewDragHelper.Callback() { | |
@Override | |
public boolean tryCaptureView(View child, int pointerId) { | |
mAborted = false; | |
if (mLockDrag) | |
return false; | |
mDragHelper.captureChildView(mMainView, pointerId); | |
return false; | |
} | |
@Override | |
public int clampViewPositionVertical(View child, int top, int dy) { | |
switch (mDragEdge) { | |
case DRAG_EDGE_TOP: | |
return Math.max( | |
Math.min(top, mRectMainClose.top + mSecondaryView.getHeight()), | |
mRectMainClose.top | |
); | |
case DRAG_EDGE_BOTTOM: | |
return Math.max( | |
Math.min(top, mRectMainClose.top), | |
mRectMainClose.top - mSecondaryView.getHeight() | |
); | |
default: | |
return child.getTop(); | |
} | |
} | |
@Override | |
public int clampViewPositionHorizontal(View child, int left, int dx) { | |
switch (mDragEdge) { | |
case DRAG_EDGE_RIGHT: | |
return Math.max( | |
Math.min(left, mRectMainClose.left), | |
mRectMainClose.left - mSecondaryView.getWidth() | |
); | |
case DRAG_EDGE_LEFT: | |
return Math.max( | |
Math.min(left, mRectMainClose.left + mSecondaryView.getWidth()), | |
mRectMainClose.left | |
); | |
default: | |
return child.getLeft(); | |
} | |
} | |
@Override | |
public void onViewReleased(View releasedChild, float xvel, float yvel) { | |
final boolean velRightExceeded = pxToDp((int) xvel) >= mMinFlingVelocity; | |
final boolean velLeftExceeded = pxToDp((int) xvel) <= -mMinFlingVelocity; | |
final boolean velUpExceeded = pxToDp((int) yvel) <= -mMinFlingVelocity; | |
final boolean velDownExceeded = pxToDp((int) yvel) >= mMinFlingVelocity; | |
final int pivotHorizontal = getHalfwayPivotHorizontal(); | |
final int pivotVertical = getHalfwayPivotVertical(); | |
switch (mDragEdge) { | |
case DRAG_EDGE_RIGHT: | |
if (velRightExceeded) { | |
close(true); | |
} else if (velLeftExceeded) { | |
open(true); | |
} else { | |
if (mMainView.getRight() < pivotHorizontal) { | |
open(true); | |
} else { | |
close(true); | |
} | |
} | |
break; | |
case DRAG_EDGE_LEFT: | |
if (velRightExceeded) { | |
open(true); | |
} else if (velLeftExceeded) { | |
close(true); | |
} else { | |
if (mMainView.getLeft() < pivotHorizontal) { | |
close(true); | |
} else { | |
open(true); | |
} | |
} | |
break; | |
case DRAG_EDGE_TOP: | |
if (velUpExceeded) { | |
close(true); | |
} else if (velDownExceeded) { | |
open(true); | |
} else { | |
if (mMainView.getTop() < pivotVertical) { | |
close(true); | |
} else { | |
open(true); | |
} | |
} | |
break; | |
case DRAG_EDGE_BOTTOM: | |
if (velUpExceeded) { | |
open(true); | |
} else if (velDownExceeded) { | |
close(true); | |
} else { | |
if (mMainView.getBottom() < pivotVertical) { | |
open(true); | |
} else { | |
close(true); | |
} | |
} | |
break; | |
} | |
} | |
@Override | |
public void onEdgeDragStarted(int edgeFlags, int pointerId) { | |
super.onEdgeDragStarted(edgeFlags, pointerId); | |
if (mLockDrag) { | |
return; | |
} | |
boolean edgeStartLeft = (mDragEdge == DRAG_EDGE_RIGHT) | |
&& edgeFlags == ViewDragHelper.EDGE_LEFT; | |
boolean edgeStartRight = (mDragEdge == DRAG_EDGE_LEFT) | |
&& edgeFlags == ViewDragHelper.EDGE_RIGHT; | |
boolean edgeStartTop = (mDragEdge == DRAG_EDGE_BOTTOM) | |
&& edgeFlags == ViewDragHelper.EDGE_TOP; | |
boolean edgeStartBottom = (mDragEdge == DRAG_EDGE_TOP) | |
&& edgeFlags == ViewDragHelper.EDGE_BOTTOM; | |
if (edgeStartLeft || edgeStartRight || edgeStartTop || edgeStartBottom) { | |
mDragHelper.captureChildView(mMainView, pointerId); | |
} | |
} | |
@Override | |
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { | |
super.onViewPositionChanged(changedView, left, top, dx, dy); | |
if (mMode == MODE_SAME_LEVEL) { | |
if (mDragEdge == DRAG_EDGE_LEFT || mDragEdge == DRAG_EDGE_RIGHT) { | |
mSecondaryView.offsetLeftAndRight(dx); | |
} else { | |
mSecondaryView.offsetTopAndBottom(dy); | |
} | |
} | |
boolean isMoved = (mMainView.getLeft() != mLastMainLeft) || (mMainView.getTop() != mLastMainTop); | |
if (mSwipeListener != null && isMoved) { | |
if (mMainView.getLeft() == mRectMainClose.left && mMainView.getTop() == mRectMainClose.top) { | |
mSwipeListener.onClosed(SwipeRevealLayout.this); | |
} | |
else if (mMainView.getLeft() == mRectMainOpen.left && mMainView.getTop() == mRectMainOpen.top) { | |
mSwipeListener.onOpened(SwipeRevealLayout.this); | |
} | |
else { | |
mSwipeListener.onSlide(SwipeRevealLayout.this, getSlideOffset()); | |
} | |
} | |
mLastMainLeft = mMainView.getLeft(); | |
mLastMainTop = mMainView.getTop(); | |
ViewCompat.postInvalidateOnAnimation(SwipeRevealLayout.this); | |
} | |
private float getSlideOffset() { | |
switch (mDragEdge) { | |
case DRAG_EDGE_LEFT: | |
return (float) (mMainView.getLeft() - mRectMainClose.left) / mSecondaryView.getWidth(); | |
case DRAG_EDGE_RIGHT: | |
return (float) (mRectMainClose.left - mMainView.getLeft()) / mSecondaryView.getWidth(); | |
case DRAG_EDGE_TOP: | |
return (float) (mMainView.getTop() - mRectMainClose.top) / mSecondaryView.getHeight(); | |
case DRAG_EDGE_BOTTOM: | |
return (float) (mRectMainClose.top - mMainView.getTop()) / mSecondaryView.getHeight(); | |
default: | |
return 0; | |
} | |
} | |
@Override | |
public void onViewDragStateChanged(int state) { | |
super.onViewDragStateChanged(state); | |
final int prevState = mState; | |
switch (state) { | |
case ViewDragHelper.STATE_DRAGGING: | |
mState = STATE_DRAGGING; | |
break; | |
case ViewDragHelper.STATE_IDLE: | |
// drag edge is left or right | |
if (mDragEdge == DRAG_EDGE_LEFT || mDragEdge == DRAG_EDGE_RIGHT) { | |
if (mMainView.getLeft() == mRectMainClose.left) { | |
mState = STATE_CLOSE; | |
} else { | |
mState = STATE_OPEN; | |
} | |
} | |
// drag edge is top or bottom | |
else { | |
if (mMainView.getTop() == mRectMainClose.top) { | |
mState = STATE_CLOSE; | |
} else { | |
mState = STATE_OPEN; | |
} | |
} | |
break; | |
} | |
if (mDragStateChangeListener != null && !mAborted && prevState != mState) { | |
mDragStateChangeListener.onDragStateChanged(mState); | |
} | |
} | |
}; | |
public static String getStateString(int state) { | |
switch (state) { | |
case STATE_CLOSE: | |
return "state_close"; | |
case STATE_CLOSING: | |
return "state_closing"; | |
case STATE_OPEN: | |
return "state_open"; | |
case STATE_OPENING: | |
return "state_opening"; | |
case STATE_DRAGGING: | |
return "state_dragging"; | |
default: | |
return "undefined"; | |
} | |
} | |
private int pxToDp(int px) { | |
Resources resources = getContext().getResources(); | |
DisplayMetrics metrics = resources.getDisplayMetrics(); | |
return (int) (px / ((float)metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT)); | |
} | |
private int dpToPx(int dp) { | |
Resources resources = getContext().getResources(); | |
DisplayMetrics metrics = resources.getDisplayMetrics(); | |
return (int) (dp * ((float) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT)); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment