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