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