Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
AndroidSlidingUpPanel with RecyclerView. RecyclerView scrolls as long as it is scrollable and then panel starts collapsing- http://naman-dwivedi.in/slidinguppanel-recyclerview/
package com.sothree.slidinguppanel;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import com.nineoldandroids.view.animation.AnimatorProxy;
import com.sothree.slidinguppanel.library.R;
public class SlidingUpPanelLayout extends ViewGroup {
private static final String TAG = SlidingUpPanelLayout.class.getSimpleName();
/**
* Default peeking out panel height
*/
private static final int DEFAULT_PANEL_HEIGHT = 68; // dp;
/**
* Default anchor point height
*/
private static final float DEFAULT_ANCHOR_POINT = 1.0f; // In relative %
/**
* Default initial state for the component
*/
private static PanelState DEFAULT_SLIDE_STATE = PanelState.COLLAPSED;
/**
* Default height of the shadow above the peeking out panel
*/
private static final int DEFAULT_SHADOW_HEIGHT = 4; // dp;
/**
* If no fade color is given by default it will fade to 80% gray.
*/
private static final int DEFAULT_FADE_COLOR = 0x99000000;
/**
* Default Minimum velocity that will be detected as a fling
*/
private static final int DEFAULT_MIN_FLING_VELOCITY = 400; // dips per second
/**
* Default is set to false because that is how it was written
*/
private static final boolean DEFAULT_OVERLAY_FLAG = false;
/**
* Default is set to true for clip panel for performance reasons
*/
private static final boolean DEFAULT_CLIP_PANEL_FLAG = true;
/**
* Default attributes for layout
*/
private static final int[] DEFAULT_ATTRS = new int[] {
android.R.attr.gravity
};
/**
* Minimum velocity that will be detected as a fling
*/
private int mMinFlingVelocity = DEFAULT_MIN_FLING_VELOCITY;
/**
* The fade color used for the panel covered by the slider. 0 = no fading.
*/
private int mCoveredFadeColor = DEFAULT_FADE_COLOR;
/**
* Default paralax length of the main view
*/
private static final int DEFAULT_PARALAX_OFFSET = 0;
/**
* The paint used to dim the main layout when sliding
*/
private final Paint mCoveredFadePaint = new Paint();
/**
* Drawable used to draw the shadow between panes.
*/
private final Drawable mShadowDrawable;
/**
* The size of the overhang in pixels.
*/
private int mPanelHeight = -1;
/**
* The size of the shadow in pixels.
*/
private int mShadowHeight = -1;
/**
* Paralax offset
*/
private int mParallaxOffset = -1;
/**
* True if the collapsed panel should be dragged up.
*/
private boolean mIsSlidingUp;
/**
* Panel overlays the windows instead of putting it underneath it.
*/
private boolean mOverlayContent = DEFAULT_OVERLAY_FLAG;
/**
* The main view is clipped to the main top border
*/
private boolean mClipPanel = DEFAULT_CLIP_PANEL_FLAG;
/**
* If provided, the panel can be dragged by only this view. Otherwise, the entire panel can be
* used for dragging.
*/
private View mDragView;
/**
* If provided, the panel can be dragged by only this view. Otherwise, the entire panel can be
* used for dragging.
*/
private int mDragViewResId = -1;
/**
* The child view that can slide, if any.
*/
private View mSlideableView;
/**
* The main view
*/
private View mMainView;
/**
* Current state of the slideable view.
*/
public enum PanelState {
EXPANDED,
COLLAPSED,
ANCHORED,
HIDDEN,
DRAGGING
}
private PanelState mSlideState = DEFAULT_SLIDE_STATE;
/**
* If the current slide state is DRAGGING, this will store the last non dragging state
*/
private PanelState mLastNotDraggingSlideState = null;
/**
* How far the panel is offset from its expanded position.
* range [0, 1] where 0 = collapsed, 1 = expanded.
*/
private float mSlideOffset;
/**
* How far in pixels the slideable panel may move.
*/
private int mSlideRange;
/**
* A panel view is locked into internal scrolling or another condition that
* is preventing a drag.
*/
private boolean mIsUnableToDrag;
/**
* Flag indicating that sliding feature is enabled\disabled
*/
private boolean mIsTouchEnabled;
/**
* Flag indicating if a drag view can have its own touch events. If set
* to true, a drag view can scroll horizontally and have its own click listener.
*
* Default is set to false.
*/
private boolean mIsUsingDragViewTouchEvents;
private float mInitialMotionX;
private float mInitialMotionY;
private float mAnchorPoint = 1.f;
View mScrollingView;
boolean isChildHandlingTouch = false;
float mPrevMotionX;
float mPrevMotionY;
private PanelSlideListener mPanelSlideListener;
private final ViewDragHelper mDragHelper;
/**
* Stores whether or not the pane was expanded the last time it was slideable.
* If expand/collapse operations are invoked this state is modified. Used by
* instance state save/restore.
*/
private boolean mFirstLayout = true;
private final Rect mTmpRect = new Rect();
/**
* Listener for monitoring events about sliding panes.
*/
public interface PanelSlideListener {
/**
* Called when a sliding pane's position changes.
* @param panel The child view that was moved
* @param slideOffset The new offset of this sliding pane within its range, from 0-1
*/
public void onPanelSlide(View panel, float slideOffset);
/**
* Called when a sliding panel becomes slid completely collapsed.
* @param panel The child view that was slid to an collapsed position
*/
public void onPanelCollapsed(View panel);
/**
* Called when a sliding panel becomes slid completely expanded.
* @param panel The child view that was slid to a expanded position
*/
public void onPanelExpanded(View panel);
/**
* Called when a sliding panel becomes anchored.
* @param panel The child view that was slid to a anchored position
*/
public void onPanelAnchored(View panel);
/**
* Called when a sliding panel becomes completely hidden.
* @param panel The child view that was slid to a hidden position
*/
public void onPanelHidden(View panel);
}
/**
* No-op stubs for {@link PanelSlideListener}. If you only want to implement a subset
* of the listener methods you can extend this instead of implement the full interface.
*/
public static class SimplePanelSlideListener implements PanelSlideListener {
@Override
public void onPanelSlide(View panel, float slideOffset) {
}
@Override
public void onPanelCollapsed(View panel) {
}
@Override
public void onPanelExpanded(View panel) {
}
@Override
public void onPanelAnchored(View panel) {
}
@Override
public void onPanelHidden(View panel) {
}
}
public SlidingUpPanelLayout(Context context) {
this(context, null);
}
public SlidingUpPanelLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SlidingUpPanelLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
if(isInEditMode()) {
mShadowDrawable = null;
mDragHelper = null;
return;
}
if (attrs != null) {
TypedArray defAttrs = context.obtainStyledAttributes(attrs, DEFAULT_ATTRS);
if (defAttrs != null) {
int gravity = defAttrs.getInt(0, Gravity.NO_GRAVITY);
setGravity(gravity);
}
defAttrs.recycle();
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.SlidingUpPanelLayout);
if (ta != null) {
mPanelHeight = ta.getDimensionPixelSize(R.styleable.SlidingUpPanelLayout_umanoPanelHeight, -1);
mShadowHeight = ta.getDimensionPixelSize(R.styleable.SlidingUpPanelLayout_umanoShadowHeight, -1);
mParallaxOffset = ta.getDimensionPixelSize(R.styleable.SlidingUpPanelLayout_umanoParalaxOffset, -1);
mMinFlingVelocity = ta.getInt(R.styleable.SlidingUpPanelLayout_umanoFlingVelocity, DEFAULT_MIN_FLING_VELOCITY);
mCoveredFadeColor = ta.getColor(R.styleable.SlidingUpPanelLayout_umanoFadeColor, DEFAULT_FADE_COLOR);
mDragViewResId = ta.getResourceId(R.styleable.SlidingUpPanelLayout_umanoDragView, -1);
mOverlayContent = ta.getBoolean(R.styleable.SlidingUpPanelLayout_umanoOverlay, DEFAULT_OVERLAY_FLAG);
mClipPanel = ta.getBoolean(R.styleable.SlidingUpPanelLayout_umanoClipPanel, DEFAULT_CLIP_PANEL_FLAG);
mAnchorPoint = ta.getFloat(R.styleable.SlidingUpPanelLayout_umanoAnchorPoint, DEFAULT_ANCHOR_POINT);
mSlideState = PanelState.values()[ta.getInt(R.styleable.SlidingUpPanelLayout_umanoInitialState, DEFAULT_SLIDE_STATE.ordinal())];
}
ta.recycle();
}
final float density = context.getResources().getDisplayMetrics().density;
if (mPanelHeight == -1) {
mPanelHeight = (int) (DEFAULT_PANEL_HEIGHT * density + 0.5f);
}
if (mShadowHeight == -1) {
mShadowHeight = (int) (DEFAULT_SHADOW_HEIGHT * density + 0.5f);
}
if (mParallaxOffset == -1) {
mParallaxOffset = (int) (DEFAULT_PARALAX_OFFSET * density);
}
// If the shadow height is zero, don't show the shadow
if (mShadowHeight > 0) {
if (mIsSlidingUp) {
mShadowDrawable = getResources().getDrawable(R.drawable.above_shadow);
} else {
mShadowDrawable = getResources().getDrawable(R.drawable.below_shadow);
}
} else {
mShadowDrawable = null;
}
setWillNotDraw(false);
mDragHelper = ViewDragHelper.create(this, 0.5f, new DragHelperCallback());
mDragHelper.setMinVelocity(mMinFlingVelocity * density);
mIsTouchEnabled = true;
}
/**
* Set the Drag View after the view is inflated
*/
@Override
protected void onFinishInflate() {
super.onFinishInflate();
if (mDragViewResId != -1) {
setDragView(findViewById(mDragViewResId));
}
}
public void setGravity(int gravity) {
if (gravity != Gravity.TOP && gravity != Gravity.BOTTOM) {
throw new IllegalArgumentException("gravity must be set to either top or bottom");
}
mIsSlidingUp = gravity == Gravity.BOTTOM;
if (!mFirstLayout) {
requestLayout();
}
}
/**
* Set the color used to fade the pane covered by the sliding pane out when the pane
* will become fully covered in the expanded state.
*
* @param color An ARGB-packed color value
*/
public void setCoveredFadeColor(int color) {
mCoveredFadeColor = color;
invalidate();
}
/**
* @return The ARGB-packed color value used to fade the fixed pane
*/
public int getCoveredFadeColor() {
return mCoveredFadeColor;
}
/**
* Set sliding enabled flag
* @param enabled flag value
*/
public void setTouchEnabled(boolean enabled) {
mIsTouchEnabled = enabled;
}
public boolean isTouchEnabled() {
return mIsTouchEnabled && mSlideableView != null && mSlideState != PanelState.HIDDEN;
}
/**
* Set the collapsed panel height in pixels
*
* @param val A height in pixels
*/
public void setPanelHeight(int val) {
if (getPanelHeight() == val) {
return;
}
mPanelHeight = val;
if (!mFirstLayout) {
requestLayout();
}
if (getPanelState() == PanelState.COLLAPSED) {
smoothToBottom();
invalidate();
return;
}
}
protected void smoothToBottom(){
smoothSlideTo(0, 0);
}
/**
* @return The current shadow height
*/
public int getShadowHeight() {
return mShadowHeight;
}
/**
* Set the shadow height
*
* @param val A height in pixels
*/
public void setShadowHeight(int val) {
mShadowHeight = val;
if (!mFirstLayout) {
invalidate();
}
}
/**
* @return The current collapsed panel height
*/
public int getPanelHeight() {
return mPanelHeight;
}
/**
* @return The current paralax offset
*/
public int getCurrentParalaxOffset() {
// Clamp slide offset at zero for parallax computation;
int offset = (int)(mParallaxOffset * Math.max(mSlideOffset, 0));
return mIsSlidingUp ? -offset : offset;
}
/**
* Set parallax offset for the panel
*
* @param val A height in pixels
*/
public void setParalaxOffset(int val) {
mParallaxOffset = val;
if (!mFirstLayout) {
requestLayout();
}
}
/**
* @return The current minimin fling velocity
*/
public int getMinFlingVelocity() {
return mMinFlingVelocity;
}
/**
* Sets the minimum fling velocity for the panel
*
* @param val the new value
*/
public void setMinFlingVelocity(int val) {
mMinFlingVelocity = val;
}
/**
* Sets the panel slide listener
* @param listener
*/
public void setPanelSlideListener(PanelSlideListener listener) {
mPanelSlideListener = listener;
}
/**
* Set the draggable view portion. Use to null, to allow the whole panel to be draggable
*
* @param dragView A view that will be used to drag the panel.
*/
public void setDragView(View dragView) {
if (mDragView != null) {
mDragView.setOnClickListener(null);
}
mDragView = dragView;
if (mDragView != null) {
mDragView.setClickable(true);
mDragView.setFocusable(false);
mDragView.setFocusableInTouchMode(false);
mDragView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (!isEnabled() || !isTouchEnabled()) return;
if (mSlideState != PanelState.EXPANDED && mSlideState != PanelState.ANCHORED) {
if (mAnchorPoint < 1.0f) {
setPanelState(PanelState.ANCHORED);
} else {
setPanelState(PanelState.EXPANDED);
}
} else {
setPanelState(PanelState.COLLAPSED);
}
}
});;
}
}
public void setScrollingView(View scrollingView) {
mScrollingView = scrollingView;
}
/**
* Set the draggable view portion. Use to null, to allow the whole panel to be draggable
*
* @param dragViewResId The resource ID of the new drag view
*/
public void setDragView(int dragViewResId) {
mDragViewResId = dragViewResId;
setDragView(findViewById(dragViewResId));
}
/**
* Set an anchor point where the panel can stop during sliding
*
* @param anchorPoint A value between 0 and 1, determining the position of the anchor point
* starting from the top of the layout.
*/
public void setAnchorPoint(float anchorPoint) {
if (anchorPoint > 0 && anchorPoint <= 1) {
mAnchorPoint = anchorPoint;
}
}
/**
* Gets the currently set anchor point
*
* @return the currently set anchor point
*/
public float getAnchorPoint() {
return mAnchorPoint;
}
/**
* Sets whether or not the panel overlays the content
* @param overlayed
*/
public void setOverlayed(boolean overlayed) {
mOverlayContent = overlayed;
}
/**
* Check if the panel is set as an overlay.
*/
public boolean isOverlayed() {
return mOverlayContent;
}
/**
* Sets whether or not the main content is clipped to the top of the panel
* @param overlayed
*/
public void setClipPanel(boolean clip) {
mClipPanel = clip;
}
/**
* Check whether or not the main content is clipped to the top of the panel
*/
public boolean isClipPanel() {
return mClipPanel;
}
void dispatchOnPanelSlide(View panel) {
if (mPanelSlideListener != null) {
mPanelSlideListener.onPanelSlide(panel, mSlideOffset);
}
}
void dispatchOnPanelExpanded(View panel) {
if (mPanelSlideListener != null) {
mPanelSlideListener.onPanelExpanded(panel);
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
}
void dispatchOnPanelCollapsed(View panel) {
if (mPanelSlideListener != null) {
mPanelSlideListener.onPanelCollapsed(panel);
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
}
void dispatchOnPanelAnchored(View panel) {
if (mPanelSlideListener != null) {
mPanelSlideListener.onPanelAnchored(panel);
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
}
void dispatchOnPanelHidden(View panel) {
if (mPanelSlideListener != null) {
mPanelSlideListener.onPanelHidden(panel);
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
}
void updateObscuredViewVisibility() {
if (getChildCount() == 0) {
return;
}
final int leftBound = getPaddingLeft();
final int rightBound = getWidth() - getPaddingRight();
final int topBound = getPaddingTop();
final int bottomBound = getHeight() - getPaddingBottom();
final int left;
final int right;
final int top;
final int bottom;
if (mSlideableView != null && hasOpaqueBackground(mSlideableView)) {
left = mSlideableView.getLeft();
right = mSlideableView.getRight();
top = mSlideableView.getTop();
bottom = mSlideableView.getBottom();
} else {
left = right = top = bottom = 0;
}
View child = getChildAt(0);
final int clampedChildLeft = Math.max(leftBound, child.getLeft());
final int clampedChildTop = Math.max(topBound, child.getTop());
final int clampedChildRight = Math.min(rightBound, child.getRight());
final int clampedChildBottom = Math.min(bottomBound, child.getBottom());
final int vis;
if (clampedChildLeft >= left && clampedChildTop >= top &&
clampedChildRight <= right && clampedChildBottom <= bottom) {
vis = INVISIBLE;
} else {
vis = VISIBLE;
}
child.setVisibility(vis);
}
void setAllChildrenVisible() {
for (int i = 0, childCount = getChildCount(); i < childCount; i++) {
final View child = getChildAt(i);
if (child.getVisibility() == INVISIBLE) {
child.setVisibility(VISIBLE);
}
}
}
private static boolean hasOpaqueBackground(View v) {
final Drawable bg = v.getBackground();
return bg != null && bg.getOpacity() == PixelFormat.OPAQUE;
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mFirstLayout = true;
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mFirstLayout = true;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthMode != MeasureSpec.EXACTLY) {
throw new IllegalStateException("Width must have an exact value or MATCH_PARENT");
} else if (heightMode != MeasureSpec.EXACTLY) {
throw new IllegalStateException("Height must have an exact value or MATCH_PARENT");
}
final int childCount = getChildCount();
if (childCount != 2) {
throw new IllegalStateException("Sliding up panel layout must have exactly 2 children!");
}
mMainView = getChildAt(0);
mSlideableView = getChildAt(1);
if (mDragView == null) {
setDragView(mSlideableView);
}
// If the sliding panel is not visible, then put the whole view in the hidden state
if (mSlideableView.getVisibility() != VISIBLE) {
mSlideState = PanelState.HIDDEN;
}
int layoutHeight = heightSize - getPaddingTop() - getPaddingBottom();
int layoutWidth = widthSize - getPaddingLeft() - getPaddingRight();
// First pass. Measure based on child LayoutParams width/height.
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// We always measure the sliding panel in order to know it's height (needed for show panel)
if (child.getVisibility() == GONE && i == 0) {
continue;
}
int height = layoutHeight;
int width = layoutWidth;
if (child == mMainView) {
if (!mOverlayContent && mSlideState != PanelState.HIDDEN) {
height -= mPanelHeight;
}
width -= lp.leftMargin + lp.rightMargin;
} else if (child == mSlideableView) {
// The slideable view should be aware of its top margin.
// See https://github.com/umano/AndroidSlidingUpPanel/issues/412.
height -= lp.topMargin;
}
int childWidthSpec;
if (lp.width == LayoutParams.WRAP_CONTENT) {
childWidthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST);
} else if (lp.width == LayoutParams.MATCH_PARENT) {
childWidthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
} else {
childWidthSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY);
}
int childHeightSpec;
if (lp.height == LayoutParams.WRAP_CONTENT) {
childHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST);
} else if (lp.height == LayoutParams.MATCH_PARENT) {
childHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
} else {
childHeightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
}
child.measure(childWidthSpec, childHeightSpec);
if (child == mSlideableView) {
mSlideRange = mSlideableView.getMeasuredHeight() - mPanelHeight;
}
}
setMeasuredDimension(widthSize, heightSize);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int paddingLeft = getPaddingLeft();
final int paddingTop = getPaddingTop();
final int childCount = getChildCount();
if (mFirstLayout) {
switch (mSlideState) {
case EXPANDED:
mSlideOffset = 1.0f;
break;
case ANCHORED:
mSlideOffset = mAnchorPoint;
break;
case HIDDEN:
int newTop = computePanelTopPosition(0.0f) + (mIsSlidingUp ? +mPanelHeight : -mPanelHeight);
mSlideOffset = computeSlideOffset(newTop);
break;
default:
mSlideOffset = 0.f;
break;
}
}
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// Always layout the sliding view on the first layout
if (child.getVisibility() == GONE && (i == 0 || mFirstLayout)) {
continue;
}
final int childHeight = child.getMeasuredHeight();
int childTop = paddingTop;
if (child == mSlideableView) {
childTop = computePanelTopPosition(mSlideOffset);
}
if (!mIsSlidingUp) {
if (child == mMainView && !mOverlayContent) {
childTop = computePanelTopPosition(mSlideOffset) + mSlideableView.getMeasuredHeight();
}
}
final int childBottom = childTop + childHeight;
final int childLeft = paddingLeft + lp.leftMargin;
final int childRight = childLeft + child.getMeasuredWidth();
child.layout(childLeft, childTop, childRight, childBottom);
}
if (mFirstLayout) {
updateObscuredViewVisibility();
}
applyParallaxForCurrentSlideOffset();
mFirstLayout = false;
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
// Recalculate sliding panes and their details
if (h != oldh) {
mFirstLayout = true;
}
}
/**
* Set if the drag view can have its own touch events. If set
* to true, a drag view can scroll horizontally and have its own click listener.
*
* Default is set to false.
*/
public void setEnableDragViewTouchEvents(boolean enabled) {
mIsUsingDragViewTouchEvents = enabled;
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mScrollingView == null)
return super.dispatchTouchEvent(ev);
final int action = MotionEvent.getActionMasked(ev);
final float x = ev.getX();
final float y = ev.getY();
if (action == MotionEvent.ACTION_DOWN) {
mDragHelper.shouldInterceptTouchEvent(ev);
mInitialMotionX = mPrevMotionX = x;
mInitialMotionY = mPrevMotionY = y;
isChildHandlingTouch = false;
} else if (action == MotionEvent.ACTION_MOVE) {
float dx = x - mPrevMotionX;
float dy = y - mPrevMotionY;
mPrevMotionX = x;
mPrevMotionY = y;
// If the scroll view isn't under the touch, pass the
// event along to the dragView.
if (!isScrollingViewUnder((int) x, (int) y))
return this.onTouchEvent(ev);
// Dragging Down
if (dy > 0) {
// Is the child still scrolling?
// Then let the child handle it.
if (isChildScrolling()) {
isChildHandlingTouch = true;
return super.dispatchTouchEvent(ev);
}
// If the child was handling touch previously
// Then we need to rearrange things so that the sliding panel gets a proper down event.
if (isChildHandlingTouch) {
// Send an 'UP' event to the child.
MotionEvent up = MotionEvent.obtain(ev);
up.setAction(MotionEvent.ACTION_UP);
super.dispatchTouchEvent(up);
up.recycle();
// Send a 'DOWN' event to the panel.
ev.setAction(MotionEvent.ACTION_DOWN);
}
isChildHandlingTouch = false;
return this.onTouchEvent(ev);
}
// Dragging Up
else if (dy < 0) {
// If panel is not fully expanded,we will handle drag here
if (mSlideOffset < 1.0f) {
isChildHandlingTouch = false;
return this.onTouchEvent(ev);
}
// If the panel was handling touch previously
// Then we need to rearrange things so that the child gets a proper down event.
if (!isChildHandlingTouch) {
mDragHelper.cancel();
ev.setAction(MotionEvent.ACTION_DOWN);
}
isChildHandlingTouch = true;
return super.dispatchTouchEvent(ev);
}
} else if ((action == MotionEvent.ACTION_CANCEL) ||
(action == MotionEvent.ACTION_UP)) {
if (!isChildHandlingTouch) {
final float dx = x - mInitialMotionX;
final float dy = y - mInitialMotionY;
final int slop = mDragHelper.getTouchSlop();
if ((mIsUsingDragViewTouchEvents) && (dx * dx + dy * dy < slop * slop))
return super.dispatchTouchEvent(ev);
return this.onTouchEvent(ev);
}
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (!isSlidingEnabled()) {
return super.onTouchEvent(ev);
}
mDragHelper.processTouchEvent(ev);
final int action = ev.getAction();
boolean captureTouch = false;
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_UP: {
final float x = ev.getX();
final float y = ev.getY();
final float dx = x - mInitialMotionX;
final float dy = y - mInitialMotionY;
final int slop = mDragHelper.getTouchSlop();
View dragView = mDragView != null ? mDragView : mSlideableView;
if (dx * dx + dy * dy < slop * slop &&
isDragViewUnder((int) x, (int) y) &&
!isScrollingViewUnder((int) x, (int) y)) {
if ((PanelState.EXPANDED != mSlideState) && (PanelState.ANCHORED != mSlideState))) {
setPanelState(PanelState.ANCHORED);
} else {
setPanelState(PanelState.COLLAPSED);
}
break;
}
break;
}
}
return captureTouch;
}
private boolean isDragViewUnder(int x, int y) {
if (mDragView == null) return false;
int[] viewLocation = new int[2];
mDragView.getLocationOnScreen(viewLocation);
int[] parentLocation = new int[2];
this.getLocationOnScreen(parentLocation);
int screenX = parentLocation[0] + x;
int screenY = parentLocation[1] + y;
return screenX >= viewLocation[0] && screenX < viewLocation[0] + mDragView.getWidth() &&
screenY >= viewLocation[1] && screenY < viewLocation[1] + mDragView.getHeight();
}
private boolean isScrollingViewUnder(int x, int y) {
if (mScrollingView == null)
return false;
int[] viewLocation = new int[2];
mScrollingView.getLocationOnScreen(viewLocation);
int[] parentLocation = new int[2];
this.getLocationOnScreen(parentLocation);
int screenX = parentLocation[0] + x;
int screenY = parentLocation[1] + y;
return screenX >= viewLocation[0] &&
screenX < viewLocation[0] + mScrollingView.getWidth() &&
screenY >= viewLocation[1] &&
screenY < viewLocation[1] + mScrollingView.getHeight();
}
/*
* Computes the top position of the panel based on the slide offset.
*/
private int computePanelTopPosition(float slideOffset) {
int slidingViewHeight = mSlideableView != null ? mSlideableView.getMeasuredHeight() : 0;
int slidePixelOffset = (int) (slideOffset * mSlideRange);
// Compute the top of the panel if its collapsed
return mIsSlidingUp
? getMeasuredHeight() - getPaddingBottom() - mPanelHeight - slidePixelOffset
: getPaddingTop() - slidingViewHeight + mPanelHeight + slidePixelOffset;
}
/*
* Computes the slide offset based on the top position of the panel
*/
private float computeSlideOffset(int topPosition) {
// Compute the panel top position if the panel is collapsed (offset 0)
final int topBoundCollapsed = computePanelTopPosition(0);
// Determine the new slide offset based on the collapsed top position and the new required
// top position
return (mIsSlidingUp
? (float) (topBoundCollapsed - topPosition) / mSlideRange
: (float) (topPosition - topBoundCollapsed) / mSlideRange);
}
/**
* Returns the current state of the panel as an enum.
* @return the current panel state
*/
public PanelState getPanelState() {
return mSlideState;
}
/**
* Change panel state to the given state with
* @param state - new panel state
*/
public void setPanelState(PanelState state) {
if (state == null || state == PanelState.DRAGGING) {
throw new IllegalArgumentException("Panel state cannot be null or DRAGGING.");
}
if (!isEnabled()
|| (!mFirstLayout && mSlideableView == null)
|| state == mSlideState
|| mSlideState == PanelState.DRAGGING) return;
if (mFirstLayout) {
mSlideState = state;
} else {
if (mSlideState == PanelState.HIDDEN) {
mSlideableView.setVisibility(View.VISIBLE);
requestLayout();
}
switch (state) {
case ANCHORED:
smoothSlideTo(mAnchorPoint, 0);
break;
case COLLAPSED:
smoothSlideTo(0, 0);
break;
case EXPANDED:
smoothSlideTo(1.0f, 0);
break;
case HIDDEN:
int newTop = computePanelTopPosition(0.0f) + (mIsSlidingUp ? +mPanelHeight : -mPanelHeight);
smoothSlideTo(computeSlideOffset(newTop), 0);
break;
}
}
}
/**
* Update the parallax based on the current slide offset.
*/
@SuppressLint("NewApi")
private void applyParallaxForCurrentSlideOffset() {
if (mParallaxOffset > 0) {
int mainViewOffset = getCurrentParalaxOffset();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
mMainView.setTranslationY(mainViewOffset);
} else {
AnimatorProxy.wrap(mMainView).setTranslationY(mainViewOffset);
}
}
}
private void onPanelDragged(int newTop) {
mLastNotDraggingSlideState = mSlideState;
mSlideState = PanelState.DRAGGING;
// Recompute the slide offset based on the new top position
mSlideOffset = computeSlideOffset(newTop);
applyParallaxForCurrentSlideOffset();
// Dispatch the slide event
dispatchOnPanelSlide(mSlideableView);
// If the slide offset is negative, and overlay is not on, we need to increase the
// height of the main content
LayoutParams lp = (LayoutParams)mMainView.getLayoutParams();
int defaultHeight = getHeight() - getPaddingBottom() - getPaddingTop() - mPanelHeight;
if (mSlideOffset <= 0 && !mOverlayContent) {
// expand the main view
lp.height = mIsSlidingUp ? (newTop - getPaddingBottom()) : (getHeight() - getPaddingBottom() - mSlideableView.getMeasuredHeight() - newTop);
mMainView.requestLayout();
} else if (lp.height != defaultHeight && !mOverlayContent) {
lp.height = defaultHeight;
mMainView.requestLayout();
}
}
@Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
boolean result;
final int save = canvas.save(Canvas.CLIP_SAVE_FLAG);
if (mSlideableView != child) { // if main view
// Clip against the slider; no sense drawing what will immediately be covered,
// Unless the panel is set to overlay content
canvas.getClipBounds(mTmpRect);
if (!mOverlayContent) {
if (mIsSlidingUp) {
mTmpRect.bottom = Math.min(mTmpRect.bottom, mSlideableView.getTop());
} else {
mTmpRect.top = Math.max(mTmpRect.top, mSlideableView.getBottom());
}
}
if (mClipPanel) {
canvas.clipRect(mTmpRect);
}
result = super.drawChild(canvas, child, drawingTime);
if (mCoveredFadeColor != 0 && mSlideOffset > 0) {
final int baseAlpha = (mCoveredFadeColor & 0xff000000) >>> 24;
final int imag = (int) (baseAlpha * mSlideOffset);
final int color = imag << 24 | (mCoveredFadeColor & 0xffffff);
mCoveredFadePaint.setColor(color);
canvas.drawRect(mTmpRect, mCoveredFadePaint);
}
} else {
result = super.drawChild(canvas, child, drawingTime);
}
canvas.restoreToCount(save);
return result;
}
/**
* Smoothly animate mDraggingPane to the target X position within its range.
*
* @param slideOffset position to animate to
* @param velocity initial velocity in case of fling, or 0.
*/
boolean smoothSlideTo(float slideOffset, int velocity) {
if (!isEnabled()) {
// Nothing to do.
return false;
}
int panelTop = computePanelTopPosition(slideOffset);
if (mDragHelper.smoothSlideViewTo(mSlideableView, mSlideableView.getLeft(), panelTop)) {
setAllChildrenVisible();
ViewCompat.postInvalidateOnAnimation(this);
return true;
}
return false;
}
@Override
public void computeScroll() {
if (mDragHelper != null && mDragHelper.continueSettling(true)) {
if (!isEnabled()) {
mDragHelper.abort();
return;
}
ViewCompat.postInvalidateOnAnimation(this);
}
}
@Override
public void draw(Canvas c) {
super.draw(c);
// draw the shadow
if (mShadowDrawable != null) {
final int right = mSlideableView.getRight();
final int top;
final int bottom;
if (mIsSlidingUp) {
top = mSlideableView.getTop() - mShadowHeight;
bottom = mSlideableView.getTop();
} else {
top = mSlideableView.getBottom();
bottom = mSlideableView.getBottom() + mShadowHeight;
}
final int left = mSlideableView.getLeft();
mShadowDrawable.setBounds(left, top, right, bottom);
mShadowDrawable.draw(c);
}
}
/**
* Tests scrollability within child views of v given a delta of dx.
*
* @param v View to test for horizontal scrollability
* @param checkV Whether the view v passed should itself be checked for scrollability (true),
* or just its children (false).
* @param dx Delta scrolled in pixels
* @param x X coordinate of the active touch point
* @param y Y coordinate of the active touch point
* @return true if child views of v can be scrolled by delta of dx.
*/
protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
if (v instanceof ViewGroup) {
final ViewGroup group = (ViewGroup) v;
final int scrollX = v.getScrollX();
final int scrollY = v.getScrollY();
final int count = group.getChildCount();
// Count backwards - let topmost views consume scroll distance first.
for (int i = count - 1; i >= 0; i--) {
final View child = group.getChildAt(i);
if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() &&
y + scrollY >= child.getTop() && y + scrollY < child.getBottom() &&
canScroll(child, true, dx, x + scrollX - child.getLeft(),
y + scrollY - child.getTop())) {
return true;
}
}
}
return checkV && ViewCompat.canScrollHorizontally(v, -dx);
}
@Override
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
return new LayoutParams();
}
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof MarginLayoutParams
? new LayoutParams((MarginLayoutParams) p)
: new LayoutParams(p);
}
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LayoutParams && super.checkLayoutParams(p);
}
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
@Override
public Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState);
if (mSlideState != PanelState.DRAGGING) {
ss.mSlideState = mSlideState;
} else {
ss.mSlideState = mLastNotDraggingSlideState;
}
return ss;
}
@Override
public void onRestoreInstanceState(Parcelable state) {
SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
mSlideState = ss.mSlideState;
}
private class DragHelperCallback extends ViewDragHelper.Callback {
@Override
public boolean tryCaptureView(View child, int pointerId) {
if (mIsUnableToDrag) {
return false;
}
return child == mSlideableView;
}
@Override
public void onViewDragStateChanged(int state) {
if (mDragHelper.getViewDragState() == ViewDragHelper.STATE_IDLE) {
mSlideOffset = computeSlideOffset(mSlideableView.getTop());
applyParallaxForCurrentSlideOffset();
if (mSlideOffset == 1) {
if (mSlideState != PanelState.EXPANDED) {
updateObscuredViewVisibility();
mSlideState = PanelState.EXPANDED;
dispatchOnPanelExpanded(mSlideableView);
}
} else if (mSlideOffset == 0) {
if (mSlideState != PanelState.COLLAPSED) {
mSlideState = PanelState.COLLAPSED;
dispatchOnPanelCollapsed(mSlideableView);
}
} else if (mSlideOffset < 0) {
mSlideState = PanelState.HIDDEN;
mSlideableView.setVisibility(View.INVISIBLE);
dispatchOnPanelHidden(mSlideableView);
} else if (mSlideState != PanelState.ANCHORED) {
updateObscuredViewVisibility();
mSlideState = PanelState.ANCHORED;
dispatchOnPanelAnchored(mSlideableView);
}
}
}
@Override
public void onViewCaptured(View capturedChild, int activePointerId) {
setAllChildrenVisible();
}
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
onPanelDragged(top);
invalidate();
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
int target = 0;
// direction is always positive if we are sliding in the expanded direction
float direction = mIsSlidingUp ? -yvel : yvel;
if (direction > 0) {
// swipe up -> expand
target = computePanelTopPosition(1.0f);
} else if (direction < 0) {
// swipe down -> collapse
target = computePanelTopPosition(0.0f);
} else if (mAnchorPoint != 1 && mSlideOffset >= (1.f + mAnchorPoint) / 2) {
// zero velocity, and far enough from anchor point => expand to the top
target = computePanelTopPosition(1.0f);
} else if (mAnchorPoint == 1 && mSlideOffset >= 0.5f) {
// zero velocity, and far enough from anchor point => expand to the top
target = computePanelTopPosition(1.0f);
} else if (mAnchorPoint != 1 && mSlideOffset >= mAnchorPoint) {
target = computePanelTopPosition(mAnchorPoint);
} else if (mAnchorPoint != 1 && mSlideOffset >= mAnchorPoint / 2) {
target = computePanelTopPosition(mAnchorPoint);
} else {
// settle at the bottom
target = computePanelTopPosition(0.0f);
}
mDragHelper.settleCapturedViewAt(releasedChild.getLeft(), target);
invalidate();
}
@Override
public int getViewVerticalDragRange(View child) {
return mSlideRange;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
final int collapsedTop = computePanelTopPosition(0.f);
final int expandedTop = computePanelTopPosition(1.0f);
if (mIsSlidingUp) {
return Math.min(Math.max(top, expandedTop), collapsedTop);
} else {
return Math.min(Math.max(top, collapsedTop), expandedTop);
}
}
}
public static class LayoutParams extends ViewGroup.MarginLayoutParams {
private static final int[] ATTRS = new int[] {
android.R.attr.layout_weight
};
public LayoutParams() {
super(MATCH_PARENT, MATCH_PARENT);
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(android.view.ViewGroup.LayoutParams source) {
super(source);
}
public LayoutParams(MarginLayoutParams source) {
super(source);
}
public LayoutParams(LayoutParams source) {
super(source);
}
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
final TypedArray a = c.obtainStyledAttributes(attrs, ATTRS);
a.recycle();
}
}
static class SavedState extends BaseSavedState {
PanelState mSlideState;
SavedState(Parcelable superState) {
super(superState);
}
private SavedState(Parcel in) {
super(in);
try {
mSlideState = Enum.valueOf(PanelState.class, in.readString());
} catch (IllegalArgumentException e) {
mSlideState = PanelState.COLLAPSED;
}
}
@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeString(mSlideState.toString());
}
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];
}
};
}
private boolean isChildScrolling() {
if (mScrollingView == null)
return false;
//for recyclerview
if (mScrollingView instanceof RecyclerView) {
RecyclerView rv = (RecyclerView) mScrollingView;
LinearLayoutManager layoutManager = ((LinearLayoutManager) rv.getLayoutManager());
int firstVisiblePosition = layoutManager.findFirstVisibleItemPosition();
if (firstVisiblePosition > 0)
return true;
//findFirstVisibleItemPosition returns the position even if 1dp of the item view is visible
//so if even some of the first item view is visible firstVisiblePosition>0 would be false
//however in that case the view can still scroll a little upto the actual height of the item.
//take care of that with below code
View v = rv.getChildAt(0);
int top = (v == null) ? (0) : (firstVisiblePosition * lv.getHeight() - v.getTop());
return top > 0;
}
//haven't tested these but should work
//for listview
else if (mScrollingView instanceof ListView) {
ListView lv = (ListView) mScrollingView;
if (lv.getFirstVisiblePosition() > 0)
return true;
View v = lv.getChildAt(0);
int top = (v == null) ? (0) : (-v.getTop() + lv.getFirstVisiblePosition() * lv.getHeight());
return top > 0;
}
//for scrollview
else if (mScrollingView instanceof ScrollView) {
return (mScrollingView.getScrollY() > 0);
}
return false;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.