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