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