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