diff options
Diffstat (limited to 'src/org/lineageos/eleven/slidinguppanel/SlidingUpPanelLayout.java')
-rw-r--r-- | src/org/lineageos/eleven/slidinguppanel/SlidingUpPanelLayout.java | 1375 |
1 files changed, 1375 insertions, 0 deletions
diff --git a/src/org/lineageos/eleven/slidinguppanel/SlidingUpPanelLayout.java b/src/org/lineageos/eleven/slidinguppanel/SlidingUpPanelLayout.java new file mode 100644 index 0000000..55bd4d3 --- /dev/null +++ b/src/org/lineageos/eleven/slidinguppanel/SlidingUpPanelLayout.java @@ -0,0 +1,1375 @@ +package com.cyanogenmod.eleven.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 com.cyanogenmod.eleven.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 SlideState DEFAULT_SLIDE_STATE = SlideState.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; + + /** + * Whether we should hook up the drag view clickable state + */ + private static final boolean DEFAULT_DRAG_VIEW_CLICKABLE = true; + + /** + * 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 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; + + /** + * Default slide panel offset when collapsed + */ + private static final int DEFAULT_SLIDE_PANEL_OFFSET = 0; + + /** + * Default direct offset flag + */ + private static final boolean DEFAULT_DIRECT_OFFSET_FLAG = false; + + /** + * 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; + + /** + * Determines how much to slide the panel off when expanded + */ + private int mSlidePanelOffset = 0; + + /** + * The size of the shadow in pixels. + */ + private int mShadowHeight = -1; + + /** + * Paralax offset + */ + private int mParallaxOffset = -1; + + /** + * Clamps the Main view to the slideable view + */ + private boolean mDirectOffset = false; + + /** + * 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; + + /** + * 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; + + /** + * Whether clicking on the drag view will expand/collapse + */ + private boolean mDragViewClickable = DEFAULT_DRAG_VIEW_CLICKABLE; + + /** + * The child view that can slide, if any. + */ + private View mSlideableView; + + /** + * The main view + */ + private View mMainView; + + /** + * The background view + */ + private View mBackgroundView; + + /** + * Current state of the slideable view. + */ + private enum SlideState { + EXPANDED, + COLLAPSED, + ANCHORED, + HIDDEN, + DRAGGING + } + private SlideState mSlideState = SlideState.COLLAPSED; + + /** + * 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 mIsSlidingEnabled; + + /** + * 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; + + 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); + if (gravity != Gravity.TOP && gravity != Gravity.BOTTOM) { + throw new IllegalArgumentException("gravity must be set to either top or bottom"); + } + mIsSlidingUp = gravity == Gravity.BOTTOM; + } + + defAttrs.recycle(); + + TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.SlidingUpPanelLayout); + + if (ta != null) { + mPanelHeight = ta.getDimensionPixelSize(R.styleable.SlidingUpPanelLayout_panelHeight, -1); + mSlidePanelOffset = ta.getDimensionPixelSize(R.styleable.SlidingUpPanelLayout_slidePanelOffset, DEFAULT_SLIDE_PANEL_OFFSET); + mShadowHeight = ta.getDimensionPixelSize(R.styleable.SlidingUpPanelLayout_shadowHeight, -1); + mParallaxOffset = ta.getDimensionPixelSize(R.styleable.SlidingUpPanelLayout_paralaxOffset, -1); + mDirectOffset = ta.getBoolean(R.styleable.SlidingUpPanelLayout_directOffset,DEFAULT_DIRECT_OFFSET_FLAG); + + mMinFlingVelocity = ta.getInt(R.styleable.SlidingUpPanelLayout_flingVelocity, DEFAULT_MIN_FLING_VELOCITY); + mCoveredFadeColor = ta.getColor(R.styleable.SlidingUpPanelLayout_fadeColor, DEFAULT_FADE_COLOR); + + mDragViewResId = ta.getResourceId(R.styleable.SlidingUpPanelLayout_dragView, -1); + mDragViewClickable = ta.getBoolean(R.styleable.SlidingUpPanelLayout_dragViewClickable, DEFAULT_DRAG_VIEW_CLICKABLE); + + mOverlayContent = ta.getBoolean(R.styleable.SlidingUpPanelLayout_overlay,DEFAULT_OVERLAY_FLAG); + + mAnchorPoint = ta.getFloat(R.styleable.SlidingUpPanelLayout_anchorPoint, DEFAULT_ANCHOR_POINT); + + mSlideState = SlideState.values()[ta.getInt(R.styleable.SlidingUpPanelLayout_initialState, 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); + + mIsSlidingEnabled = true; + } + + /** + * Set the Drag View after the view is inflated + */ + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + if (mDragViewResId != -1) { + setDragView(findViewById(mDragViewResId)); + } + } + + /** + * 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 setSlidingEnabled(boolean enabled) { + mIsSlidingEnabled = enabled; + } + + public boolean isSlidingEnabled() { + return mIsSlidingEnabled && mSlideableView != null; + } + + /** + * Set the collapsed panel height in pixels + * + * @param val A height in pixels + */ + public void setPanelHeight(int val) { + mPanelHeight = val; + requestLayout(); + } + + /** + * @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(); + } + + /** + * @return The current paralax offset + */ + public int getCurrentParalaxOffset() { + if (mParallaxOffset < 0) { + return 0; + } + + return (int)(mParallaxOffset * getDirectionalSlideOffset()); + } + + /** + * @return The directional slide offset + */ + protected float getDirectionalSlideOffset() { + return mIsSlidingUp ? -mSlideOffset : mSlideOffset; + } + + /** + * 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 && mDragViewClickable) { + mDragView.setOnClickListener(null); + } + mDragView = dragView; + if (mDragView != null) { + mDragView.setClickable(true); + mDragView.setFocusable(false); + mDragView.setFocusableInTouchMode(false); + if (mDragViewClickable) { + mDragView.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (!isEnabled()) return; + if (!isPanelExpanded() && !isPanelAnchored()) { + expandPanel(mAnchorPoint); + } else { + collapsePanel(); + } + } + }); + } + } + } + + /** + * 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; + } + + 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 = mMainView; + 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 && childCount != 3) { + throw new IllegalStateException("Sliding up panel layout must have exactly 2 or 3 children!"); + } + + if (childCount == 2) { + mMainView = getChildAt(0); + mSlideableView = getChildAt(1); + } else { + mBackgroundView = getChildAt(0); + mMainView = getChildAt(1); + mSlideableView = getChildAt(2); + } + + if (mDragView == null) { + setDragView(mSlideableView); + } + + // If the sliding panel is not visible, then put the whole view in the hidden state + if (mSlideableView.getVisibility() == GONE) { + mSlideState = SlideState.HIDDEN; + } + + int layoutHeight = heightSize - getPaddingTop() - getPaddingBottom(); + + // 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 && child == mMainView) { + continue; + } + + int height = layoutHeight; + if (child == mMainView && !mOverlayContent && mSlideState != SlideState.HIDDEN) { + height -= mPanelHeight; + } + + int childWidthSpec; + if (lp.width == LayoutParams.WRAP_CONTENT) { + childWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.AT_MOST); + } else if (lp.width == LayoutParams.MATCH_PARENT) { + childWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, 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); + } + + if (child == mSlideableView) { + mSlideRange = MeasureSpec.getSize(childHeightSpec) - mPanelHeight + mSlidePanelOffset; + childHeightSpec += mSlidePanelOffset; + } + + child.measure(childWidthSpec, childHeightSpec); + } + + 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); + + // Always layout the sliding view on the first layout + if (child.getVisibility() == GONE && (child == mMainView || 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; + final int childRight = childLeft + child.getMeasuredWidth(); + + child.layout(childLeft, childTop, childRight, childBottom); + } + + if (mFirstLayout) { + updateObscuredViewVisibility(); + } + + 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 void setEnabled(boolean enabled) { + if (!enabled) { + collapsePanel(); + } + super.setEnabled(enabled); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + final int action = MotionEventCompat.getActionMasked(ev); + + + if (!isEnabled() || !mIsSlidingEnabled || (mIsUnableToDrag && action != MotionEvent.ACTION_DOWN)) { + mDragHelper.cancel(); + return super.onInterceptTouchEvent(ev); + } + + if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { + mDragHelper.cancel(); + return false; + } + + final float x = ev.getX(); + final float y = ev.getY(); + + switch (action) { + case MotionEvent.ACTION_DOWN: { + mIsUnableToDrag = false; + mInitialMotionX = x; + mInitialMotionY = y; + break; + } + + case MotionEvent.ACTION_MOVE: { + final float adx = Math.abs(x - mInitialMotionX); + final float ady = Math.abs(y - mInitialMotionY); + final int dragSlop = mDragHelper.getTouchSlop(); + + // Handle any horizontal scrolling on the drag view. + if (mIsUsingDragViewTouchEvents && adx > dragSlop && ady < dragSlop) { + return super.onInterceptTouchEvent(ev); + } + + if ((ady > dragSlop && adx > ady) || !isDragViewUnder((int)mInitialMotionX, (int)mInitialMotionY)) { + mDragHelper.cancel(); + mIsUnableToDrag = true; + return false; + } + break; + } + } + + return mDragHelper.shouldInterceptTouchEvent(ev); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (!isSlidingEnabled()) { + return super.onTouchEvent(ev); + } + mDragHelper.processTouchEvent(ev); + return true; + } + + 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 expandPanel(View pane, int initialVelocity, float mSlideOffset) { + return mFirstLayout || smoothSlideTo(mSlideOffset, initialVelocity); + } + + private boolean collapsePanel(View pane, int initialVelocity) { + return mFirstLayout || smoothSlideTo(0.0f, initialVelocity); + } + + /* + * 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); + } + + /** + * Collapse the sliding pane if it is currently slideable. If first layout + * has already completed this will animate. + * + * @return true if the pane was slideable and is now collapsed/in the process of collapsing + */ + public boolean collapsePanel() { + if (mFirstLayout) { + mSlideState = SlideState.COLLAPSED; + return true; + } else { + if (mSlideState == SlideState.HIDDEN || mSlideState == SlideState.COLLAPSED) + return false; + return collapsePanel(mSlideableView, 0); + } + } + + /** + * Expand the sliding pane if it is currently slideable. + * + * @return true if the pane was slideable and is now expanded/in the process of expading + */ + public boolean expandPanel() { + if (mFirstLayout) { + mSlideState = SlideState.EXPANDED; + return true; + } else { + return expandPanel(1.0f); + } + } + + /** + * Expand the sliding pane to the anchor point if it is currently slideable. + * + * @return true if the pane was slideable and is now expanded/in the process of expading + */ + public boolean anchorPanel() { + if (mFirstLayout) { + mSlideState = SlideState.ANCHORED; + return true; + } else { + return expandPanel(mAnchorPoint); + } + } + + /** + * Partially expand the sliding panel up to a specific offset + * + * @param mSlideOffset Value between 0 and 1, where 0 is completely expanded. + * @return true if the pane was slideable and is now expanded/in the process of expanding + */ + public boolean expandPanel(float mSlideOffset) { + if (mSlideableView == null || mSlideState == SlideState.EXPANDED) return false; + mSlideableView.setVisibility(View.VISIBLE); + return expandPanel(mSlideableView, 0, mSlideOffset); + } + + /** + * Check if the sliding panel in this layout is fully expanded. + * + * @return true if sliding panel is completely expanded + */ + public boolean isPanelExpanded() { + return mSlideState == SlideState.EXPANDED; + } + + /** + * Check if the sliding panel in this layout is anchored. + * + * @return true if sliding panel is anchored + */ + public boolean isPanelAnchored() { + return mSlideState == SlideState.ANCHORED; + } + + /** + * Check if the sliding panel in this layout is currently visible. + * + * @return true if the sliding panel is visible. + */ + public boolean isPanelHidden() { + return mSlideState == SlideState.HIDDEN; + } + + /** + * Shows the panel from the hidden state + */ + public void showPanel() { + if (mFirstLayout) { + mSlideState = SlideState.COLLAPSED; + } else { + if (mSlideableView == null || mSlideState != SlideState.HIDDEN) return; + mSlideableView.setVisibility(View.VISIBLE); + requestLayout(); + smoothSlideTo(0, 0); + } + } + + /** + * Hides the sliding panel entirely. + */ + public void hidePanel() { + if (mFirstLayout) { + mSlideState = SlideState.HIDDEN; + } else { + if (mSlideState == SlideState.DRAGGING || mSlideState == SlideState.HIDDEN) return; + int newTop = computePanelTopPosition(0.0f) + (mIsSlidingUp ? +mPanelHeight : -mPanelHeight); + smoothSlideTo(computeSlideOffset(newTop), 0); + } + } + + @SuppressLint("NewApi") + private void onPanelDragged(int newTop) { + mSlideState = SlideState.DRAGGING; + // Recompute the slide offset based on the new top position + mSlideOffset = computeSlideOffset(newTop); + // Update the parallax based on the new slide offset + if ((mParallaxOffset > 0 || mDirectOffset) && mSlideOffset >= 0) { + int mainViewOffset = 0; + if (mParallaxOffset > 0) { + mainViewOffset = getCurrentParalaxOffset(); + } else { + mainViewOffset = (int)(getDirectionalSlideOffset() * mSlideRange); + } + + mMainView.setTranslationY(mainViewOffset); + } + + // 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 + if (mSlideOffset <= 0 && !mOverlayContent) { + // expand the main view + LayoutParams lp = (LayoutParams)mMainView.getLayoutParams(); + lp.height = mIsSlidingUp ? (newTop - getPaddingBottom()) : (getHeight() - getPaddingBottom() - mSlideableView.getMeasuredHeight() - newTop); + mMainView.requestLayout(); + } + } + + @Override + protected boolean drawChild(Canvas canvas, View child, long drawingTime) { + boolean result; + final int save = canvas.save(Canvas.CLIP_SAVE_FLAG); + + if (isSlidingEnabled() && mMainView == child) { + // Clip against the slider; no sense drawing what will immediately be covered, + // Unless the panel is set to overlay content + if (!mOverlayContent) { + canvas.getClipBounds(mTmpRect); + if (mIsSlidingUp) { + mTmpRect.bottom = Math.min(mTmpRect.bottom, mSlideableView.getTop()); + } else { + mTmpRect.top = Math.max(mTmpRect.top, mSlideableView.getBottom()); + } + canvas.clipRect(mTmpRect); + } + } + + result = super.drawChild(canvas, child, drawingTime); + canvas.restoreToCount(save); + + 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); + } + + 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 (!isSlidingEnabled()) { + // 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 (!isSlidingEnabled()) { + mDragHelper.abort(); + return; + } + + ViewCompat.postInvalidateOnAnimation(this); + } + } + + @Override + public void draw(Canvas c) { + super.draw(c); + + if (!isSlidingEnabled()) { + // No need to draw a shadow if we don't have one. + return; + } + + 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(); + + if (mShadowDrawable != null) { + 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); + ss.mSlideState = mSlideState; + + 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()); + + if (mSlideOffset == 1) { + if (mSlideState != SlideState.EXPANDED) { + updateObscuredViewVisibility(); + mSlideState = SlideState.EXPANDED; + dispatchOnPanelExpanded(mSlideableView); + } + } else if (mSlideOffset == 0) { + if (mSlideState != SlideState.COLLAPSED) { + mSlideState = SlideState.COLLAPSED; + dispatchOnPanelCollapsed(mSlideableView); + } + } else if (mSlideOffset < 0) { + mSlideState = SlideState.HIDDEN; + mSlideableView.setVisibility(View.GONE); + dispatchOnPanelHidden(mSlideableView); + } else if (mSlideState != SlideState.ANCHORED) { + updateObscuredViewVisibility(); + mSlideState = SlideState.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 { + SlideState mSlideState; + + SavedState(Parcelable superState) { + super(superState); + } + + private SavedState(Parcel in) { + super(in); + try { + mSlideState = Enum.valueOf(SlideState.class, in.readString()); + } catch (IllegalArgumentException e) { + mSlideState = SlideState.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]; + } + }; + } +} |