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