SlidingPaneLayout.java revision 3783c4b814bf70711fc4d674e9c7fbc34b1e5a83
1/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.support.v4.widget;
18
19import android.content.Context;
20import android.content.res.TypedArray;
21import android.graphics.Bitmap;
22import android.graphics.Canvas;
23import android.graphics.Paint;
24import android.graphics.PorterDuff;
25import android.graphics.PorterDuffColorFilter;
26import android.graphics.drawable.Drawable;
27import android.os.Build;
28import android.os.Parcel;
29import android.os.Parcelable;
30import android.support.v4.view.MotionEventCompat;
31import android.support.v4.view.VelocityTrackerCompat;
32import android.support.v4.view.ViewCompat;
33import android.support.v4.view.ViewConfigurationCompat;
34import android.util.AttributeSet;
35import android.util.Log;
36import android.view.MotionEvent;
37import android.view.VelocityTracker;
38import android.view.View;
39import android.view.ViewConfiguration;
40import android.view.ViewGroup;
41import android.view.animation.Interpolator;
42import android.widget.Scroller;
43
44import java.lang.reflect.Field;
45import java.lang.reflect.Method;
46
47/**
48 * SlidingPaneLayout provides a horizontal, multi-pane layout for use at the top level
49 * of a UI. A left (or first) pane is treated as a content list or browser, subordinate to a
50 * primary detail view for displaying content.
51 *
52 * <p>Child views may overlap if their combined width exceeds the available width
53 * in the SlidingPaneLayout. When this occurs the user may slide the topmost view out of the way
54 * by dragging it, or by navigating in the direction of the overlapped view using a keyboard.
55 * If the content of the dragged child view is itself horizontally scrollable, the user may
56 * grab it by the very edge.</p>
57 *
58 * <p>Thanks to this sliding behavior, SlidingPaneLayout may be suitable for creating layouts
59 * that can smoothly adapt across many different screen sizes, expanding out fully on larger
60 * screens and collapsing on smaller screens.</p>
61 *
62 * <p>SlidingPaneLayout is distinct from a navigation drawer as described in the design
63 * guide and should not be used in the same scenarios. SlidingPaneLayout should be thought
64 * of only as a way to allow a two-pane layout normally used on larger screens to adapt to smaller
65 * screens in a natural way. The interaction patterns expressed by SlidingPaneLayout imply
66 * a physicality and direct information hierarchy between panes that does not necessarily exist
67 * in a scenario where a navigation drawer should be used instead.</p>
68 *
69 * <p>Appropriate uses of SlidingPaneLayout include pairings of panes such as a contact list and
70 * subordinate interactions with those contacts, or an email thread list with the content pane
71 * displaying the contents of the selected thread. Inappropriate uses of SlidingPaneLayout include
72 * switching between disparate functions of your app, such as jumping from a social stream view
73 * to a view of your personal profile - cases such as this should use the navigation drawer
74 * pattern instead. (TODO: insert doc link to nav drawer widget.)</p>
75 *
76 * <p>Like {@link android.widget.LinearLayout LinearLayout}, SlidingPaneLayout supports
77 * the use of the layout parameter <code>layout_weight</code> on child views to determine
78 * how to divide leftover space after measurement is complete. It is only relevant for width.
79 * When views do not overlap weight behaves as it does in a LinearLayout.</p>
80 *
81 * <p>When views do overlap, weight on a slideable pane indicates that the pane should be
82 * sized to fill all available space in the closed state. Weight on a pane that becomes covered
83 * indicates that the pane should be sized to fill all available space except a small minimum strip
84 * that the user may use to grab the slideable view and pull it back over into a closed state.</p>
85 *
86 * <p>Experimental. This class may be removed.</p>
87 */
88public class SlidingPaneLayout extends ViewGroup {
89    private static final String TAG = "SlidingPaneLayout";
90
91    /**
92     * Default size of the touch gutter along the edge where the user
93     * may grab and drag a sliding pane, even if its internal content
94     * may horizontally scroll.
95     */
96    private static final int DEFAULT_GUTTER_SIZE = 16; // dp
97
98    /**
99     * Default size of the overhang for a pane in the open state.
100     * At least this much of a sliding pane will remain visible.
101     * This indicates that there is more content available and provides
102     * a "physical" edge to grab to pull it closed.
103     */
104    private static final int DEFAULT_OVERHANG_SIZE = 32; // dp;
105
106    private static final int MAX_SETTLE_DURATION = 600; // ms
107
108    /**
109     * If no fade color is given by default it will fade to 80% gray.
110     */
111    private static final int DEFAULT_FADE_COLOR = 0xcccccccc;
112
113    /**
114     * Drawable used to draw the shadow between panes.
115     */
116    private Drawable mShadowDrawable;
117
118    /**
119     * The size of the touch gutter in pixels
120     */
121    private final int mGutterSize;
122
123    /**
124     * The size of the overhang in pixels.
125     * This is the minimum section of the sliding panel that will
126     * be visible in the open state to allow for a closing drag.
127     */
128    private final int mOverhangSize;
129
130    /**
131     * True if a panel can slide with the current measurements
132     */
133    private boolean mCanSlide;
134
135    /**
136     * The child view that can slide, if any.
137     */
138    private View mSlideableView;
139
140    /**
141     * How far the panel is offset from its closed position.
142     * range [0, 1] where 0 = closed, 1 = open.
143     */
144    private float mSlideOffset;
145
146    /**
147     * How far the non-sliding panel is parallaxed from its usual position when open.
148     * range [0, 1]
149     */
150    private float mParallaxOffset;
151
152    /**
153     * How far in pixels the slideable panel may move.
154     */
155    private int mSlideRange;
156
157    /**
158     * A panel view is locked into internal scrolling or another condition that
159     * is preventing a drag.
160     */
161    private boolean mIsUnableToDrag;
162
163    /**
164     * Distance in pixels to parallax the fixed pane by when fully closed
165     */
166    private int mParallaxBy;
167
168    private int mTouchSlop;
169    private float mInitialMotionX;
170    private float mInitialMotionY;
171    private float mLastMotionX;
172    private float mLastMotionY;
173    private int mActivePointerId = INVALID_POINTER;
174
175    private VelocityTracker mVelocityTracker;
176    private float mMaxVelocity;
177
178    private PanelSlideListener mPanelSlideListener;
179
180    private static final int INVALID_POINTER = -1;
181
182    /**
183     * Indicates that the panels are in an idle, settled state. The current panel
184     * is fully in view and no animation is in progress.
185     */
186    public static final int SCROLL_STATE_IDLE = 0;
187
188    /**
189     * Indicates that a panel is currently being dragged by the user.
190     */
191    public static final int SCROLL_STATE_DRAGGING = 1;
192
193    /**
194     * Indicates that a panel is in the process of settling to a final position.
195     */
196    public static final int SCROLL_STATE_SETTLING = 2;
197
198    private int mScrollState = SCROLL_STATE_IDLE;
199
200    /**
201     * Interpolator defining the animation curve for mScroller
202     */
203    private static final Interpolator sInterpolator = new Interpolator() {
204        public float getInterpolation(float t) {
205            t -= 1.0f;
206            return t * t * t * t * t + 1.0f;
207        }
208    };
209
210    /**
211     * Used to animate flinging panes.
212     */
213    private final Scroller mScroller;
214
215    static final SlidingPanelLayoutImpl IMPL;
216
217    static {
218        final int deviceVersion = Build.VERSION.SDK_INT;
219        if (deviceVersion >= 17) {
220            IMPL = new SlidingPanelLayoutImplJBMR1();
221        } else if (deviceVersion >= 16) {
222            IMPL = new SlidingPanelLayoutImplJB();
223        } else {
224            IMPL = new SlidingPanelLayoutImplBase();
225        }
226    }
227
228    /**
229     * Listener for monitoring events about sliding panes.
230     */
231    public interface PanelSlideListener {
232        /**
233         * Called when a sliding pane's position changes.
234         * @param panel The child view that was moved
235         * @param slideOffset The new offset of this sliding pane within its range, from 0-1
236         */
237        public void onPanelSlide(View panel, float slideOffset);
238        /**
239         * Called when a sliding pane becomes slid completely open. The pane may or may not
240         * be interactive at this point depending on how much of the pane is visible.
241         * @param panel The child view that was slid to an open position, revealing other panes
242         */
243        public void onPanelOpened(View panel);
244
245        /**
246         * Called when a sliding pane becomes slid completely closed. The pane is now guaranteed
247         * to be interactive. It may now obscure other views in the layout.
248         * @param panel The child view that was slid to a closed position
249         */
250        public void onPanelClosed(View panel);
251    }
252
253    /**
254     * No-op stubs for {@link PanelSlideListener}. If you only want to implement a subset
255     * of the listener methods you can extend this instead of implement the full interface.
256     */
257    public static class SimplePanelSlideListener implements PanelSlideListener {
258        @Override
259        public void onPanelSlide(View panel, float slideOffset) {
260        }
261        @Override
262        public void onPanelOpened(View panel) {
263        }
264        @Override
265        public void onPanelClosed(View panel) {
266        }
267    }
268
269    public SlidingPaneLayout(Context context) {
270        this(context, null);
271    }
272
273    public SlidingPaneLayout(Context context, AttributeSet attrs) {
274        this(context, attrs, 0);
275    }
276
277    public SlidingPaneLayout(Context context, AttributeSet attrs, int defStyle) {
278        super(context, attrs, defStyle);
279
280        mScroller = new Scroller(context, sInterpolator);
281
282        final float density = context.getResources().getDisplayMetrics().density;
283        mGutterSize = (int) (DEFAULT_GUTTER_SIZE * density + 0.5f);
284        mOverhangSize = (int) (DEFAULT_OVERHANG_SIZE * density + 0.5f);
285
286        final ViewConfiguration viewConfig = ViewConfiguration.get(context);
287        mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(viewConfig);
288        mMaxVelocity = viewConfig.getScaledMaximumFlingVelocity();
289
290        setWillNotDraw(false);
291    }
292
293    /**
294     * Set a distance to parallax the lower pane by when the upper pane is in its
295     * fully closed state. The lower pane will scroll between this position and
296     * its fully open state.
297     *
298     * @param parallaxBy Distance to parallax by in pixels
299     */
300    public void setParallaxDistance(int parallaxBy) {
301        mParallaxBy = parallaxBy;
302        requestLayout();
303    }
304
305    /**
306     * @return The distance the lower pane will parallax by when the upper pane is fully closed.
307     *
308     * @see #setParallaxDistance(int)
309     */
310    public int getParallaxDistance() {
311        return mParallaxBy;
312    }
313
314    void setScrollState(int state) {
315        if (mScrollState != state) {
316            mScrollState = state;
317        }
318    }
319
320    public void setPanelSlideListener(PanelSlideListener listener) {
321        mPanelSlideListener = listener;
322    }
323
324    void dispatchOnPanelSlide(View panel) {
325        if (mPanelSlideListener != null) {
326            mPanelSlideListener.onPanelSlide(panel, mSlideOffset);
327        }
328    }
329
330    void dispatchOnPanelOpened(View panel) {
331        if (mPanelSlideListener != null) {
332            mPanelSlideListener.onPanelOpened(panel);
333        }
334    }
335
336    void dispatchOnPanelClosed(View panel) {
337        if (mPanelSlideListener != null) {
338            mPanelSlideListener.onPanelClosed(panel);
339        }
340    }
341
342    @Override
343    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
344        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
345        final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
346        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
347        final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
348
349        if (widthMode != MeasureSpec.EXACTLY) {
350            throw new IllegalStateException("Width must have an exact value or MATCH_PARENT");
351        } else if (heightMode == MeasureSpec.UNSPECIFIED) {
352            throw new IllegalStateException("Height must not be UNSPECIFIED");
353        }
354
355        int layoutHeight = 0;
356        int maxLayoutHeight = -1;
357        switch (heightMode) {
358            case MeasureSpec.EXACTLY:
359                layoutHeight = maxLayoutHeight = heightSize - getPaddingTop() - getPaddingBottom();
360                break;
361            case MeasureSpec.AT_MOST:
362                maxLayoutHeight = heightSize - getPaddingTop() - getPaddingBottom();
363                break;
364        }
365
366        float weightSum = 0;
367        boolean canSlide = false;
368        int widthRemaining = widthSize - getPaddingLeft() - getPaddingRight();
369        final int childCount = getChildCount();
370
371        if (childCount > 2) {
372            Log.e(TAG, "onMeasure: More than two child views are not supported.");
373        }
374
375        // First pass. Measure based on child LayoutParams width/height.
376        // Weight will incur a second pass.
377        for (int i = 0; i < childCount; i++) {
378            final View child = getChildAt(i);
379            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
380
381            if (child.getVisibility() == GONE) {
382                lp.dimWhenOffset = false;
383                continue;
384            }
385
386            if (lp.weight > 0) {
387                weightSum += lp.weight;
388
389                // If we have no width, weight is the only contributor to the final size.
390                // Measure this view on the weight pass only.
391                if (lp.width == 0) continue;
392            }
393
394            int childWidthSpec;
395            final int horizontalMargin = lp.leftMargin + lp.rightMargin;
396            if (lp.width == LayoutParams.WRAP_CONTENT) {
397                childWidthSpec = MeasureSpec.makeMeasureSpec(widthSize - horizontalMargin,
398                        MeasureSpec.AT_MOST);
399            } else if (lp.width == LayoutParams.FILL_PARENT) {
400                childWidthSpec = MeasureSpec.makeMeasureSpec(widthSize - horizontalMargin,
401                        MeasureSpec.EXACTLY);
402            } else {
403                childWidthSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY);
404            }
405
406            int childHeightSpec;
407            if (lp.height == LayoutParams.WRAP_CONTENT) {
408                childHeightSpec = MeasureSpec.makeMeasureSpec(maxLayoutHeight, MeasureSpec.AT_MOST);
409            } else if (lp.height == LayoutParams.FILL_PARENT) {
410                childHeightSpec = MeasureSpec.makeMeasureSpec(maxLayoutHeight, MeasureSpec.EXACTLY);
411            } else {
412                childHeightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
413            }
414
415            child.measure(childWidthSpec, childHeightSpec);
416            final int childWidth = child.getMeasuredWidth();
417            final int childHeight = child.getMeasuredHeight();
418
419            if (heightMode == MeasureSpec.AT_MOST && childHeight > layoutHeight) {
420                layoutHeight = Math.min(childHeight, maxLayoutHeight);
421            }
422
423            widthRemaining -= childWidth;
424            canSlide |= lp.slideable = widthRemaining < 0;
425            if (lp.slideable) {
426                mSlideableView = child;
427            }
428        }
429
430        // Resolve weight and make sure non-sliding panels are smaller than the full screen.
431        if (canSlide || weightSum > 0) {
432            final int fixedPanelWidthLimit = widthSize - mOverhangSize;
433
434            for (int i = 0; i < childCount; i++) {
435                final View child = getChildAt(i);
436
437                if (child.getVisibility() == GONE) {
438                    continue;
439                }
440
441                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
442
443                final boolean skippedFirstPass = lp.width == 0 && lp.weight > 0;
444                final int measuredWidth = skippedFirstPass ? 0 : child.getMeasuredWidth();
445                if (canSlide && child != mSlideableView) {
446                    if (lp.width < 0 && (measuredWidth > fixedPanelWidthLimit || lp.weight > 0)) {
447                        // Fixed panels in a sliding configuration should
448                        // be clamped to the fixed panel limit.
449                        final int childHeightSpec;
450                        if (skippedFirstPass) {
451                            // Do initial height measurement if we skipped measuring this view
452                            // the first time around.
453                            if (lp.height == LayoutParams.WRAP_CONTENT) {
454                                childHeightSpec = MeasureSpec.makeMeasureSpec(maxLayoutHeight,
455                                        MeasureSpec.AT_MOST);
456                            } else if (lp.height == LayoutParams.FILL_PARENT) {
457                                childHeightSpec = MeasureSpec.makeMeasureSpec(maxLayoutHeight,
458                                        MeasureSpec.EXACTLY);
459                            } else {
460                                childHeightSpec = MeasureSpec.makeMeasureSpec(lp.height,
461                                        MeasureSpec.EXACTLY);
462                            }
463                        } else {
464                            childHeightSpec = MeasureSpec.makeMeasureSpec(
465                                    child.getMeasuredHeight(), MeasureSpec.EXACTLY);
466                        }
467                        final int childWidthSpec = MeasureSpec.makeMeasureSpec(
468                                fixedPanelWidthLimit, MeasureSpec.EXACTLY);
469                        child.measure(childWidthSpec, childHeightSpec);
470                    }
471                } else if (lp.weight > 0) {
472                    int childHeightSpec;
473                    if (lp.width == 0) {
474                        // This was skipped the first time; figure out a real height spec.
475                        if (lp.height == LayoutParams.WRAP_CONTENT) {
476                            childHeightSpec = MeasureSpec.makeMeasureSpec(maxLayoutHeight,
477                                    MeasureSpec.AT_MOST);
478                        } else if (lp.height == LayoutParams.FILL_PARENT) {
479                            childHeightSpec = MeasureSpec.makeMeasureSpec(maxLayoutHeight,
480                                    MeasureSpec.EXACTLY);
481                        } else {
482                            childHeightSpec = MeasureSpec.makeMeasureSpec(lp.height,
483                                    MeasureSpec.EXACTLY);
484                        }
485                    } else {
486                        childHeightSpec = MeasureSpec.makeMeasureSpec(
487                                child.getMeasuredHeight(), MeasureSpec.EXACTLY);
488                    }
489
490                    if (canSlide) {
491                        // Consume available space
492                        final int horizontalMargin = lp.leftMargin + lp.rightMargin;
493                        final int newWidth = widthSize - horizontalMargin;
494                        final int childWidthSpec = MeasureSpec.makeMeasureSpec(
495                                newWidth, MeasureSpec.EXACTLY);
496                        if (measuredWidth != newWidth) {
497                            child.measure(childWidthSpec, childHeightSpec);
498                        }
499                    } else {
500                        // Distribute the extra width proportionally similar to LinearLayout
501                        final int widthToDistribute = Math.max(0, widthRemaining);
502                        final int addedWidth = (int) (lp.weight * widthToDistribute / weightSum);
503                        final int childWidthSpec = MeasureSpec.makeMeasureSpec(
504                                measuredWidth + addedWidth, MeasureSpec.EXACTLY);
505                        child.measure(childWidthSpec, childHeightSpec);
506                    }
507                }
508            }
509        }
510
511        setMeasuredDimension(widthSize, layoutHeight);
512        mCanSlide = canSlide;
513        if (mScrollState != SCROLL_STATE_IDLE && !canSlide) {
514            // Cancel scrolling in progress, it's no longer relevant.
515            setScrollState(SCROLL_STATE_IDLE);
516        }
517    }
518
519    @Override
520    protected void onLayout(boolean changed, int l, int t, int r, int b) {
521        final int width = r - l;
522        final int height = b - t;
523        final int paddingLeft = getPaddingLeft();
524        final int paddingRight = getPaddingRight();
525        final int paddingTop = getPaddingTop();
526        final int paddingBottom = getPaddingBottom();
527
528        final int childCount = getChildCount();
529        int xStart = paddingLeft;
530        int nextXStart = xStart;
531
532        for (int i = 0; i < childCount; i++) {
533            final View child = getChildAt(i);
534
535            if (child.getVisibility() == GONE) {
536                continue;
537            }
538
539            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
540
541            final int childWidth = child.getMeasuredWidth();
542            int offset = 0;
543
544            if (lp.slideable) {
545                final int margin = lp.leftMargin + lp.rightMargin;
546                final int range = Math.min(nextXStart,
547                        width - paddingRight - mOverhangSize) - xStart - margin;
548                mSlideRange = range;
549                lp.dimWhenOffset = width - paddingRight - (xStart + range) < childWidth / 2;
550                xStart += (int) (range * mSlideOffset) + lp.leftMargin;
551            } else if (mCanSlide && mParallaxBy != 0) {
552                offset = (int) ((1 - mSlideOffset) * mParallaxBy);
553                xStart = nextXStart;
554            } else {
555                xStart = nextXStart;
556            }
557
558            final int childLeft = xStart - offset;
559            final int childRight = childLeft + childWidth;
560            final int childTop = paddingTop;
561            final int childBottom = childTop + child.getMeasuredHeight();
562            child.layout(childLeft, paddingTop, childRight, childBottom);
563
564            nextXStart += child.getWidth();
565        }
566    }
567
568    @Override
569    public boolean onInterceptTouchEvent(MotionEvent ev) {
570        final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
571
572        if (!mCanSlide || (mIsUnableToDrag && action != MotionEvent.ACTION_DOWN)) {
573            return super.onInterceptTouchEvent(ev);
574        }
575
576        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
577            mActivePointerId = INVALID_POINTER;
578            if (mVelocityTracker != null) {
579                mVelocityTracker.recycle();
580                mVelocityTracker = null;
581            }
582            return false;
583        }
584
585        boolean interceptTap = false;
586
587        switch (action) {
588            case MotionEvent.ACTION_MOVE: {
589                final int activePointerId = mActivePointerId;
590                if (activePointerId == INVALID_POINTER) {
591                    // No valid pointer = no valid drag. Ignore.
592                    break;
593                }
594
595                final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId);
596                final float x = MotionEventCompat.getX(ev, pointerIndex);
597                final float dx = x - mLastMotionX;
598                final float xDiff = Math.abs(dx);
599                final float y = MotionEventCompat.getY(ev, pointerIndex);
600                final float yDiff = Math.abs(y - mLastMotionY);
601
602                if (dx != 0 && !isGutterDrag(mLastMotionX, dx) &&
603                        canScroll(this, false, (int) dx, (int) x, (int) y)) {
604                    mInitialMotionX = mLastMotionX = x;
605                    mLastMotionY = y;
606                    mIsUnableToDrag = true;
607                    return false;
608                }
609                if (xDiff > mTouchSlop && xDiff > yDiff && isSlideablePaneUnder(x, y)) {
610                    mLastMotionX = dx > 0 ? mInitialMotionX + mTouchSlop :
611                            mInitialMotionX - mTouchSlop;
612                    setScrollState(SCROLL_STATE_DRAGGING);
613                } else if (yDiff > mTouchSlop) {
614                    mIsUnableToDrag = true;
615                }
616                if (mScrollState == SCROLL_STATE_DRAGGING && performDrag(x)) {
617                    invalidate();
618                }
619                break;
620            }
621
622            case MotionEvent.ACTION_DOWN: {
623                final float x = ev.getX();
624                final float y = ev.getY();
625                mIsUnableToDrag = false;
626                mInitialMotionX = x;
627                mInitialMotionY = y;
628                mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
629                if (isSlideablePaneUnder(x, y)) {
630                    mLastMotionX = x;
631                    mLastMotionY = y;
632                    if (mScrollState == SCROLL_STATE_SETTLING) {
633                        // Start dragging immediately. "Catch"
634                        setScrollState(SCROLL_STATE_DRAGGING);
635                    } else if (isDimmed(mSlideableView)) {
636                        interceptTap = true;
637                    }
638                }
639                break;
640            }
641
642            case MotionEventCompat.ACTION_POINTER_UP:
643                onSecondaryPointerUp(ev);
644                break;
645        }
646
647        if (mVelocityTracker == null) {
648            mVelocityTracker = VelocityTracker.obtain();
649        }
650        mVelocityTracker.addMovement(ev);
651        return mScrollState == SCROLL_STATE_DRAGGING || interceptTap;
652    }
653
654    @Override
655    public boolean onTouchEvent(MotionEvent ev) {
656        if (!mCanSlide) {
657            return super.onTouchEvent(ev);
658        }
659
660        if (mVelocityTracker == null) {
661            mVelocityTracker = VelocityTracker.obtain();
662        }
663        mVelocityTracker.addMovement(ev);
664
665        final int action = ev.getAction();
666        boolean needsInvalidate = false;
667        boolean wantTouchEvents = true;
668
669        switch (action & MotionEventCompat.ACTION_MASK) {
670            case MotionEvent.ACTION_DOWN: {
671                final float x = ev.getX();
672                final float y = ev.getY();
673                mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
674                mInitialMotionX = x;
675                mInitialMotionY = y;
676
677                if (isSlideablePaneUnder(x, y)) {
678                    mScroller.abortAnimation();
679                    wantTouchEvents = true;
680                    mLastMotionX = x;
681                    setScrollState(SCROLL_STATE_DRAGGING);
682                }
683                break;
684            }
685
686            case MotionEvent.ACTION_MOVE: {
687                if (mScrollState != SCROLL_STATE_DRAGGING) {
688                    final int pointerIndex = MotionEventCompat.findPointerIndex(
689                            ev, mActivePointerId);
690                    final float x = MotionEventCompat.getX(ev, pointerIndex);
691                    final float y = MotionEventCompat.getY(ev, pointerIndex);
692                    final float dx = Math.abs(x - mLastMotionX);
693                    final float dy = Math.abs(y - mLastMotionY);
694                    if (dx > mTouchSlop && dx > dy && isSlideablePaneUnder(x, y)) {
695                        mLastMotionX = x - mInitialMotionX > 0 ? mInitialMotionX + mTouchSlop :
696                                mInitialMotionX - mTouchSlop;
697                        setScrollState(SCROLL_STATE_DRAGGING);
698                    }
699                }
700                if (mScrollState == SCROLL_STATE_DRAGGING) {
701                    final int activePointerIndex = MotionEventCompat.findPointerIndex(
702                            ev, mActivePointerId);
703                    final float x = MotionEventCompat.getX(ev, activePointerIndex);
704                    needsInvalidate |= performDrag(x);
705                }
706                break;
707            }
708
709            case MotionEvent.ACTION_UP: {
710                if (isDimmed(mSlideableView)) {
711                    final int pi = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
712                    final float x = MotionEventCompat.getX(ev, pi);
713                    final float y = MotionEventCompat.getY(ev, pi);
714                    final float dx = x - mInitialMotionX;
715                    final float dy = y - mInitialMotionY;
716                    if (dx * dx + dy * dy < mTouchSlop * mTouchSlop && isSlideablePaneUnder(x, y)) {
717                        // Taps close a dimmed open pane.
718                        closePane(mSlideableView, 0);
719                        mActivePointerId = INVALID_POINTER;
720                        break;
721                    }
722                }
723                if (mScrollState == SCROLL_STATE_DRAGGING) {
724                    final VelocityTracker vt = mVelocityTracker;
725                    vt.computeCurrentVelocity(1000, mMaxVelocity);
726                    int initialVelocity = (int) VelocityTrackerCompat.getXVelocity(vt,
727                            mActivePointerId);
728                    if (initialVelocity < 0 || (initialVelocity == 0 && mSlideOffset < 0.5f)) {
729                        closePane(mSlideableView, initialVelocity);
730                    } else {
731                        openPane(mSlideableView, initialVelocity);
732                    }
733                    mActivePointerId = INVALID_POINTER;
734                }
735                break;
736            }
737
738            case MotionEvent.ACTION_CANCEL: {
739                if (mScrollState == SCROLL_STATE_DRAGGING) {
740                    mActivePointerId = INVALID_POINTER;
741                    if (mSlideOffset < 0.5f) {
742                        closePane(mSlideableView, 0);
743                    } else {
744                        openPane(mSlideableView, 0);
745                    }
746                }
747                break;
748            }
749
750            case MotionEventCompat.ACTION_POINTER_DOWN: {
751                final int index = MotionEventCompat.getActionIndex(ev);
752                mLastMotionX = MotionEventCompat.getX(ev, index);
753                mLastMotionY = MotionEventCompat.getY(ev, index);
754                mActivePointerId = MotionEventCompat.getPointerId(ev, index);
755                break;
756            }
757
758            case MotionEventCompat.ACTION_POINTER_UP: {
759                onSecondaryPointerUp(ev);
760                break;
761            }
762        }
763
764        if (needsInvalidate) {
765            invalidate();
766        }
767        return wantTouchEvents;
768    }
769
770    private void closePane(View pane, int initialVelocity) {
771        if (mCanSlide) {
772            smoothSlideTo(0.f, initialVelocity);
773        }
774    }
775
776    private void openPane(View pane, int initialVelocity) {
777        if (mCanSlide) {
778            smoothSlideTo(1.f, initialVelocity);
779        }
780    }
781
782    /**
783     * Animate the sliding panel to its open state.
784     */
785    public void smoothSlideOpen() {
786        if (mCanSlide) {
787            openPane(mSlideableView, 0);
788        }
789    }
790
791    /**
792     * Animate the sliding panel to its closed state.
793     */
794    public void smoothSlideClosed() {
795        if (mCanSlide) {
796            closePane(mSlideableView, 0);
797        }
798    }
799
800    /**
801     * @return true if sliding panels are completely open
802     */
803    public boolean isOpen() {
804        return !mCanSlide || mSlideOffset == 1;
805    }
806
807    /**
808     * @return true if content in this layout can be slid open and closed
809     */
810    public boolean canSlide() {
811        return mCanSlide;
812    }
813
814    private boolean performDrag(float x) {
815        final float dxMotion = x - mLastMotionX;
816        mLastMotionX = x;
817
818        final LayoutParams lp = (LayoutParams) mSlideableView.getLayoutParams();
819        final int leftBound = getPaddingLeft() + lp.leftMargin;
820        final int rightBound = leftBound + mSlideRange;
821
822        final float oldLeft = mSlideableView.getLeft();
823        final float newLeft = Math.min(Math.max(oldLeft + dxMotion, leftBound), rightBound);
824
825        final int dxPane = (int) (newLeft - oldLeft);
826
827        if (dxPane == 0) {
828            return false;
829        }
830
831        mSlideableView.offsetLeftAndRight(dxPane);
832
833        mSlideOffset = (newLeft - leftBound) / mSlideRange;
834
835        if (mParallaxBy != 0) {
836            parallaxOtherViews(mSlideOffset);
837        }
838
839        mLastMotionX += newLeft - (int) newLeft;
840        if (lp.dimWhenOffset) {
841            dimChildView(mSlideableView, mSlideOffset);
842        }
843        dispatchOnPanelSlide(mSlideableView);
844
845        return true;
846    }
847
848    private void dimChildView(View v, float mag) {
849        final LayoutParams lp = (LayoutParams) v.getLayoutParams();
850
851        if (mag > 0) {
852            final int baseAlpha = (DEFAULT_FADE_COLOR & 0xff000000) >>> 24;
853            int imag = (int) (baseAlpha * mag);
854            int color = imag << 24 | (DEFAULT_FADE_COLOR & 0xffffff);
855            if (lp.dimPaint == null) {
856                lp.dimPaint = new Paint();
857            }
858            lp.dimPaint.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_OVER));
859            if (ViewCompat.getLayerType(v) != ViewCompat.LAYER_TYPE_HARDWARE) {
860                ViewCompat.setLayerType(v, ViewCompat.LAYER_TYPE_HARDWARE, lp.dimPaint);
861            }
862            invalidateChildRegion(v);
863        } else if (ViewCompat.getLayerType(v) != ViewCompat.LAYER_TYPE_NONE) {
864            ViewCompat.setLayerType(v, ViewCompat.LAYER_TYPE_NONE, null);
865        }
866    }
867
868    @Override
869    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
870        if (Build.VERSION.SDK_INT >= 11) { // HC
871            return super.drawChild(canvas, child, drawingTime);
872        }
873
874        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
875        if (lp.dimWhenOffset && mSlideOffset > 0) {
876            if (!child.isDrawingCacheEnabled()) {
877                child.setDrawingCacheEnabled(true);
878            }
879            final Bitmap cache = child.getDrawingCache();
880            canvas.drawBitmap(cache, child.getLeft(), child.getTop(), lp.dimPaint);
881            return false;
882        } else {
883            if (child.isDrawingCacheEnabled()) {
884                child.setDrawingCacheEnabled(false);
885            }
886            return super.drawChild(canvas, child, drawingTime);
887        }
888    }
889
890    private void invalidateChildRegion(View v) {
891        IMPL.invalidateChildRegion(this, v);
892    }
893
894    private boolean isGutterDrag(float x, float dx) {
895        return (x < mGutterSize && dx > 0) || (x > getWidth() - mGutterSize && dx < 0);
896    }
897
898    /**
899     * Smoothly animate mDraggingPane to the target X position within its range.
900     *
901     * @param slideOffset position to animate to
902     * @param velocity initial velocity in case of fling, or 0.
903     */
904    void smoothSlideTo(float slideOffset, int velocity) {
905        if (!mCanSlide) {
906            // Nothing to do.
907            return;
908        }
909
910        final LayoutParams lp = (LayoutParams) mSlideableView.getLayoutParams();
911
912        final int leftBound = getPaddingLeft() + lp.leftMargin;
913        int sx = mSlideableView.getLeft();
914        int x = (int) (leftBound + slideOffset * mSlideRange);
915        int dx = x - sx;
916        if (dx == 0) {
917            setScrollState(SCROLL_STATE_IDLE);
918            if (mSlideOffset == 0) {
919                dispatchOnPanelClosed(mSlideableView);
920            } else {
921                dispatchOnPanelOpened(mSlideableView);
922            }
923            return;
924        }
925
926        setScrollState(SCROLL_STATE_SETTLING);
927
928        final int width = getWidth();
929        final int halfWidth = width / 2;
930        final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / width);
931        final float distance = halfWidth + halfWidth *
932                distanceInfluenceForSnapDuration(distanceRatio);
933
934        int duration = 0;
935        velocity = Math.abs(velocity);
936        if (velocity > 0) {
937            duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
938        } else {
939            final float range = (float) Math.abs(dx) / mSlideRange;
940            duration = (int) ((range + 1) * 100);
941        }
942        duration = Math.min(duration, MAX_SETTLE_DURATION);
943
944        mScroller.startScroll(sx, 0, dx, 0, duration);
945        ViewCompat.postInvalidateOnAnimation(this);
946    }
947
948    // We want the duration of the page snap animation to be influenced by the distance that
949    // the screen has to travel, however, we don't want this duration to be effected in a
950    // purely linear fashion. Instead, we use this method to moderate the effect that the distance
951    // of travel has on the overall snap duration.
952    float distanceInfluenceForSnapDuration(float f) {
953        f -= 0.5f; // center the values about 0.
954        f *= 0.3f * Math.PI / 2.0f;
955        return (float) Math.sin(f);
956    }
957
958    @Override
959    public void computeScroll() {
960        if (!mScroller.isFinished() && mScroller.computeScrollOffset()) {
961            if (!mCanSlide) {
962                mScroller.abortAnimation();
963                return;
964            }
965
966            final int oldLeft = mSlideableView.getLeft();
967            final int newLeft = mScroller.getCurrX();
968            final int dx = newLeft - oldLeft;
969            mSlideableView.offsetLeftAndRight(dx);
970
971            final LayoutParams lp = (LayoutParams) mSlideableView.getLayoutParams();
972            final int leftBound = getPaddingLeft() + lp.leftMargin;
973            mSlideOffset = (float) (newLeft - leftBound) / mSlideRange;
974            if (lp.dimWhenOffset) {
975                dimChildView(mSlideableView, mSlideOffset);
976            }
977            dispatchOnPanelSlide(mSlideableView);
978
979            if (mParallaxBy != 0) {
980                parallaxOtherViews(mSlideOffset);
981            }
982
983            if (mScroller.isFinished()) {
984                setScrollState(SCROLL_STATE_IDLE);
985                post(new Runnable() {
986                    public void run() {
987                        if (mSlideOffset == 0) {
988                            dispatchOnPanelClosed(mSlideableView);
989                        } else {
990                            dispatchOnPanelOpened(mSlideableView);
991                        }
992                    }
993                });
994            }
995            ViewCompat.postInvalidateOnAnimation(this);
996        }
997
998    }
999
1000    /**
1001     * Set a drawable to use as a shadow cast by the right pane onto the left pane
1002     * during opening/closing.
1003     *
1004     * @param d drawable to use as a shadow
1005     */
1006    public void setShadowDrawable(Drawable d) {
1007        mShadowDrawable = d;
1008    }
1009
1010    /**
1011     * Set a drawable to use as a shadow cast by the right pane onto the left pane
1012     * during opening/closing.
1013     *
1014     * @param resId Resource ID of a drawable to use
1015     */
1016    public void setShadowResource(int resId) {
1017        setShadowDrawable(getResources().getDrawable(resId));
1018    }
1019
1020    @Override
1021    public void draw(Canvas c) {
1022        super.draw(c);
1023
1024        if (mSlideableView == null || mSlideableView.getVisibility() == GONE ||
1025                mShadowDrawable == null) {
1026            // No need to draw a shadow if we don't have one.
1027            return;
1028        }
1029
1030        final int shadowWidth = mShadowDrawable.getIntrinsicWidth();
1031        final int right = mSlideableView.getLeft();
1032        final int top = mSlideableView.getTop();
1033        final int bottom = mSlideableView.getBottom();
1034        final int left = right - shadowWidth;
1035        mShadowDrawable.setBounds(left, top, right, bottom);
1036        mShadowDrawable.draw(c);
1037    }
1038
1039    private void parallaxOtherViews(float slideOffset) {
1040        final LayoutParams slideLp = (LayoutParams) mSlideableView.getLayoutParams();
1041        final boolean dimViews = slideLp.dimWhenOffset && slideLp.leftMargin <= 0;
1042        final int childCount = getChildCount();
1043        for (int i = 0; i < childCount; i++) {
1044            final View v = getChildAt(i);
1045            if (v == mSlideableView) continue;
1046
1047            final int oldOffset = (int) ((1 - mParallaxOffset) * mParallaxBy);
1048            mParallaxOffset = slideOffset;
1049            final int newOffset = (int) ((1 - slideOffset) * mParallaxBy);
1050            final int dx = oldOffset - newOffset;
1051
1052            v.offsetLeftAndRight(dx);
1053
1054            if (dimViews) {
1055                dimChildView(v, 1 - mParallaxOffset);
1056            }
1057        }
1058    }
1059
1060    /**
1061     * Tests scrollability within child views of v given a delta of dx.
1062     *
1063     * @param v View to test for horizontal scrollability
1064     * @param checkV Whether the view v passed should itself be checked for scrollability (true),
1065     *               or just its children (false).
1066     * @param dx Delta scrolled in pixels
1067     * @param x X coordinate of the active touch point
1068     * @param y Y coordinate of the active touch point
1069     * @return true if child views of v can be scrolled by delta of dx.
1070     */
1071    protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
1072        if (v instanceof ViewGroup) {
1073            final ViewGroup group = (ViewGroup) v;
1074            final int scrollX = v.getScrollX();
1075            final int scrollY = v.getScrollY();
1076            final int count = group.getChildCount();
1077            // Count backwards - let topmost views consume scroll distance first.
1078            for (int i = count - 1; i >= 0; i--) {
1079                // TODO: Add versioned support here for transformed views.
1080                // This will not work for transformed views in Honeycomb+
1081                final View child = group.getChildAt(i);
1082                if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() &&
1083                        y + scrollY >= child.getTop() && y + scrollY < child.getBottom() &&
1084                        canScroll(child, true, dx, x + scrollX - child.getLeft(),
1085                                y + scrollY - child.getTop())) {
1086                    return true;
1087                }
1088            }
1089        }
1090
1091        return checkV && ViewCompat.canScrollHorizontally(v, -dx);
1092    }
1093
1094    private void onSecondaryPointerUp(MotionEvent ev) {
1095        final int pointerIndex = MotionEventCompat.getActionIndex(ev);
1096        final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
1097        if (pointerId == mActivePointerId) {
1098            // This was our active pointer going up. Choose a new
1099            // active pointer and adjust accordingly.
1100            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
1101            mLastMotionX = MotionEventCompat.getX(ev, newPointerIndex);
1102            mLastMotionY = MotionEventCompat.getY(ev, newPointerIndex);
1103            mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
1104            if (mVelocityTracker != null) {
1105                mVelocityTracker.clear();
1106            }
1107        }
1108    }
1109
1110    boolean isSlideablePaneUnder(float x, float y) {
1111        final View child = mSlideableView;
1112        return child != null &&
1113                x >= child.getLeft() - mGutterSize &&
1114                x < child.getRight() + mGutterSize &&
1115                y >= child.getTop() &&
1116                y < child.getBottom();
1117    }
1118
1119    boolean isDimmed(View child) {
1120        if (child == null) {
1121            return false;
1122        }
1123        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
1124        return mCanSlide && lp.dimWhenOffset && mSlideOffset > 0;
1125    }
1126
1127    @Override
1128    protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
1129        return new LayoutParams();
1130    }
1131
1132    @Override
1133    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
1134        return p instanceof MarginLayoutParams
1135                ? new LayoutParams((MarginLayoutParams) p)
1136                : new LayoutParams(p);
1137    }
1138
1139    @Override
1140    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
1141        return p instanceof LayoutParams && super.checkLayoutParams(p);
1142    }
1143
1144    @Override
1145    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
1146        return new LayoutParams(getContext(), attrs);
1147    }
1148
1149    public static class LayoutParams extends ViewGroup.MarginLayoutParams {
1150        private static final int[] ATTRS = new int[] {
1151            android.R.attr.layout_weight
1152        };
1153
1154        /**
1155         * The weighted proportion of how much of the leftover space
1156         * this child should consume after measurement.
1157         */
1158        public float weight = 0;
1159
1160        /**
1161         * True if this pane is the slideable pane in the layout.
1162         */
1163        boolean slideable;
1164
1165        /**
1166         * True if this view should be drawn dimmed
1167         * when it's been offset from its default position.
1168         */
1169        boolean dimWhenOffset;
1170
1171        Paint dimPaint;
1172
1173        public LayoutParams() {
1174            super(FILL_PARENT, FILL_PARENT);
1175        }
1176
1177        public LayoutParams(int width, int height) {
1178            super(width, height);
1179        }
1180
1181        public LayoutParams(android.view.ViewGroup.LayoutParams source) {
1182            super(source);
1183        }
1184
1185        public LayoutParams(MarginLayoutParams source) {
1186            super(source);
1187        }
1188
1189        public LayoutParams(LayoutParams source) {
1190            super(source);
1191            this.weight = source.weight;
1192        }
1193
1194        public LayoutParams(Context c, AttributeSet attrs) {
1195            super(c, attrs);
1196
1197            final TypedArray a = c.obtainStyledAttributes(attrs, ATTRS);
1198            this.weight = a.getFloat(0, 0);
1199            a.recycle();
1200        }
1201
1202    }
1203
1204    static class SavedState extends BaseSavedState {
1205        boolean canSlide;
1206        boolean isOpen;
1207
1208        SavedState(Parcelable superState) {
1209            super(superState);
1210        }
1211
1212        private SavedState(Parcel in) {
1213            super(in);
1214            canSlide = in.readInt() != 0;
1215            isOpen = in.readInt() != 0;
1216        }
1217
1218        @Override
1219        public void writeToParcel(Parcel out, int flags) {
1220            super.writeToParcel(out, flags);
1221            out.writeInt(canSlide ? 1 : 0);
1222            out.writeInt(isOpen ? 1 : 0);
1223        }
1224
1225        public static final Parcelable.Creator<SavedState> CREATOR =
1226                new Parcelable.Creator<SavedState>() {
1227            public SavedState createFromParcel(Parcel in) {
1228                return new SavedState(in);
1229            }
1230
1231            public SavedState[] newArray(int size) {
1232                return new SavedState[size];
1233            }
1234        };
1235    }
1236
1237    interface SlidingPanelLayoutImpl {
1238        void invalidateChildRegion(SlidingPaneLayout parent, View child);
1239    }
1240
1241    static class SlidingPanelLayoutImplBase implements SlidingPanelLayoutImpl {
1242        public void invalidateChildRegion(SlidingPaneLayout parent, View child) {
1243            ViewCompat.postInvalidateOnAnimation(parent, child.getLeft(), child.getTop(),
1244                    child.getRight(), child.getBottom());
1245        }
1246    }
1247
1248    static class SlidingPanelLayoutImplJB extends SlidingPanelLayoutImplBase {
1249        /*
1250         * Private API hacks! Nasty! Bad!
1251         *
1252         * In Jellybean, some optimizations in the hardware UI renderer
1253         * prevent a changed Paint on a View using a hardware layer from having
1254         * the intended effect. This twiddles some internal bits on the view to force
1255         * it to recreate the display list.
1256         */
1257        private Method mGetDisplayList;
1258        private Field mRecreateDisplayList;
1259
1260        SlidingPanelLayoutImplJB() {
1261            try {
1262                mGetDisplayList = View.class.getDeclaredMethod("getDisplayList", (Class[]) null);
1263            } catch (NoSuchMethodException e) {
1264                Log.e(TAG, "Couldn't fetch getDisplayList method; dimming won't work right.", e);
1265            }
1266            try {
1267                mRecreateDisplayList = View.class.getDeclaredField("mRecreateDisplayList");
1268                mRecreateDisplayList.setAccessible(true);
1269            } catch (NoSuchFieldException e) {
1270                Log.e(TAG, "Couldn't fetch mRecreateDisplayList field; dimming will be slow.", e);
1271            }
1272        }
1273
1274        @Override
1275        public void invalidateChildRegion(SlidingPaneLayout parent, View child) {
1276            if (mGetDisplayList != null && mRecreateDisplayList != null) {
1277                try {
1278                    mRecreateDisplayList.setBoolean(child, true);
1279                    mGetDisplayList.invoke(child, (Object[]) null);
1280                } catch (Exception e) {
1281                    Log.e(TAG, "Error refreshing display list state", e);
1282                }
1283            } else {
1284                // Slow path. REALLY slow path. Let's hope we don't get here.
1285                child.invalidate();
1286                return;
1287            }
1288            super.invalidateChildRegion(parent, child);
1289        }
1290    }
1291
1292    static class SlidingPanelLayoutImplJBMR1 extends SlidingPanelLayoutImplBase {
1293        @Override
1294        public void invalidateChildRegion(SlidingPaneLayout parent, View child) {
1295            ViewCompat.setLayerPaint(child, ((LayoutParams) child.getLayoutParams()).dimPaint);
1296        }
1297    }
1298}
1299