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