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.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 android.view.animation.AnimationUtils; import android.view.animation.Interpolator; import com.sothree.slidinguppanel.library.R; import java.util.ArrayList; import java.util.List; 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 parallax length of the main view */ private static final int DEFAULT_PARALLAX_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; private int mSlidePanelOffset = 0; /** * The size of the shadow in pixels. */ private int mShadowHeight = -1; /** * Parallax offset */ private int mParallaxOffset = -1; /** * True if the collapsed panel should be dragged up. */ 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; /** * If provided, the panel will transfer the scroll from this view to itself when needed. */ private View mScrollableView; private int mScrollableViewResId; private ScrollableViewHelper mScrollableViewHelper = new ScrollableViewHelper(); /** * The child view that can slide, if any. */ View mSlideableView; /** * The main view */ private View mMainView; /** * Current state of the slideable view. */ public enum PanelState { EXPANDED, COLLAPSED, ANCHORED, HIDDEN, DRAGGING } /** * Shows the panel from the hidden state */ public void showPanel() { if (mFirstLayout) { mSlideState = PanelState.COLLAPSED; } else { if (mSlideableView == null || mSlideState != PanelState.HIDDEN) return; mSlideableView.setVisibility(View.VISIBLE); requestLayout(); smoothSlideTo(0, 0, true); } } /** * Hides the sliding panel entirely. */ public void hidePanel() { if (mFirstLayout) { mSlideState = PanelState.HIDDEN; } else { if (mSlideState == PanelState.DRAGGING || mSlideState == PanelState.HIDDEN) return; int newTop = computePanelTopPosition(0.0f) + (mIsSlidingUp ? +mPanelHeight : -mPanelHeight); smoothSlideTo(computeSlideOffset(newTop), 0, true); } } PanelState mSlideState = DEFAULT_SLIDE_STATE; /** * If the current slide state is DRAGGING, this will store the last non dragging state */ private PanelState mLastNotDraggingSlideState = DEFAULT_SLIDE_STATE; /** * How far the panel is offset from its expanded position. * range [0, 1] where 0 = collapsed, 1 = expanded. */ float mSlideOffset; /** * How far in pixels the slideable panel may move. */ int mSlideRange; /** * An anchor point where the panel can stop during sliding */ float mAnchorPoint = 1.f; /** * A panel view is locked into internal scrolling or another condition that * is preventing a drag. */ boolean mIsUnableToDrag; /** * Flag indicating that sliding feature is enabled\disabled */ private boolean mIsTouchEnabled; private float mPrevMotionY; private float mInitialMotionX; private float mInitialMotionY; private boolean mIsScrollableViewHandlingTouch = false; private List<PanelSlideListener> mPanelSlideListeners = new ArrayList<>(); private View.OnClickListener mFadeOnClickListener; 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 state changes * * @param panel The child view that was slid to an collapsed position */ public void onPanelStateChanged(View panel, PanelState previousState, PanelState newState); } /** * 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 onPanelStateChanged(View panel, PanelState previousState, PanelState newState) { } } 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; } Interpolator scrollerInterpolator = null; 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); mSlidePanelOffset = ta.getDimensionPixelSize(R.styleable.SlidingUpPanelLayout_umanoSlideOffset, 0); mShadowHeight = ta.getDimensionPixelSize(R.styleable.SlidingUpPanelLayout_umanoShadowHeight, -1); mParallaxOffset = ta.getDimensionPixelSize(R.styleable.SlidingUpPanelLayout_umanoParallaxOffset, -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); mScrollableViewResId = ta.getResourceId(R.styleable.SlidingUpPanelLayout_umanoScrollableView, -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())]; int interpolatorResId = ta.getResourceId(R.styleable.SlidingUpPanelLayout_umanoScrollInterpolator, -1); if (interpolatorResId != -1) { scrollerInterpolator = AnimationUtils.loadInterpolator(context, interpolatorResId); } } 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_PARALLAX_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, scrollerInterpolator, new DragHelperCallback()); mDragHelper.setMinVelocity(mMinFlingVelocity * density); mIsTouchEnabled = true; } @Override protected boolean fitSystemWindows(Rect insets) { return true; } /** * Set the Drag View after the view is inflated */ @Override protected void onFinishInflate() { super.onFinishInflate(); if (mDragViewResId != -1) { setDragView(findViewById(mDragViewResId)); } if (mScrollableViewResId != -1) { setScrollableView(findViewById(mScrollableViewResId)); } } 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; requestLayout(); } /** * @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, true); } /** * @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; } /** * Sets the panel offset when collapsed so you can exit * the boundaries of the top of the screen * * @param val Offset in pixels */ public void setSlidePanelOffset(int val) { mSlidePanelOffset = val; requestLayout(); } public int getSlidePanelOffset() { return mSlidePanelOffset; } /** * @return The current parallax offset */ public int getCurrentParallaxOffset() { // 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 setParallaxOffset(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; } /** * Adds a panel slide listener * * @param listener */ public void addPanelSlideListener(PanelSlideListener listener) { mPanelSlideListeners.add(listener); } /** * Removes a panel slide listener * * @param listener */ public void removePanelSlideListener(PanelSlideListener listener) { mPanelSlideListeners.remove(listener); } /** * Provides an on click for the portion of the main view that is dimmed. The listener is not * triggered if the panel is in a collapsed or a hidden position. If the on click listener is * not provided, the clicks on the dimmed area are passed through to the main layout. * * @param listener */ public void setFadeOnClickListener(View.OnClickListener listener) { mFadeOnClickListener = 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, true); } else { setPanelState(PanelState.EXPANDED, true); } } else { setPanelState(PanelState.COLLAPSED, true); } } }); } } /** * 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 the scrollable child of the sliding layout. If set, scrolling will be transfered between * the panel and the view when necessary * * @param scrollableView The scrollable view */ public void setScrollableView(View scrollableView) { mScrollableView = scrollableView; } /** * Sets the current scrollable view helper. See ScrollableViewHelper description for details. * * @param helper */ public void setScrollableViewHelper(ScrollableViewHelper helper) { mScrollableViewHelper = helper; } /** * 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; mFirstLayout = true; requestLayout(); } } /** * 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 clip */ 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) { for (PanelSlideListener l : mPanelSlideListeners) { l.onPanelSlide(panel, mSlideOffset); } } void dispatchOnPanelStateChanged(View panel, PanelState previousState, PanelState newState) { for (PanelSlideListener l : mPanelSlideListeners) { l.onPanelStateChanged(panel, previousState, newState); } 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; } @SuppressWarnings("Range") @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 { // Modify the height based on the weight. if (lp.weight > 0 && lp.weight < 1) { height = (int) (height * lp.weight); } else if (lp.height != LayoutParams.MATCH_PARENT) { height = lp.height; } childHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); } if (child == mSlideableView) { childHeightSpec += mSlidePanelOffset; } 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; } } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { // If the scrollable view is handling touch, never intercept if (mIsScrollableViewHandlingTouch || !isTouchEnabled()) { mDragHelper.abort(); return false; } final int action = MotionEventCompat.getActionMasked(ev); final float x = ev.getX(); final float y = ev.getY(); final float adx = Math.abs(x - mInitialMotionX); final float ady = Math.abs(y - mInitialMotionY); final int dragSlop = mDragHelper.getTouchSlop(); switch (action) { case MotionEvent.ACTION_DOWN: { mIsUnableToDrag = false; mInitialMotionX = x; mInitialMotionY = y; break; } case MotionEvent.ACTION_MOVE: { if ((ady > dragSlop && adx > ady) || !isViewUnder(mDragView, (int) mInitialMotionX, (int) mInitialMotionY)) { mDragHelper.cancel(); mIsUnableToDrag = true; return false; } break; } case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: // If the dragView is still dragging when we get here, we need to call processTouchEvent // so that the view is settled // Added to make scrollable views work (tokudu) if (mDragHelper.isDragging()) { mDragHelper.processTouchEvent(ev); return true; } // Check if this was a click on the faded part of the screen, and fire off the listener if there is one. if (ady <= dragSlop && adx <= dragSlop && mSlideOffset > 0 && !isViewUnder(mSlideableView, (int) mInitialMotionX, (int) mInitialMotionY) && mFadeOnClickListener != null) { playSoundEffect(android.view.SoundEffectConstants.CLICK); mFadeOnClickListener.onClick(this); return true; } break; } return mDragHelper.shouldInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent ev) { if (!isEnabled() || !isTouchEnabled()) { return super.onTouchEvent(ev); } try { mDragHelper.processTouchEvent(ev); return true; } catch (Exception ex) { // Ignore the pointer out of range exception return false; } } @Override public boolean dispatchTouchEvent(MotionEvent ev) { final int action = MotionEventCompat.getActionMasked(ev); if (!isEnabled() || !isTouchEnabled() || (mIsUnableToDrag && action != MotionEvent.ACTION_DOWN)) { mDragHelper.abort(); return super.dispatchTouchEvent(ev); } final float y = ev.getY(); if (action == MotionEvent.ACTION_DOWN) { mIsScrollableViewHandlingTouch = false; mPrevMotionY = y; } else if (action == MotionEvent.ACTION_MOVE) { float dy = y - mPrevMotionY; mPrevMotionY = y; // If the scroll view isn't under the touch, pass the // event along to the dragView. if (!isViewUnder(mScrollableView, (int) mInitialMotionX, (int) mInitialMotionY)) { return super.dispatchTouchEvent(ev); } // Which direction (up or down) is the drag moving? if (dy * (mIsSlidingUp ? 1 : -1) > 0) { // Collapsing // Is the child less than fully scrolled? // Then let the child handle it. if (mScrollableViewHelper.getScrollableViewScrollPosition(mScrollableView, mIsSlidingUp) > 0) { mIsScrollableViewHandlingTouch = true; return super.dispatchTouchEvent(ev); } // Was the child handling the touch previously? // Then we need to rejigger things so that the // drag panel gets a proper down event. if (mIsScrollableViewHandlingTouch) { // Send an 'UP' event to the child. MotionEvent up = MotionEvent.obtain(ev); up.setAction(MotionEvent.ACTION_CANCEL); super.dispatchTouchEvent(up); up.recycle(); // Send a 'DOWN' event to the panel. (We'll cheat // and hijack this one) ev.setAction(MotionEvent.ACTION_DOWN); } mIsScrollableViewHandlingTouch = false; return this.onTouchEvent(ev); } else if (dy * (mIsSlidingUp ? 1 : -1) < 0) { // Expanding // Is the panel less than fully expanded? // Then we'll handle the drag here. if (mSlideOffset < 1.0f) { mIsScrollableViewHandlingTouch = false; return this.onTouchEvent(ev); } // Was the panel handling the touch previously? // Then we need to rejigger things so that the // child gets a proper down event. if (!mIsScrollableViewHandlingTouch && mDragHelper.isDragging()) { mDragHelper.cancel(); ev.setAction(MotionEvent.ACTION_DOWN); } mIsScrollableViewHandlingTouch = true; return super.dispatchTouchEvent(ev); } } else if (action == MotionEvent.ACTION_UP) { // If the scrollable view was handling the touch and we receive an up // we want to clear any previous dragging state so we don't intercept a touch stream accidentally if (mIsScrollableViewHandlingTouch) { mDragHelper.setDragState(ViewDragHelper.STATE_IDLE); } } // In all other cases, just let the default behavior take over. return super.dispatchTouchEvent(ev); } private boolean isViewUnder(View view, int x, int y) { if (view == null) return false; int[] viewLocation = new int[2]; view.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] + view.getWidth() && screenY >= viewLocation[1] && screenY < viewLocation[1] + view.getHeight(); } /* * Computes the top position of the panel based on the slide offset. */ 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 */ 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, boolean smoothSlide) { 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) { setPanelStateInternal(state); } else { if (mSlideState == PanelState.HIDDEN) { mSlideableView.setVisibility(View.VISIBLE); requestLayout(); } switch (state) { case ANCHORED: smoothSlideTo(mAnchorPoint, 0, smoothSlide); break; case COLLAPSED: smoothSlideTo(0, 0, smoothSlide); break; case EXPANDED: smoothSlideTo(1.0f, 0, smoothSlide); break; case HIDDEN: int newTop = computePanelTopPosition(0.0f) + (mIsSlidingUp ? +mPanelHeight : -mPanelHeight); smoothSlideTo(computeSlideOffset(newTop), 0, smoothSlide); break; } } } void setPanelStateInternal(PanelState state) { if (mSlideState == state) return; PanelState oldState = mSlideState; mSlideState = state; dispatchOnPanelStateChanged(this, oldState, state); } /** * Update the parallax based on the current slide offset. */ @SuppressLint("NewApi") void applyParallaxForCurrentSlideOffset() { if (mParallaxOffset > 0) { int mainViewOffset = getCurrentParallaxOffset(); mMainView.setTranslationY(mainViewOffset); } } void onPanelDragged(int newTop) { mLastNotDraggingSlideState = mSlideState; setPanelStateInternal(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); if (lp.height == defaultHeight) { lp.height = LayoutParams.MATCH_PARENT; } mMainView.requestLayout(); } else if (lp.height != LayoutParams.MATCH_PARENT && !mOverlayContent) { lp.height = LayoutParams.MATCH_PARENT; 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, boolean smoothSlide) { if (!isEnabled()) { // Nothing to do. return false; } int panelTop = computePanelTopPosition(slideOffset); if (mDragHelper.smoothSlideViewTo(mSlideableView, mSlideableView.getLeft(), panelTop, smoothSlide)) { 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 && mSlideableView != 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 != null ? ss.mSlideState : DEFAULT_SLIDE_STATE; } private class DragHelperCallback extends ViewDragHelper.Callback { DragHelperCallback() { } @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) { updateObscuredViewVisibility(); setPanelStateInternal(PanelState.EXPANDED); } else if (mSlideOffset == 0) { setPanelStateInternal(PanelState.COLLAPSED); } else if (mSlideOffset < 0) { setPanelStateInternal(PanelState.HIDDEN); mSlideableView.setVisibility(View.INVISIBLE); } else { updateObscuredViewVisibility(); setPanelStateInternal(PanelState.ANCHORED); } } } @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 && mSlideOffset <= mAnchorPoint) { // swipe up -> expand and stop at anchor point target = computePanelTopPosition(mAnchorPoint); } else if (direction > 0 && mSlideOffset > mAnchorPoint) { // swipe up past anchor -> expand target = computePanelTopPosition(1.0f); } else if (direction < 0 && mSlideOffset >= mAnchorPoint) { // swipe down -> collapse and stop at anchor point target = computePanelTopPosition(mAnchorPoint); } else if (direction < 0 && mSlideOffset < mAnchorPoint) { // swipe down past anchor -> collapse target = computePanelTopPosition(0.0f); } else if (mSlideOffset >= (1.f + mAnchorPoint) / 2) { // zero velocity, and far enough from anchor point => expand to the top target = computePanelTopPosition(1.0f); } else if (mSlideOffset >= mAnchorPoint / 2) { // zero velocity, and close enough to anchor point => go to anchor 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 float weight = 0; public LayoutParams() { super(MATCH_PARENT, MATCH_PARENT); } public LayoutParams(int width, int height) { super(width, height); } public LayoutParams(int width, int height, float weight) { super(width, height); this.weight = weight; } 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 ta = c.obtainStyledAttributes(attrs, ATTRS); if (ta != null) { this.weight = ta.getFloat(0, 0); } ta.recycle(); } } static class SavedState extends BaseSavedState { PanelState mSlideState; SavedState(Parcelable superState) { super(superState); } SavedState(Parcel in) { super(in); String panelStateString = in.readString(); try { mSlideState = panelStateString != null ? Enum.valueOf(PanelState.class, panelStateString) : PanelState.COLLAPSED; } catch (IllegalArgumentException e) { mSlideState = PanelState.COLLAPSED; } } @Override public void writeToParcel(Parcel out, int flags) { super.writeToParcel(out, flags); out.writeString(mSlideState == null ? null : 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]; } }; } }