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