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