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