package com.android.widget; /* * Copyright (C) 2012 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /* * This class originally comes from the android support library v4: * android.support.v4.widget.SlidingPaneLayout * It was modified to slide the view vertically instead of horizontally * and a simplified to fit the needs of the VLC Android app. */ import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; 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.AccessibilityDelegateCompat; import android.support.v4.view.MotionEventCompat; import android.support.v4.view.ViewCompat; import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; import android.support.v4.widget.ViewDragHelper; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; import android.view.accessibility.AccessibilityEvent; import org.videolan.vlc.BuildConfig; import org.videolan.vlc.R; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.ArrayList; public class SlidingPaneLayout extends ViewGroup { private static final String TAG = "VLC/SlidingPaneLayout"; /** * Default size of the overhang for a pane in the open state. * At least this much of a sliding pane will remain visible. * This indicates that there is more content available and provides * a "physical" edge to grab to pull it closed. */ private static final int DEFAULT_OVERHANG_SIZE = 64; // dp; /** * Minimum velocity that will be detected as a fling */ private static final int MIN_FLING_VELOCITY = 400; // dips per second /** * The fade color used for the panel covered by the slider. 0 = no fading. */ private int mCoveredFadeColor; /** * Drawable used to draw the shadow between panes. */ private Drawable mShadowDrawable; /** * The size of the overhang in pixels. * This is the minimum section of the sliding panel that will * be visible in the open state to allow for a closing drag. */ private int mOverhangSize; /** * True if a panel can slide with the current measurements */ private boolean mCanSlide; /** * The child view that can slide, if any. */ private View mSlideableView; /** * How far the panel is offset from its closed position. * range [0, 1] where 0 = closed, 1 = open. */ 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; /** * The opening states of the pane. */ public final int STATE_OPENED_ENTIRELY = 0; public final int STATE_OPENED = 1; public final int STATE_CLOSED = 2; private int mState; private float mInitialMotionX; private float mInitialMotionY; private final ViewDragHelper mDragHelper; private boolean mFirstLayout = true; private final Rect mTmpRect = new Rect(); private final ArrayList<DisableLayerRunnable> mPostedRunnables = new ArrayList<DisableLayerRunnable>(); static final SlidingPanelLayoutImpl IMPL; static { final int deviceVersion = Build.VERSION.SDK_INT; if (deviceVersion >= 16) { IMPL = new SlidingPanelLayoutImplJB(); } else { IMPL = new SlidingPanelLayoutImplBase(); } } private PanelSlideListener mPanelSlideListener; /** * Listener for monitoring events about the sliding pane. */ public interface PanelSlideListener { /** * Called when the sliding pane position changes. * @param slideOffset The new offset of this sliding pane within its range, from 0-1 */ public void onPanelSlide(float slideOffset); /** * Called when the sliding pane becomes slid open. */ public void onPanelOpened(); /** * Called when the sliding pane becomes slid open entirely. */ public void onPanelOpenedEntirely(); /** * Called when a sliding pane becomes slid completely closed. */ public void onPanelClosed(); } public SlidingPaneLayout(Context context) { this(context, null); } public SlidingPaneLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public SlidingPaneLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mOverhangSize = -1; if (attrs != null) { TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.SlidingPaneLayout); if (ta != null) { mOverhangSize = ta.getDimensionPixelSize(R.styleable.SlidingPaneLayout_overhangSize, -1); ta.recycle(); } } final float density = context.getResources().getDisplayMetrics().density; if (mOverhangSize == -1) { mOverhangSize = (int) (DEFAULT_OVERHANG_SIZE * density + 0.5f); } setWillNotDraw(false); ViewCompat.setAccessibilityDelegate(this, new AccessibilityDelegate()); ViewCompat.setImportantForAccessibility(this, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); mDragHelper = ViewDragHelper.create(this, 1.0f, new DragHelperCallback()); mDragHelper.setMinVelocity(MIN_FLING_VELOCITY * density); } /** * Set the panel slide listener. * @param l the PanelSlideListener */ public void setPanelSlideListener(PanelSlideListener l) { mPanelSlideListener = l; } /** * Set the color used to fade the pane covered by the sliding pane out when the pane * will become fully covered in the closed state. * * @param color An ARGB-packed color value */ public void setCoveredFadeColor(int color) { mCoveredFadeColor = color; } /** * @return The ARGB-packed color value used to fade the fixed pane */ public int getCoveredFadeColor() { return mCoveredFadeColor; } void updateObscuredViewsVisibility(View panel) { 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 (panel != null && viewIsOpaque(panel)) { left = panel.getLeft(); right = panel.getRight(); top = panel.getTop(); bottom = panel.getBottom(); } else { left = right = top = bottom = 0; } for (int i = 0, childCount = getChildCount(); i < childCount; i++) { final View child = getChildAt(i); if (child == panel) { // There are still more children above the panel but they won't be affected. break; } 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 viewIsOpaque(View v) { if (ViewCompat.isOpaque(v)) return true; // View#isOpaque didn't take all valid opaque scrollbar modes into account // before API 18 (JB-MR2). On newer devices rely solely on isOpaque above and return false // here. On older devices, check the view's background drawable directly as a fallback. if (Build.VERSION.SDK_INT >= 18) return false; final Drawable bg = v.getBackground(); if (bg != null) { return bg.getOpacity() == PixelFormat.OPAQUE; } return false; } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); mFirstLayout = true; } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); mFirstLayout = true; for (int i = 0, count = mPostedRunnables.size(); i < count; i++) { final DisableLayerRunnable dlr = mPostedRunnables.get(i); dlr.run(); } mPostedRunnables.clear(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); if (heightMode != MeasureSpec.EXACTLY) { if (isInEditMode()) { // Don't crash the layout editor. Consume all of the space if specified // or pick a magic number from thin air otherwise. // TODO Better communication with tools of this bogus state. // It will crash on a real device. if (heightMode == MeasureSpec.AT_MOST) { heightMode = MeasureSpec.EXACTLY; } else if (heightMode == MeasureSpec.UNSPECIFIED) { heightMode = 300; } } else { throw new IllegalStateException("Height must have an exact value or MATCH_PARENT"); } } else if (widthMode == MeasureSpec.UNSPECIFIED) { if (isInEditMode()) { // Don't crash the layout editor. Pick a magic number from thin air instead. // TODO Better communication with tools of this bogus state. // It will crash on a real device. if (widthMode == MeasureSpec.UNSPECIFIED) { widthMode = 300; } } else { throw new IllegalStateException("Width must not be UNSPECIFIED"); } } int layoutWidth = 0; int maxLayoutWidth = -1; switch (widthMode) { case MeasureSpec.EXACTLY: layoutWidth = maxLayoutWidth = widthSize - getPaddingLeft() - getPaddingRight(); break; case MeasureSpec.AT_MOST: maxLayoutWidth = widthSize - getPaddingLeft() - getPaddingRight(); break; } boolean canSlide = false; final int heightAvailable = heightSize - getPaddingTop() - getPaddingBottom(); final int childCount = getChildCount(); if (childCount > 2) { Log.e(TAG, "onMeasure: More than two child views are not supported."); } // We'll find the current one below. mSlideableView = null; // 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(); if (child.getVisibility() == GONE) { continue; } int childHeightSpec; final int verticalMargin = lp.topMargin + lp.bottomMargin; if (lp.height == LayoutParams.WRAP_CONTENT) { childHeightSpec = MeasureSpec.makeMeasureSpec(heightAvailable - verticalMargin, MeasureSpec.AT_MOST); } else if (lp.height == LayoutParams.MATCH_PARENT) { childHeightSpec = MeasureSpec.makeMeasureSpec(heightAvailable - verticalMargin, MeasureSpec.EXACTLY); } else { childHeightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY); } int childWidthSpec; if (lp.width == LayoutParams.WRAP_CONTENT) { childWidthSpec = MeasureSpec.makeMeasureSpec(maxLayoutWidth, MeasureSpec.AT_MOST); } else if (lp.width == LayoutParams.MATCH_PARENT) { childWidthSpec = MeasureSpec.makeMeasureSpec(maxLayoutWidth, MeasureSpec.EXACTLY); } else { childWidthSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY); } child.measure(childWidthSpec, childHeightSpec); final int childWidth = child.getMeasuredWidth(); if (widthMode == MeasureSpec.AT_MOST && childWidth > layoutWidth) { layoutWidth = Math.min(childWidth, maxLayoutWidth); } if (i == childCount - 1) { canSlide |= lp.slideable = true; mSlideableView = child; } } final int measuredHeight = heightSize; final int measuredWidth = layoutWidth + getPaddingLeft() + getPaddingRight(); setMeasuredDimension(measuredWidth, measuredHeight); mCanSlide = canSlide; if (mDragHelper.getViewDragState() != ViewDragHelper.STATE_IDLE && !canSlide) { // Cancel scrolling in progress, it's no longer relevant. mDragHelper.abort(); } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { final int height = b - t; final int paddingTop = getPaddingTop(); final int paddingBottom = getPaddingBottom(); final int paddingLeft = getPaddingLeft(); final int childCount = getChildCount(); int yStart = paddingTop; int nextYStart = yStart; for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); if (child.getVisibility() == GONE) { continue; } final LayoutParams lp = (LayoutParams) child.getLayoutParams(); final int childHeight = child.getMeasuredHeight(); int offset = 0; if (lp.slideable) { final int margin = lp.topMargin + lp.bottomMargin; final int range = Math.min(nextYStart, height - paddingBottom) - yStart - margin; mSlideRange = range; if (mFirstLayout) { switch (mState) { case STATE_CLOSED: mSlideOffset = mCanSlide ? 0.f : 1.f; break; case STATE_OPENED: mSlideOffset = mCanSlide ? 1 - (float)mOverhangSize / mSlideRange : 1.f; break; default: // STATE_OPENED_ENTIRELY mSlideOffset = 1.f; break; } } final int pos = (int) (range * mSlideOffset); yStart += pos + lp.topMargin; mSlideOffset = (float) pos / mSlideRange; } else { yStart = nextYStart; } final int childTop = yStart - offset; final int childBottom = childTop + childHeight; final int childLeft = paddingLeft; final int childRight = childLeft + child.getMeasuredWidth(); child.layout(paddingLeft, childTop, childRight, childBottom); nextYStart += child.getHeight(); } if (mFirstLayout) { updateObscuredViewsVisibility(mSlideableView); } 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) { final int action = MotionEventCompat.getActionMasked(ev); if (!mCanSlide || (mIsUnableToDrag && action != MotionEvent.ACTION_DOWN)) { mDragHelper.cancel(); return super.onInterceptTouchEvent(ev); } if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { mDragHelper.cancel(); return false; } float x,y; switch (action) { case MotionEvent.ACTION_DOWN: mIsUnableToDrag = false; x = ev.getX(); y = ev.getY(); mInitialMotionX = x; mInitialMotionY = y; break; case MotionEvent.ACTION_MOVE: x = ev.getX(); y = ev.getY(); final float adx = Math.abs(x - mInitialMotionX); final float ady = Math.abs(y - mInitialMotionY); final int slop = mDragHelper.getTouchSlop(); if (ady > slop && adx > ady) { mDragHelper.cancel(); mIsUnableToDrag = true; return false; } } final boolean interceptForDrag = mDragHelper.shouldInterceptTouchEvent(ev) // Intercept touch events only in the overhang area. && (ev.getY() <= mSlideOffset * mSlideRange + mOverhangSize && mState != STATE_OPENED_ENTIRELY); return interceptForDrag; } @Override public boolean onTouchEvent(MotionEvent ev) { if (!mCanSlide) { return super.onTouchEvent(ev); } try { mDragHelper.processTouchEvent(ev); } catch (IllegalArgumentException ex) { } catch (ArrayIndexOutOfBoundsException ex) { } return true; } private boolean closePane(View pane, int initialVelocity) { if (mFirstLayout) { mState = STATE_CLOSED; if (mPanelSlideListener != null) mPanelSlideListener.onPanelClosed(); return true; } else if (smoothSlideTo(0.f, initialVelocity)) return true; else return false; } private boolean openPaneEntirely(View pane, int initialVelocity) { if (mFirstLayout) { mState = STATE_OPENED_ENTIRELY; if (mPanelSlideListener != null) mPanelSlideListener.onPanelOpenedEntirely(); return true; } else if (smoothSlideTo(1.f, initialVelocity)) return true; else return false; } private boolean openPane(View pane, int initialVelocity) { if (mFirstLayout) { mState = STATE_OPENED; if (mPanelSlideListener != null) mPanelSlideListener.onPanelOpened(); return true; } else if (smoothSlideTo(1 - (float)mOverhangSize / mSlideRange, initialVelocity)) return true; else return false; } /** * Open partially 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 open/in the process of opening */ public boolean openPane() { return openPane(mSlideableView, 0); } /** * Open entirely 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 closed/in the process of closing */ public boolean openPaneEntirely() { return openPaneEntirely(mSlideableView, 0); } /** * Close 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 closed/in the process of closing */ public boolean closePane() { return closePane(mSlideableView, 0); } /** * Return the open state of the pane. * @return STATE_OPEN, STATE_OPEN_ENTIRELY or STATE_CLOSED */ public int getState() { return mState; } /** * Check if the content in this layout cannot fully fit side by side and therefore * the content pane can be slid back and forth. * * @return true if content in this layout can be slid open and closed */ public boolean isSlideable() { return mCanSlide; } private void onPanelDragged(int newTop) { if (mSlideableView == null) { // This can happen if we're aborting motion during layout because everything now fits. mSlideOffset = 0; return; } final LayoutParams lp = (LayoutParams) mSlideableView.getLayoutParams(); final int topBound = getPaddingTop() + lp.topMargin; mSlideOffset = (float) (newTop - topBound) / mSlideRange; } @Override protected boolean drawChild(Canvas canvas, View child, long drawingTime) { final LayoutParams lp = (LayoutParams) child.getLayoutParams(); boolean result; final int save = canvas.save(Canvas.CLIP_SAVE_FLAG); if (mCanSlide && !lp.slideable && mSlideableView != null) { // Clip against the slider; no sense drawing what will immediately be covered. canvas.getClipBounds(mTmpRect); mTmpRect.bottom = Math.min(mTmpRect.bottom, mSlideableView.getTop()); canvas.clipRect(mTmpRect); } result = super.drawChild(canvas, child, drawingTime); canvas.restoreToCount(save); return result; } private void invalidateChildRegion(View v) { IMPL.invalidateChildRegion(this, v); } /** * 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 (!mCanSlide) { // Nothing to do. return false; } final LayoutParams lp = (LayoutParams) mSlideableView.getLayoutParams(); final int topBound = getPaddingTop() + lp.topMargin; int y = (int) (topBound + slideOffset * mSlideRange); if (mDragHelper.smoothSlideViewTo(mSlideableView, mSlideableView.getLeft(), y)) { setAllChildrenVisible(); ViewCompat.postInvalidateOnAnimation(this); return true; } return false; } @Override public void computeScroll() { if (mDragHelper.continueSettling(true)) { if (!mCanSlide) { mDragHelper.abort(); return; } ViewCompat.postInvalidateOnAnimation(this); } } /** * Set a drawable to use as a shadow cast by the right pane onto the left pane * during opening/closing. * * @param d drawable to use as a shadow */ public void setShadowDrawable(Drawable d) { mShadowDrawable = d; } /** * Set a drawable to use as a shadow cast by the right pane onto the left pane * during opening/closing. * * @param resId Resource ID of a drawable to use */ public void setShadowResource(int resId) { setShadowDrawable(getResources().getDrawable(resId)); } @Override public void draw(Canvas c) { super.draw(c); final View shadowView = getChildCount() > 1 ? getChildAt(1) : null; if (shadowView == null || mShadowDrawable == null) { // No need to draw a shadow if we don't have one. return; } final int shadowHeight = mShadowDrawable.getIntrinsicHeight(); final int right = shadowView.getRight(); final int left = shadowView.getLeft(); final int bottom = shadowView.getTop(); final int top = bottom - shadowHeight; mShadowDrawable.setBounds(left, top, right, bottom); mShadowDrawable.draw(c); } /** * Tests scrollability within child views of v given a delta of dy. * * @param v View to test for vertical scrollability * @param checkV Whether the view v passed should itself be checked for scrollability (true), * or just its children (false). * @param dy 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 dy. */ protected boolean canScroll(View v, boolean checkV, int dy, 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--) { // TODO: Add versioned support here for transformed views. // This will not work for transformed views in Honeycomb+ 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, dy, x + scrollX - child.getLeft(), y + scrollY - child.getTop())) { return true; } } } return checkV && ViewCompat.canScrollVertically(v, -dy); } @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 protected Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); SavedState ss = new SavedState(superState); ss.state = mState; return ss; } @Override protected void onRestoreInstanceState(Parcelable state) { SavedState ss = (SavedState) state; super.onRestoreInstanceState(ss.getSuperState()); switch(ss.state) { case STATE_OPENED: openPane(); break; case STATE_OPENED_ENTIRELY: openPaneEntirely(); break; case STATE_CLOSED: closePane(); } } private class DragHelperCallback extends ViewDragHelper.Callback { @Override public boolean tryCaptureView(View child, int pointerId) { if (mIsUnableToDrag) { return false; } return ((LayoutParams) child.getLayoutParams()).slideable; } @Override public void onViewCaptured(View capturedChild, int activePointerId) { // Make all child views visible in preparation for sliding things around setAllChildrenVisible(); } @Override public void onViewDragStateChanged(int state) { if (mDragHelper.getViewDragState() == ViewDragHelper.STATE_IDLE) { if (mSlideOffset == 0) { if (mState != STATE_CLOSED) { mState = STATE_CLOSED; if (mPanelSlideListener != null) mPanelSlideListener.onPanelClosed(); } } else if (Math.abs(mSlideOffset - (1 - (float)mOverhangSize / mSlideRange)) <= 0.001) { if (mState != STATE_OPENED) { mState = STATE_OPENED; if (mPanelSlideListener != null) mPanelSlideListener.onPanelOpened(); } } else if (mSlideOffset == 1) { if (mState != STATE_OPENED_ENTIRELY) { mState = STATE_OPENED_ENTIRELY; if (mPanelSlideListener != null) mPanelSlideListener.onPanelOpenedEntirely(); } } } } @Override public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { onPanelDragged(top); if (mPanelSlideListener != null) mPanelSlideListener.onPanelSlide(mSlideOffset); invalidate(); } @Override public void onViewReleased(View releasedChild, float xvel, float yvel) { final LayoutParams lp = (LayoutParams) releasedChild.getLayoutParams(); int top = getPaddingTop() + lp.topMargin; if (yvel > 0 || (yvel == 0 && mSlideOffset > 0.5f)) { top += mSlideRange - mOverhangSize; } mDragHelper.settleCapturedViewAt(releasedChild.getLeft(), top); invalidate(); } @Override public int getViewVerticalDragRange(View child) { return mSlideRange; } @Override public int clampViewPositionVertical(View child, int top, int dy) { final LayoutParams lp = (LayoutParams) mSlideableView.getLayoutParams(); final int topBound = getPaddingTop() + lp.topMargin; final int bottomBound = topBound + mSlideRange - mOverhangSize; final int newTop = Math.min(Math.max(top, topBound), bottomBound); return newTop; } @Override public int clampViewPositionHorizontal(View child, int left, int dx) { // Make sure we never move views horizontally. // This could happen if the child has less width than its parent. return child.getLeft(); } } /** * Test if the second child is displayed under the coordinates given in parameters. * @param x * @param y * @return true if the second child is displayed else false. */ public boolean isSecondChildUnder(int x, int y) { View secondChild = getChildAt(1); if (secondChild == null) return false; return mDragHelper.isViewUnder(secondChild, x, y); } public static class LayoutParams extends ViewGroup.MarginLayoutParams { /** * True if this pane is the slidable pane in the layout. */ boolean slideable; 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); } } static class SavedState extends BaseSavedState { int state; SavedState(Parcelable superState) { super(superState); } private SavedState(Parcel in) { super(in); state = in.readInt(); } @Override public void writeToParcel(Parcel out, int flags) { super.writeToParcel(out, flags); out.writeInt(state); } 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]; } }; } interface SlidingPanelLayoutImpl { void invalidateChildRegion(SlidingPaneLayout parent, View child); } static class SlidingPanelLayoutImplBase implements SlidingPanelLayoutImpl { @Override public void invalidateChildRegion(SlidingPaneLayout parent, View child) { ViewCompat.postInvalidateOnAnimation(parent, child.getLeft(), child.getTop(), child.getRight(), child.getBottom()); } } static class SlidingPanelLayoutImplJB extends SlidingPanelLayoutImplBase { /* * Private API hacks! Nasty! Bad! * * In Jellybean, some optimizations in the hardware UI renderer * prevent a changed Paint on a View using a hardware layer from having * the intended effect. This twiddles some internal bits on the view to force * it to recreate the display list. */ private Method mGetDisplayList; private Field mRecreateDisplayList; SlidingPanelLayoutImplJB() { try { mGetDisplayList = View.class.getDeclaredMethod("getDisplayList", (Class[]) null); } catch (NoSuchMethodException e) { Log.e(TAG, "Couldn't fetch getDisplayList method; dimming won't work right.", e); } try { mRecreateDisplayList = View.class.getDeclaredField("mRecreateDisplayList"); mRecreateDisplayList.setAccessible(true); } catch (NoSuchFieldException e) { Log.e(TAG, "Couldn't fetch mRecreateDisplayList field; dimming will be slow.", e); } } @Override public void invalidateChildRegion(SlidingPaneLayout parent, View child) { if (mGetDisplayList != null && mRecreateDisplayList != null) { try { mRecreateDisplayList.setBoolean(child, true); mGetDisplayList.invoke(child, (Object[]) null); } catch (Exception e) { Log.e(TAG, "Error refreshing display list state", e); } } else { // Slow path. REALLY slow path. Let's hope we don't get here. child.invalidate(); return; } super.invalidateChildRegion(parent, child); } } class AccessibilityDelegate extends AccessibilityDelegateCompat { private final Rect mTmpRect = new Rect(); @Override public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { final AccessibilityNodeInfoCompat superNode = AccessibilityNodeInfoCompat.obtain(info); super.onInitializeAccessibilityNodeInfo(host, superNode); copyNodeInfoNoChildren(info, superNode); superNode.recycle(); info.setClassName(SlidingPaneLayout.class.getName()); info.setSource(host); final ViewParent parent = ViewCompat.getParentForAccessibility(host); if (parent instanceof View) { info.setParent((View) parent); } // This is a best-approximation of addChildrenForAccessibility() // that accounts for filtering. final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); if (child.getVisibility() == View.VISIBLE) { // Force importance to "yes" since we can't read the value. ViewCompat.setImportantForAccessibility( child, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); info.addChild(child); } } } @Override public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { super.onInitializeAccessibilityEvent(host, event); event.setClassName(SlidingPaneLayout.class.getName()); } @Override public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child, AccessibilityEvent event) { return super.onRequestSendAccessibilityEvent(host, child, event); } /** * This should really be in AccessibilityNodeInfoCompat, but there unfortunately * seem to be a few elements that are not easily cloneable using the underlying API. * Leave it private here as it's not general-purpose useful. */ private void copyNodeInfoNoChildren(AccessibilityNodeInfoCompat dest, AccessibilityNodeInfoCompat src) { final Rect rect = mTmpRect; src.getBoundsInParent(rect); dest.setBoundsInParent(rect); src.getBoundsInScreen(rect); dest.setBoundsInScreen(rect); dest.setVisibleToUser(src.isVisibleToUser()); dest.setPackageName(BuildConfig.APPLICATION_ID); dest.setClassName(src.getClassName()); dest.setContentDescription(src.getContentDescription()); dest.setEnabled(src.isEnabled()); dest.setClickable(src.isClickable()); dest.setFocusable(src.isFocusable()); dest.setFocused(src.isFocused()); dest.setAccessibilityFocused(src.isAccessibilityFocused()); dest.setSelected(src.isSelected()); dest.setLongClickable(src.isLongClickable()); dest.addAction(src.getActions()); dest.setMovementGranularities(src.getMovementGranularities()); } } private class DisableLayerRunnable implements Runnable { final View mChildView; @SuppressWarnings("unused") DisableLayerRunnable(View childView) { mChildView = childView; } @Override public void run() { if (mChildView.getParent() == SlidingPaneLayout.this) { ViewCompat.setLayerType(mChildView, ViewCompat.LAYER_TYPE_NONE, null); invalidateChildRegion(mChildView); } mPostedRunnables.remove(this); } } }