blob: d0edd5b8af5166d054c30803ff404dcb17a559f3 [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);
Alexandre Lision8b9d8e82013-10-04 09:21:27 -0400536 // callback.getActionBar().show();
alision7297bdb2013-05-21 11:56:55 -0400537 }
538
539 private void animateOpen(int position) {
540 prepareTracking(position);
541 performFling(position, -mMaximumAcceleration, true);
Alexandre Lision8b9d8e82013-10-04 09:21:27 -0400542 // callback.getActionBar().hide();;
alision7297bdb2013-05-21 11:56:55 -0400543 }
544
545 private void performFling(int position, float velocity, boolean always) {
546 mAnimationPosition = position;
547 mAnimatedVelocity = velocity;
548
549 if (mExpanded) {
alision907bde72013-06-20 14:40:37 -0400550 if (always
551 || (velocity > mMaximumMajorVelocity || (position > mTopOffset + (mVertical ? mHandleHeight : mHandleWidth) && velocity > -mMaximumMajorVelocity))) {
alision7297bdb2013-05-21 11:56:55 -0400552 // We are expanded, but they didn't move sufficiently to cause
553 // us to retract. Animate back to the expanded position.
554 mAnimatedAcceleration = mMaximumAcceleration;
555 if (velocity < 0) {
556 mAnimatedVelocity = 0;
557 }
558 } else {
559 // We are expanded and are now going to animate away.
560 mAnimatedAcceleration = -mMaximumAcceleration;
561 if (velocity > 0) {
562 mAnimatedVelocity = 0;
563 }
564 }
565 } else {
alision907bde72013-06-20 14:40:37 -0400566 if (!always
567 && (velocity > mMaximumMajorVelocity || (position > (mVertical ? getHeight() : getWidth()) / 2 && velocity > -mMaximumMajorVelocity))) {
alision7297bdb2013-05-21 11:56:55 -0400568 // We are collapsed, and they moved enough to allow us to
569 // expand.
570 mAnimatedAcceleration = mMaximumAcceleration;
571 if (velocity < 0) {
572 mAnimatedVelocity = 0;
573 }
574 } else {
575 // We are collapsed, but they didn't move sufficiently to cause
576 // us to retract. Animate back to the collapsed position.
577 mAnimatedAcceleration = -mMaximumAcceleration;
578 if (velocity > 0) {
579 mAnimatedVelocity = 0;
580 }
581 }
582 }
583
584 long now = SystemClock.uptimeMillis();
585 mAnimationLastTime = now;
586 mCurrentAnimationTime = now + ANIMATION_FRAME_DURATION;
587 mAnimating = true;
588 mHandler.removeMessages(MSG_ANIMATE);
589 mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_ANIMATE), mCurrentAnimationTime);
590 stopTracking();
591 }
592
593 private void prepareTracking(int position) {
594 mTracking = true;
595 mVelocityTracker = VelocityTracker.obtain();
596 boolean opening = !mExpanded;
597 if (opening) {
598 mAnimatedAcceleration = mMaximumAcceleration;
599 mAnimatedVelocity = mMaximumMajorVelocity;
600 mAnimationPosition = mBottomOffset + (mVertical ? getHeight() - mHandleHeight : getWidth() - mHandleWidth);
601 moveHandle((int) mAnimationPosition);
602 mAnimating = true;
603 mHandler.removeMessages(MSG_ANIMATE);
604 long now = SystemClock.uptimeMillis();
605 mAnimationLastTime = now;
606 mCurrentAnimationTime = now + ANIMATION_FRAME_DURATION;
607 mAnimating = true;
608 } else {
609 if (mAnimating) {
610 mAnimating = false;
611 mHandler.removeMessages(MSG_ANIMATE);
612 }
613 moveHandle(position);
614 }
615 }
616
617 private void moveHandle(int position) {
618 final View handle = mHandle;
619
620 if (mVertical) {
621 if (position == EXPANDED_FULL_OPEN) {
622 handle.offsetTopAndBottom(mTopOffset - handle.getTop());
623 invalidate();
624 } else if (position == COLLAPSED_FULL_CLOSED) {
625 handle.offsetTopAndBottom(mBottomOffset + getBottom() - getTop() - mHandleHeight - handle.getTop());
626 invalidate();
627 } else {
628 final int top = handle.getTop();
629 int deltaY = position - top;
630 if (position < mTopOffset) {
631 deltaY = mTopOffset - top;
632 } else if (deltaY > mBottomOffset + getBottom() - getTop() - mHandleHeight - top) {
633 deltaY = mBottomOffset + getBottom() - getTop() - mHandleHeight - top;
634 }
635 handle.offsetTopAndBottom(deltaY);
636
637 final Rect frame = mFrame;
638 final Rect region = mInvalidate;
639
640 handle.getHitRect(frame);
641 region.set(frame);
642
643 region.union(frame.left, frame.top - deltaY, frame.right, frame.bottom - deltaY);
644 region.union(0, frame.bottom - deltaY, getWidth(), frame.bottom - deltaY + mContent.getHeight());
645
646 invalidate(region);
Alexandre Lisione1c96db2013-10-04 14:34:21 -0400647
648 if (mOnDrawerScrollListener != null) {
Alexandre Lision8b9d8e82013-10-04 09:21:27 -0400649 mOnDrawerScrollListener.onScroll(handle.getTop());
650 }
alision7297bdb2013-05-21 11:56:55 -0400651 }
652 } else {
653 if (position == EXPANDED_FULL_OPEN) {
654 handle.offsetLeftAndRight(mTopOffset - handle.getLeft());
655 invalidate();
656 } else if (position == COLLAPSED_FULL_CLOSED) {
657 handle.offsetLeftAndRight(mBottomOffset + getRight() - getLeft() - mHandleWidth - handle.getLeft());
658 invalidate();
659 } else {
660 final int left = handle.getLeft();
661 int deltaX = position - left;
662 if (position < mTopOffset) {
663 deltaX = mTopOffset - left;
664 } else if (deltaX > mBottomOffset + getRight() - getLeft() - mHandleWidth - left) {
665 deltaX = mBottomOffset + getRight() - getLeft() - mHandleWidth - left;
666 }
667 handle.offsetLeftAndRight(deltaX);
668
669 final Rect frame = mFrame;
670 final Rect region = mInvalidate;
671
672 handle.getHitRect(frame);
673 region.set(frame);
674
675 region.union(frame.left - deltaX, frame.top, frame.right - deltaX, frame.bottom);
676 region.union(frame.right - deltaX, 0, frame.right - deltaX + mContent.getWidth(), getHeight());
677
678 invalidate(region);
679 }
680 }
681 }
682
683 private void prepareContent() {
684 if (mAnimating) {
685 return;
686 }
687
688 // Something changed in the content, we need to honor the layout request
689 // before creating the cached bitmap
690 final View content = mContent;
691 if (content.isLayoutRequested()) {
692 if (mVertical) {
693 final int childHeight = mHandleHeight;
694 int height = getBottom() - getTop() - childHeight - mTopOffset;
alision907bde72013-06-20 14:40:37 -0400695 content.measure(MeasureSpec.makeMeasureSpec(getRight() - getLeft(), MeasureSpec.EXACTLY),
696 MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
alision7297bdb2013-05-21 11:56:55 -0400697 content.layout(0, mTopOffset + childHeight, content.getMeasuredWidth(), mTopOffset + childHeight + content.getMeasuredHeight());
698 } else {
699 final int childWidth = mHandle.getWidth();
700 int width = getRight() - getLeft() - childWidth - mTopOffset;
alision907bde72013-06-20 14:40:37 -0400701 content.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
702 MeasureSpec.makeMeasureSpec(getBottom() - getTop(), MeasureSpec.EXACTLY));
alision7297bdb2013-05-21 11:56:55 -0400703 content.layout(childWidth + mTopOffset, 0, mTopOffset + childWidth + content.getMeasuredWidth(), content.getMeasuredHeight());
704 }
705 }
706 // Try only once... we should really loop but it's not a big deal
707 // if the draw was cancelled, it will only be temporary anyway
708 content.getViewTreeObserver().dispatchOnPreDraw();
709 // if (!content.isHardwareAccelerated()) content.buildDrawingCache();
710
711 content.setVisibility(View.GONE);
712 }
713
714 private void stopTracking() {
715 mHandle.setPressed(false);
716 mTracking = false;
717
718 if (mOnDrawerScrollListener != null) {
719 mOnDrawerScrollListener.onScrollEnded();
720 }
721
722 if (mVelocityTracker != null) {
723 mVelocityTracker.recycle();
724 mVelocityTracker = null;
725 }
726 }
727
728 private void doAnimation() {
729 if (mAnimating) {
730 incrementAnimation();
731 if (mAnimationPosition >= mBottomOffset + (mVertical ? getHeight() : getWidth()) - 1) {
732 mAnimating = false;
733 closeDrawer();
734 } else if (mAnimationPosition < mTopOffset) {
735 mAnimating = false;
736 openDrawer();
737 } else {
738 moveHandle((int) mAnimationPosition);
739 mCurrentAnimationTime += ANIMATION_FRAME_DURATION;
740 mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_ANIMATE), mCurrentAnimationTime);
741 }
742 }
743 }
744
745 private void incrementAnimation() {
746 long now = SystemClock.uptimeMillis();
747 float t = (now - mAnimationLastTime) / 1000.0f; // ms -> s
748 final float position = mAnimationPosition;
749 final float v = mAnimatedVelocity; // px/s
750 final float a = mAnimatedAcceleration; // px/s/s
751 mAnimationPosition = position + (v * t) + (0.5f * a * t * t); // px
752 mAnimatedVelocity = v + (a * t); // px/s
753 mAnimationLastTime = now; // ms
754 }
755
756 /**
757 * Toggles the drawer open and close. Takes effect immediately.
758 *
759 * @see #open()
760 * @see #close()
761 * @see #animateClose()
762 * @see #animateOpen()
763 * @see #animateToggle()
764 */
765 public void toggle() {
766 if (!mExpanded) {
767 openDrawer();
768 } else {
769 closeDrawer();
770 }
771 invalidate();
772 requestLayout();
773 }
774
775 /**
776 * Toggles the drawer open and close with an animation.
777 *
778 * @see #open()
779 * @see #close()
780 * @see #animateClose()
781 * @see #animateOpen()
782 * @see #toggle()
783 */
784 public void animateToggle() {
785 if (!mExpanded) {
786 animateOpen();
787 } else {
788 animateClose();
789 }
790 }
791
792 /**
793 * Opens the drawer immediately.
794 *
795 * @see #toggle()
796 * @see #close()
797 * @see #animateOpen()
798 */
799 public void open() {
800 openDrawer();
801 invalidate();
802 requestLayout();
803
804 sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
805 }
806
807 /**
808 * Closes the drawer immediately.
809 *
810 * @see #toggle()
811 * @see #open()
812 * @see #animateClose()
813 */
814 public void close() {
815 closeDrawer();
816 invalidate();
817 requestLayout();
818 }
819
820 /**
821 * Closes the drawer with an animation.
822 *
823 * @see #close()
824 * @see #open()
825 * @see #animateOpen()
826 * @see #animateToggle()
827 * @see #toggle()
828 */
829 public void animateClose() {
830 prepareContent();
831 final OnDrawerScrollListener scrollListener = mOnDrawerScrollListener;
832 if (scrollListener != null) {
833 scrollListener.onScrollStarted();
834 }
835 animateClose(mVertical ? mHandle.getTop() : mHandle.getLeft());
836
837 if (scrollListener != null) {
838 scrollListener.onScrollEnded();
839 }
840 }
841
842 /**
843 * Opens the drawer with an animation.
844 *
845 * @see #close()
846 * @see #open()
847 * @see #animateClose()
848 * @see #animateToggle()
849 * @see #toggle()
850 */
851 public void animateOpen() {
alision907bde72013-06-20 14:40:37 -0400852
alision55c36cb2013-06-14 14:57:38 -0400853 if (mExpanded) {
854 return;
855 }
alision7297bdb2013-05-21 11:56:55 -0400856 prepareContent();
857 final OnDrawerScrollListener scrollListener = mOnDrawerScrollListener;
858 if (scrollListener != null) {
859 scrollListener.onScrollStarted();
860 }
861 animateOpen(mVertical ? mHandle.getTop() : mHandle.getLeft());
862
863 sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
864
865 if (scrollListener != null) {
866 scrollListener.onScrollEnded();
867 }
868 }
869
870 // @Override
871 // public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
872 // super.onInitializeAccessibilityEvent(event);
873 // event.setClassName(SlidingDrawer.class.getName());
874 // }
875
876 // @Override
877 // public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)
878 // {
879 // super.onInitializeAccessibilityNodeInfo(info);
880 // info.setClassName(SlidingDrawer.class.getName());
881 // }
882
883 private void closeDrawer() {
884 moveHandle(COLLAPSED_FULL_CLOSED);
885 mContent.setVisibility(View.GONE);
886 mContent.destroyDrawingCache();
887
888 if (!mExpanded) {
889 return;
890 }
891
892 mExpanded = false;
893 if (mOnDrawerCloseListener != null) {
894 mOnDrawerCloseListener.onDrawerClosed();
895 }
896 }
897
898 private void openDrawer() {
899 moveHandle(EXPANDED_FULL_OPEN);
900 mContent.setVisibility(View.VISIBLE);
901
902 if (mExpanded) {
903 return;
904 }
905
906 mExpanded = true;
907
908 if (mOnDrawerOpenListener != null) {
909 mOnDrawerOpenListener.onDrawerOpened();
910 }
911 }
912
913 /**
alision907bde72013-06-20 14:40:37 -0400914 * Sets the listener that receives a notification when the drawer becomes open.
alision7297bdb2013-05-21 11:56:55 -0400915 *
916 * @param onDrawerOpenListener
917 * The listener to be notified when the drawer is opened.
918 */
919 public void setOnDrawerOpenListener(OnDrawerOpenListener onDrawerOpenListener) {
920 mOnDrawerOpenListener = onDrawerOpenListener;
921 }
922
923 /**
alision907bde72013-06-20 14:40:37 -0400924 * Sets the listener that receives a notification when the drawer becomes close.
alision7297bdb2013-05-21 11:56:55 -0400925 *
926 * @param onDrawerCloseListener
927 * The listener to be notified when the drawer is closed.
928 */
929 public void setOnDrawerCloseListener(OnDrawerCloseListener onDrawerCloseListener) {
930 mOnDrawerCloseListener = onDrawerCloseListener;
931 }
932
933 /**
alision907bde72013-06-20 14:40:37 -0400934 * 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 -0400935 * trigger a drawer opened or drawer closed event.
936 *
937 * @param onDrawerScrollListener
938 * The listener to be notified when scrolling starts or stops.
939 */
940 public void setOnDrawerScrollListener(OnDrawerScrollListener onDrawerScrollListener) {
941 mOnDrawerScrollListener = onDrawerScrollListener;
942 }
943
944 /**
945 * Returns the handle of the drawer.
946 *
alision907bde72013-06-20 14:40:37 -0400947 * @return The View reprenseting the handle of the drawer, identified by the "handle" id in XML.
alision7297bdb2013-05-21 11:56:55 -0400948 */
949 public View getHandle() {
950 return mHandle;
951 }
952
953 /**
954 * Returns the content of the drawer.
955 *
alision907bde72013-06-20 14:40:37 -0400956 * @return The View reprenseting the content of the drawer, identified by the "content" id in XML.
alision7297bdb2013-05-21 11:56:55 -0400957 */
958 public View getContent() {
959 return mContent;
960 }
961
962 /**
963 * Unlocks the SlidingDrawer so that touch events are processed.
964 *
965 * @see #lock()
966 */
967 public void unlock() {
968 mLocked = false;
969 }
970
971 /**
972 * Locks the SlidingDrawer so that touch events are ignores.
973 *
974 * @see #unlock()
975 */
976 public void lock() {
977 mLocked = true;
978 }
979
980 /**
981 * Indicates whether the drawer is currently fully opened.
982 *
983 * @return True if the drawer is opened, false otherwise.
984 */
985 public boolean isOpened() {
986 return mExpanded;
987 }
988
989 /**
990 * Indicates whether the drawer is scrolling or flinging.
991 *
992 * @return True if the drawer is scroller or flinging, false otherwise.
993 */
994 public boolean isMoving() {
995 return mTracking || mAnimating;
996 }
997
998 private class DrawerToggler implements OnClickListener {
999 public void onClick(View v) {
1000 if (mLocked) {
1001 return;
1002 }
1003 // mAllowSingleTap isn't relevant here; you're *always*
1004 // allowed to open/close the drawer by clicking with the
1005 // trackball.
1006
1007 if (mAnimateOnClick) {
1008 animateToggle();
1009 } else {
1010 toggle();
1011 }
1012 }
1013 }
1014
1015 public void setmTrackHandle(View mTrackHandle) {
1016 this.mTrackHandle = mTrackHandle;
1017 }
1018
Alexandre Lision6e8931e2013-09-19 16:49:34 -04001019 private static class SlidingHandler extends Handler {
Alexandre Lision8b9d8e82013-10-04 09:21:27 -04001020
Alexandre Lision6e8931e2013-09-19 16:49:34 -04001021 WeakReference<CustomSlidingDrawer> ref;
Alexandre Lision8b9d8e82013-10-04 09:21:27 -04001022
1023 public SlidingHandler(CustomSlidingDrawer r) {
Alexandre Lision6e8931e2013-09-19 16:49:34 -04001024 ref = new WeakReference<CustomSlidingDrawer>(r);
1025 }
1026
alision7297bdb2013-05-21 11:56:55 -04001027 public void handleMessage(Message m) {
1028 switch (m.what) {
1029 case MSG_ANIMATE:
Alexandre Lision8b9d8e82013-10-04 09:21:27 -04001030 if (ref.get() != null)
Alexandre Lision6e8931e2013-09-19 16:49:34 -04001031 ref.get().doAnimation();
alision7297bdb2013-05-21 11:56:55 -04001032 break;
1033 }
1034 }
1035 }
1036}