1/*
2 * Copyright (C) 2008 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.widget;
18
19import android.R;
20import android.content.Context;
21import android.content.res.TypedArray;
22import android.graphics.Bitmap;
23import android.graphics.Canvas;
24import android.graphics.Rect;
25import android.os.Handler;
26import android.os.Message;
27import android.os.SystemClock;
28import android.util.AttributeSet;
29import android.view.MotionEvent;
30import android.view.SoundEffectConstants;
31import android.view.VelocityTracker;
32import android.view.View;
33import android.view.ViewGroup;
34import android.view.accessibility.AccessibilityEvent;
35
36/**
37 * SlidingDrawer hides content out of the screen and allows the user to drag a handle
38 * to bring the content on screen. SlidingDrawer can be used vertically or horizontally.
39 *
40 * A special widget composed of two children views: the handle, that the users drags,
41 * and the content, attached to the handle and dragged with it.
42 *
43 * SlidingDrawer should be used as an overlay inside layouts. This means SlidingDrawer
44 * should only be used inside of a FrameLayout or a RelativeLayout for instance. The
45 * size of the SlidingDrawer defines how much space the content will occupy once slid
46 * out so SlidingDrawer should usually use match_parent for both its dimensions.
47 *
48 * Inside an XML layout, SlidingDrawer must define the id of the handle and of the
49 * content:
50 *
51 * <pre class="prettyprint">
52 * &lt;SlidingDrawer
53 *     android:id="@+id/drawer"
54 *     android:layout_width="match_parent"
55 *     android:layout_height="match_parent"
56 *
57 *     android:handle="@+id/handle"
58 *     android:content="@+id/content"&gt;
59 *
60 *     &lt;ImageView
61 *         android:id="@id/handle"
62 *         android:layout_width="88dip"
63 *         android:layout_height="44dip" /&gt;
64 *
65 *     &lt;GridView
66 *         android:id="@id/content"
67 *         android:layout_width="match_parent"
68 *         android:layout_height="match_parent" /&gt;
69 *
70 * &lt;/SlidingDrawer&gt;
71 * </pre>
72 *
73 * @attr ref android.R.styleable#SlidingDrawer_content
74 * @attr ref android.R.styleable#SlidingDrawer_handle
75 * @attr ref android.R.styleable#SlidingDrawer_topOffset
76 * @attr ref android.R.styleable#SlidingDrawer_bottomOffset
77 * @attr ref android.R.styleable#SlidingDrawer_orientation
78 * @attr ref android.R.styleable#SlidingDrawer_allowSingleTap
79 * @attr ref android.R.styleable#SlidingDrawer_animateOnClick
80 */
81public class SlidingDrawer extends ViewGroup {
82    public static final int ORIENTATION_HORIZONTAL = 0;
83    public static final int ORIENTATION_VERTICAL = 1;
84
85    private static final int TAP_THRESHOLD = 6;
86    private static final float MAXIMUM_TAP_VELOCITY = 100.0f;
87    private static final float MAXIMUM_MINOR_VELOCITY = 150.0f;
88    private static final float MAXIMUM_MAJOR_VELOCITY = 200.0f;
89    private static final float MAXIMUM_ACCELERATION = 2000.0f;
90    private static final int VELOCITY_UNITS = 1000;
91    private static final int MSG_ANIMATE = 1000;
92    private static final int ANIMATION_FRAME_DURATION = 1000 / 60;
93
94    private static final int EXPANDED_FULL_OPEN = -10001;
95    private static final int COLLAPSED_FULL_CLOSED = -10002;
96
97    private final int mHandleId;
98    private final int mContentId;
99
100    private View mHandle;
101    private View mContent;
102
103    private final Rect mFrame = new Rect();
104    private final Rect mInvalidate = new Rect();
105    private boolean mTracking;
106    private boolean mLocked;
107
108    private VelocityTracker mVelocityTracker;
109
110    private boolean mVertical;
111    private boolean mExpanded;
112    private int mBottomOffset;
113    private int mTopOffset;
114    private int mHandleHeight;
115    private int mHandleWidth;
116
117    private OnDrawerOpenListener mOnDrawerOpenListener;
118    private OnDrawerCloseListener mOnDrawerCloseListener;
119    private OnDrawerScrollListener mOnDrawerScrollListener;
120
121    private final Handler mHandler = new SlidingHandler();
122    private float mAnimatedAcceleration;
123    private float mAnimatedVelocity;
124    private float mAnimationPosition;
125    private long mAnimationLastTime;
126    private long mCurrentAnimationTime;
127    private int mTouchDelta;
128    private boolean mAnimating;
129    private boolean mAllowSingleTap;
130    private boolean mAnimateOnClick;
131
132    private final int mTapThreshold;
133    private final int mMaximumTapVelocity;
134    private final int mMaximumMinorVelocity;
135    private final int mMaximumMajorVelocity;
136    private final int mMaximumAcceleration;
137    private final int mVelocityUnits;
138
139    /**
140     * Callback invoked when the drawer is opened.
141     */
142    public static interface OnDrawerOpenListener {
143        /**
144         * Invoked when the drawer becomes fully open.
145         */
146        public void onDrawerOpened();
147    }
148
149    /**
150     * Callback invoked when the drawer is closed.
151     */
152    public static interface OnDrawerCloseListener {
153        /**
154         * Invoked when the drawer becomes fully closed.
155         */
156        public void onDrawerClosed();
157    }
158
159    /**
160     * Callback invoked when the drawer is scrolled.
161     */
162    public static interface OnDrawerScrollListener {
163        /**
164         * Invoked when the user starts dragging/flinging the drawer's handle.
165         */
166        public void onScrollStarted();
167
168        /**
169         * Invoked when the user stops dragging/flinging the drawer's handle.
170         */
171        public void onScrollEnded();
172    }
173
174    /**
175     * Creates a new SlidingDrawer from a specified set of attributes defined in XML.
176     *
177     * @param context The application's environment.
178     * @param attrs The attributes defined in XML.
179     */
180    public SlidingDrawer(Context context, AttributeSet attrs) {
181        this(context, attrs, 0);
182    }
183
184    /**
185     * Creates a new SlidingDrawer from a specified set of attributes defined in XML.
186     *
187     * @param context The application's environment.
188     * @param attrs The attributes defined in XML.
189     * @param defStyle The style to apply to this widget.
190     */
191    public SlidingDrawer(Context context, AttributeSet attrs, int defStyle) {
192        super(context, attrs, defStyle);
193        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SlidingDrawer, defStyle, 0);
194
195        int orientation = a.getInt(R.styleable.SlidingDrawer_orientation, ORIENTATION_VERTICAL);
196        mVertical = orientation == ORIENTATION_VERTICAL;
197        mBottomOffset = (int) a.getDimension(R.styleable.SlidingDrawer_bottomOffset, 0.0f);
198        mTopOffset = (int) a.getDimension(R.styleable.SlidingDrawer_topOffset, 0.0f);
199        mAllowSingleTap = a.getBoolean(R.styleable.SlidingDrawer_allowSingleTap, true);
200        mAnimateOnClick = a.getBoolean(R.styleable.SlidingDrawer_animateOnClick, true);
201
202        int handleId = a.getResourceId(R.styleable.SlidingDrawer_handle, 0);
203        if (handleId == 0) {
204            throw new IllegalArgumentException("The handle attribute is required and must refer "
205                    + "to a valid child.");
206        }
207
208        int contentId = a.getResourceId(R.styleable.SlidingDrawer_content, 0);
209        if (contentId == 0) {
210            throw new IllegalArgumentException("The content attribute is required and must refer "
211                    + "to a valid child.");
212        }
213
214        if (handleId == contentId) {
215            throw new IllegalArgumentException("The content and handle attributes must refer "
216                    + "to different children.");
217        }
218
219        mHandleId = handleId;
220        mContentId = contentId;
221
222        final float density = getResources().getDisplayMetrics().density;
223        mTapThreshold = (int) (TAP_THRESHOLD * density + 0.5f);
224        mMaximumTapVelocity = (int) (MAXIMUM_TAP_VELOCITY * density + 0.5f);
225        mMaximumMinorVelocity = (int) (MAXIMUM_MINOR_VELOCITY * density + 0.5f);
226        mMaximumMajorVelocity = (int) (MAXIMUM_MAJOR_VELOCITY * density + 0.5f);
227        mMaximumAcceleration = (int) (MAXIMUM_ACCELERATION * density + 0.5f);
228        mVelocityUnits = (int) (VELOCITY_UNITS * density + 0.5f);
229
230        a.recycle();
231
232        setAlwaysDrawnWithCacheEnabled(false);
233    }
234
235    @Override
236    protected void onFinishInflate() {
237        mHandle = findViewById(mHandleId);
238        if (mHandle == null) {
239            throw new IllegalArgumentException("The handle attribute is must refer to an"
240                    + " existing child.");
241        }
242        mHandle.setOnClickListener(new DrawerToggler());
243
244        mContent = findViewById(mContentId);
245        if (mContent == null) {
246            throw new IllegalArgumentException("The content attribute is must refer to an"
247                    + " existing child.");
248        }
249        mContent.setVisibility(View.GONE);
250    }
251
252    @Override
253    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
254        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
255        int widthSpecSize =  MeasureSpec.getSize(widthMeasureSpec);
256
257        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
258        int heightSpecSize =  MeasureSpec.getSize(heightMeasureSpec);
259
260        if (widthSpecMode == MeasureSpec.UNSPECIFIED || heightSpecMode == MeasureSpec.UNSPECIFIED) {
261            throw new RuntimeException("SlidingDrawer cannot have UNSPECIFIED dimensions");
262        }
263
264        final View handle = mHandle;
265        measureChild(handle, widthMeasureSpec, heightMeasureSpec);
266
267        if (mVertical) {
268            int height = heightSpecSize - handle.getMeasuredHeight() - mTopOffset;
269            mContent.measure(MeasureSpec.makeMeasureSpec(widthSpecSize, MeasureSpec.EXACTLY),
270                    MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
271        } else {
272            int width = widthSpecSize - handle.getMeasuredWidth() - mTopOffset;
273            mContent.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
274                    MeasureSpec.makeMeasureSpec(heightSpecSize, MeasureSpec.EXACTLY));
275        }
276
277        setMeasuredDimension(widthSpecSize, heightSpecSize);
278    }
279
280    @Override
281    protected void dispatchDraw(Canvas canvas) {
282        final long drawingTime = getDrawingTime();
283        final View handle = mHandle;
284        final boolean isVertical = mVertical;
285
286        drawChild(canvas, handle, drawingTime);
287
288        if (mTracking || mAnimating) {
289            final Bitmap cache = mContent.getDrawingCache();
290            if (cache != null) {
291                if (isVertical) {
292                    canvas.drawBitmap(cache, 0, handle.getBottom(), null);
293                } else {
294                    canvas.drawBitmap(cache, handle.getRight(), 0, null);
295                }
296            } else {
297                canvas.save();
298                canvas.translate(isVertical ? 0 : handle.getLeft() - mTopOffset,
299                        isVertical ? handle.getTop() - mTopOffset : 0);
300                drawChild(canvas, mContent, drawingTime);
301                canvas.restore();
302            }
303        } else if (mExpanded) {
304            drawChild(canvas, mContent, drawingTime);
305        }
306    }
307
308    @Override
309    protected void onLayout(boolean changed, int l, int t, int r, int b) {
310        if (mTracking) {
311            return;
312        }
313
314        final int width = r - l;
315        final int height = b - t;
316
317        final View handle = mHandle;
318
319        int childWidth = handle.getMeasuredWidth();
320        int childHeight = handle.getMeasuredHeight();
321
322        int childLeft;
323        int childTop;
324
325        final View content = mContent;
326
327        if (mVertical) {
328            childLeft = (width - childWidth) / 2;
329            childTop = mExpanded ? mTopOffset : height - childHeight + mBottomOffset;
330
331            content.layout(0, mTopOffset + childHeight, content.getMeasuredWidth(),
332                    mTopOffset + childHeight + content.getMeasuredHeight());
333        } else {
334            childLeft = mExpanded ? mTopOffset : width - childWidth + mBottomOffset;
335            childTop = (height - childHeight) / 2;
336
337            content.layout(mTopOffset + childWidth, 0,
338                    mTopOffset + childWidth + content.getMeasuredWidth(),
339                    content.getMeasuredHeight());
340        }
341
342        handle.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
343        mHandleHeight = handle.getHeight();
344        mHandleWidth = handle.getWidth();
345    }
346
347    @Override
348    public boolean onInterceptTouchEvent(MotionEvent event) {
349        if (mLocked) {
350            return false;
351        }
352
353        final int action = event.getAction();
354
355        float x = event.getX();
356        float y = event.getY();
357
358        final Rect frame = mFrame;
359        final View handle = mHandle;
360
361        handle.getHitRect(frame);
362        if (!mTracking && !frame.contains((int) x, (int) y)) {
363            return false;
364        }
365
366        if (action == MotionEvent.ACTION_DOWN) {
367            mTracking = true;
368
369            handle.setPressed(true);
370            // Must be called before prepareTracking()
371            prepareContent();
372
373            // Must be called after prepareContent()
374            if (mOnDrawerScrollListener != null) {
375                mOnDrawerScrollListener.onScrollStarted();
376            }
377
378            if (mVertical) {
379                final int top = mHandle.getTop();
380                mTouchDelta = (int) y - top;
381                prepareTracking(top);
382            } else {
383                final int left = mHandle.getLeft();
384                mTouchDelta = (int) x - left;
385                prepareTracking(left);
386            }
387            mVelocityTracker.addMovement(event);
388        }
389
390        return true;
391    }
392
393    @Override
394    public boolean onTouchEvent(MotionEvent event) {
395        if (mLocked) {
396            return true;
397        }
398
399        if (mTracking) {
400            mVelocityTracker.addMovement(event);
401            final int action = event.getAction();
402            switch (action) {
403                case MotionEvent.ACTION_MOVE:
404                    moveHandle((int) (mVertical ? event.getY() : event.getX()) - mTouchDelta);
405                    break;
406                case MotionEvent.ACTION_UP:
407                case MotionEvent.ACTION_CANCEL: {
408                    final VelocityTracker velocityTracker = mVelocityTracker;
409                    velocityTracker.computeCurrentVelocity(mVelocityUnits);
410
411                    float yVelocity = velocityTracker.getYVelocity();
412                    float xVelocity = velocityTracker.getXVelocity();
413                    boolean negative;
414
415                    final boolean vertical = mVertical;
416                    if (vertical) {
417                        negative = yVelocity < 0;
418                        if (xVelocity < 0) {
419                            xVelocity = -xVelocity;
420                        }
421                        if (xVelocity > mMaximumMinorVelocity) {
422                            xVelocity = mMaximumMinorVelocity;
423                        }
424                    } else {
425                        negative = xVelocity < 0;
426                        if (yVelocity < 0) {
427                            yVelocity = -yVelocity;
428                        }
429                        if (yVelocity > mMaximumMinorVelocity) {
430                            yVelocity = mMaximumMinorVelocity;
431                        }
432                    }
433
434                    float velocity = (float) Math.hypot(xVelocity, yVelocity);
435                    if (negative) {
436                        velocity = -velocity;
437                    }
438
439                    final int top = mHandle.getTop();
440                    final int left = mHandle.getLeft();
441
442                    if (Math.abs(velocity) < mMaximumTapVelocity) {
443                        if (vertical ? (mExpanded && top < mTapThreshold + mTopOffset) ||
444                                (!mExpanded && top > mBottomOffset + mBottom - mTop -
445                                        mHandleHeight - mTapThreshold) :
446                                (mExpanded && left < mTapThreshold + mTopOffset) ||
447                                (!mExpanded && left > mBottomOffset + mRight - mLeft -
448                                        mHandleWidth - mTapThreshold)) {
449
450                            if (mAllowSingleTap) {
451                                playSoundEffect(SoundEffectConstants.CLICK);
452
453                                if (mExpanded) {
454                                    animateClose(vertical ? top : left);
455                                } else {
456                                    animateOpen(vertical ? top : left);
457                                }
458                            } else {
459                                performFling(vertical ? top : left, velocity, false);
460                            }
461
462                        } else {
463                            performFling(vertical ? top : left, velocity, false);
464                        }
465                    } else {
466                        performFling(vertical ? top : left, velocity, false);
467                    }
468                }
469                break;
470            }
471        }
472
473        return mTracking || mAnimating || super.onTouchEvent(event);
474    }
475
476    private void animateClose(int position) {
477        prepareTracking(position);
478        performFling(position, mMaximumAcceleration, true);
479    }
480
481    private void animateOpen(int position) {
482        prepareTracking(position);
483        performFling(position, -mMaximumAcceleration, true);
484    }
485
486    private void performFling(int position, float velocity, boolean always) {
487        mAnimationPosition = position;
488        mAnimatedVelocity = velocity;
489
490        if (mExpanded) {
491            if (always || (velocity > mMaximumMajorVelocity ||
492                    (position > mTopOffset + (mVertical ? mHandleHeight : mHandleWidth) &&
493                            velocity > -mMaximumMajorVelocity))) {
494                // We are expanded, but they didn't move sufficiently to cause
495                // us to retract.  Animate back to the expanded position.
496                mAnimatedAcceleration = mMaximumAcceleration;
497                if (velocity < 0) {
498                    mAnimatedVelocity = 0;
499                }
500            } else {
501                // We are expanded and are now going to animate away.
502                mAnimatedAcceleration = -mMaximumAcceleration;
503                if (velocity > 0) {
504                    mAnimatedVelocity = 0;
505                }
506            }
507        } else {
508            if (!always && (velocity > mMaximumMajorVelocity ||
509                    (position > (mVertical ? getHeight() : getWidth()) / 2 &&
510                            velocity > -mMaximumMajorVelocity))) {
511                // We are collapsed, and they moved enough to allow us to expand.
512                mAnimatedAcceleration = mMaximumAcceleration;
513                if (velocity < 0) {
514                    mAnimatedVelocity = 0;
515                }
516            } else {
517                // We are collapsed, but they didn't move sufficiently to cause
518                // us to retract.  Animate back to the collapsed position.
519                mAnimatedAcceleration = -mMaximumAcceleration;
520                if (velocity > 0) {
521                    mAnimatedVelocity = 0;
522                }
523            }
524        }
525
526        long now = SystemClock.uptimeMillis();
527        mAnimationLastTime = now;
528        mCurrentAnimationTime = now + ANIMATION_FRAME_DURATION;
529        mAnimating = true;
530        mHandler.removeMessages(MSG_ANIMATE);
531        mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_ANIMATE), mCurrentAnimationTime);
532        stopTracking();
533    }
534
535    private void prepareTracking(int position) {
536        mTracking = true;
537        mVelocityTracker = VelocityTracker.obtain();
538        boolean opening = !mExpanded;
539        if (opening) {
540            mAnimatedAcceleration = mMaximumAcceleration;
541            mAnimatedVelocity = mMaximumMajorVelocity;
542            mAnimationPosition = mBottomOffset +
543                    (mVertical ? getHeight() - mHandleHeight : getWidth() - mHandleWidth);
544            moveHandle((int) mAnimationPosition);
545            mAnimating = true;
546            mHandler.removeMessages(MSG_ANIMATE);
547            long now = SystemClock.uptimeMillis();
548            mAnimationLastTime = now;
549            mCurrentAnimationTime = now + ANIMATION_FRAME_DURATION;
550            mAnimating = true;
551        } else {
552            if (mAnimating) {
553                mAnimating = false;
554                mHandler.removeMessages(MSG_ANIMATE);
555            }
556            moveHandle(position);
557        }
558    }
559
560    private void moveHandle(int position) {
561        final View handle = mHandle;
562
563        if (mVertical) {
564            if (position == EXPANDED_FULL_OPEN) {
565                handle.offsetTopAndBottom(mTopOffset - handle.getTop());
566                invalidate();
567            } else if (position == COLLAPSED_FULL_CLOSED) {
568                handle.offsetTopAndBottom(mBottomOffset + mBottom - mTop -
569                        mHandleHeight - handle.getTop());
570                invalidate();
571            } else {
572                final int top = handle.getTop();
573                int deltaY = position - top;
574                if (position < mTopOffset) {
575                    deltaY = mTopOffset - top;
576                } else if (deltaY > mBottomOffset + mBottom - mTop - mHandleHeight - top) {
577                    deltaY = mBottomOffset + mBottom - mTop - mHandleHeight - top;
578                }
579                handle.offsetTopAndBottom(deltaY);
580
581                final Rect frame = mFrame;
582                final Rect region = mInvalidate;
583
584                handle.getHitRect(frame);
585                region.set(frame);
586
587                region.union(frame.left, frame.top - deltaY, frame.right, frame.bottom - deltaY);
588                region.union(0, frame.bottom - deltaY, getWidth(),
589                        frame.bottom - deltaY + mContent.getHeight());
590
591                invalidate(region);
592            }
593        } else {
594            if (position == EXPANDED_FULL_OPEN) {
595                handle.offsetLeftAndRight(mTopOffset - handle.getLeft());
596                invalidate();
597            } else if (position == COLLAPSED_FULL_CLOSED) {
598                handle.offsetLeftAndRight(mBottomOffset + mRight - mLeft -
599                        mHandleWidth - handle.getLeft());
600                invalidate();
601            } else {
602                final int left = handle.getLeft();
603                int deltaX = position - left;
604                if (position < mTopOffset) {
605                    deltaX = mTopOffset - left;
606                } else if (deltaX > mBottomOffset + mRight - mLeft - mHandleWidth - left) {
607                    deltaX = mBottomOffset + mRight - mLeft - mHandleWidth - left;
608                }
609                handle.offsetLeftAndRight(deltaX);
610
611                final Rect frame = mFrame;
612                final Rect region = mInvalidate;
613
614                handle.getHitRect(frame);
615                region.set(frame);
616
617                region.union(frame.left - deltaX, frame.top, frame.right - deltaX, frame.bottom);
618                region.union(frame.right - deltaX, 0,
619                        frame.right - deltaX + mContent.getWidth(), getHeight());
620
621                invalidate(region);
622            }
623        }
624    }
625
626    private void prepareContent() {
627        if (mAnimating) {
628            return;
629        }
630
631        // Something changed in the content, we need to honor the layout request
632        // before creating the cached bitmap
633        final View content = mContent;
634        if (content.isLayoutRequested()) {
635            if (mVertical) {
636                final int childHeight = mHandleHeight;
637                int height = mBottom - mTop - childHeight - mTopOffset;
638                content.measure(MeasureSpec.makeMeasureSpec(mRight - mLeft, MeasureSpec.EXACTLY),
639                        MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
640                content.layout(0, mTopOffset + childHeight, content.getMeasuredWidth(),
641                        mTopOffset + childHeight + content.getMeasuredHeight());
642            } else {
643                final int childWidth = mHandle.getWidth();
644                int width = mRight - mLeft - childWidth - mTopOffset;
645                content.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
646                        MeasureSpec.makeMeasureSpec(mBottom - mTop, MeasureSpec.EXACTLY));
647                content.layout(childWidth + mTopOffset, 0,
648                        mTopOffset + childWidth + content.getMeasuredWidth(),
649                        content.getMeasuredHeight());
650            }
651        }
652        // Try only once... we should really loop but it's not a big deal
653        // if the draw was cancelled, it will only be temporary anyway
654        content.getViewTreeObserver().dispatchOnPreDraw();
655        content.buildDrawingCache();
656
657        content.setVisibility(View.GONE);
658    }
659
660    private void stopTracking() {
661        mHandle.setPressed(false);
662        mTracking = false;
663
664        if (mOnDrawerScrollListener != null) {
665            mOnDrawerScrollListener.onScrollEnded();
666        }
667
668        if (mVelocityTracker != null) {
669            mVelocityTracker.recycle();
670            mVelocityTracker = null;
671        }
672    }
673
674    private void doAnimation() {
675        if (mAnimating) {
676            incrementAnimation();
677            if (mAnimationPosition >= mBottomOffset + (mVertical ? getHeight() : getWidth()) - 1) {
678                mAnimating = false;
679                closeDrawer();
680            } else if (mAnimationPosition < mTopOffset) {
681                mAnimating = false;
682                openDrawer();
683            } else {
684                moveHandle((int) mAnimationPosition);
685                mCurrentAnimationTime += ANIMATION_FRAME_DURATION;
686                mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_ANIMATE),
687                        mCurrentAnimationTime);
688            }
689        }
690    }
691
692    private void incrementAnimation() {
693        long now = SystemClock.uptimeMillis();
694        float t = (now - mAnimationLastTime) / 1000.0f;                   // ms -> s
695        final float position = mAnimationPosition;
696        final float v = mAnimatedVelocity;                                // px/s
697        final float a = mAnimatedAcceleration;                            // px/s/s
698        mAnimationPosition = position + (v * t) + (0.5f * a * t * t);     // px
699        mAnimatedVelocity = v + (a * t);                                  // px/s
700        mAnimationLastTime = now;                                         // ms
701    }
702
703    /**
704     * Toggles the drawer open and close. Takes effect immediately.
705     *
706     * @see #open()
707     * @see #close()
708     * @see #animateClose()
709     * @see #animateOpen()
710     * @see #animateToggle()
711     */
712    public void toggle() {
713        if (!mExpanded) {
714            openDrawer();
715        } else {
716            closeDrawer();
717        }
718        invalidate();
719        requestLayout();
720    }
721
722    /**
723     * Toggles the drawer open and close with an animation.
724     *
725     * @see #open()
726     * @see #close()
727     * @see #animateClose()
728     * @see #animateOpen()
729     * @see #toggle()
730     */
731    public void animateToggle() {
732        if (!mExpanded) {
733            animateOpen();
734        } else {
735            animateClose();
736        }
737    }
738
739    /**
740     * Opens the drawer immediately.
741     *
742     * @see #toggle()
743     * @see #close()
744     * @see #animateOpen()
745     */
746    public void open() {
747        openDrawer();
748        invalidate();
749        requestLayout();
750
751        sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
752    }
753
754    /**
755     * Closes the drawer immediately.
756     *
757     * @see #toggle()
758     * @see #open()
759     * @see #animateClose()
760     */
761    public void close() {
762        closeDrawer();
763        invalidate();
764        requestLayout();
765    }
766
767    /**
768     * Closes the drawer with an animation.
769     *
770     * @see #close()
771     * @see #open()
772     * @see #animateOpen()
773     * @see #animateToggle()
774     * @see #toggle()
775     */
776    public void animateClose() {
777        prepareContent();
778        final OnDrawerScrollListener scrollListener = mOnDrawerScrollListener;
779        if (scrollListener != null) {
780            scrollListener.onScrollStarted();
781        }
782        animateClose(mVertical ? mHandle.getTop() : mHandle.getLeft());
783
784        if (scrollListener != null) {
785            scrollListener.onScrollEnded();
786        }
787    }
788
789    /**
790     * Opens the drawer with an animation.
791     *
792     * @see #close()
793     * @see #open()
794     * @see #animateClose()
795     * @see #animateToggle()
796     * @see #toggle()
797     */
798    public void animateOpen() {
799        prepareContent();
800        final OnDrawerScrollListener scrollListener = mOnDrawerScrollListener;
801        if (scrollListener != null) {
802            scrollListener.onScrollStarted();
803        }
804        animateOpen(mVertical ? mHandle.getTop() : mHandle.getLeft());
805
806        sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
807
808        if (scrollListener != null) {
809            scrollListener.onScrollEnded();
810        }
811    }
812
813    private void closeDrawer() {
814        moveHandle(COLLAPSED_FULL_CLOSED);
815        mContent.setVisibility(View.GONE);
816        mContent.destroyDrawingCache();
817
818        if (!mExpanded) {
819            return;
820        }
821
822        mExpanded = false;
823        if (mOnDrawerCloseListener != null) {
824            mOnDrawerCloseListener.onDrawerClosed();
825        }
826    }
827
828    private void openDrawer() {
829        moveHandle(EXPANDED_FULL_OPEN);
830        mContent.setVisibility(View.VISIBLE);
831
832        if (mExpanded) {
833            return;
834        }
835
836        mExpanded = true;
837
838        if (mOnDrawerOpenListener != null) {
839            mOnDrawerOpenListener.onDrawerOpened();
840        }
841    }
842
843    /**
844     * Sets the listener that receives a notification when the drawer becomes open.
845     *
846     * @param onDrawerOpenListener The listener to be notified when the drawer is opened.
847     */
848    public void setOnDrawerOpenListener(OnDrawerOpenListener onDrawerOpenListener) {
849        mOnDrawerOpenListener = onDrawerOpenListener;
850    }
851
852    /**
853     * Sets the listener that receives a notification when the drawer becomes close.
854     *
855     * @param onDrawerCloseListener The listener to be notified when the drawer is closed.
856     */
857    public void setOnDrawerCloseListener(OnDrawerCloseListener onDrawerCloseListener) {
858        mOnDrawerCloseListener = onDrawerCloseListener;
859    }
860
861    /**
862     * Sets the listener that receives a notification when the drawer starts or ends
863     * a scroll. A fling is considered as a scroll. A fling will also trigger a
864     * drawer opened or drawer closed event.
865     *
866     * @param onDrawerScrollListener The listener to be notified when scrolling
867     *        starts or stops.
868     */
869    public void setOnDrawerScrollListener(OnDrawerScrollListener onDrawerScrollListener) {
870        mOnDrawerScrollListener = onDrawerScrollListener;
871    }
872
873    /**
874     * Returns the handle of the drawer.
875     *
876     * @return The View reprenseting the handle of the drawer, identified by
877     *         the "handle" id in XML.
878     */
879    public View getHandle() {
880        return mHandle;
881    }
882
883    /**
884     * Returns the content of the drawer.
885     *
886     * @return The View reprenseting the content of the drawer, identified by
887     *         the "content" id in XML.
888     */
889    public View getContent() {
890        return mContent;
891    }
892
893    /**
894     * Unlocks the SlidingDrawer so that touch events are processed.
895     *
896     * @see #lock()
897     */
898    public void unlock() {
899        mLocked = false;
900    }
901
902    /**
903     * Locks the SlidingDrawer so that touch events are ignores.
904     *
905     * @see #unlock()
906     */
907    public void lock() {
908        mLocked = true;
909    }
910
911    /**
912     * Indicates whether the drawer is currently fully opened.
913     *
914     * @return True if the drawer is opened, false otherwise.
915     */
916    public boolean isOpened() {
917        return mExpanded;
918    }
919
920    /**
921     * Indicates whether the drawer is scrolling or flinging.
922     *
923     * @return True if the drawer is scroller or flinging, false otherwise.
924     */
925    public boolean isMoving() {
926        return mTracking || mAnimating;
927    }
928
929    private class DrawerToggler implements OnClickListener {
930        public void onClick(View v) {
931            if (mLocked) {
932                return;
933            }
934            // mAllowSingleTap isn't relevant here; you're *always*
935            // allowed to open/close the drawer by clicking with the
936            // trackball.
937
938            if (mAnimateOnClick) {
939                animateToggle();
940            } else {
941                toggle();
942            }
943        }
944    }
945
946    private class SlidingHandler extends Handler {
947        public void handleMessage(Message m) {
948            switch (m.what) {
949                case MSG_ANIMATE:
950                    doAnimation();
951                    break;
952            }
953        }
954    }
955}
956