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