blob: 78c1e9c19e807e8c03a871431d009c4905ad85f2 [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;
Alexandre Lisiona764c682013-09-09 10:02:07 -0400127 private long pressTime;
alision7297bdb2013-05-21 11:56:55 -0400128
129 /**
130 * Callback invoked when the drawer is opened.
131 */
132 public static interface OnDrawerOpenListener {
133 /**
134 * Invoked when the drawer becomes fully open.
135 */
136 public void onDrawerOpened();
137 }
138
139 /**
140 * Callback invoked when the drawer is closed.
141 */
142 public static interface OnDrawerCloseListener {
143 /**
144 * Invoked when the drawer becomes fully closed.
145 */
146 public void onDrawerClosed();
147 }
148
149 /**
150 * Callback invoked when the drawer is scrolled.
151 */
152 public static interface OnDrawerScrollListener {
153 /**
154 * Invoked when the user starts dragging/flinging the drawer's handle.
155 */
156 public void onScrollStarted();
157
158 /**
159 * Invoked when the user stops dragging/flinging the drawer's handle.
160 */
161 public void onScrollEnded();
162 }
163
164 /**
alision907bde72013-06-20 14:40:37 -0400165 * Creates a new SlidingDrawer from a specified set of attributes defined in XML.
alision7297bdb2013-05-21 11:56:55 -0400166 *
167 * @param context
168 * The application's environment.
169 * @param attrs
170 * The attributes defined in XML.
171 */
172 public CustomSlidingDrawer(Context context, AttributeSet attrs) {
173 this(context, attrs, 0);
174 }
175
176 /**
alision907bde72013-06-20 14:40:37 -0400177 * Creates a new SlidingDrawer from a specified set of attributes defined in XML.
alision7297bdb2013-05-21 11:56:55 -0400178 *
179 * @param context
180 * The application's environment.
181 * @param attrs
182 * The attributes defined in XML.
183 * @param defStyle
184 * The style to apply to this widget.
185 */
186 public CustomSlidingDrawer(Context context, AttributeSet attrs, int defStyle) {
187 super(context, attrs, defStyle);
188 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomSlidingDrawer, defStyle, 0);
189
190 int orientation = a.getInt(R.styleable.CustomSlidingDrawer_orientation, ORIENTATION_VERTICAL);
191 mVertical = orientation == ORIENTATION_VERTICAL;
192 mBottomOffset = (int) a.getDimension(R.styleable.CustomSlidingDrawer_bottomOffset, 0.0f);
193 mTopOffset = (int) a.getDimension(R.styleable.CustomSlidingDrawer_topOffset, 0.0f) - 5;
194 mAllowSingleTap = a.getBoolean(R.styleable.CustomSlidingDrawer_allowSingleTap, false);
195 mAnimateOnClick = a.getBoolean(R.styleable.CustomSlidingDrawer_animateOnClick, false);
196
197 int handleId = a.getResourceId(R.styleable.CustomSlidingDrawer_handle, 0);
198 if (handleId == 0) {
199 throw new IllegalArgumentException("The handle attribute is required and must refer " + "to a valid child.");
200 }
201
202 int contentId = a.getResourceId(R.styleable.CustomSlidingDrawer_content, 0);
203 if (contentId == 0) {
204 throw new IllegalArgumentException("The content attribute is required and must refer " + "to a valid child.");
205 }
206
207 if (handleId == contentId) {
208 throw new IllegalArgumentException("The content and handle attributes must refer " + "to different children.");
209 }
210
211 mHandleId = handleId;
212 mContentId = contentId;
213
214 final float density = getResources().getDisplayMetrics().density;
215 mTapThreshold = (int) (TAP_THRESHOLD * density + 0.5f);
216 mMaximumTapVelocity = (int) (MAXIMUM_TAP_VELOCITY * density + 0.5f);
217 mMaximumMinorVelocity = (int) (MAXIMUM_MINOR_VELOCITY * density + 0.5f);
218 mMaximumMajorVelocity = (int) (MAXIMUM_MAJOR_VELOCITY * density + 0.5f);
219 mMaximumAcceleration = (int) (MAXIMUM_ACCELERATION * density + 0.5f);
220 mVelocityUnits = (int) (VELOCITY_UNITS * density + 0.5f);
221
222 a.recycle();
223
224 setAlwaysDrawnWithCacheEnabled(false);
225 }
226
227 @Override
228 protected void onFinishInflate() {
229 mHandle = findViewById(mHandleId);
230 if (mHandle == null) {
231 throw new IllegalArgumentException("The handle attribute is must refer to an" + " existing child.");
232 }
233 mHandle.setOnClickListener(new DrawerToggler());
234
235 mContent = findViewById(mContentId);
236 if (mContent == null) {
237 throw new IllegalArgumentException("The content attribute is must refer to an" + " existing child.");
238 }
239 mContent.setVisibility(View.GONE);
240 }
241
242 @Override
243 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
244 int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
245 int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
246
247 int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
248 int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
249
250 if (widthSpecMode == MeasureSpec.UNSPECIFIED || heightSpecMode == MeasureSpec.UNSPECIFIED) {
251 throw new RuntimeException("SlidingDrawer cannot have UNSPECIFIED dimensions");
252 }
253
254 final View handle = mHandle;
255 measureChild(handle, widthMeasureSpec, heightMeasureSpec);
256
257 if (mVertical) {
258 int height = heightSpecSize - handle.getMeasuredHeight() - mTopOffset;
alision907bde72013-06-20 14:40:37 -0400259 mContent.measure(MeasureSpec.makeMeasureSpec(widthSpecSize, MeasureSpec.EXACTLY),
260 MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
alision7297bdb2013-05-21 11:56:55 -0400261 } else {
262 int width = widthSpecSize - handle.getMeasuredWidth() - mTopOffset;
alision907bde72013-06-20 14:40:37 -0400263 mContent.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
264 MeasureSpec.makeMeasureSpec(heightSpecSize, MeasureSpec.EXACTLY));
alision7297bdb2013-05-21 11:56:55 -0400265 }
266
267 setMeasuredDimension(widthSpecSize, heightSpecSize);
268 }
269
270 @Override
271 protected void dispatchDraw(Canvas canvas) {
272 final long drawingTime = getDrawingTime();
273 final View handle = mHandle;
274 final boolean isVertical = mVertical;
275
276 drawChild(canvas, handle, drawingTime);
277
278 if (mTracking || mAnimating) {
279 final Bitmap cache = mContent.getDrawingCache();
280 if (cache != null) {
281 if (isVertical) {
282 canvas.drawBitmap(cache, 0, handle.getBottom(), null);
283 } else {
284 canvas.drawBitmap(cache, handle.getRight(), 0, null);
285 }
286 } else {
287 canvas.save();
288 canvas.translate(isVertical ? 0 : handle.getLeft() - mTopOffset, isVertical ? handle.getTop() - mTopOffset : 0);
289 drawChild(canvas, mContent, drawingTime);
290 canvas.restore();
291 }
292 } else if (mExpanded) {
293 drawChild(canvas, mContent, drawingTime);
294 }
295 }
296
297 @Override
298 protected void onLayout(boolean changed, int l, int t, int r, int b) {
299 if (mTracking) {
300 return;
301 }
302
303 final int width = r - l;
304 final int height = b - t;
305
306 final View handle = mHandle;
307
308 int childWidth = handle.getMeasuredWidth();
309 int childHeight = handle.getMeasuredHeight();
310
311 int childLeft;
312 int childTop;
313
314 final View content = mContent;
315
316 if (mVertical) {
317 childLeft = (width - childWidth) / 2;
318 childTop = mExpanded ? mTopOffset : height - childHeight + mBottomOffset;
319
320 content.layout(0, mTopOffset + childHeight, content.getMeasuredWidth(), mTopOffset + childHeight + content.getMeasuredHeight());
321 } else {
322 childLeft = mExpanded ? mTopOffset : width - childWidth + mBottomOffset;
323 childTop = (height - childHeight) / 2;
324
325 content.layout(mTopOffset + childWidth, 0, mTopOffset + childWidth + content.getMeasuredWidth(), content.getMeasuredHeight());
326 }
327
328 handle.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
329 mHandleHeight = handle.getHeight();
330 mHandleWidth = handle.getWidth();
331 }
332
333 @Override
334 public boolean onInterceptTouchEvent(MotionEvent event) {
335
alisionf7053602013-07-09 10:25:20 -0400336// Log.i(TAG, "onInterceptTouchEvent");
alision7297bdb2013-05-21 11:56:55 -0400337 if (mLocked) {
Alexandre Lisiona764c682013-09-09 10:02:07 -0400338// Log.i(TAG, "Locked");
alision7297bdb2013-05-21 11:56:55 -0400339 return false;
340 }
341
342 final int action = event.getAction();
343
344 float x = event.getX();
345 float y = event.getY();
346
347 final Rect frame = mFrame;
348 final View handle = mHandle;
alision907bde72013-06-20 14:40:37 -0400349
350
alision7297bdb2013-05-21 11:56:55 -0400351
352 // New code
353 View trackHandle = mTrackHandle;
alision907bde72013-06-20 14:40:37 -0400354
alision7297bdb2013-05-21 11:56:55 -0400355 // set the rect frame to the mTrackHandle view borders instead of the
356 // hole handle view
alision907bde72013-06-20 14:40:37 -0400357
alision55c36cb2013-06-14 14:57:38 -0400358 Resources r = getResources();
359 int px = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 50, r.getDisplayMetrics());
alision7297bdb2013-05-21 11:56:55 -0400360
361 // getParent() => The right and left are valid, but we need to get the
362 // parent top and bottom to have absolute values (in screen)
alision907bde72013-06-20 14:40:37 -0400363 frame.set(trackHandle.getLeft(), ((ViewGroup) trackHandle.getParent()).getTop(), trackHandle.getRight() - px,
364 ((ViewGroup) trackHandle.getParent()).getBottom());
alision7297bdb2013-05-21 11:56:55 -0400365
alision907bde72013-06-20 14:40:37 -0400366 // handle.getHitRect(frame);
alision7297bdb2013-05-21 11:56:55 -0400367 if (!mTracking && !frame.contains((int) x, (int) y)) {
Alexandre Lisiona764c682013-09-09 10:02:07 -0400368// Log.i(TAG, "not tracking and not in frame");
alision7297bdb2013-05-21 11:56:55 -0400369 return false;
370 }
371
372 if (action == MotionEvent.ACTION_DOWN) {
373 mTracking = true;
Alexandre Lisiona764c682013-09-09 10:02:07 -0400374// Log.i(TAG, "action down");
alision7297bdb2013-05-21 11:56:55 -0400375 handle.setPressed(true);
376 // Must be called before prepareTracking()
377 prepareContent();
Alexandre Lisiona764c682013-09-09 10:02:07 -0400378
379 pressTime = System.currentTimeMillis();
alision7297bdb2013-05-21 11:56:55 -0400380
381 // Must be called after prepareContent()
382 if (mOnDrawerScrollListener != null) {
383 mOnDrawerScrollListener.onScrollStarted();
384 }
385
386 if (mVertical) {
387 final int top = mHandle.getTop();
388 mTouchDelta = (int) y - top;
389 prepareTracking(top);
390 } else {
391 final int left = mHandle.getLeft();
392 mTouchDelta = (int) x - left;
393 prepareTracking(left);
394 }
395 mVelocityTracker.addMovement(event);
alision907bde72013-06-20 14:40:37 -0400396 return true;
alision7297bdb2013-05-21 11:56:55 -0400397 }
alision907bde72013-06-20 14:40:37 -0400398 return false;
alision7297bdb2013-05-21 11:56:55 -0400399
alision7297bdb2013-05-21 11:56:55 -0400400 }
401
402 @Override
403 public boolean onTouchEvent(MotionEvent event) {
404 if (mLocked) {
405 return true;
406 }
407
alisionf7053602013-07-09 10:25:20 -0400408// Log.i(TAG, "onTouchEvent");
alision7297bdb2013-05-21 11:56:55 -0400409 if (mTracking) {
410 mVelocityTracker.addMovement(event);
411 final int action = event.getAction();
412 switch (action) {
413 case MotionEvent.ACTION_MOVE:
414 moveHandle((int) (mVertical ? event.getY() : event.getX()) - mTouchDelta);
415 break;
416 case MotionEvent.ACTION_UP:
Alexandre Lisiona764c682013-09-09 10:02:07 -0400417 if(System.currentTimeMillis() - pressTime <= 100){
418 animateToggle();
419 break;
420 }
421
alision7297bdb2013-05-21 11:56:55 -0400422 case MotionEvent.ACTION_CANCEL: {
423 final VelocityTracker velocityTracker = mVelocityTracker;
424 velocityTracker.computeCurrentVelocity(mVelocityUnits);
425
426 float yVelocity = velocityTracker.getYVelocity();
427 float xVelocity = velocityTracker.getXVelocity();
428 boolean negative;
429
430 final boolean vertical = mVertical;
431 if (vertical) {
432 negative = yVelocity < 0;
433 if (xVelocity < 0) {
434 xVelocity = -xVelocity;
435 }
436 if (xVelocity > mMaximumMinorVelocity) {
437 xVelocity = mMaximumMinorVelocity;
438 }
439 } else {
440 negative = xVelocity < 0;
441 if (yVelocity < 0) {
442 yVelocity = -yVelocity;
443 }
444 if (yVelocity > mMaximumMinorVelocity) {
445 yVelocity = mMaximumMinorVelocity;
446 }
447 }
448
449 float velocity = (float) Math.hypot(xVelocity, yVelocity);
450 if (negative) {
451 velocity = -velocity;
452 }
453
454 final int top = mHandle.getTop();
455 final int left = mHandle.getLeft();
456
457 if (Math.abs(velocity) < mMaximumTapVelocity) {
alision907bde72013-06-20 14:40:37 -0400458 if (vertical ? (mExpanded && top < mTapThreshold + mTopOffset)
459 || (!mExpanded && top > mBottomOffset + getBottom() - getTop() - mHandleHeight - mTapThreshold)
460 : (mExpanded && left < mTapThreshold + mTopOffset)
461 || (!mExpanded && left > mBottomOffset + getRight() - getLeft() - mHandleWidth - mTapThreshold)) {
alision7297bdb2013-05-21 11:56:55 -0400462
463 if (mAllowSingleTap) {
464 playSoundEffect(SoundEffectConstants.CLICK);
465
466 if (mExpanded) {
467 animateClose(vertical ? top : left);
468 } else {
469 animateOpen(vertical ? top : left);
470 }
471 } else {
472 performFling(vertical ? top : left, velocity, false);
473 }
474
475 } else {
476 performFling(vertical ? top : left, velocity, false);
477 }
478 } else {
479 performFling(vertical ? top : left, velocity, false);
480 }
481 }
482 break;
483 }
484 }
485
486 return mTracking || mAnimating || super.onTouchEvent(event);
487 }
488
489 private void animateClose(int position) {
490 prepareTracking(position);
491 performFling(position, mMaximumAcceleration, true);
492 }
493
494 private void animateOpen(int position) {
495 prepareTracking(position);
496 performFling(position, -mMaximumAcceleration, true);
497 }
498
499 private void performFling(int position, float velocity, boolean always) {
500 mAnimationPosition = position;
501 mAnimatedVelocity = velocity;
502
503 if (mExpanded) {
alision907bde72013-06-20 14:40:37 -0400504 if (always
505 || (velocity > mMaximumMajorVelocity || (position > mTopOffset + (mVertical ? mHandleHeight : mHandleWidth) && velocity > -mMaximumMajorVelocity))) {
alision7297bdb2013-05-21 11:56:55 -0400506 // We are expanded, but they didn't move sufficiently to cause
507 // us to retract. Animate back to the expanded position.
508 mAnimatedAcceleration = mMaximumAcceleration;
509 if (velocity < 0) {
510 mAnimatedVelocity = 0;
511 }
512 } else {
513 // We are expanded and are now going to animate away.
514 mAnimatedAcceleration = -mMaximumAcceleration;
515 if (velocity > 0) {
516 mAnimatedVelocity = 0;
517 }
518 }
519 } else {
alision907bde72013-06-20 14:40:37 -0400520 if (!always
521 && (velocity > mMaximumMajorVelocity || (position > (mVertical ? getHeight() : getWidth()) / 2 && velocity > -mMaximumMajorVelocity))) {
alision7297bdb2013-05-21 11:56:55 -0400522 // We are collapsed, and they moved enough to allow us to
523 // expand.
524 mAnimatedAcceleration = mMaximumAcceleration;
525 if (velocity < 0) {
526 mAnimatedVelocity = 0;
527 }
528 } else {
529 // We are collapsed, but they didn't move sufficiently to cause
530 // us to retract. Animate back to the collapsed position.
531 mAnimatedAcceleration = -mMaximumAcceleration;
532 if (velocity > 0) {
533 mAnimatedVelocity = 0;
534 }
535 }
536 }
537
538 long now = SystemClock.uptimeMillis();
539 mAnimationLastTime = now;
540 mCurrentAnimationTime = now + ANIMATION_FRAME_DURATION;
541 mAnimating = true;
542 mHandler.removeMessages(MSG_ANIMATE);
543 mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_ANIMATE), mCurrentAnimationTime);
544 stopTracking();
545 }
546
547 private void prepareTracking(int position) {
548 mTracking = true;
549 mVelocityTracker = VelocityTracker.obtain();
550 boolean opening = !mExpanded;
551 if (opening) {
552 mAnimatedAcceleration = mMaximumAcceleration;
553 mAnimatedVelocity = mMaximumMajorVelocity;
554 mAnimationPosition = mBottomOffset + (mVertical ? getHeight() - mHandleHeight : getWidth() - mHandleWidth);
555 moveHandle((int) mAnimationPosition);
556 mAnimating = true;
557 mHandler.removeMessages(MSG_ANIMATE);
558 long now = SystemClock.uptimeMillis();
559 mAnimationLastTime = now;
560 mCurrentAnimationTime = now + ANIMATION_FRAME_DURATION;
561 mAnimating = true;
562 } else {
563 if (mAnimating) {
564 mAnimating = false;
565 mHandler.removeMessages(MSG_ANIMATE);
566 }
567 moveHandle(position);
568 }
569 }
570
571 private void moveHandle(int position) {
572 final View handle = mHandle;
573
574 if (mVertical) {
575 if (position == EXPANDED_FULL_OPEN) {
576 handle.offsetTopAndBottom(mTopOffset - handle.getTop());
577 invalidate();
578 } else if (position == COLLAPSED_FULL_CLOSED) {
579 handle.offsetTopAndBottom(mBottomOffset + getBottom() - getTop() - mHandleHeight - handle.getTop());
580 invalidate();
581 } else {
582 final int top = handle.getTop();
583 int deltaY = position - top;
584 if (position < mTopOffset) {
585 deltaY = mTopOffset - top;
586 } else if (deltaY > mBottomOffset + getBottom() - getTop() - mHandleHeight - top) {
587 deltaY = mBottomOffset + getBottom() - getTop() - mHandleHeight - top;
588 }
589 handle.offsetTopAndBottom(deltaY);
590
591 final Rect frame = mFrame;
592 final Rect region = mInvalidate;
593
594 handle.getHitRect(frame);
595 region.set(frame);
596
597 region.union(frame.left, frame.top - deltaY, frame.right, frame.bottom - deltaY);
598 region.union(0, frame.bottom - deltaY, getWidth(), frame.bottom - deltaY + mContent.getHeight());
599
600 invalidate(region);
601 }
602 } else {
603 if (position == EXPANDED_FULL_OPEN) {
604 handle.offsetLeftAndRight(mTopOffset - handle.getLeft());
605 invalidate();
606 } else if (position == COLLAPSED_FULL_CLOSED) {
607 handle.offsetLeftAndRight(mBottomOffset + getRight() - getLeft() - mHandleWidth - handle.getLeft());
608 invalidate();
609 } else {
610 final int left = handle.getLeft();
611 int deltaX = position - left;
612 if (position < mTopOffset) {
613 deltaX = mTopOffset - left;
614 } else if (deltaX > mBottomOffset + getRight() - getLeft() - mHandleWidth - left) {
615 deltaX = mBottomOffset + getRight() - getLeft() - mHandleWidth - left;
616 }
617 handle.offsetLeftAndRight(deltaX);
618
619 final Rect frame = mFrame;
620 final Rect region = mInvalidate;
621
622 handle.getHitRect(frame);
623 region.set(frame);
624
625 region.union(frame.left - deltaX, frame.top, frame.right - deltaX, frame.bottom);
626 region.union(frame.right - deltaX, 0, frame.right - deltaX + mContent.getWidth(), getHeight());
627
628 invalidate(region);
629 }
630 }
631 }
632
633 private void prepareContent() {
634 if (mAnimating) {
635 return;
636 }
637
638 // Something changed in the content, we need to honor the layout request
639 // before creating the cached bitmap
640 final View content = mContent;
641 if (content.isLayoutRequested()) {
642 if (mVertical) {
643 final int childHeight = mHandleHeight;
644 int height = getBottom() - getTop() - childHeight - mTopOffset;
alision907bde72013-06-20 14:40:37 -0400645 content.measure(MeasureSpec.makeMeasureSpec(getRight() - getLeft(), MeasureSpec.EXACTLY),
646 MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
alision7297bdb2013-05-21 11:56:55 -0400647 content.layout(0, mTopOffset + childHeight, content.getMeasuredWidth(), mTopOffset + childHeight + content.getMeasuredHeight());
648 } else {
649 final int childWidth = mHandle.getWidth();
650 int width = getRight() - getLeft() - childWidth - mTopOffset;
alision907bde72013-06-20 14:40:37 -0400651 content.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
652 MeasureSpec.makeMeasureSpec(getBottom() - getTop(), MeasureSpec.EXACTLY));
alision7297bdb2013-05-21 11:56:55 -0400653 content.layout(childWidth + mTopOffset, 0, mTopOffset + childWidth + content.getMeasuredWidth(), content.getMeasuredHeight());
654 }
655 }
656 // Try only once... we should really loop but it's not a big deal
657 // if the draw was cancelled, it will only be temporary anyway
658 content.getViewTreeObserver().dispatchOnPreDraw();
659 // if (!content.isHardwareAccelerated()) content.buildDrawingCache();
660
661 content.setVisibility(View.GONE);
662 }
663
664 private void stopTracking() {
665 mHandle.setPressed(false);
666 mTracking = false;
667
668 if (mOnDrawerScrollListener != null) {
669 mOnDrawerScrollListener.onScrollEnded();
670 }
671
672 if (mVelocityTracker != null) {
673 mVelocityTracker.recycle();
674 mVelocityTracker = null;
675 }
676 }
677
678 private void doAnimation() {
679 if (mAnimating) {
680 incrementAnimation();
681 if (mAnimationPosition >= mBottomOffset + (mVertical ? getHeight() : getWidth()) - 1) {
682 mAnimating = false;
683 closeDrawer();
684 } else if (mAnimationPosition < mTopOffset) {
685 mAnimating = false;
686 openDrawer();
687 } else {
688 moveHandle((int) mAnimationPosition);
689 mCurrentAnimationTime += ANIMATION_FRAME_DURATION;
690 mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_ANIMATE), mCurrentAnimationTime);
691 }
692 }
693 }
694
695 private void incrementAnimation() {
696 long now = SystemClock.uptimeMillis();
697 float t = (now - mAnimationLastTime) / 1000.0f; // ms -> s
698 final float position = mAnimationPosition;
699 final float v = mAnimatedVelocity; // px/s
700 final float a = mAnimatedAcceleration; // px/s/s
701 mAnimationPosition = position + (v * t) + (0.5f * a * t * t); // px
702 mAnimatedVelocity = v + (a * t); // px/s
703 mAnimationLastTime = now; // ms
704 }
705
706 /**
707 * Toggles the drawer open and close. Takes effect immediately.
708 *
709 * @see #open()
710 * @see #close()
711 * @see #animateClose()
712 * @see #animateOpen()
713 * @see #animateToggle()
714 */
715 public void toggle() {
716 if (!mExpanded) {
717 openDrawer();
718 } else {
719 closeDrawer();
720 }
721 invalidate();
722 requestLayout();
723 }
724
725 /**
726 * Toggles the drawer open and close with an animation.
727 *
728 * @see #open()
729 * @see #close()
730 * @see #animateClose()
731 * @see #animateOpen()
732 * @see #toggle()
733 */
734 public void animateToggle() {
735 if (!mExpanded) {
736 animateOpen();
737 } else {
738 animateClose();
739 }
740 }
741
742 /**
743 * Opens the drawer immediately.
744 *
745 * @see #toggle()
746 * @see #close()
747 * @see #animateOpen()
748 */
749 public void open() {
750 openDrawer();
751 invalidate();
752 requestLayout();
753
754 sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
755 }
756
757 /**
758 * Closes the drawer immediately.
759 *
760 * @see #toggle()
761 * @see #open()
762 * @see #animateClose()
763 */
764 public void close() {
765 closeDrawer();
766 invalidate();
767 requestLayout();
768 }
769
770 /**
771 * Closes the drawer with an animation.
772 *
773 * @see #close()
774 * @see #open()
775 * @see #animateOpen()
776 * @see #animateToggle()
777 * @see #toggle()
778 */
779 public void animateClose() {
780 prepareContent();
781 final OnDrawerScrollListener scrollListener = mOnDrawerScrollListener;
782 if (scrollListener != null) {
783 scrollListener.onScrollStarted();
784 }
785 animateClose(mVertical ? mHandle.getTop() : mHandle.getLeft());
786
787 if (scrollListener != null) {
788 scrollListener.onScrollEnded();
789 }
790 }
791
792 /**
793 * Opens the drawer with an animation.
794 *
795 * @see #close()
796 * @see #open()
797 * @see #animateClose()
798 * @see #animateToggle()
799 * @see #toggle()
800 */
801 public void animateOpen() {
alision907bde72013-06-20 14:40:37 -0400802
alision55c36cb2013-06-14 14:57:38 -0400803 if (mExpanded) {
804 return;
805 }
alision7297bdb2013-05-21 11:56:55 -0400806 prepareContent();
807 final OnDrawerScrollListener scrollListener = mOnDrawerScrollListener;
808 if (scrollListener != null) {
809 scrollListener.onScrollStarted();
810 }
811 animateOpen(mVertical ? mHandle.getTop() : mHandle.getLeft());
812
813 sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
814
815 if (scrollListener != null) {
816 scrollListener.onScrollEnded();
817 }
818 }
819
820 // @Override
821 // public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
822 // super.onInitializeAccessibilityEvent(event);
823 // event.setClassName(SlidingDrawer.class.getName());
824 // }
825
826 // @Override
827 // public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)
828 // {
829 // super.onInitializeAccessibilityNodeInfo(info);
830 // info.setClassName(SlidingDrawer.class.getName());
831 // }
832
833 private void closeDrawer() {
834 moveHandle(COLLAPSED_FULL_CLOSED);
835 mContent.setVisibility(View.GONE);
836 mContent.destroyDrawingCache();
837
838 if (!mExpanded) {
839 return;
840 }
841
842 mExpanded = false;
843 if (mOnDrawerCloseListener != null) {
844 mOnDrawerCloseListener.onDrawerClosed();
845 }
846 }
847
848 private void openDrawer() {
849 moveHandle(EXPANDED_FULL_OPEN);
850 mContent.setVisibility(View.VISIBLE);
851
852 if (mExpanded) {
853 return;
854 }
855
856 mExpanded = true;
857
858 if (mOnDrawerOpenListener != null) {
859 mOnDrawerOpenListener.onDrawerOpened();
860 }
861 }
862
863 /**
alision907bde72013-06-20 14:40:37 -0400864 * Sets the listener that receives a notification when the drawer becomes open.
alision7297bdb2013-05-21 11:56:55 -0400865 *
866 * @param onDrawerOpenListener
867 * The listener to be notified when the drawer is opened.
868 */
869 public void setOnDrawerOpenListener(OnDrawerOpenListener onDrawerOpenListener) {
870 mOnDrawerOpenListener = onDrawerOpenListener;
871 }
872
873 /**
alision907bde72013-06-20 14:40:37 -0400874 * Sets the listener that receives a notification when the drawer becomes close.
alision7297bdb2013-05-21 11:56:55 -0400875 *
876 * @param onDrawerCloseListener
877 * The listener to be notified when the drawer is closed.
878 */
879 public void setOnDrawerCloseListener(OnDrawerCloseListener onDrawerCloseListener) {
880 mOnDrawerCloseListener = onDrawerCloseListener;
881 }
882
883 /**
alision907bde72013-06-20 14:40:37 -0400884 * 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 -0400885 * trigger a drawer opened or drawer closed event.
886 *
887 * @param onDrawerScrollListener
888 * The listener to be notified when scrolling starts or stops.
889 */
890 public void setOnDrawerScrollListener(OnDrawerScrollListener onDrawerScrollListener) {
891 mOnDrawerScrollListener = onDrawerScrollListener;
892 }
893
894 /**
895 * Returns the handle of the drawer.
896 *
alision907bde72013-06-20 14:40:37 -0400897 * @return The View reprenseting the handle of the drawer, identified by the "handle" id in XML.
alision7297bdb2013-05-21 11:56:55 -0400898 */
899 public View getHandle() {
900 return mHandle;
901 }
902
903 /**
904 * Returns the content of the drawer.
905 *
alision907bde72013-06-20 14:40:37 -0400906 * @return The View reprenseting the content of the drawer, identified by the "content" id in XML.
alision7297bdb2013-05-21 11:56:55 -0400907 */
908 public View getContent() {
909 return mContent;
910 }
911
912 /**
913 * Unlocks the SlidingDrawer so that touch events are processed.
914 *
915 * @see #lock()
916 */
917 public void unlock() {
918 mLocked = false;
919 }
920
921 /**
922 * Locks the SlidingDrawer so that touch events are ignores.
923 *
924 * @see #unlock()
925 */
926 public void lock() {
927 mLocked = true;
928 }
929
930 /**
931 * Indicates whether the drawer is currently fully opened.
932 *
933 * @return True if the drawer is opened, false otherwise.
934 */
935 public boolean isOpened() {
936 return mExpanded;
937 }
938
939 /**
940 * Indicates whether the drawer is scrolling or flinging.
941 *
942 * @return True if the drawer is scroller or flinging, false otherwise.
943 */
944 public boolean isMoving() {
945 return mTracking || mAnimating;
946 }
947
948 private class DrawerToggler implements OnClickListener {
949 public void onClick(View v) {
950 if (mLocked) {
951 return;
952 }
953 // mAllowSingleTap isn't relevant here; you're *always*
954 // allowed to open/close the drawer by clicking with the
955 // trackball.
956
957 if (mAnimateOnClick) {
958 animateToggle();
959 } else {
960 toggle();
961 }
962 }
963 }
964
965 public void setmTrackHandle(View mTrackHandle) {
966 this.mTrackHandle = mTrackHandle;
967 }
968
969 private class SlidingHandler extends Handler {
970 public void handleMessage(Message m) {
971 switch (m.what) {
972 case MSG_ANIMATE:
973 doAnimation();
974 break;
975 }
976 }
977 }
978}