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