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