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