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