blob: cd812e2117624ae356897ef54a3f1ccdebe57bf5 [file] [log] [blame]
Alexandre Lision2affbaa2013-09-27 16:30:35 -04001/*
2 * Copyright (C) 2004-2013 Savoir-Faire Linux Inc.
3 *
4 * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 3 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
19 *
20 * Additional permission under GNU GPL version 3 section 7:
21 *
22 * If you modify this program, or any covered work, by linking or
23 * combining it with the OpenSSL project's OpenSSL library (or a
24 * modified version of that library), containing parts covered by the
25 * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
26 * grants you additional permission to convey the resulting work.
27 * Corresponding Source for a non-source form of such a combination
28 * shall include the source code for the parts of OpenSSL used as well
29 * as that of the covered work.
30 */
31
Alexandre Lision064e1e02013-10-01 16:18:42 -040032package org.sflphone.views;
alision7297bdb2013-05-21 11:56:55 -040033
Alexandre Lision6e8931e2013-09-19 16:49:34 -040034import java.lang.ref.WeakReference;
35
alision7297bdb2013-05-21 11:56:55 -040036import android.content.Context;
alision55c36cb2013-06-14 14:57:38 -040037import android.content.res.Resources;
alision7297bdb2013-05-21 11:56:55 -040038import android.content.res.TypedArray;
39import android.graphics.Bitmap;
40import android.graphics.Canvas;
41import android.graphics.Rect;
42import android.os.Handler;
43import android.os.Message;
44import android.os.SystemClock;
45import android.util.AttributeSet;
Alexandre Lision2affbaa2013-09-27 16:30:35 -040046import android.util.Log;
alision55c36cb2013-06-14 14:57:38 -040047import android.util.TypedValue;
alision7297bdb2013-05-21 11:56:55 -040048import android.view.MotionEvent;
49import android.view.SoundEffectConstants;
50import android.view.VelocityTracker;
51import android.view.View;
52import android.view.ViewGroup;
53import android.view.accessibility.AccessibilityEvent;
54
Alexandre Lision064e1e02013-10-01 16:18:42 -040055import org.sflphone.R;
alision7297bdb2013-05-21 11:56:55 -040056
57/**
alision907bde72013-06-20 14:40:37 -040058 * 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
59 * vertically or horizontally.
alision7297bdb2013-05-21 11:56:55 -040060 *
alision907bde72013-06-20 14:40:37 -040061 * 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 -040062 *
alision907bde72013-06-20 14:40:37 -040063 * SlidingDrawer should be used as an overlay inside layouts. This means SlidingDrawer should only be used inside of a FrameLayout or a RelativeLayout
64 * 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 -040065 * match_parent for both its dimensions.
66 *
alision907bde72013-06-20 14:40:37 -040067 * Inside an XML layout, SlidingDrawer must define the id of the handle and of the content:
alision7297bdb2013-05-21 11:56:55 -040068 *
69 * <pre class="prettyprint">
70 * &lt;SlidingDrawer
71 * android:id="@+id/drawer"
72 * android:layout_width="match_parent"
73 * android:layout_height="match_parent"
74 *
75 * android:handle="@+id/handle"
76 * android:content="@+id/content"&gt;
77 *
78 * &lt;ImageView
79 * android:id="@id/handle"
80 * android:layout_width="88dip"
81 * android:layout_height="44dip" /&gt;
82 *
83 * &lt;GridView
84 * android:id="@id/content"
85 * android:layout_width="match_parent"
86 * android:layout_height="match_parent" /&gt;
87 *
88 * &lt;/SlidingDrawer&gt;
89 * </pre>
90 *
91 * @attr ref android.R.styleable#SlidingDrawer_content
92 * @attr ref android.R.styleable#SlidingDrawer_handle
93 * @attr ref android.R.styleable#SlidingDrawer_topOffset
94 * @attr ref android.R.styleable#SlidingDrawer_bottomOffset
95 * @attr ref android.R.styleable#SlidingDrawer_orientation
96 * @attr ref android.R.styleable#SlidingDrawer_allowSingleTap
97 * @attr ref android.R.styleable#SlidingDrawer_animateOnClick
98 *
99 */
100
101public class CustomSlidingDrawer extends ViewGroup {
102 public static final int ORIENTATION_HORIZONTAL = 0;
103 public static final int ORIENTATION_VERTICAL = 1;
104
105 private static final int TAP_THRESHOLD = 6;
106 private static final float MAXIMUM_TAP_VELOCITY = 100.0f;
107 private static final float MAXIMUM_MINOR_VELOCITY = 150.0f;
108 private static final float MAXIMUM_MAJOR_VELOCITY = 200.0f;
109 private static final float MAXIMUM_ACCELERATION = 2000.0f;
110 private static final int VELOCITY_UNITS = 1000;
111 private static final int MSG_ANIMATE = 1000;
112 private static final int ANIMATION_FRAME_DURATION = 1000 / 60;
113
114 private static final int EXPANDED_FULL_OPEN = -10001;
115 private static final int COLLAPSED_FULL_CLOSED = -10002;
Alexandre Lision6e8931e2013-09-19 16:49:34 -0400116// private static final String TAG = CustomSlidingDrawer.class.getSimpleName();
alision7297bdb2013-05-21 11:56:55 -0400117
118 private final int mHandleId;
119 private final int mContentId;
120
121 private View mHandle;
122 private View mTrackHandle;
123 private View mContent;
124
125 private final Rect mFrame = new Rect();
126 private final Rect mInvalidate = new Rect();
127 private boolean mTracking;
128 private boolean mLocked;
129
130 private VelocityTracker mVelocityTracker;
131
132 private boolean mVertical;
133 private boolean mExpanded;
134 private int mBottomOffset;
135 private int mTopOffset;
136 private int mHandleHeight;
137 private int mHandleWidth;
138
139 private OnDrawerOpenListener mOnDrawerOpenListener;
140 private OnDrawerCloseListener mOnDrawerCloseListener;
141 private OnDrawerScrollListener mOnDrawerScrollListener;
142
Alexandre Lision6e8931e2013-09-19 16:49:34 -0400143 private SlidingHandler mHandler;
alision7297bdb2013-05-21 11:56:55 -0400144 private float mAnimatedAcceleration;
145 private float mAnimatedVelocity;
146 private float mAnimationPosition;
147 private long mAnimationLastTime;
148 private long mCurrentAnimationTime;
149 private int mTouchDelta;
150 private boolean mAnimating;
151 private boolean mAllowSingleTap;
152 private boolean mAnimateOnClick;
153
154 private final int mTapThreshold;
155 private final int mMaximumTapVelocity;
156 private final int mMaximumMinorVelocity;
157 private final int mMaximumMajorVelocity;
158 private final int mMaximumAcceleration;
159 private final int mVelocityUnits;
Alexandre Lisiona764c682013-09-09 10:02:07 -0400160 private long pressTime;
alision7297bdb2013-05-21 11:56:55 -0400161
162 /**
163 * Callback invoked when the drawer is opened.
164 */
165 public static interface OnDrawerOpenListener {
166 /**
167 * Invoked when the drawer becomes fully open.
168 */
169 public void onDrawerOpened();
170 }
171
172 /**
173 * Callback invoked when the drawer is closed.
174 */
175 public static interface OnDrawerCloseListener {
176 /**
177 * Invoked when the drawer becomes fully closed.
178 */
179 public void onDrawerClosed();
180 }
181
182 /**
183 * Callback invoked when the drawer is scrolled.
184 */
185 public static interface OnDrawerScrollListener {
186 /**
187 * Invoked when the user starts dragging/flinging the drawer's handle.
188 */
189 public void onScrollStarted();
190
191 /**
192 * Invoked when the user stops dragging/flinging the drawer's handle.
193 */
194 public void onScrollEnded();
195 }
196
197 /**
alision907bde72013-06-20 14:40:37 -0400198 * Creates a new SlidingDrawer from a specified set of attributes defined in XML.
alision7297bdb2013-05-21 11:56:55 -0400199 *
200 * @param context
201 * The application's environment.
202 * @param attrs
203 * The attributes defined in XML.
204 */
205 public CustomSlidingDrawer(Context context, AttributeSet attrs) {
206 this(context, attrs, 0);
207 }
208
209 /**
alision907bde72013-06-20 14:40:37 -0400210 * Creates a new SlidingDrawer from a specified set of attributes defined in XML.
alision7297bdb2013-05-21 11:56:55 -0400211 *
212 * @param context
213 * The application's environment.
214 * @param attrs
215 * The attributes defined in XML.
216 * @param defStyle
217 * The style to apply to this widget.
218 */
219 public CustomSlidingDrawer(Context context, AttributeSet attrs, int defStyle) {
220 super(context, attrs, defStyle);
221 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomSlidingDrawer, defStyle, 0);
Alexandre Lision6e8931e2013-09-19 16:49:34 -0400222
223 mHandler = new SlidingHandler(this);
224
alision7297bdb2013-05-21 11:56:55 -0400225 int orientation = a.getInt(R.styleable.CustomSlidingDrawer_orientation, ORIENTATION_VERTICAL);
226 mVertical = orientation == ORIENTATION_VERTICAL;
227 mBottomOffset = (int) a.getDimension(R.styleable.CustomSlidingDrawer_bottomOffset, 0.0f);
228 mTopOffset = (int) a.getDimension(R.styleable.CustomSlidingDrawer_topOffset, 0.0f) - 5;
229 mAllowSingleTap = a.getBoolean(R.styleable.CustomSlidingDrawer_allowSingleTap, false);
230 mAnimateOnClick = a.getBoolean(R.styleable.CustomSlidingDrawer_animateOnClick, false);
231
232 int handleId = a.getResourceId(R.styleable.CustomSlidingDrawer_handle, 0);
233 if (handleId == 0) {
234 throw new IllegalArgumentException("The handle attribute is required and must refer " + "to a valid child.");
235 }
236
237 int contentId = a.getResourceId(R.styleable.CustomSlidingDrawer_content, 0);
238 if (contentId == 0) {
239 throw new IllegalArgumentException("The content attribute is required and must refer " + "to a valid child.");
240 }
241
242 if (handleId == contentId) {
243 throw new IllegalArgumentException("The content and handle attributes must refer " + "to different children.");
244 }
245
246 mHandleId = handleId;
247 mContentId = contentId;
248
249 final float density = getResources().getDisplayMetrics().density;
250 mTapThreshold = (int) (TAP_THRESHOLD * density + 0.5f);
251 mMaximumTapVelocity = (int) (MAXIMUM_TAP_VELOCITY * density + 0.5f);
252 mMaximumMinorVelocity = (int) (MAXIMUM_MINOR_VELOCITY * density + 0.5f);
253 mMaximumMajorVelocity = (int) (MAXIMUM_MAJOR_VELOCITY * density + 0.5f);
254 mMaximumAcceleration = (int) (MAXIMUM_ACCELERATION * density + 0.5f);
255 mVelocityUnits = (int) (VELOCITY_UNITS * density + 0.5f);
256
257 a.recycle();
258
259 setAlwaysDrawnWithCacheEnabled(false);
260 }
261
262 @Override
263 protected void onFinishInflate() {
264 mHandle = findViewById(mHandleId);
265 if (mHandle == null) {
266 throw new IllegalArgumentException("The handle attribute is must refer to an" + " existing child.");
267 }
268 mHandle.setOnClickListener(new DrawerToggler());
269
270 mContent = findViewById(mContentId);
271 if (mContent == null) {
272 throw new IllegalArgumentException("The content attribute is must refer to an" + " existing child.");
273 }
274 mContent.setVisibility(View.GONE);
275 }
276
277 @Override
278 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
279 int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
280 int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
281
282 int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
283 int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
284
285 if (widthSpecMode == MeasureSpec.UNSPECIFIED || heightSpecMode == MeasureSpec.UNSPECIFIED) {
286 throw new RuntimeException("SlidingDrawer cannot have UNSPECIFIED dimensions");
287 }
288
289 final View handle = mHandle;
290 measureChild(handle, widthMeasureSpec, heightMeasureSpec);
291
292 if (mVertical) {
293 int height = heightSpecSize - handle.getMeasuredHeight() - mTopOffset;
alision907bde72013-06-20 14:40:37 -0400294 mContent.measure(MeasureSpec.makeMeasureSpec(widthSpecSize, MeasureSpec.EXACTLY),
295 MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
alision7297bdb2013-05-21 11:56:55 -0400296 } else {
297 int width = widthSpecSize - handle.getMeasuredWidth() - mTopOffset;
alision907bde72013-06-20 14:40:37 -0400298 mContent.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
299 MeasureSpec.makeMeasureSpec(heightSpecSize, MeasureSpec.EXACTLY));
alision7297bdb2013-05-21 11:56:55 -0400300 }
301
302 setMeasuredDimension(widthSpecSize, heightSpecSize);
303 }
304
305 @Override
306 protected void dispatchDraw(Canvas canvas) {
307 final long drawingTime = getDrawingTime();
308 final View handle = mHandle;
309 final boolean isVertical = mVertical;
310
311 drawChild(canvas, handle, drawingTime);
312
313 if (mTracking || mAnimating) {
314 final Bitmap cache = mContent.getDrawingCache();
315 if (cache != null) {
316 if (isVertical) {
317 canvas.drawBitmap(cache, 0, handle.getBottom(), null);
318 } else {
319 canvas.drawBitmap(cache, handle.getRight(), 0, null);
320 }
321 } else {
322 canvas.save();
323 canvas.translate(isVertical ? 0 : handle.getLeft() - mTopOffset, isVertical ? handle.getTop() - mTopOffset : 0);
324 drawChild(canvas, mContent, drawingTime);
325 canvas.restore();
326 }
327 } else if (mExpanded) {
328 drawChild(canvas, mContent, drawingTime);
329 }
330 }
331
332 @Override
333 protected void onLayout(boolean changed, int l, int t, int r, int b) {
334 if (mTracking) {
335 return;
336 }
337
338 final int width = r - l;
339 final int height = b - t;
340
341 final View handle = mHandle;
342
343 int childWidth = handle.getMeasuredWidth();
344 int childHeight = handle.getMeasuredHeight();
345
346 int childLeft;
347 int childTop;
348
349 final View content = mContent;
350
351 if (mVertical) {
352 childLeft = (width - childWidth) / 2;
353 childTop = mExpanded ? mTopOffset : height - childHeight + mBottomOffset;
354
355 content.layout(0, mTopOffset + childHeight, content.getMeasuredWidth(), mTopOffset + childHeight + content.getMeasuredHeight());
356 } else {
357 childLeft = mExpanded ? mTopOffset : width - childWidth + mBottomOffset;
358 childTop = (height - childHeight) / 2;
359
360 content.layout(mTopOffset + childWidth, 0, mTopOffset + childWidth + content.getMeasuredWidth(), content.getMeasuredHeight());
361 }
362
363 handle.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
364 mHandleHeight = handle.getHeight();
365 mHandleWidth = handle.getWidth();
366 }
367
368 @Override
369 public boolean onInterceptTouchEvent(MotionEvent event) {
370
Alexandre Lision2affbaa2013-09-27 16:30:35 -0400371 Log.i("SlidingDrawer", "onInterceptTouchEvent");
alision7297bdb2013-05-21 11:56:55 -0400372 if (mLocked) {
Alexandre Lisiona764c682013-09-09 10:02:07 -0400373// Log.i(TAG, "Locked");
alision7297bdb2013-05-21 11:56:55 -0400374 return false;
375 }
376
377 final int action = event.getAction();
378
379 float x = event.getX();
380 float y = event.getY();
381
382 final Rect frame = mFrame;
383 final View handle = mHandle;
alision907bde72013-06-20 14:40:37 -0400384
385
alision7297bdb2013-05-21 11:56:55 -0400386
387 // New code
388 View trackHandle = mTrackHandle;
alision907bde72013-06-20 14:40:37 -0400389
alision7297bdb2013-05-21 11:56:55 -0400390 // set the rect frame to the mTrackHandle view borders instead of the
391 // hole handle view
alision907bde72013-06-20 14:40:37 -0400392
alision55c36cb2013-06-14 14:57:38 -0400393 Resources r = getResources();
394 int px = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 50, r.getDisplayMetrics());
alision7297bdb2013-05-21 11:56:55 -0400395
396 // getParent() => The right and left are valid, but we need to get the
397 // parent top and bottom to have absolute values (in screen)
alision907bde72013-06-20 14:40:37 -0400398 frame.set(trackHandle.getLeft(), ((ViewGroup) trackHandle.getParent()).getTop(), trackHandle.getRight() - px,
399 ((ViewGroup) trackHandle.getParent()).getBottom());
alision7297bdb2013-05-21 11:56:55 -0400400
alision907bde72013-06-20 14:40:37 -0400401 // handle.getHitRect(frame);
alision7297bdb2013-05-21 11:56:55 -0400402 if (!mTracking && !frame.contains((int) x, (int) y)) {
Alexandre Lisiona764c682013-09-09 10:02:07 -0400403// Log.i(TAG, "not tracking and not in frame");
alision7297bdb2013-05-21 11:56:55 -0400404 return false;
405 }
406
407 if (action == MotionEvent.ACTION_DOWN) {
408 mTracking = true;
Alexandre Lisiona764c682013-09-09 10:02:07 -0400409// Log.i(TAG, "action down");
alision7297bdb2013-05-21 11:56:55 -0400410 handle.setPressed(true);
411 // Must be called before prepareTracking()
412 prepareContent();
Alexandre Lisiona764c682013-09-09 10:02:07 -0400413
414 pressTime = System.currentTimeMillis();
alision7297bdb2013-05-21 11:56:55 -0400415
416 // Must be called after prepareContent()
417 if (mOnDrawerScrollListener != null) {
418 mOnDrawerScrollListener.onScrollStarted();
419 }
420
421 if (mVertical) {
422 final int top = mHandle.getTop();
423 mTouchDelta = (int) y - top;
424 prepareTracking(top);
425 } else {
426 final int left = mHandle.getLeft();
427 mTouchDelta = (int) x - left;
428 prepareTracking(left);
429 }
430 mVelocityTracker.addMovement(event);
alision907bde72013-06-20 14:40:37 -0400431 return true;
alision7297bdb2013-05-21 11:56:55 -0400432 }
alision907bde72013-06-20 14:40:37 -0400433 return false;
alision7297bdb2013-05-21 11:56:55 -0400434
alision7297bdb2013-05-21 11:56:55 -0400435 }
436
437 @Override
438 public boolean onTouchEvent(MotionEvent event) {
439 if (mLocked) {
440 return true;
441 }
442
Alexandre Lision2affbaa2013-09-27 16:30:35 -0400443
alision7297bdb2013-05-21 11:56:55 -0400444 if (mTracking) {
445 mVelocityTracker.addMovement(event);
446 final int action = event.getAction();
447 switch (action) {
448 case MotionEvent.ACTION_MOVE:
449 moveHandle((int) (mVertical ? event.getY() : event.getX()) - mTouchDelta);
450 break;
451 case MotionEvent.ACTION_UP:
Alexandre Lisiona764c682013-09-09 10:02:07 -0400452 if(System.currentTimeMillis() - pressTime <= 100){
453 animateToggle();
454 break;
455 }
456
alision7297bdb2013-05-21 11:56:55 -0400457 case MotionEvent.ACTION_CANCEL: {
458 final VelocityTracker velocityTracker = mVelocityTracker;
459 velocityTracker.computeCurrentVelocity(mVelocityUnits);
460
461 float yVelocity = velocityTracker.getYVelocity();
462 float xVelocity = velocityTracker.getXVelocity();
463 boolean negative;
464
465 final boolean vertical = mVertical;
466 if (vertical) {
467 negative = yVelocity < 0;
468 if (xVelocity < 0) {
469 xVelocity = -xVelocity;
470 }
471 if (xVelocity > mMaximumMinorVelocity) {
472 xVelocity = mMaximumMinorVelocity;
473 }
474 } else {
475 negative = xVelocity < 0;
476 if (yVelocity < 0) {
477 yVelocity = -yVelocity;
478 }
479 if (yVelocity > mMaximumMinorVelocity) {
480 yVelocity = mMaximumMinorVelocity;
481 }
482 }
483
484 float velocity = (float) Math.hypot(xVelocity, yVelocity);
485 if (negative) {
486 velocity = -velocity;
487 }
488
489 final int top = mHandle.getTop();
490 final int left = mHandle.getLeft();
491
492 if (Math.abs(velocity) < mMaximumTapVelocity) {
alision907bde72013-06-20 14:40:37 -0400493 if (vertical ? (mExpanded && top < mTapThreshold + mTopOffset)
494 || (!mExpanded && top > mBottomOffset + getBottom() - getTop() - mHandleHeight - mTapThreshold)
495 : (mExpanded && left < mTapThreshold + mTopOffset)
496 || (!mExpanded && left > mBottomOffset + getRight() - getLeft() - mHandleWidth - mTapThreshold)) {
alision7297bdb2013-05-21 11:56:55 -0400497
498 if (mAllowSingleTap) {
499 playSoundEffect(SoundEffectConstants.CLICK);
500
501 if (mExpanded) {
502 animateClose(vertical ? top : left);
503 } else {
504 animateOpen(vertical ? top : left);
505 }
506 } else {
507 performFling(vertical ? top : left, velocity, false);
508 }
509
510 } else {
511 performFling(vertical ? top : left, velocity, false);
512 }
513 } else {
514 performFling(vertical ? top : left, velocity, false);
515 }
516 }
517 break;
518 }
519 }
520
521 return mTracking || mAnimating || super.onTouchEvent(event);
522 }
523
524 private void animateClose(int position) {
525 prepareTracking(position);
526 performFling(position, mMaximumAcceleration, true);
527 }
528
529 private void animateOpen(int position) {
530 prepareTracking(position);
531 performFling(position, -mMaximumAcceleration, true);
532 }
533
534 private void performFling(int position, float velocity, boolean always) {
535 mAnimationPosition = position;
536 mAnimatedVelocity = velocity;
537
538 if (mExpanded) {
alision907bde72013-06-20 14:40:37 -0400539 if (always
540 || (velocity > mMaximumMajorVelocity || (position > mTopOffset + (mVertical ? mHandleHeight : mHandleWidth) && velocity > -mMaximumMajorVelocity))) {
alision7297bdb2013-05-21 11:56:55 -0400541 // We are expanded, but they didn't move sufficiently to cause
542 // us to retract. Animate back to the expanded position.
543 mAnimatedAcceleration = mMaximumAcceleration;
544 if (velocity < 0) {
545 mAnimatedVelocity = 0;
546 }
547 } else {
548 // We are expanded and are now going to animate away.
549 mAnimatedAcceleration = -mMaximumAcceleration;
550 if (velocity > 0) {
551 mAnimatedVelocity = 0;
552 }
553 }
554 } else {
alision907bde72013-06-20 14:40:37 -0400555 if (!always
556 && (velocity > mMaximumMajorVelocity || (position > (mVertical ? getHeight() : getWidth()) / 2 && velocity > -mMaximumMajorVelocity))) {
alision7297bdb2013-05-21 11:56:55 -0400557 // We are collapsed, and they moved enough to allow us to
558 // expand.
559 mAnimatedAcceleration = mMaximumAcceleration;
560 if (velocity < 0) {
561 mAnimatedVelocity = 0;
562 }
563 } else {
564 // We are collapsed, but they didn't move sufficiently to cause
565 // us to retract. Animate back to the collapsed position.
566 mAnimatedAcceleration = -mMaximumAcceleration;
567 if (velocity > 0) {
568 mAnimatedVelocity = 0;
569 }
570 }
571 }
572
573 long now = SystemClock.uptimeMillis();
574 mAnimationLastTime = now;
575 mCurrentAnimationTime = now + ANIMATION_FRAME_DURATION;
576 mAnimating = true;
577 mHandler.removeMessages(MSG_ANIMATE);
578 mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_ANIMATE), mCurrentAnimationTime);
579 stopTracking();
580 }
581
582 private void prepareTracking(int position) {
583 mTracking = true;
584 mVelocityTracker = VelocityTracker.obtain();
585 boolean opening = !mExpanded;
586 if (opening) {
587 mAnimatedAcceleration = mMaximumAcceleration;
588 mAnimatedVelocity = mMaximumMajorVelocity;
589 mAnimationPosition = mBottomOffset + (mVertical ? getHeight() - mHandleHeight : getWidth() - mHandleWidth);
590 moveHandle((int) mAnimationPosition);
591 mAnimating = true;
592 mHandler.removeMessages(MSG_ANIMATE);
593 long now = SystemClock.uptimeMillis();
594 mAnimationLastTime = now;
595 mCurrentAnimationTime = now + ANIMATION_FRAME_DURATION;
596 mAnimating = true;
597 } else {
598 if (mAnimating) {
599 mAnimating = false;
600 mHandler.removeMessages(MSG_ANIMATE);
601 }
602 moveHandle(position);
603 }
604 }
605
606 private void moveHandle(int position) {
607 final View handle = mHandle;
608
609 if (mVertical) {
610 if (position == EXPANDED_FULL_OPEN) {
611 handle.offsetTopAndBottom(mTopOffset - handle.getTop());
612 invalidate();
613 } else if (position == COLLAPSED_FULL_CLOSED) {
614 handle.offsetTopAndBottom(mBottomOffset + getBottom() - getTop() - mHandleHeight - handle.getTop());
615 invalidate();
616 } else {
617 final int top = handle.getTop();
618 int deltaY = position - top;
619 if (position < mTopOffset) {
620 deltaY = mTopOffset - top;
621 } else if (deltaY > mBottomOffset + getBottom() - getTop() - mHandleHeight - top) {
622 deltaY = mBottomOffset + getBottom() - getTop() - mHandleHeight - top;
623 }
624 handle.offsetTopAndBottom(deltaY);
625
626 final Rect frame = mFrame;
627 final Rect region = mInvalidate;
628
629 handle.getHitRect(frame);
630 region.set(frame);
631
632 region.union(frame.left, frame.top - deltaY, frame.right, frame.bottom - deltaY);
633 region.union(0, frame.bottom - deltaY, getWidth(), frame.bottom - deltaY + mContent.getHeight());
634
635 invalidate(region);
636 }
637 } else {
638 if (position == EXPANDED_FULL_OPEN) {
639 handle.offsetLeftAndRight(mTopOffset - handle.getLeft());
640 invalidate();
641 } else if (position == COLLAPSED_FULL_CLOSED) {
642 handle.offsetLeftAndRight(mBottomOffset + getRight() - getLeft() - mHandleWidth - handle.getLeft());
643 invalidate();
644 } else {
645 final int left = handle.getLeft();
646 int deltaX = position - left;
647 if (position < mTopOffset) {
648 deltaX = mTopOffset - left;
649 } else if (deltaX > mBottomOffset + getRight() - getLeft() - mHandleWidth - left) {
650 deltaX = mBottomOffset + getRight() - getLeft() - mHandleWidth - left;
651 }
652 handle.offsetLeftAndRight(deltaX);
653
654 final Rect frame = mFrame;
655 final Rect region = mInvalidate;
656
657 handle.getHitRect(frame);
658 region.set(frame);
659
660 region.union(frame.left - deltaX, frame.top, frame.right - deltaX, frame.bottom);
661 region.union(frame.right - deltaX, 0, frame.right - deltaX + mContent.getWidth(), getHeight());
662
663 invalidate(region);
664 }
665 }
666 }
667
668 private void prepareContent() {
669 if (mAnimating) {
670 return;
671 }
672
673 // Something changed in the content, we need to honor the layout request
674 // before creating the cached bitmap
675 final View content = mContent;
676 if (content.isLayoutRequested()) {
677 if (mVertical) {
678 final int childHeight = mHandleHeight;
679 int height = getBottom() - getTop() - childHeight - mTopOffset;
alision907bde72013-06-20 14:40:37 -0400680 content.measure(MeasureSpec.makeMeasureSpec(getRight() - getLeft(), MeasureSpec.EXACTLY),
681 MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
alision7297bdb2013-05-21 11:56:55 -0400682 content.layout(0, mTopOffset + childHeight, content.getMeasuredWidth(), mTopOffset + childHeight + content.getMeasuredHeight());
683 } else {
684 final int childWidth = mHandle.getWidth();
685 int width = getRight() - getLeft() - childWidth - mTopOffset;
alision907bde72013-06-20 14:40:37 -0400686 content.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
687 MeasureSpec.makeMeasureSpec(getBottom() - getTop(), MeasureSpec.EXACTLY));
alision7297bdb2013-05-21 11:56:55 -0400688 content.layout(childWidth + mTopOffset, 0, mTopOffset + childWidth + content.getMeasuredWidth(), content.getMeasuredHeight());
689 }
690 }
691 // Try only once... we should really loop but it's not a big deal
692 // if the draw was cancelled, it will only be temporary anyway
693 content.getViewTreeObserver().dispatchOnPreDraw();
694 // if (!content.isHardwareAccelerated()) content.buildDrawingCache();
695
696 content.setVisibility(View.GONE);
697 }
698
699 private void stopTracking() {
700 mHandle.setPressed(false);
701 mTracking = false;
702
703 if (mOnDrawerScrollListener != null) {
704 mOnDrawerScrollListener.onScrollEnded();
705 }
706
707 if (mVelocityTracker != null) {
708 mVelocityTracker.recycle();
709 mVelocityTracker = null;
710 }
711 }
712
713 private void doAnimation() {
714 if (mAnimating) {
715 incrementAnimation();
716 if (mAnimationPosition >= mBottomOffset + (mVertical ? getHeight() : getWidth()) - 1) {
717 mAnimating = false;
718 closeDrawer();
719 } else if (mAnimationPosition < mTopOffset) {
720 mAnimating = false;
721 openDrawer();
722 } else {
723 moveHandle((int) mAnimationPosition);
724 mCurrentAnimationTime += ANIMATION_FRAME_DURATION;
725 mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_ANIMATE), mCurrentAnimationTime);
726 }
727 }
728 }
729
730 private void incrementAnimation() {
731 long now = SystemClock.uptimeMillis();
732 float t = (now - mAnimationLastTime) / 1000.0f; // ms -> s
733 final float position = mAnimationPosition;
734 final float v = mAnimatedVelocity; // px/s
735 final float a = mAnimatedAcceleration; // px/s/s
736 mAnimationPosition = position + (v * t) + (0.5f * a * t * t); // px
737 mAnimatedVelocity = v + (a * t); // px/s
738 mAnimationLastTime = now; // ms
739 }
740
741 /**
742 * Toggles the drawer open and close. Takes effect immediately.
743 *
744 * @see #open()
745 * @see #close()
746 * @see #animateClose()
747 * @see #animateOpen()
748 * @see #animateToggle()
749 */
750 public void toggle() {
751 if (!mExpanded) {
752 openDrawer();
753 } else {
754 closeDrawer();
755 }
756 invalidate();
757 requestLayout();
758 }
759
760 /**
761 * Toggles the drawer open and close with an animation.
762 *
763 * @see #open()
764 * @see #close()
765 * @see #animateClose()
766 * @see #animateOpen()
767 * @see #toggle()
768 */
769 public void animateToggle() {
770 if (!mExpanded) {
771 animateOpen();
772 } else {
773 animateClose();
774 }
775 }
776
777 /**
778 * Opens the drawer immediately.
779 *
780 * @see #toggle()
781 * @see #close()
782 * @see #animateOpen()
783 */
784 public void open() {
785 openDrawer();
786 invalidate();
787 requestLayout();
788
789 sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
790 }
791
792 /**
793 * Closes the drawer immediately.
794 *
795 * @see #toggle()
796 * @see #open()
797 * @see #animateClose()
798 */
799 public void close() {
800 closeDrawer();
801 invalidate();
802 requestLayout();
803 }
804
805 /**
806 * Closes the drawer with an animation.
807 *
808 * @see #close()
809 * @see #open()
810 * @see #animateOpen()
811 * @see #animateToggle()
812 * @see #toggle()
813 */
814 public void animateClose() {
815 prepareContent();
816 final OnDrawerScrollListener scrollListener = mOnDrawerScrollListener;
817 if (scrollListener != null) {
818 scrollListener.onScrollStarted();
819 }
820 animateClose(mVertical ? mHandle.getTop() : mHandle.getLeft());
821
822 if (scrollListener != null) {
823 scrollListener.onScrollEnded();
824 }
825 }
826
827 /**
828 * Opens the drawer with an animation.
829 *
830 * @see #close()
831 * @see #open()
832 * @see #animateClose()
833 * @see #animateToggle()
834 * @see #toggle()
835 */
836 public void animateOpen() {
alision907bde72013-06-20 14:40:37 -0400837
alision55c36cb2013-06-14 14:57:38 -0400838 if (mExpanded) {
839 return;
840 }
alision7297bdb2013-05-21 11:56:55 -0400841 prepareContent();
842 final OnDrawerScrollListener scrollListener = mOnDrawerScrollListener;
843 if (scrollListener != null) {
844 scrollListener.onScrollStarted();
845 }
846 animateOpen(mVertical ? mHandle.getTop() : mHandle.getLeft());
847
848 sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
849
850 if (scrollListener != null) {
851 scrollListener.onScrollEnded();
852 }
853 }
854
855 // @Override
856 // public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
857 // super.onInitializeAccessibilityEvent(event);
858 // event.setClassName(SlidingDrawer.class.getName());
859 // }
860
861 // @Override
862 // public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)
863 // {
864 // super.onInitializeAccessibilityNodeInfo(info);
865 // info.setClassName(SlidingDrawer.class.getName());
866 // }
867
868 private void closeDrawer() {
869 moveHandle(COLLAPSED_FULL_CLOSED);
870 mContent.setVisibility(View.GONE);
871 mContent.destroyDrawingCache();
872
873 if (!mExpanded) {
874 return;
875 }
876
877 mExpanded = false;
878 if (mOnDrawerCloseListener != null) {
879 mOnDrawerCloseListener.onDrawerClosed();
880 }
881 }
882
883 private void openDrawer() {
884 moveHandle(EXPANDED_FULL_OPEN);
885 mContent.setVisibility(View.VISIBLE);
886
887 if (mExpanded) {
888 return;
889 }
890
891 mExpanded = true;
892
893 if (mOnDrawerOpenListener != null) {
894 mOnDrawerOpenListener.onDrawerOpened();
895 }
896 }
897
898 /**
alision907bde72013-06-20 14:40:37 -0400899 * Sets the listener that receives a notification when the drawer becomes open.
alision7297bdb2013-05-21 11:56:55 -0400900 *
901 * @param onDrawerOpenListener
902 * The listener to be notified when the drawer is opened.
903 */
904 public void setOnDrawerOpenListener(OnDrawerOpenListener onDrawerOpenListener) {
905 mOnDrawerOpenListener = onDrawerOpenListener;
906 }
907
908 /**
alision907bde72013-06-20 14:40:37 -0400909 * Sets the listener that receives a notification when the drawer becomes close.
alision7297bdb2013-05-21 11:56:55 -0400910 *
911 * @param onDrawerCloseListener
912 * The listener to be notified when the drawer is closed.
913 */
914 public void setOnDrawerCloseListener(OnDrawerCloseListener onDrawerCloseListener) {
915 mOnDrawerCloseListener = onDrawerCloseListener;
916 }
917
918 /**
alision907bde72013-06-20 14:40:37 -0400919 * 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 -0400920 * trigger a drawer opened or drawer closed event.
921 *
922 * @param onDrawerScrollListener
923 * The listener to be notified when scrolling starts or stops.
924 */
925 public void setOnDrawerScrollListener(OnDrawerScrollListener onDrawerScrollListener) {
926 mOnDrawerScrollListener = onDrawerScrollListener;
927 }
928
929 /**
930 * Returns the handle of the drawer.
931 *
alision907bde72013-06-20 14:40:37 -0400932 * @return The View reprenseting the handle of the drawer, identified by the "handle" id in XML.
alision7297bdb2013-05-21 11:56:55 -0400933 */
934 public View getHandle() {
935 return mHandle;
936 }
937
938 /**
939 * Returns the content of the drawer.
940 *
alision907bde72013-06-20 14:40:37 -0400941 * @return The View reprenseting the content of the drawer, identified by the "content" id in XML.
alision7297bdb2013-05-21 11:56:55 -0400942 */
943 public View getContent() {
944 return mContent;
945 }
946
947 /**
948 * Unlocks the SlidingDrawer so that touch events are processed.
949 *
950 * @see #lock()
951 */
952 public void unlock() {
953 mLocked = false;
954 }
955
956 /**
957 * Locks the SlidingDrawer so that touch events are ignores.
958 *
959 * @see #unlock()
960 */
961 public void lock() {
962 mLocked = true;
963 }
964
965 /**
966 * Indicates whether the drawer is currently fully opened.
967 *
968 * @return True if the drawer is opened, false otherwise.
969 */
970 public boolean isOpened() {
971 return mExpanded;
972 }
973
974 /**
975 * Indicates whether the drawer is scrolling or flinging.
976 *
977 * @return True if the drawer is scroller or flinging, false otherwise.
978 */
979 public boolean isMoving() {
980 return mTracking || mAnimating;
981 }
982
983 private class DrawerToggler implements OnClickListener {
984 public void onClick(View v) {
985 if (mLocked) {
986 return;
987 }
988 // mAllowSingleTap isn't relevant here; you're *always*
989 // allowed to open/close the drawer by clicking with the
990 // trackball.
991
992 if (mAnimateOnClick) {
993 animateToggle();
994 } else {
995 toggle();
996 }
997 }
998 }
999
1000 public void setmTrackHandle(View mTrackHandle) {
1001 this.mTrackHandle = mTrackHandle;
1002 }
1003
Alexandre Lision6e8931e2013-09-19 16:49:34 -04001004 private static class SlidingHandler extends Handler {
1005
1006 WeakReference<CustomSlidingDrawer> ref;
1007
1008 public SlidingHandler(CustomSlidingDrawer r){
1009 ref = new WeakReference<CustomSlidingDrawer>(r);
1010 }
1011
alision7297bdb2013-05-21 11:56:55 -04001012 public void handleMessage(Message m) {
1013 switch (m.what) {
1014 case MSG_ANIMATE:
Alexandre Lision6e8931e2013-09-19 16:49:34 -04001015 if(ref.get() != null)
1016 ref.get().doAnimation();
alision7297bdb2013-05-21 11:56:55 -04001017 break;
1018 }
1019 }
1020 }
1021}