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