DrawerLayout.java revision 8bc268e9c40e4ae375a0d65dc1293dccc541186f
1/*
2 * Copyright (C) 2013 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
17
18package android.support.v4.widget;
19
20import android.content.Context;
21import android.content.res.TypedArray;
22import android.graphics.Canvas;
23import android.graphics.Paint;
24import android.graphics.PixelFormat;
25import android.graphics.drawable.Drawable;
26import android.os.Parcel;
27import android.os.Parcelable;
28import android.support.v4.view.GravityCompat;
29import android.support.v4.view.KeyEventCompat;
30import android.support.v4.view.MotionEventCompat;
31import android.support.v4.view.ViewCompat;
32import android.util.AttributeSet;
33import android.view.Gravity;
34import android.view.KeyEvent;
35import android.view.MotionEvent;
36import android.view.View;
37import android.view.ViewGroup;
38
39/**
40 * DrawerLayout acts as a top-level container for window content that allows for
41 * interactive "drawer" views to be pulled out from the edge of the window.
42 *
43 * <p>Drawer positioning and layout is controlled using the <code>android:layout_gravity</code>
44 * attribute on child views corresponding to </p>
45 *
46 * <p>As per the Android Design guide, any drawers positioned to the left/start should
47 * always contain content for navigating around the application, whereas any drawers
48 * positioned to the right/end should always contain actions to take on the current content.
49 * This preserves the same navigation left, actions right structure present in the Action Bar
50 * and elsewhere.</p>
51 */
52public class DrawerLayout extends ViewGroup {
53    private static final String TAG = "DrawerLayout";
54
55    private static final int INVALID_POINTER = -1;
56
57    /**
58     * Indicates that any drawers are in an idle, settled state. No animation is in progress.
59     */
60    public static final int STATE_IDLE = ViewDragHelper.STATE_IDLE;
61
62    /**
63     * Indicates that a drawer is currently being dragged by the user.
64     */
65    public static final int STATE_DRAGGING = ViewDragHelper.STATE_DRAGGING;
66
67    /**
68     * Indicates that a drawer is in the process of settling to a final position.
69     */
70    public static final int STATE_SETTLING = ViewDragHelper.STATE_SETTLING;
71
72    private static final int MIN_DRAWER_MARGIN = 64; // dp
73
74    private static final int DRAWER_PEEK_DISTANCE = 16; // dp
75
76    private static final int DEFAULT_SCRIM_COLOR = 0x99000000;
77
78    private static final int[] LAYOUT_ATTRS = new int[] {
79            android.R.attr.layout_gravity
80    };
81
82    private int mMinDrawerMargin;
83    private int mDrawerPeekDistance;
84
85    private int mScrimColor = DEFAULT_SCRIM_COLOR;
86    private float mScrimOpacity;
87    private Paint mScrimPaint = new Paint();
88
89    private final ViewDragHelper mLeftDragger;
90    private final ViewDragHelper mRightDragger;
91    private int mDrawerState;
92    private boolean mInLayout;
93    private boolean mFirstLayout = true;
94
95    private DrawerListener mListener;
96
97    private float mInitialMotionX;
98    private float mInitialMotionY;
99
100    private Drawable mShadowLeft;
101    private Drawable mShadowRight;
102
103    /**
104     * Listener for monitoring events about drawers.
105     */
106    public interface DrawerListener {
107        /**
108         * Called when a drawer's position changes.
109         * @param drawerView The child view that was moved
110         * @param slideOffset The new offset of this drawer within its range, from 0-1
111         */
112        public void onDrawerSlide(View drawerView, float slideOffset);
113
114        /**
115         * Called when a drawer has settled in a completely open state.
116         * The drawer is interactive at this point.
117         *
118         * @param drawerView Drawer view that is now open
119         */
120        public void onDrawerOpened(View drawerView);
121
122        /**
123         * Called when a drawer has settled in a completely closed state.
124         *
125         * @param drawerView Drawer view that is now closed
126         */
127        public void onDrawerClosed(View drawerView);
128
129        /**
130         * Called when the drawer motion state changes. The new state will
131         * be one of {@link #STATE_IDLE}, {@link #STATE_DRAGGING} or {@link #STATE_SETTLING}.
132         *
133         * @param newState The new drawer motion state
134         */
135        public void onDrawerStateChanged(int newState);
136    }
137
138    /**
139     * Stub/no-op implementations of all methods of {@link DrawerListener}.
140     * Override this if you only care about a few of the available callback methods.
141     */
142    public static abstract class SimpleDrawerListener implements DrawerListener {
143        @Override
144        public void onDrawerSlide(View drawerView, float slideOffset) {
145        }
146
147        @Override
148        public void onDrawerOpened(View drawerView) {
149        }
150
151        @Override
152        public void onDrawerClosed(View drawerView) {
153        }
154
155        @Override
156        public void onDrawerStateChanged(int newState) {
157        }
158    }
159
160    public DrawerLayout(Context context) {
161        this(context, null);
162    }
163
164    public DrawerLayout(Context context, AttributeSet attrs) {
165        this(context, attrs, 0);
166    }
167
168    public DrawerLayout(Context context, AttributeSet attrs, int defStyle) {
169        super(context, attrs, defStyle);
170
171        final float density = getResources().getDisplayMetrics().density;
172        mMinDrawerMargin = (int) (MIN_DRAWER_MARGIN * density + 0.5f);
173        mDrawerPeekDistance = (int) (DRAWER_PEEK_DISTANCE * density + 0.5f);
174
175        final ViewDragCallback leftCallback = new ViewDragCallback(Gravity.LEFT);
176        final ViewDragCallback rightCallback = new ViewDragCallback(Gravity.RIGHT);
177
178        mLeftDragger = ViewDragHelper.create(this, 0.5f, leftCallback);
179        mLeftDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);
180        leftCallback.setDragger(mLeftDragger);
181
182        mRightDragger = ViewDragHelper.create(this, 0.5f, rightCallback);
183        mRightDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_RIGHT);
184        rightCallback.setDragger(mRightDragger);
185
186        // So that we can catch the back button
187        setFocusableInTouchMode(true);
188    }
189
190    /**
191     * Set a simple drawable used for the left or right shadow.
192     * The drawable provided must have a nonzero intrinsic width.
193     *
194     * @param shadowDrawable Shadow drawable to use at the edge of a drawer
195     * @param gravity Which drawer the shadow should apply to
196     */
197    public void setDrawerShadow(Drawable shadowDrawable, int gravity) {
198        /*
199         * TODO Someone someday might want to set more complex drawables here.
200         * They're probably nuts, but we might want to consider registering callbacks,
201         * setting states, etc. properly.
202         */
203
204        final int absGravity = GravityCompat.getAbsoluteGravity(gravity,
205                ViewCompat.getLayoutDirection(this));
206        if ((absGravity & Gravity.LEFT) == Gravity.LEFT) {
207            mShadowLeft = shadowDrawable;
208            invalidate();
209        }
210        if ((absGravity & Gravity.RIGHT) == Gravity.RIGHT) {
211            mShadowRight = shadowDrawable;
212            invalidate();
213        }
214    }
215
216    /**
217     * Set a simple drawable used for the left or right shadow.
218     * The drawable provided must have a nonzero intrinsic width.
219     *
220     * @param resId Resource id of a shadow drawable to use at the edge of a drawer
221     * @param gravity Which drawer the shadow should apply to
222     */
223    public void setDrawerShadow(int resId, int gravity) {
224        setDrawerShadow(getResources().getDrawable(resId), gravity);
225    }
226
227    /**
228     * Set a listener to be notified of drawer events.
229     *
230     * @param listener Listener to notify when drawer events occur
231     * @see DrawerListener
232     */
233    public void setDrawerListener(DrawerListener listener) {
234        mListener = listener;
235    }
236
237    /**
238     * Resolve the shared state of all drawers from the component ViewDragHelpers.
239     * Should be called whenever a ViewDragHelper's state changes.
240     */
241    void updateDrawerState(int forGravity, int activeState, View activeDrawer) {
242        final int leftState = mLeftDragger.getViewDragState();
243        final int rightState = mRightDragger.getViewDragState();
244
245        final int state;
246        if (leftState == STATE_DRAGGING || rightState == STATE_DRAGGING) {
247            state = STATE_DRAGGING;
248        } else if (leftState == STATE_SETTLING || rightState == STATE_SETTLING) {
249            state = STATE_SETTLING;
250        } else {
251            state = STATE_IDLE;
252        }
253
254        if (activeDrawer != null && activeState == STATE_IDLE) {
255            final LayoutParams lp = (LayoutParams) activeDrawer.getLayoutParams();
256            if (lp.onScreen == 0) {
257                dispatchOnDrawerClosed(activeDrawer);
258            } else if (lp.onScreen == 1) {
259                dispatchOnDrawerOpened(activeDrawer);
260            }
261        }
262
263        if (state != mDrawerState) {
264            mDrawerState = state;
265
266            if (mListener != null) {
267                mListener.onDrawerStateChanged(state);
268            }
269        }
270    }
271
272    void dispatchOnDrawerClosed(View drawerView) {
273        final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams();
274        if (lp.knownOpen) {
275            lp.knownOpen = false;
276            if (mListener != null) {
277                mListener.onDrawerClosed(drawerView);
278            }
279        }
280    }
281
282    void dispatchOnDrawerOpened(View drawerView) {
283        final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams();
284        if (!lp.knownOpen) {
285            lp.knownOpen = true;
286            if (mListener != null) {
287                mListener.onDrawerOpened(drawerView);
288            }
289        }
290    }
291
292    void dispatchOnDrawerSlide(View drawerView, float slideOffset) {
293        if (mListener != null) {
294            mListener.onDrawerSlide(drawerView, slideOffset);
295        }
296    }
297
298    void setDrawerViewOffset(View drawerView, float slideOffset) {
299        final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams();
300        if (slideOffset == lp.onScreen) {
301            return;
302        }
303
304        lp.onScreen = slideOffset;
305        dispatchOnDrawerSlide(drawerView, slideOffset);
306    }
307
308    float getDrawerViewOffset(View drawerView) {
309        return ((LayoutParams) drawerView.getLayoutParams()).onScreen;
310    }
311
312    int getDrawerViewGravity(View drawerView) {
313        final int gravity = ((LayoutParams) drawerView.getLayoutParams()).gravity;
314        return GravityCompat.getAbsoluteGravity(gravity, ViewCompat.getLayoutDirection(drawerView));
315    }
316
317    boolean checkDrawerViewGravity(View drawerView, int checkFor) {
318        final int absGrav = getDrawerViewGravity(drawerView);
319        return (absGrav & checkFor) == checkFor;
320    }
321
322    void moveDrawerToOffset(View drawerView, float slideOffset) {
323        final float oldOffset = getDrawerViewOffset(drawerView);
324        final int width = drawerView.getWidth();
325        final int oldPos = (int) (width * oldOffset);
326        final int newPos = (int) (width * slideOffset);
327        final int dx = newPos - oldPos;
328
329        drawerView.offsetLeftAndRight(checkDrawerViewGravity(drawerView, Gravity.LEFT) ? dx : -dx);
330        setDrawerViewOffset(drawerView, slideOffset);
331    }
332
333    View findDrawerWithGravity(int gravity) {
334        final int childCount = getChildCount();
335        for (int i = 0; i < childCount; i++) {
336            final View child = getChildAt(i);
337            final int childGravity = getDrawerViewGravity(child);
338            if ((childGravity & Gravity.HORIZONTAL_GRAVITY_MASK) ==
339                    (gravity & Gravity.HORIZONTAL_GRAVITY_MASK)) {
340                return child;
341            }
342        }
343        return null;
344    }
345
346    /**
347     * Simple gravity to string - only supports LEFT and RIGHT for debugging output.
348     *
349     * @param gravity Absolute gravity value
350     * @return LEFT or RIGHT as appropriate, or a hex string
351     */
352    static String gravityToString(int gravity) {
353        if ((gravity & Gravity.LEFT) == Gravity.LEFT) {
354            return "LEFT";
355        }
356        if ((gravity & Gravity.RIGHT) == Gravity.RIGHT) {
357            return "RIGHT";
358        }
359        return Integer.toHexString(gravity);
360    }
361
362    @Override
363    protected void onDetachedFromWindow() {
364        super.onDetachedFromWindow();
365        mFirstLayout = true;
366    }
367
368    @Override
369    protected void onAttachedToWindow() {
370        super.onAttachedToWindow();
371        mFirstLayout = true;
372    }
373
374    @Override
375    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
376        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
377        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
378        final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
379        final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
380
381        if (widthMode != MeasureSpec.EXACTLY || heightMode != MeasureSpec.EXACTLY) {
382            throw new IllegalArgumentException(
383                    "DrawerLayout must be measured with MeasureSpec.EXACTLY.");
384        }
385
386        setMeasuredDimension(widthSize, heightSize);
387
388        // Gravity value for each drawer we've seen. Only one of each permitted.
389        int foundDrawers = 0;
390        final int childCount = getChildCount();
391        for (int i = 0; i < childCount; i++) {
392            final View child = getChildAt(i);
393
394            if (child.getVisibility() == GONE) {
395                continue;
396            }
397
398            if (isContentView(child)) {
399                // Content views get measured at exactly the layout's size.
400                child.measure(widthMeasureSpec, heightMeasureSpec);
401            } else if (isDrawerView(child)) {
402                final int childGravity =
403                        getDrawerViewGravity(child) & Gravity.HORIZONTAL_GRAVITY_MASK;
404                if ((foundDrawers & childGravity) != 0) {
405                    throw new IllegalStateException("Child drawer has absolute gravity " +
406                            gravityToString(childGravity) + " but this " + TAG + " already has a " +
407                            "drawer view along that edge");
408                }
409                final int drawerWidthSpec = getChildMeasureSpec(widthMeasureSpec, mMinDrawerMargin,
410                        child.getLayoutParams().width);
411                child.measure(drawerWidthSpec, heightMeasureSpec);
412            } else {
413                throw new IllegalStateException("Child " + child + " at index " + i +
414                        " does not have a valid layout_gravity - must be Gravity.LEFT, " +
415                        "Gravity.RIGHT or Gravity.NO_GRAVITY");
416            }
417        }
418    }
419
420    @Override
421    protected void onLayout(boolean changed, int l, int t, int r, int b) {
422        mInLayout = true;
423        final int childCount = getChildCount();
424        for (int i = 0; i < childCount; i++) {
425            final View child = getChildAt(i);
426
427            if (child.getVisibility() == GONE) {
428                continue;
429            }
430
431            if (isContentView(child)) {
432                child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight());
433            } else { // Drawer, if it wasn't onMeasure would have thrown an exception.
434                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
435
436                final int childWidth = child.getMeasuredWidth();
437                int childLeft;
438
439                if (checkDrawerViewGravity(child, Gravity.LEFT)) {
440                    childLeft = -childWidth + (int) (childWidth * lp.onScreen);
441                } else { // Right; onMeasure checked for us.
442                    childLeft = r - l - (int) (childWidth * lp.onScreen);
443                }
444
445                child.layout(childLeft, 0, childLeft + childWidth, child.getMeasuredHeight());
446
447                if (lp.onScreen == 0) {
448                    child.setVisibility(INVISIBLE);
449                }
450            }
451        }
452        mInLayout = false;
453        mFirstLayout = false;
454    }
455
456    @Override
457    public void requestLayout() {
458        if (!mInLayout) {
459            super.requestLayout();
460        }
461    }
462
463    @Override
464    public void computeScroll() {
465        final int childCount = getChildCount();
466        float scrimOpacity = 0;
467        for (int i = 0; i < childCount; i++) {
468            final float onscreen = ((LayoutParams) getChildAt(i).getLayoutParams()).onScreen;
469            scrimOpacity = Math.max(scrimOpacity, onscreen);
470        }
471        mScrimOpacity = scrimOpacity;
472
473        // "|" used on purpose; both need to run.
474        if (mLeftDragger.continueSettling(true) | mRightDragger.continueSettling(true)) {
475            ViewCompat.postInvalidateOnAnimation(this);
476        }
477    }
478
479    private static boolean hasOpaqueBackground(View v) {
480        final Drawable bg = v.getBackground();
481        if (bg != null) {
482            return bg.getOpacity() == PixelFormat.OPAQUE;
483        }
484        return false;
485    }
486
487    @Override
488    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
489        final boolean drawingContent = isContentView(child);
490        int clipLeft = 0, clipRight = getWidth();
491
492        final int restoreCount = canvas.save();
493        if (drawingContent) {
494            final int childCount = getChildCount();
495            for (int i = 0; i < childCount; i++) {
496                final View v = getChildAt(i);
497                if (v == child || v.getVisibility() != VISIBLE ||
498                        !hasOpaqueBackground(v) || !isDrawerView(v)) {
499                    continue;
500                }
501
502                if (checkDrawerViewGravity(v, Gravity.LEFT)) {
503                    final int vright = v.getRight();
504                    if (vright > clipLeft) clipLeft = vright;
505                } else {
506                    final int vleft = v.getLeft();
507                    if (vleft < clipRight) clipRight = vleft;
508                }
509            }
510            canvas.clipRect(clipLeft, 0, clipRight, getHeight());
511        }
512        final boolean result = super.drawChild(canvas, child, drawingTime);
513        canvas.restoreToCount(restoreCount);
514
515        if (mScrimOpacity > 0 && drawingContent) {
516            final int baseAlpha = (mScrimColor & 0xff000000) >>> 24;
517            final int imag = (int) (baseAlpha * mScrimOpacity);
518            final int color = imag << 24 | (mScrimColor & 0xffffff);
519            mScrimPaint.setColor(color);
520
521            canvas.drawRect(clipLeft, 0, clipRight, getHeight(), mScrimPaint);
522        } else if (mShadowLeft != null && checkDrawerViewGravity(child, Gravity.LEFT)) {
523            final int shadowWidth = mShadowLeft.getIntrinsicWidth();
524            final int childRight = child.getRight();
525            final float alpha =
526                    Math.max(0, Math.min((float) childRight / mDrawerPeekDistance, 1.f));
527            mShadowLeft.setBounds(childRight, child.getTop(),
528                    childRight + shadowWidth, child.getBottom());
529            mShadowLeft.setAlpha((int) (0xff * alpha));
530            mShadowLeft.draw(canvas);
531        } else if (mShadowRight != null && checkDrawerViewGravity(child, Gravity.RIGHT)) {
532            final int shadowWidth = mShadowRight.getIntrinsicWidth();
533            final int childLeft = child.getLeft();
534            final int showing = getWidth() - childLeft;
535            final float alpha =
536                    Math.max(0, Math.min((float) showing / mDrawerPeekDistance, 1.f));
537            mShadowRight.setBounds(childLeft - shadowWidth, child.getTop(),
538                    childLeft, child.getBottom());
539            mShadowRight.setAlpha((int) (0xff * alpha));
540            mShadowRight.draw(canvas);
541        }
542        return result;
543    }
544
545    boolean isContentView(View child) {
546        return ((LayoutParams) child.getLayoutParams()).gravity == Gravity.NO_GRAVITY;
547    }
548
549    boolean isDrawerView(View child) {
550        final int gravity = ((LayoutParams) child.getLayoutParams()).gravity;
551        final int absGravity = GravityCompat.getAbsoluteGravity(gravity,
552                ViewCompat.getLayoutDirection(child));
553        return (absGravity & (Gravity.LEFT | Gravity.RIGHT)) != 0;
554    }
555
556    @Override
557    public boolean onInterceptTouchEvent(MotionEvent ev) {
558        final int action = MotionEventCompat.getActionMasked(ev);
559
560        // "|" used deliberately here; both methods should be invoked.
561        final boolean interceptForDrag = mLeftDragger.shouldInterceptTouchEvent(ev) |
562                mRightDragger.shouldInterceptTouchEvent(ev);
563
564        boolean interceptForTap = false;
565
566        switch (action) {
567            case MotionEvent.ACTION_DOWN: {
568                final float x = ev.getX();
569                final float y = ev.getY();
570                mInitialMotionX = x;
571                mInitialMotionY = y;
572                if (mScrimOpacity > 0 &&
573                        isContentView(mLeftDragger.findTopChildUnder((int) x, (int) y))) {
574                    interceptForTap = true;
575                }
576                break;
577            }
578
579            case MotionEvent.ACTION_CANCEL:
580            case MotionEvent.ACTION_UP: {
581                closeDrawers(true);
582            }
583        }
584        return interceptForDrag || interceptForTap;
585    }
586
587    @Override
588    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
589        final int childCount = getChildCount();
590        for (int i = 0; i < childCount; i++) {
591            final LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
592
593            if (lp.isPeeking) {
594                // Don't disallow intercept at all if we have a peeking view, we're probably
595                // going to intercept it later anyway.
596                return;
597            }
598        }
599        super.requestDisallowInterceptTouchEvent(disallowIntercept);
600        if (disallowIntercept) {
601            closeDrawers(true);
602        }
603    }
604
605    @Override
606    public boolean onTouchEvent(MotionEvent ev) {
607        mLeftDragger.processTouchEvent(ev);
608        mRightDragger.processTouchEvent(ev);
609
610        final int action = ev.getAction();
611        boolean wantTouchEvents = true;
612
613        switch (action & MotionEventCompat.ACTION_MASK) {
614            case MotionEvent.ACTION_DOWN: {
615                final float x = ev.getX();
616                final float y = ev.getY();
617                mInitialMotionX = x;
618                mInitialMotionY = y;
619                break;
620            }
621
622            case MotionEvent.ACTION_UP: {
623                final float x = ev.getX();
624                final float y = ev.getY();
625                boolean peekingOnly = true;
626                final View touchedView = mLeftDragger.findTopChildUnder((int) x, (int) y);
627                if (touchedView != null && isContentView(touchedView)) {
628                    final float dx = x - mInitialMotionX;
629                    final float dy = y - mInitialMotionY;
630                    final int slop = mLeftDragger.getTouchSlop();
631                    if (dx * dx + dy * dy < slop * slop) {
632                        // Taps close a dimmed open pane.
633                        peekingOnly = false;
634                    }
635                }
636                closeDrawers(peekingOnly);
637                break;
638            }
639
640            case MotionEvent.ACTION_CANCEL: {
641                closeDrawers(true);
642                break;
643            }
644        }
645
646        return wantTouchEvents;
647    }
648
649    /**
650     * Close all currently open drawer views by animating them out of view.
651     */
652    public void closeDrawers() {
653        closeDrawers(false);
654    }
655
656    void closeDrawers(boolean peekingOnly) {
657        boolean needsInvalidate = false;
658        final int childCount = getChildCount();
659        for (int i = 0; i < childCount; i++) {
660            final View child = getChildAt(i);
661            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
662
663            if (!isDrawerView(child) || (peekingOnly && !lp.isPeeking)) {
664                continue;
665            }
666
667            final int childWidth = child.getWidth();
668
669            if (checkDrawerViewGravity(child, Gravity.LEFT)) {
670                needsInvalidate |= mLeftDragger.smoothSlideViewTo(child,
671                        -childWidth, child.getTop());
672            } else {
673                needsInvalidate |= mRightDragger.smoothSlideViewTo(child,
674                        getWidth(), child.getTop());
675            }
676
677            lp.isPeeking = false;
678        }
679
680        if (needsInvalidate) {
681            invalidate();
682        }
683    }
684
685    /**
686     * Open the specified drawer view by animating it into view.
687     *
688     * @param drawerView Drawer view to open
689     */
690    public void openDrawer(View drawerView) {
691        if (!isDrawerView(drawerView)) {
692            throw new IllegalArgumentException("View " + drawerView + " is not a sliding drawer");
693        }
694
695        if (mFirstLayout) {
696            final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams();
697            lp.onScreen = 1.f;
698            lp.knownOpen = true;
699        } else {
700            if (checkDrawerViewGravity(drawerView, Gravity.LEFT)) {
701                mLeftDragger.smoothSlideViewTo(drawerView, 0, drawerView.getTop());
702            } else {
703                mRightDragger.smoothSlideViewTo(drawerView, getWidth() - drawerView.getWidth(),
704                        drawerView.getTop());
705            }
706        }
707        invalidate();
708    }
709
710    /**
711     * Open the specified drawer by animating it out of view.
712     *
713     * @param gravity Gravity.LEFT to move the left drawer or Gravity.RIGHT for the right.
714     *                GravityCompat.START or GravityCompat.END may also be used.
715     */
716    public void openDrawer(int gravity) {
717        final int absGravity = GravityCompat.getAbsoluteGravity(gravity,
718                ViewCompat.getLayoutDirection(this));
719        final View drawerView = findDrawerWithGravity(absGravity);
720
721        if (drawerView == null) {
722            throw new IllegalArgumentException("No drawer view found with absolute gravity " +
723                    gravityToString(absGravity));
724        }
725        openDrawer(drawerView);
726    }
727
728    /**
729     * Close the specified drawer view by animating it into view.
730     *
731     * @param drawerView Drawer view to close
732     */
733    public void closeDrawer(View drawerView) {
734        if (!isDrawerView(drawerView)) {
735            throw new IllegalArgumentException("View " + drawerView + " is not a sliding drawer");
736        }
737
738        if (mFirstLayout) {
739            final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams();
740            lp.onScreen = 0.f;
741            lp.knownOpen = false;
742        } else {
743            if (checkDrawerViewGravity(drawerView, Gravity.LEFT)) {
744                mLeftDragger.smoothSlideViewTo(drawerView, -drawerView.getWidth(),
745                        drawerView.getTop());
746            } else {
747                mRightDragger.smoothSlideViewTo(drawerView, getWidth(), drawerView.getTop());
748            }
749        }
750        invalidate();
751    }
752
753    /**
754     * Close the specified drawer by animating it out of view.
755     *
756     * @param gravity Gravity.LEFT to move the left drawer or Gravity.RIGHT for the right.
757     *                GravityCompat.START or GravityCompat.END may also be used.
758     */
759    public void closeDrawer(int gravity) {
760        final int absGravity = GravityCompat.getAbsoluteGravity(gravity,
761                ViewCompat.getLayoutDirection(this));
762        final View drawerView = findDrawerWithGravity(absGravity);
763
764        if (drawerView == null) {
765            throw new IllegalArgumentException("No drawer view found with absolute gravity " +
766                    gravityToString(absGravity));
767        }
768        closeDrawer(drawerView);
769    }
770
771    /**
772     * Check if the given drawer view is currently in an open state.
773     * To be considered "open" the drawer must have settled into its fully
774     * visible state. To check for partial visibility use
775     * {@link #isDrawerVisible(android.view.View)}.
776     *
777     * @param drawer Drawer view to check
778     * @return true if the given drawer view is in an open state
779     * @see #isDrawerVisible(android.view.View)
780     */
781    public boolean isDrawerOpen(View drawer) {
782        if (!isDrawerView(drawer)) {
783            throw new IllegalArgumentException("View " + drawer + " is not a drawer");
784        }
785        return ((LayoutParams) drawer.getLayoutParams()).knownOpen;
786    }
787
788    /**
789     * Check if a given drawer view is currently visible on-screen. The drawer
790     * may be only peeking onto the screen, fully extended, or anywhere inbetween.
791     *
792     * @param drawer Drawer view to check
793     * @return true if the given drawer is visible on-screen
794     * @see #isDrawerOpen(android.view.View)
795     */
796    public boolean isDrawerVisible(View drawer) {
797        if (!isDrawerView(drawer)) {
798            throw new IllegalArgumentException("View " + drawer + " is not a drawer");
799        }
800        return ((LayoutParams) drawer.getLayoutParams()).onScreen > 0;
801    }
802
803    @Override
804    protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
805        return new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);
806    }
807
808    @Override
809    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
810        return p instanceof LayoutParams
811                ? new LayoutParams((LayoutParams) p)
812                : new LayoutParams(p);
813    }
814
815    @Override
816    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
817        return p instanceof LayoutParams && super.checkLayoutParams(p);
818    }
819
820    @Override
821    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
822        return new LayoutParams(getContext(), attrs);
823    }
824
825    private boolean hasVisibleDrawer() {
826        final int childCount = getChildCount();
827        for (int i = 0; i < childCount; i++) {
828            final View child = getChildAt(i);
829            if (isDrawerView(child) && isDrawerVisible(child)) {
830                return true;
831            }
832        }
833        return false;
834    }
835
836    @Override
837    public boolean onKeyDown(int keyCode, KeyEvent event) {
838        if (keyCode == KeyEvent.KEYCODE_BACK && hasVisibleDrawer()) {
839            KeyEventCompat.startTracking(event);
840            return true;
841        }
842        return super.onKeyDown(keyCode, event);
843    }
844
845    @Override
846    public boolean onKeyUp(int keyCode, KeyEvent event) {
847        if (keyCode == KeyEvent.KEYCODE_BACK && hasVisibleDrawer()) {
848            closeDrawers();
849            return true;
850        }
851        return super.onKeyUp(keyCode, event);
852    }
853
854    @Override
855    protected void onRestoreInstanceState(Parcelable state) {
856        final SavedState ss = (SavedState) state;
857        super.onRestoreInstanceState(ss.getSuperState());
858
859        if (ss.openDrawerGravity != Gravity.NO_GRAVITY) {
860            final View toOpen = findDrawerWithGravity(ss.openDrawerGravity);
861            if (toOpen != null) {
862                openDrawer(toOpen);
863            }
864        }
865    }
866
867    @Override
868    protected Parcelable onSaveInstanceState() {
869        final Parcelable superState = super.onSaveInstanceState();
870
871        final SavedState ss = new SavedState(superState);
872
873        final int childCount = getChildCount();
874        for (int i = 0; i < childCount; i++) {
875            final View child = getChildAt(i);
876            if (!isDrawerView(child)) {
877                continue;
878            }
879
880            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
881            if (lp.knownOpen) {
882                ss.openDrawerGravity = lp.gravity;
883                // Only one drawer can be open at a time.
884                break;
885            }
886        }
887
888        return ss;
889    }
890
891    /**
892     * State persisted across instances
893     */
894    protected static class SavedState extends BaseSavedState {
895        int openDrawerGravity = Gravity.NO_GRAVITY;
896
897        public SavedState(Parcel in) {
898            super(in);
899            openDrawerGravity = in.readInt();
900        }
901
902        public SavedState(Parcelable superState) {
903            super(superState);
904        }
905
906        @Override
907        public void writeToParcel(Parcel dest, int flags) {
908            super.writeToParcel(dest, flags);
909            dest.writeInt(openDrawerGravity);
910        }
911
912        public static final Parcelable.Creator<SavedState> CREATOR =
913                new Parcelable.Creator<SavedState>() {
914            @Override
915            public SavedState createFromParcel(Parcel source) {
916                return new SavedState(source);
917            }
918
919            @Override
920            public SavedState[] newArray(int size) {
921                return new SavedState[size];
922            }
923        };
924    }
925
926    private class ViewDragCallback extends ViewDragHelper.Callback {
927
928        private final int mGravity;
929        private ViewDragHelper mDragger;
930
931        public ViewDragCallback(int gravity) {
932            mGravity = gravity;
933        }
934
935        public void setDragger(ViewDragHelper dragger) {
936            mDragger = dragger;
937        }
938
939        @Override
940        public boolean tryCaptureView(View child, int pointerId) {
941            // Only capture views where the gravity matches what we're looking for.
942            // This lets us use two ViewDragHelpers, one for each side drawer.
943            return isDrawerView(child) && checkDrawerViewGravity(child, mGravity);
944        }
945
946        @Override
947        public void onViewDragStateChanged(int state) {
948            updateDrawerState(mGravity, state, mDragger.getCapturedView());
949        }
950
951        @Override
952        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
953            float offset;
954            final int childWidth = changedView.getWidth();
955
956            // This reverses the positioning shown in onLayout.
957            if (checkDrawerViewGravity(changedView, Gravity.LEFT)) {
958                offset = (float) (childWidth + left) / childWidth;
959            } else {
960                final int width = getWidth();
961                offset = (float) (width - left) / childWidth;
962            }
963            setDrawerViewOffset(changedView, offset);
964            changedView.setVisibility(offset == 0 ? INVISIBLE : VISIBLE);
965            invalidate();
966        }
967
968        @Override
969        public void onViewCaptured(View capturedChild, int activePointerId) {
970            final LayoutParams lp = (LayoutParams) capturedChild.getLayoutParams();
971            lp.isPeeking = false;
972
973            closeOtherDrawer();
974        }
975
976        private void closeOtherDrawer() {
977            final int otherGrav = mGravity == Gravity.LEFT ? Gravity.RIGHT : Gravity.LEFT;
978            final View toClose = findDrawerWithGravity(otherGrav);
979            if (toClose != null) {
980                closeDrawer(toClose);
981            }
982        }
983
984        @Override
985        public void onViewReleased(View releasedChild, float xvel, float yvel) {
986            // Offset is how open the drawer is, therefore left/right values
987            // are reversed from one another.
988            final float offset = getDrawerViewOffset(releasedChild);
989            final int childWidth = releasedChild.getWidth();
990
991            int left;
992            if (checkDrawerViewGravity(releasedChild, Gravity.LEFT)) {
993                left = xvel > 0 || xvel == 0 && offset > 0.5f ? 0 : -childWidth;
994            } else {
995                final int width = getWidth();
996                left = xvel < 0 || xvel == 0 && offset < 0.5f ? width - childWidth : width;
997            }
998
999            mDragger.settleCapturedViewAt(left, releasedChild.getTop());
1000            invalidate();
1001        }
1002
1003        @Override
1004        public void onEdgeTouched(int edgeFlags, int pointerId) {
1005            final View toCapture;
1006            final int childLeft;
1007            final boolean leftEdge =
1008                    (edgeFlags & ViewDragHelper.EDGE_LEFT) == ViewDragHelper.EDGE_LEFT;
1009            if (leftEdge) {
1010                toCapture = findDrawerWithGravity(Gravity.LEFT);
1011                childLeft = -toCapture.getWidth() + mDrawerPeekDistance;
1012            } else {
1013                toCapture = findDrawerWithGravity(Gravity.RIGHT);
1014                childLeft = getWidth() - mDrawerPeekDistance;
1015            }
1016
1017            // Only peek if it would mean making the drawer more visible
1018            if (toCapture != null && ((leftEdge && toCapture.getLeft() < childLeft) ||
1019                    (!leftEdge && toCapture.getLeft() > childLeft))) {
1020                final LayoutParams lp = (LayoutParams) toCapture.getLayoutParams();
1021                mDragger.smoothSlideViewTo(toCapture, childLeft, toCapture.getTop());
1022                lp.isPeeking = true;
1023                invalidate();
1024
1025                closeOtherDrawer();
1026            }
1027        }
1028
1029        @Override
1030        public void onEdgeDragStarted(int edgeFlags, int pointerId) {
1031            final View toCapture;
1032            if ((edgeFlags & ViewDragHelper.EDGE_LEFT) == ViewDragHelper.EDGE_LEFT) {
1033                toCapture = findDrawerWithGravity(Gravity.LEFT);
1034            } else {
1035                toCapture = findDrawerWithGravity(Gravity.RIGHT);
1036            }
1037
1038            if (toCapture != null) {
1039                mDragger.captureChildView(toCapture, pointerId);
1040            }
1041        }
1042
1043        @Override
1044        public int getViewHorizontalDragRange(View child) {
1045            return child.getWidth();
1046        }
1047
1048        @Override
1049        public int clampViewPositionHorizontal(View child, int left, int dx) {
1050            if (checkDrawerViewGravity(child, Gravity.LEFT)) {
1051                return Math.max(-child.getWidth(), Math.min(left, 0));
1052            } else {
1053                final int width = getWidth();
1054                return Math.max(width - child.getWidth(), Math.min(left, width));
1055            }
1056        }
1057    }
1058
1059    public static class LayoutParams extends ViewGroup.LayoutParams {
1060
1061        public int gravity = Gravity.NO_GRAVITY;
1062        float onScreen;
1063        boolean isPeeking;
1064        boolean knownOpen;
1065
1066        public LayoutParams(Context c, AttributeSet attrs) {
1067            super(c, attrs);
1068
1069            final TypedArray a = c.obtainStyledAttributes(attrs, LAYOUT_ATTRS);
1070            this.gravity = a.getInt(0, Gravity.NO_GRAVITY);
1071            a.recycle();
1072        }
1073
1074        public LayoutParams(int width, int height) {
1075            super(width, height);
1076        }
1077
1078        public LayoutParams(int width, int height, int gravity) {
1079            this(width, height);
1080            this.gravity = gravity;
1081        }
1082
1083        public LayoutParams(LayoutParams source) {
1084            super(source);
1085            this.gravity = source.gravity;
1086        }
1087
1088        public LayoutParams(ViewGroup.LayoutParams source) {
1089            super(source);
1090        }
1091    }
1092}
1093