blob: 41f75d3142df0d934cb4b80faa380f695cfbd05e [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
Alexandre Lision8b9d8e82013-10-04 09:21:27 -040036import org.sflphone.R;
37
alision7297bdb2013-05-21 11:56:55 -040038import android.content.Context;
alision55c36cb2013-06-14 14:57:38 -040039import android.content.res.Resources;
alision7297bdb2013-05-21 11:56:55 -040040import android.content.res.TypedArray;
41import android.graphics.Bitmap;
42import android.graphics.Canvas;
43import android.graphics.Rect;
44import android.os.Handler;
45import android.os.Message;
46import android.os.SystemClock;
47import android.util.AttributeSet;
alision55c36cb2013-06-14 14:57:38 -040048import android.util.TypedValue;
alision7297bdb2013-05-21 11:56:55 -040049import android.view.MotionEvent;
50import android.view.SoundEffectConstants;
51import android.view.VelocityTracker;
52import android.view.View;
53import android.view.ViewGroup;
54import android.view.accessibility.AccessibilityEvent;
55
alision7297bdb2013-05-21 11:56:55 -040056/**
alision907bde72013-06-20 14:40:37 -040057 * 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
58 * vertically or horizontally.
alision7297bdb2013-05-21 11:56:55 -040059 *
alision907bde72013-06-20 14:40:37 -040060 * 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 -040061 *
alision907bde72013-06-20 14:40:37 -040062 * SlidingDrawer should be used as an overlay inside layouts. This means SlidingDrawer should only be used inside of a FrameLayout or a RelativeLayout
63 * 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 -040064 * match_parent for both its dimensions.
65 *
alision907bde72013-06-20 14:40:37 -040066 * Inside an XML layout, SlidingDrawer must define the id of the handle and of the content:
alision7297bdb2013-05-21 11:56:55 -040067 *
68 * <pre class="prettyprint">
69 * &lt;SlidingDrawer
70 * android:id="@+id/drawer"
71 * android:layout_width="match_parent"
72 * android:layout_height="match_parent"
73 *
74 * android:handle="@+id/handle"
75 * android:content="@+id/content"&gt;
76 *
77 * &lt;ImageView
78 * android:id="@id/handle"
79 * android:layout_width="88dip"
80 * android:layout_height="44dip" /&gt;
81 *
82 * &lt;GridView
83 * android:id="@id/content"
84 * android:layout_width="match_parent"
85 * android:layout_height="match_parent" /&gt;
86 *
87 * &lt;/SlidingDrawer&gt;
88 * </pre>
89 *
90 * @attr ref android.R.styleable#SlidingDrawer_content
91 * @attr ref android.R.styleable#SlidingDrawer_handle
92 * @attr ref android.R.styleable#SlidingDrawer_topOffset
93 * @attr ref android.R.styleable#SlidingDrawer_bottomOffset
94 * @attr ref android.R.styleable#SlidingDrawer_orientation
95 * @attr ref android.R.styleable#SlidingDrawer_allowSingleTap
96 * @attr ref android.R.styleable#SlidingDrawer_animateOnClick
97 *
98 */
99
100public class CustomSlidingDrawer extends ViewGroup {
101 public static final int ORIENTATION_HORIZONTAL = 0;
102 public static final int ORIENTATION_VERTICAL = 1;
103
104 private static final int TAP_THRESHOLD = 6;
105 private static final float MAXIMUM_TAP_VELOCITY = 100.0f;
106 private static final float MAXIMUM_MINOR_VELOCITY = 150.0f;
107 private static final float MAXIMUM_MAJOR_VELOCITY = 200.0f;
Alexandre Lisiond5dbcdf2013-10-07 14:13:09 -0400108 private static final float MAXIMUM_ACCELERATION = 1500.0f;
alision7297bdb2013-05-21 11:56:55 -0400109 private static final int VELOCITY_UNITS = 1000;
110 private static final int MSG_ANIMATE = 1000;
111 private static final int ANIMATION_FRAME_DURATION = 1000 / 60;
112
113 private static final int EXPANDED_FULL_OPEN = -10001;
114 private static final int COLLAPSED_FULL_CLOSED = -10002;
Alexandre Lision8b9d8e82013-10-04 09:21:27 -0400115 // private static final String TAG = CustomSlidingDrawer.class.getSimpleName();
alision7297bdb2013-05-21 11:56:55 -0400116
117 private final int mHandleId;
118 private final int mContentId;
119
120 private View mHandle;
121 private View mTrackHandle;
122 private View mContent;
123
124 private final Rect mFrame = new Rect();
125 private final Rect mInvalidate = new Rect();
126 private boolean mTracking;
127 private boolean mLocked;
128
129 private VelocityTracker mVelocityTracker;
130
131 private boolean mVertical;
132 private boolean mExpanded;
133 private int mBottomOffset;
134 private int mTopOffset;
135 private int mHandleHeight;
136 private int mHandleWidth;
137
138 private OnDrawerOpenListener mOnDrawerOpenListener;
139 private OnDrawerCloseListener mOnDrawerCloseListener;
140 private OnDrawerScrollListener mOnDrawerScrollListener;
141
Alexandre Lision6e8931e2013-09-19 16:49:34 -0400142 private SlidingHandler mHandler;
alision7297bdb2013-05-21 11:56:55 -0400143 private float mAnimatedAcceleration;
144 private float mAnimatedVelocity;
145 private float mAnimationPosition;
146 private long mAnimationLastTime;
147 private long mCurrentAnimationTime;
148 private int mTouchDelta;
149 private boolean mAnimating;
150 private boolean mAllowSingleTap;
151 private boolean mAnimateOnClick;
152
153 private final int mTapThreshold;
154 private final int mMaximumTapVelocity;
155 private final int mMaximumMinorVelocity;
156 private final int mMaximumMajorVelocity;
157 private final int mMaximumAcceleration;
158 private final int mVelocityUnits;
Alexandre Lisiona764c682013-09-09 10:02:07 -0400159 private long pressTime;
alision7297bdb2013-05-21 11:56:55 -0400160
161 /**
162 * Callback invoked when the drawer is opened.
163 */
164 public static interface OnDrawerOpenListener {
165 /**
166 * Invoked when the drawer becomes fully open.
167 */
168 public void onDrawerOpened();
169 }
170
171 /**
172 * Callback invoked when the drawer is closed.
173 */
174 public static interface OnDrawerCloseListener {
175 /**
176 * Invoked when the drawer becomes fully closed.
177 */
178 public void onDrawerClosed();
179 }
180
181 /**
182 * Callback invoked when the drawer is scrolled.
183 */
184 public static interface OnDrawerScrollListener {
185 /**
186 * Invoked when the user starts dragging/flinging the drawer's handle.
187 */
188 public void onScrollStarted();
189
190 /**
191 * Invoked when the user stops dragging/flinging the drawer's handle.
192 */
193 public void onScrollEnded();
Alexandre Lisione1c96db2013-10-04 14:34:21 -0400194
Alexandre Lision8b9d8e82013-10-04 09:21:27 -0400195 /**
196 * Invoked when the user stops dragging/flinging the drawer's handle.
Alexandre Lisione1c96db2013-10-04 14:34:21 -0400197 *
198 * @param i
Alexandre Lision8b9d8e82013-10-04 09:21:27 -0400199 */
200 public void onScroll(int i);
alision7297bdb2013-05-21 11:56:55 -0400201 }
202
203 /**
alision907bde72013-06-20 14:40:37 -0400204 * Creates a new SlidingDrawer from a specified set of attributes defined in XML.
alision7297bdb2013-05-21 11:56:55 -0400205 *
206 * @param context
207 * The application's environment.
208 * @param attrs
209 * The attributes defined in XML.
210 */
211 public CustomSlidingDrawer(Context context, AttributeSet attrs) {
212 this(context, attrs, 0);
213 }
214
215 /**
alision907bde72013-06-20 14:40:37 -0400216 * Creates a new SlidingDrawer from a specified set of attributes defined in XML.
alision7297bdb2013-05-21 11:56:55 -0400217 *
218 * @param context
219 * The application's environment.
220 * @param attrs
221 * The attributes defined in XML.
222 * @param defStyle
223 * The style to apply to this widget.
224 */
225 public CustomSlidingDrawer(Context context, AttributeSet attrs, int defStyle) {
226 super(context, attrs, defStyle);
227 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomSlidingDrawer, defStyle, 0);
Alexandre Lision8b9d8e82013-10-04 09:21:27 -0400228
Alexandre Lision6e8931e2013-09-19 16:49:34 -0400229 mHandler = new SlidingHandler(this);
Alexandre Lision8b9d8e82013-10-04 09:21:27 -0400230
alision7297bdb2013-05-21 11:56:55 -0400231 int orientation = a.getInt(R.styleable.CustomSlidingDrawer_orientation, ORIENTATION_VERTICAL);
232 mVertical = orientation == ORIENTATION_VERTICAL;
233 mBottomOffset = (int) a.getDimension(R.styleable.CustomSlidingDrawer_bottomOffset, 0.0f);
234 mTopOffset = (int) a.getDimension(R.styleable.CustomSlidingDrawer_topOffset, 0.0f) - 5;
235 mAllowSingleTap = a.getBoolean(R.styleable.CustomSlidingDrawer_allowSingleTap, false);
236 mAnimateOnClick = a.getBoolean(R.styleable.CustomSlidingDrawer_animateOnClick, false);
237
238 int handleId = a.getResourceId(R.styleable.CustomSlidingDrawer_handle, 0);
239 if (handleId == 0) {
240 throw new IllegalArgumentException("The handle attribute is required and must refer " + "to a valid child.");
241 }
242
243 int contentId = a.getResourceId(R.styleable.CustomSlidingDrawer_content, 0);
244 if (contentId == 0) {
245 throw new IllegalArgumentException("The content attribute is required and must refer " + "to a valid child.");
246 }
247
248 if (handleId == contentId) {
249 throw new IllegalArgumentException("The content and handle attributes must refer " + "to different children.");
250 }
251
252 mHandleId = handleId;
253 mContentId = contentId;
254
255 final float density = getResources().getDisplayMetrics().density;
256 mTapThreshold = (int) (TAP_THRESHOLD * density + 0.5f);
257 mMaximumTapVelocity = (int) (MAXIMUM_TAP_VELOCITY * density + 0.5f);
258 mMaximumMinorVelocity = (int) (MAXIMUM_MINOR_VELOCITY * density + 0.5f);
259 mMaximumMajorVelocity = (int) (MAXIMUM_MAJOR_VELOCITY * density + 0.5f);
260 mMaximumAcceleration = (int) (MAXIMUM_ACCELERATION * density + 0.5f);
261 mVelocityUnits = (int) (VELOCITY_UNITS * density + 0.5f);
262
263 a.recycle();
264
265 setAlwaysDrawnWithCacheEnabled(false);
266 }
267
268 @Override
269 protected void onFinishInflate() {
270 mHandle = findViewById(mHandleId);
271 if (mHandle == null) {
272 throw new IllegalArgumentException("The handle attribute is must refer to an" + " existing child.");
273 }
274 mHandle.setOnClickListener(new DrawerToggler());
275
276 mContent = findViewById(mContentId);
277 if (mContent == null) {
278 throw new IllegalArgumentException("The content attribute is must refer to an" + " existing child.");
279 }
280 mContent.setVisibility(View.GONE);
281 }
282
283 @Override
284 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
285 int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
286 int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
287
288 int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
289 int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
290
291 if (widthSpecMode == MeasureSpec.UNSPECIFIED || heightSpecMode == MeasureSpec.UNSPECIFIED) {
292 throw new RuntimeException("SlidingDrawer cannot have UNSPECIFIED dimensions");
293 }
294
295 final View handle = mHandle;
296 measureChild(handle, widthMeasureSpec, heightMeasureSpec);
Alexandre Lisione1c96db2013-10-04 14:34:21 -0400297
alision7297bdb2013-05-21 11:56:55 -0400298 if (mVertical) {
Alexandre Lision8b9d8e82013-10-04 09:21:27 -0400299
alision7297bdb2013-05-21 11:56:55 -0400300 int height = heightSpecSize - handle.getMeasuredHeight() - mTopOffset;
Alexandre Lisione1c96db2013-10-04 14:34:21 -0400301 // Log.i("CustomSliding","heightSpecSize:"+heightSpecSize);
Alexandre Lision8b9d8e82013-10-04 09:21:27 -0400302
alision907bde72013-06-20 14:40:37 -0400303 mContent.measure(MeasureSpec.makeMeasureSpec(widthSpecSize, MeasureSpec.EXACTLY),
304 MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
Alexandre Lisione1c96db2013-10-04 14:34:21 -0400305
alision7297bdb2013-05-21 11:56:55 -0400306 } else {
307 int width = widthSpecSize - handle.getMeasuredWidth() - mTopOffset;
alision907bde72013-06-20 14:40:37 -0400308 mContent.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
309 MeasureSpec.makeMeasureSpec(heightSpecSize, MeasureSpec.EXACTLY));
alision7297bdb2013-05-21 11:56:55 -0400310 }
311
312 setMeasuredDimension(widthSpecSize, heightSpecSize);
313 }
314
315 @Override
316 protected void dispatchDraw(Canvas canvas) {
317 final long drawingTime = getDrawingTime();
318 final View handle = mHandle;
319 final boolean isVertical = mVertical;
320
321 drawChild(canvas, handle, drawingTime);
322
323 if (mTracking || mAnimating) {
324 final Bitmap cache = mContent.getDrawingCache();
325 if (cache != null) {
326 if (isVertical) {
327 canvas.drawBitmap(cache, 0, handle.getBottom(), null);
328 } else {
329 canvas.drawBitmap(cache, handle.getRight(), 0, null);
330 }
331 } else {
332 canvas.save();
333 canvas.translate(isVertical ? 0 : handle.getLeft() - mTopOffset, isVertical ? handle.getTop() - mTopOffset : 0);
334 drawChild(canvas, mContent, drawingTime);
335 canvas.restore();
336 }
337 } else if (mExpanded) {
338 drawChild(canvas, mContent, drawingTime);
339 }
340 }
341
342 @Override
343 protected void onLayout(boolean changed, int l, int t, int r, int b) {
344 if (mTracking) {
345 return;
346 }
347
348 final int width = r - l;
349 final int height = b - t;
Alexandre Lisione1c96db2013-10-04 14:34:21 -0400350
351 // Log.i("Drawer","onLayout, height:"+height);
alision7297bdb2013-05-21 11:56:55 -0400352
353 final View handle = mHandle;
354
355 int childWidth = handle.getMeasuredWidth();
356 int childHeight = handle.getMeasuredHeight();
357
358 int childLeft;
359 int childTop;
360
361 final View content = mContent;
362
363 if (mVertical) {
364 childLeft = (width - childWidth) / 2;
365 childTop = mExpanded ? mTopOffset : height - childHeight + mBottomOffset;
366
367 content.layout(0, mTopOffset + childHeight, content.getMeasuredWidth(), mTopOffset + childHeight + content.getMeasuredHeight());
368 } else {
369 childLeft = mExpanded ? mTopOffset : width - childWidth + mBottomOffset;
370 childTop = (height - childHeight) / 2;
371
372 content.layout(mTopOffset + childWidth, 0, mTopOffset + childWidth + content.getMeasuredWidth(), content.getMeasuredHeight());
373 }
374
375 handle.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
376 mHandleHeight = handle.getHeight();
377 mHandleWidth = handle.getWidth();
378 }
379
380 @Override
381 public boolean onInterceptTouchEvent(MotionEvent event) {
382
Alexandre Lisione1c96db2013-10-04 14:34:21 -0400383 // Log.i("SlidingDrawer", "onInterceptTouchEvent");
alision7297bdb2013-05-21 11:56:55 -0400384 if (mLocked) {
Alexandre Lision8b9d8e82013-10-04 09:21:27 -0400385 // Log.i(TAG, "Locked");
alision7297bdb2013-05-21 11:56:55 -0400386 return false;
387 }
388
389 final int action = event.getAction();
390
391 float x = event.getX();
392 float y = event.getY();
393
394 final Rect frame = mFrame;
395 final View handle = mHandle;
396
397 // New code
398 View trackHandle = mTrackHandle;
Alexandre Lision8b9d8e82013-10-04 09:21:27 -0400399
alision7297bdb2013-05-21 11:56:55 -0400400 // set the rect frame to the mTrackHandle view borders instead of the
401 // hole handle view
alision907bde72013-06-20 14:40:37 -0400402
alision55c36cb2013-06-14 14:57:38 -0400403 Resources r = getResources();
404 int px = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 50, r.getDisplayMetrics());
alision7297bdb2013-05-21 11:56:55 -0400405
406 // getParent() => The right and left are valid, but we need to get the
407 // parent top and bottom to have absolute values (in screen)
alision907bde72013-06-20 14:40:37 -0400408 frame.set(trackHandle.getLeft(), ((ViewGroup) trackHandle.getParent()).getTop(), trackHandle.getRight() - px,
409 ((ViewGroup) trackHandle.getParent()).getBottom());
alision7297bdb2013-05-21 11:56:55 -0400410
alision907bde72013-06-20 14:40:37 -0400411 // handle.getHitRect(frame);
alision7297bdb2013-05-21 11:56:55 -0400412 if (!mTracking && !frame.contains((int) x, (int) y)) {
Alexandre Lision8b9d8e82013-10-04 09:21:27 -0400413 // Log.i(TAG, "not tracking and not in frame");
alision7297bdb2013-05-21 11:56:55 -0400414 return false;
415 }
416
417 if (action == MotionEvent.ACTION_DOWN) {
418 mTracking = true;
Alexandre Lision8b9d8e82013-10-04 09:21:27 -0400419 // Log.i(TAG, "action down");
alision7297bdb2013-05-21 11:56:55 -0400420 handle.setPressed(true);
421 // Must be called before prepareTracking()
422 prepareContent();
Alexandre Lision8b9d8e82013-10-04 09:21:27 -0400423
Alexandre Lisiona764c682013-09-09 10:02:07 -0400424 pressTime = System.currentTimeMillis();
alision7297bdb2013-05-21 11:56:55 -0400425
426 // Must be called after prepareContent()
427 if (mOnDrawerScrollListener != null) {
428 mOnDrawerScrollListener.onScrollStarted();
429 }
430
431 if (mVertical) {
432 final int top = mHandle.getTop();
433 mTouchDelta = (int) y - top;
434 prepareTracking(top);
435 } else {
436 final int left = mHandle.getLeft();
437 mTouchDelta = (int) x - left;
438 prepareTracking(left);
439 }
440 mVelocityTracker.addMovement(event);
alision907bde72013-06-20 14:40:37 -0400441 return true;
alision7297bdb2013-05-21 11:56:55 -0400442 }
alision907bde72013-06-20 14:40:37 -0400443 return false;
alision7297bdb2013-05-21 11:56:55 -0400444
alision7297bdb2013-05-21 11:56:55 -0400445 }
446
447 @Override
448 public boolean onTouchEvent(MotionEvent event) {
449 if (mLocked) {
450 return true;
451 }
452
453 if (mTracking) {
454 mVelocityTracker.addMovement(event);
455 final int action = event.getAction();
456 switch (action) {
457 case MotionEvent.ACTION_MOVE:
458 moveHandle((int) (mVertical ? event.getY() : event.getX()) - mTouchDelta);
459 break;
460 case MotionEvent.ACTION_UP:
Alexandre Lision8b9d8e82013-10-04 09:21:27 -0400461 if (System.currentTimeMillis() - pressTime <= 100) {
Alexandre Lisiona764c682013-09-09 10:02:07 -0400462 animateToggle();
463 break;
464 }
Alexandre Lision8b9d8e82013-10-04 09:21:27 -0400465
alision7297bdb2013-05-21 11:56:55 -0400466 case MotionEvent.ACTION_CANCEL: {
467 final VelocityTracker velocityTracker = mVelocityTracker;
468 velocityTracker.computeCurrentVelocity(mVelocityUnits);
469
470 float yVelocity = velocityTracker.getYVelocity();
471 float xVelocity = velocityTracker.getXVelocity();
472 boolean negative;
473
474 final boolean vertical = mVertical;
475 if (vertical) {
476 negative = yVelocity < 0;
477 if (xVelocity < 0) {
478 xVelocity = -xVelocity;
479 }
480 if (xVelocity > mMaximumMinorVelocity) {
481 xVelocity = mMaximumMinorVelocity;
482 }
483 } else {
484 negative = xVelocity < 0;
485 if (yVelocity < 0) {
486 yVelocity = -yVelocity;
487 }
488 if (yVelocity > mMaximumMinorVelocity) {
489 yVelocity = mMaximumMinorVelocity;
490 }
491 }
492
493 float velocity = (float) Math.hypot(xVelocity, yVelocity);
494 if (negative) {
495 velocity = -velocity;
496 }
497
498 final int top = mHandle.getTop();
499 final int left = mHandle.getLeft();
500
501 if (Math.abs(velocity) < mMaximumTapVelocity) {
alision907bde72013-06-20 14:40:37 -0400502 if (vertical ? (mExpanded && top < mTapThreshold + mTopOffset)
503 || (!mExpanded && top > mBottomOffset + getBottom() - getTop() - mHandleHeight - mTapThreshold)
504 : (mExpanded && left < mTapThreshold + mTopOffset)
505 || (!mExpanded && left > mBottomOffset + getRight() - getLeft() - mHandleWidth - mTapThreshold)) {
alision7297bdb2013-05-21 11:56:55 -0400506
507 if (mAllowSingleTap) {
508 playSoundEffect(SoundEffectConstants.CLICK);
509
510 if (mExpanded) {
511 animateClose(vertical ? top : left);
512 } else {
513 animateOpen(vertical ? top : left);
514 }
515 } else {
516 performFling(vertical ? top : left, velocity, false);
517 }
518
519 } else {
520 performFling(vertical ? top : left, velocity, false);
521 }
522 } else {
523 performFling(vertical ? top : left, velocity, false);
524 }
525 }
526 break;
527 }
528 }
529
530 return mTracking || mAnimating || super.onTouchEvent(event);
531 }
532
533 private void animateClose(int position) {
534 prepareTracking(position);
535 performFling(position, mMaximumAcceleration, true);
536 }
537
538 private void animateOpen(int position) {
539 prepareTracking(position);
540 performFling(position, -mMaximumAcceleration, true);
541 }
542
543 private void performFling(int position, float velocity, boolean always) {
544 mAnimationPosition = position;
545 mAnimatedVelocity = velocity;
546
547 if (mExpanded) {
alision907bde72013-06-20 14:40:37 -0400548 if (always
549 || (velocity > mMaximumMajorVelocity || (position > mTopOffset + (mVertical ? mHandleHeight : mHandleWidth) && velocity > -mMaximumMajorVelocity))) {
alision7297bdb2013-05-21 11:56:55 -0400550 // We are expanded, but they didn't move sufficiently to cause
551 // us to retract. Animate back to the expanded position.
552 mAnimatedAcceleration = mMaximumAcceleration;
553 if (velocity < 0) {
554 mAnimatedVelocity = 0;
555 }
556 } else {
557 // We are expanded and are now going to animate away.
558 mAnimatedAcceleration = -mMaximumAcceleration;
559 if (velocity > 0) {
560 mAnimatedVelocity = 0;
561 }
562 }
563 } else {
alision907bde72013-06-20 14:40:37 -0400564 if (!always
565 && (velocity > mMaximumMajorVelocity || (position > (mVertical ? getHeight() : getWidth()) / 2 && velocity > -mMaximumMajorVelocity))) {
alision7297bdb2013-05-21 11:56:55 -0400566 // We are collapsed, and they moved enough to allow us to
567 // expand.
568 mAnimatedAcceleration = mMaximumAcceleration;
569 if (velocity < 0) {
570 mAnimatedVelocity = 0;
571 }
572 } else {
573 // We are collapsed, but they didn't move sufficiently to cause
574 // us to retract. Animate back to the collapsed position.
575 mAnimatedAcceleration = -mMaximumAcceleration;
576 if (velocity > 0) {
577 mAnimatedVelocity = 0;
578 }
579 }
580 }
581
582 long now = SystemClock.uptimeMillis();
583 mAnimationLastTime = now;
584 mCurrentAnimationTime = now + ANIMATION_FRAME_DURATION;
585 mAnimating = true;
586 mHandler.removeMessages(MSG_ANIMATE);
587 mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_ANIMATE), mCurrentAnimationTime);
588 stopTracking();
589 }
590
591 private void prepareTracking(int position) {
592 mTracking = true;
593 mVelocityTracker = VelocityTracker.obtain();
594 boolean opening = !mExpanded;
595 if (opening) {
596 mAnimatedAcceleration = mMaximumAcceleration;
597 mAnimatedVelocity = mMaximumMajorVelocity;
598 mAnimationPosition = mBottomOffset + (mVertical ? getHeight() - mHandleHeight : getWidth() - mHandleWidth);
599 moveHandle((int) mAnimationPosition);
600 mAnimating = true;
601 mHandler.removeMessages(MSG_ANIMATE);
602 long now = SystemClock.uptimeMillis();
603 mAnimationLastTime = now;
604 mCurrentAnimationTime = now + ANIMATION_FRAME_DURATION;
605 mAnimating = true;
606 } else {
607 if (mAnimating) {
608 mAnimating = false;
609 mHandler.removeMessages(MSG_ANIMATE);
610 }
611 moveHandle(position);
612 }
613 }
614
615 private void moveHandle(int position) {
616 final View handle = mHandle;
617
618 if (mVertical) {
619 if (position == EXPANDED_FULL_OPEN) {
620 handle.offsetTopAndBottom(mTopOffset - handle.getTop());
621 invalidate();
622 } else if (position == COLLAPSED_FULL_CLOSED) {
623 handle.offsetTopAndBottom(mBottomOffset + getBottom() - getTop() - mHandleHeight - handle.getTop());
624 invalidate();
625 } else {
626 final int top = handle.getTop();
627 int deltaY = position - top;
628 if (position < mTopOffset) {
629 deltaY = mTopOffset - top;
630 } else if (deltaY > mBottomOffset + getBottom() - getTop() - mHandleHeight - top) {
631 deltaY = mBottomOffset + getBottom() - getTop() - mHandleHeight - top;
632 }
633 handle.offsetTopAndBottom(deltaY);
634
635 final Rect frame = mFrame;
636 final Rect region = mInvalidate;
637
638 handle.getHitRect(frame);
639 region.set(frame);
640
641 region.union(frame.left, frame.top - deltaY, frame.right, frame.bottom - deltaY);
642 region.union(0, frame.bottom - deltaY, getWidth(), frame.bottom - deltaY + mContent.getHeight());
643
644 invalidate(region);
Alexandre Lisione1c96db2013-10-04 14:34:21 -0400645
646 if (mOnDrawerScrollListener != null) {
Alexandre Lision8b9d8e82013-10-04 09:21:27 -0400647 mOnDrawerScrollListener.onScroll(handle.getTop());
648 }
alision7297bdb2013-05-21 11:56:55 -0400649 }
650 } else {
651 if (position == EXPANDED_FULL_OPEN) {
652 handle.offsetLeftAndRight(mTopOffset - handle.getLeft());
653 invalidate();
654 } else if (position == COLLAPSED_FULL_CLOSED) {
655 handle.offsetLeftAndRight(mBottomOffset + getRight() - getLeft() - mHandleWidth - handle.getLeft());
656 invalidate();
657 } else {
658 final int left = handle.getLeft();
659 int deltaX = position - left;
660 if (position < mTopOffset) {
661 deltaX = mTopOffset - left;
662 } else if (deltaX > mBottomOffset + getRight() - getLeft() - mHandleWidth - left) {
663 deltaX = mBottomOffset + getRight() - getLeft() - mHandleWidth - left;
664 }
665 handle.offsetLeftAndRight(deltaX);
666
667 final Rect frame = mFrame;
668 final Rect region = mInvalidate;
669
670 handle.getHitRect(frame);
671 region.set(frame);
672
673 region.union(frame.left - deltaX, frame.top, frame.right - deltaX, frame.bottom);
674 region.union(frame.right - deltaX, 0, frame.right - deltaX + mContent.getWidth(), getHeight());
675
676 invalidate(region);
677 }
678 }
679 }
680
681 private void prepareContent() {
682 if (mAnimating) {
683 return;
684 }
685
686 // Something changed in the content, we need to honor the layout request
687 // before creating the cached bitmap
688 final View content = mContent;
689 if (content.isLayoutRequested()) {
690 if (mVertical) {
691 final int childHeight = mHandleHeight;
692 int height = getBottom() - getTop() - childHeight - mTopOffset;
alision907bde72013-06-20 14:40:37 -0400693 content.measure(MeasureSpec.makeMeasureSpec(getRight() - getLeft(), MeasureSpec.EXACTLY),
694 MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
alision7297bdb2013-05-21 11:56:55 -0400695 content.layout(0, mTopOffset + childHeight, content.getMeasuredWidth(), mTopOffset + childHeight + content.getMeasuredHeight());
696 } else {
697 final int childWidth = mHandle.getWidth();
698 int width = getRight() - getLeft() - childWidth - mTopOffset;
alision907bde72013-06-20 14:40:37 -0400699 content.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
700 MeasureSpec.makeMeasureSpec(getBottom() - getTop(), MeasureSpec.EXACTLY));
alision7297bdb2013-05-21 11:56:55 -0400701 content.layout(childWidth + mTopOffset, 0, mTopOffset + childWidth + content.getMeasuredWidth(), content.getMeasuredHeight());
702 }
703 }
704 // Try only once... we should really loop but it's not a big deal
705 // if the draw was cancelled, it will only be temporary anyway
706 content.getViewTreeObserver().dispatchOnPreDraw();
707 // if (!content.isHardwareAccelerated()) content.buildDrawingCache();
708
709 content.setVisibility(View.GONE);
710 }
711
712 private void stopTracking() {
713 mHandle.setPressed(false);
714 mTracking = false;
715
716 if (mOnDrawerScrollListener != null) {
717 mOnDrawerScrollListener.onScrollEnded();
718 }
719
720 if (mVelocityTracker != null) {
721 mVelocityTracker.recycle();
722 mVelocityTracker = null;
723 }
724 }
725
726 private void doAnimation() {
727 if (mAnimating) {
728 incrementAnimation();
729 if (mAnimationPosition >= mBottomOffset + (mVertical ? getHeight() : getWidth()) - 1) {
730 mAnimating = false;
731 closeDrawer();
732 } else if (mAnimationPosition < mTopOffset) {
733 mAnimating = false;
734 openDrawer();
735 } else {
736 moveHandle((int) mAnimationPosition);
737 mCurrentAnimationTime += ANIMATION_FRAME_DURATION;
738 mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_ANIMATE), mCurrentAnimationTime);
739 }
740 }
741 }
742
743 private void incrementAnimation() {
744 long now = SystemClock.uptimeMillis();
745 float t = (now - mAnimationLastTime) / 1000.0f; // ms -> s
746 final float position = mAnimationPosition;
747 final float v = mAnimatedVelocity; // px/s
748 final float a = mAnimatedAcceleration; // px/s/s
749 mAnimationPosition = position + (v * t) + (0.5f * a * t * t); // px
750 mAnimatedVelocity = v + (a * t); // px/s
751 mAnimationLastTime = now; // ms
752 }
753
754 /**
755 * Toggles the drawer open and close. Takes effect immediately.
756 *
757 * @see #open()
758 * @see #close()
759 * @see #animateClose()
760 * @see #animateOpen()
761 * @see #animateToggle()
762 */
763 public void toggle() {
764 if (!mExpanded) {
765 openDrawer();
766 } else {
767 closeDrawer();
768 }
769 invalidate();
770 requestLayout();
771 }
772
773 /**
774 * Toggles the drawer open and close with an animation.
775 *
776 * @see #open()
777 * @see #close()
778 * @see #animateClose()
779 * @see #animateOpen()
780 * @see #toggle()
781 */
782 public void animateToggle() {
783 if (!mExpanded) {
784 animateOpen();
785 } else {
786 animateClose();
787 }
788 }
789
790 /**
791 * Opens the drawer immediately.
792 *
793 * @see #toggle()
794 * @see #close()
795 * @see #animateOpen()
796 */
797 public void open() {
798 openDrawer();
799 invalidate();
800 requestLayout();
801
802 sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
803 }
804
805 /**
806 * Closes the drawer immediately.
807 *
808 * @see #toggle()
809 * @see #open()
810 * @see #animateClose()
811 */
812 public void close() {
813 closeDrawer();
814 invalidate();
815 requestLayout();
816 }
817
818 /**
819 * Closes the drawer with an animation.
820 *
821 * @see #close()
822 * @see #open()
823 * @see #animateOpen()
824 * @see #animateToggle()
825 * @see #toggle()
826 */
827 public void animateClose() {
828 prepareContent();
829 final OnDrawerScrollListener scrollListener = mOnDrawerScrollListener;
830 if (scrollListener != null) {
831 scrollListener.onScrollStarted();
832 }
833 animateClose(mVertical ? mHandle.getTop() : mHandle.getLeft());
834
835 if (scrollListener != null) {
836 scrollListener.onScrollEnded();
837 }
838 }
839
840 /**
841 * Opens the drawer with an animation.
842 *
843 * @see #close()
844 * @see #open()
845 * @see #animateClose()
846 * @see #animateToggle()
847 * @see #toggle()
848 */
849 public void animateOpen() {
alision907bde72013-06-20 14:40:37 -0400850
alision55c36cb2013-06-14 14:57:38 -0400851 if (mExpanded) {
852 return;
853 }
alision7297bdb2013-05-21 11:56:55 -0400854 prepareContent();
855 final OnDrawerScrollListener scrollListener = mOnDrawerScrollListener;
856 if (scrollListener != null) {
857 scrollListener.onScrollStarted();
858 }
859 animateOpen(mVertical ? mHandle.getTop() : mHandle.getLeft());
860
861 sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
862
863 if (scrollListener != null) {
864 scrollListener.onScrollEnded();
865 }
866 }
867
868 // @Override
869 // public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
870 // super.onInitializeAccessibilityEvent(event);
871 // event.setClassName(SlidingDrawer.class.getName());
872 // }
873
874 // @Override
875 // public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)
876 // {
877 // super.onInitializeAccessibilityNodeInfo(info);
878 // info.setClassName(SlidingDrawer.class.getName());
879 // }
880
881 private void closeDrawer() {
882 moveHandle(COLLAPSED_FULL_CLOSED);
883 mContent.setVisibility(View.GONE);
884 mContent.destroyDrawingCache();
885
886 if (!mExpanded) {
887 return;
888 }
889
890 mExpanded = false;
891 if (mOnDrawerCloseListener != null) {
892 mOnDrawerCloseListener.onDrawerClosed();
893 }
894 }
895
896 private void openDrawer() {
897 moveHandle(EXPANDED_FULL_OPEN);
898 mContent.setVisibility(View.VISIBLE);
899
900 if (mExpanded) {
901 return;
902 }
903
904 mExpanded = true;
905
906 if (mOnDrawerOpenListener != null) {
907 mOnDrawerOpenListener.onDrawerOpened();
908 }
909 }
910
911 /**
alision907bde72013-06-20 14:40:37 -0400912 * Sets the listener that receives a notification when the drawer becomes open.
alision7297bdb2013-05-21 11:56:55 -0400913 *
914 * @param onDrawerOpenListener
915 * The listener to be notified when the drawer is opened.
916 */
917 public void setOnDrawerOpenListener(OnDrawerOpenListener onDrawerOpenListener) {
918 mOnDrawerOpenListener = onDrawerOpenListener;
919 }
920
921 /**
alision907bde72013-06-20 14:40:37 -0400922 * Sets the listener that receives a notification when the drawer becomes close.
alision7297bdb2013-05-21 11:56:55 -0400923 *
924 * @param onDrawerCloseListener
925 * The listener to be notified when the drawer is closed.
926 */
927 public void setOnDrawerCloseListener(OnDrawerCloseListener onDrawerCloseListener) {
928 mOnDrawerCloseListener = onDrawerCloseListener;
929 }
930
931 /**
alision907bde72013-06-20 14:40:37 -0400932 * 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 -0400933 * trigger a drawer opened or drawer closed event.
934 *
935 * @param onDrawerScrollListener
936 * The listener to be notified when scrolling starts or stops.
937 */
938 public void setOnDrawerScrollListener(OnDrawerScrollListener onDrawerScrollListener) {
939 mOnDrawerScrollListener = onDrawerScrollListener;
940 }
941
942 /**
943 * Returns the handle of the drawer.
944 *
alision907bde72013-06-20 14:40:37 -0400945 * @return The View reprenseting the handle of the drawer, identified by the "handle" id in XML.
alision7297bdb2013-05-21 11:56:55 -0400946 */
947 public View getHandle() {
948 return mHandle;
949 }
950
951 /**
952 * Returns the content of the drawer.
953 *
alision907bde72013-06-20 14:40:37 -0400954 * @return The View reprenseting the content of the drawer, identified by the "content" id in XML.
alision7297bdb2013-05-21 11:56:55 -0400955 */
956 public View getContent() {
957 return mContent;
958 }
959
960 /**
961 * Unlocks the SlidingDrawer so that touch events are processed.
962 *
963 * @see #lock()
964 */
965 public void unlock() {
966 mLocked = false;
967 }
968
969 /**
970 * Locks the SlidingDrawer so that touch events are ignores.
971 *
972 * @see #unlock()
973 */
974 public void lock() {
975 mLocked = true;
976 }
977
978 /**
979 * Indicates whether the drawer is currently fully opened.
980 *
981 * @return True if the drawer is opened, false otherwise.
982 */
983 public boolean isOpened() {
984 return mExpanded;
985 }
986
987 /**
988 * Indicates whether the drawer is scrolling or flinging.
989 *
990 * @return True if the drawer is scroller or flinging, false otherwise.
991 */
992 public boolean isMoving() {
993 return mTracking || mAnimating;
994 }
995
996 private class DrawerToggler implements OnClickListener {
997 public void onClick(View v) {
998 if (mLocked) {
999 return;
1000 }
1001 // mAllowSingleTap isn't relevant here; you're *always*
1002 // allowed to open/close the drawer by clicking with the
1003 // trackball.
1004
1005 if (mAnimateOnClick) {
1006 animateToggle();
1007 } else {
1008 toggle();
1009 }
1010 }
1011 }
1012
1013 public void setmTrackHandle(View mTrackHandle) {
1014 this.mTrackHandle = mTrackHandle;
1015 }
1016
Alexandre Lision6e8931e2013-09-19 16:49:34 -04001017 private static class SlidingHandler extends Handler {
Alexandre Lision8b9d8e82013-10-04 09:21:27 -04001018
Alexandre Lision6e8931e2013-09-19 16:49:34 -04001019 WeakReference<CustomSlidingDrawer> ref;
Alexandre Lision8b9d8e82013-10-04 09:21:27 -04001020
1021 public SlidingHandler(CustomSlidingDrawer r) {
Alexandre Lision6e8931e2013-09-19 16:49:34 -04001022 ref = new WeakReference<CustomSlidingDrawer>(r);
1023 }
1024
alision7297bdb2013-05-21 11:56:55 -04001025 public void handleMessage(Message m) {
1026 switch (m.what) {
1027 case MSG_ANIMATE:
Alexandre Lision8b9d8e82013-10-04 09:21:27 -04001028 if (ref.get() != null)
Alexandre Lision6e8931e2013-09-19 16:49:34 -04001029 ref.get().doAnimation();
alision7297bdb2013-05-21 11:56:55 -04001030 break;
1031 }
1032 }
1033 }
1034}