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