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