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