1 /* 2 * Copyright (C) 2008 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.widget; 18 19 import android.R; 20 import android.content.Context; 21 import android.content.res.TypedArray; 22 import android.graphics.Bitmap; 23 import android.graphics.Canvas; 24 import android.graphics.Rect; 25 import android.os.Handler; 26 import android.os.Message; 27 import android.os.SystemClock; 28 import android.util.AttributeSet; 29 import android.view.MotionEvent; 30 import android.view.SoundEffectConstants; 31 import android.view.VelocityTracker; 32 import android.view.View; 33 import android.view.ViewGroup; 34 import android.view.accessibility.AccessibilityEvent; 35 36 /** 37 * SlidingDrawer hides content out of the screen and allows the user to drag a handle 38 * to bring the content on screen. SlidingDrawer can be used vertically or horizontally. 39 * 40 * A special widget composed of two children views: the handle, that the users drags, 41 * and the content, attached to the handle and dragged with it. 42 * 43 * SlidingDrawer should be used as an overlay inside layouts. This means SlidingDrawer 44 * should only be used inside of a FrameLayout or a RelativeLayout for instance. The 45 * size of the SlidingDrawer defines how much space the content will occupy once slid 46 * out so SlidingDrawer should usually use match_parent for both its dimensions. 47 * 48 * Inside an XML layout, SlidingDrawer must define the id of the handle and of the 49 * content: 50 * 51 * <pre class="prettyprint"> 52 * <SlidingDrawer 53 * android:id="@+id/drawer" 54 * android:layout_width="match_parent" 55 * android:layout_height="match_parent" 56 * 57 * android:handle="@+id/handle" 58 * android:content="@+id/content"> 59 * 60 * <ImageView 61 * android:id="@id/handle" 62 * android:layout_width="88dip" 63 * android:layout_height="44dip" /> 64 * 65 * <GridView 66 * android:id="@id/content" 67 * android:layout_width="match_parent" 68 * android:layout_height="match_parent" /> 69 * 70 * </SlidingDrawer> 71 * </pre> 72 * 73 * @attr ref android.R.styleable#SlidingDrawer_content 74 * @attr ref android.R.styleable#SlidingDrawer_handle 75 * @attr ref android.R.styleable#SlidingDrawer_topOffset 76 * @attr ref android.R.styleable#SlidingDrawer_bottomOffset 77 * @attr ref android.R.styleable#SlidingDrawer_orientation 78 * @attr ref android.R.styleable#SlidingDrawer_allowSingleTap 79 * @attr ref android.R.styleable#SlidingDrawer_animateOnClick 80 */ 81 public class SlidingDrawer extends ViewGroup { 82 public static final int ORIENTATION_HORIZONTAL = 0; 83 public static final int ORIENTATION_VERTICAL = 1; 84 85 private static final int TAP_THRESHOLD = 6; 86 private static final float MAXIMUM_TAP_VELOCITY = 100.0f; 87 private static final float MAXIMUM_MINOR_VELOCITY = 150.0f; 88 private static final float MAXIMUM_MAJOR_VELOCITY = 200.0f; 89 private static final float MAXIMUM_ACCELERATION = 2000.0f; 90 private static final int VELOCITY_UNITS = 1000; 91 private static final int MSG_ANIMATE = 1000; 92 private static final int ANIMATION_FRAME_DURATION = 1000 / 60; 93 94 private static final int EXPANDED_FULL_OPEN = -10001; 95 private static final int COLLAPSED_FULL_CLOSED = -10002; 96 97 private final int mHandleId; 98 private final int mContentId; 99 100 private View mHandle; 101 private View mContent; 102 103 private final Rect mFrame = new Rect(); 104 private final Rect mInvalidate = new Rect(); 105 private boolean mTracking; 106 private boolean mLocked; 107 108 private VelocityTracker mVelocityTracker; 109 110 private boolean mVertical; 111 private boolean mExpanded; 112 private int mBottomOffset; 113 private int mTopOffset; 114 private int mHandleHeight; 115 private int mHandleWidth; 116 117 private OnDrawerOpenListener mOnDrawerOpenListener; 118 private OnDrawerCloseListener mOnDrawerCloseListener; 119 private OnDrawerScrollListener mOnDrawerScrollListener; 120 121 private final Handler mHandler = new SlidingHandler(); 122 private float mAnimatedAcceleration; 123 private float mAnimatedVelocity; 124 private float mAnimationPosition; 125 private long mAnimationLastTime; 126 private long mCurrentAnimationTime; 127 private int mTouchDelta; 128 private boolean mAnimating; 129 private boolean mAllowSingleTap; 130 private boolean mAnimateOnClick; 131 132 private final int mTapThreshold; 133 private final int mMaximumTapVelocity; 134 private final int mMaximumMinorVelocity; 135 private final int mMaximumMajorVelocity; 136 private final int mMaximumAcceleration; 137 private final int mVelocityUnits; 138 139 /** 140 * Callback invoked when the drawer is opened. 141 */ 142 public static interface OnDrawerOpenListener { 143 /** 144 * Invoked when the drawer becomes fully open. 145 */ 146 public void onDrawerOpened(); 147 } 148 149 /** 150 * Callback invoked when the drawer is closed. 151 */ 152 public static interface OnDrawerCloseListener { 153 /** 154 * Invoked when the drawer becomes fully closed. 155 */ 156 public void onDrawerClosed(); 157 } 158 159 /** 160 * Callback invoked when the drawer is scrolled. 161 */ 162 public static interface OnDrawerScrollListener { 163 /** 164 * Invoked when the user starts dragging/flinging the drawer's handle. 165 */ 166 public void onScrollStarted(); 167 168 /** 169 * Invoked when the user stops dragging/flinging the drawer's handle. 170 */ 171 public void onScrollEnded(); 172 } 173 174 /** 175 * Creates a new SlidingDrawer from a specified set of attributes defined in XML. 176 * 177 * @param context The application's environment. 178 * @param attrs The attributes defined in XML. 179 */ 180 public SlidingDrawer(Context context, AttributeSet attrs) { 181 this(context, attrs, 0); 182 } 183 184 /** 185 * Creates a new SlidingDrawer from a specified set of attributes defined in XML. 186 * 187 * @param context The application's environment. 188 * @param attrs The attributes defined in XML. 189 * @param defStyle The style to apply to this widget. 190 */ 191 public SlidingDrawer(Context context, AttributeSet attrs, int defStyle) { 192 super(context, attrs, defStyle); 193 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SlidingDrawer, defStyle, 0); 194 195 int orientation = a.getInt(R.styleable.SlidingDrawer_orientation, ORIENTATION_VERTICAL); 196 mVertical = orientation == ORIENTATION_VERTICAL; 197 mBottomOffset = (int) a.getDimension(R.styleable.SlidingDrawer_bottomOffset, 0.0f); 198 mTopOffset = (int) a.getDimension(R.styleable.SlidingDrawer_topOffset, 0.0f); 199 mAllowSingleTap = a.getBoolean(R.styleable.SlidingDrawer_allowSingleTap, true); 200 mAnimateOnClick = a.getBoolean(R.styleable.SlidingDrawer_animateOnClick, true); 201 202 int handleId = a.getResourceId(R.styleable.SlidingDrawer_handle, 0); 203 if (handleId == 0) { 204 throw new IllegalArgumentException("The handle attribute is required and must refer " 205 + "to a valid child."); 206 } 207 208 int contentId = a.getResourceId(R.styleable.SlidingDrawer_content, 0); 209 if (contentId == 0) { 210 throw new IllegalArgumentException("The content attribute is required and must refer " 211 + "to a valid child."); 212 } 213 214 if (handleId == contentId) { 215 throw new IllegalArgumentException("The content and handle attributes must refer " 216 + "to different children."); 217 } 218 219 mHandleId = handleId; 220 mContentId = contentId; 221 222 final float density = getResources().getDisplayMetrics().density; 223 mTapThreshold = (int) (TAP_THRESHOLD * density + 0.5f); 224 mMaximumTapVelocity = (int) (MAXIMUM_TAP_VELOCITY * density + 0.5f); 225 mMaximumMinorVelocity = (int) (MAXIMUM_MINOR_VELOCITY * density + 0.5f); 226 mMaximumMajorVelocity = (int) (MAXIMUM_MAJOR_VELOCITY * density + 0.5f); 227 mMaximumAcceleration = (int) (MAXIMUM_ACCELERATION * density + 0.5f); 228 mVelocityUnits = (int) (VELOCITY_UNITS * density + 0.5f); 229 230 a.recycle(); 231 232 setAlwaysDrawnWithCacheEnabled(false); 233 } 234 235 @Override 236 protected void onFinishInflate() { 237 mHandle = findViewById(mHandleId); 238 if (mHandle == null) { 239 throw new IllegalArgumentException("The handle attribute is must refer to an" 240 + " existing child."); 241 } 242 mHandle.setOnClickListener(new DrawerToggler()); 243 244 mContent = findViewById(mContentId); 245 if (mContent == null) { 246 throw new IllegalArgumentException("The content attribute is must refer to an" 247 + " existing child."); 248 } 249 mContent.setVisibility(View.GONE); 250 } 251 252 @Override 253 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 254 int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); 255 int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); 256 257 int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); 258 int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); 259 260 if (widthSpecMode == MeasureSpec.UNSPECIFIED || heightSpecMode == MeasureSpec.UNSPECIFIED) { 261 throw new RuntimeException("SlidingDrawer cannot have UNSPECIFIED dimensions"); 262 } 263 264 final View handle = mHandle; 265 measureChild(handle, widthMeasureSpec, heightMeasureSpec); 266 267 if (mVertical) { 268 int height = heightSpecSize - handle.getMeasuredHeight() - mTopOffset; 269 mContent.measure(MeasureSpec.makeMeasureSpec(widthSpecSize, MeasureSpec.EXACTLY), 270 MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); 271 } else { 272 int width = widthSpecSize - handle.getMeasuredWidth() - mTopOffset; 273 mContent.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), 274 MeasureSpec.makeMeasureSpec(heightSpecSize, MeasureSpec.EXACTLY)); 275 } 276 277 setMeasuredDimension(widthSpecSize, heightSpecSize); 278 } 279 280 @Override 281 protected void dispatchDraw(Canvas canvas) { 282 final long drawingTime = getDrawingTime(); 283 final View handle = mHandle; 284 final boolean isVertical = mVertical; 285 286 drawChild(canvas, handle, drawingTime); 287 288 if (mTracking || mAnimating) { 289 final Bitmap cache = mContent.getDrawingCache(); 290 if (cache != null) { 291 if (isVertical) { 292 canvas.drawBitmap(cache, 0, handle.getBottom(), null); 293 } else { 294 canvas.drawBitmap(cache, handle.getRight(), 0, null); 295 } 296 } else { 297 canvas.save(); 298 canvas.translate(isVertical ? 0 : handle.getLeft() - mTopOffset, 299 isVertical ? handle.getTop() - mTopOffset : 0); 300 drawChild(canvas, mContent, drawingTime); 301 canvas.restore(); 302 } 303 } else if (mExpanded) { 304 drawChild(canvas, mContent, drawingTime); 305 } 306 } 307 308 @Override 309 protected void onLayout(boolean changed, int l, int t, int r, int b) { 310 if (mTracking) { 311 return; 312 } 313 314 final int width = r - l; 315 final int height = b - t; 316 317 final View handle = mHandle; 318 319 int childWidth = handle.getMeasuredWidth(); 320 int childHeight = handle.getMeasuredHeight(); 321 322 int childLeft; 323 int childTop; 324 325 final View content = mContent; 326 327 if (mVertical) { 328 childLeft = (width - childWidth) / 2; 329 childTop = mExpanded ? mTopOffset : height - childHeight + mBottomOffset; 330 331 content.layout(0, mTopOffset + childHeight, content.getMeasuredWidth(), 332 mTopOffset + childHeight + content.getMeasuredHeight()); 333 } else { 334 childLeft = mExpanded ? mTopOffset : width - childWidth + mBottomOffset; 335 childTop = (height - childHeight) / 2; 336 337 content.layout(mTopOffset + childWidth, 0, 338 mTopOffset + childWidth + content.getMeasuredWidth(), 339 content.getMeasuredHeight()); 340 } 341 342 handle.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight); 343 mHandleHeight = handle.getHeight(); 344 mHandleWidth = handle.getWidth(); 345 } 346 347 @Override 348 public boolean onInterceptTouchEvent(MotionEvent event) { 349 if (mLocked) { 350 return false; 351 } 352 353 final int action = event.getAction(); 354 355 float x = event.getX(); 356 float y = event.getY(); 357 358 final Rect frame = mFrame; 359 final View handle = mHandle; 360 361 handle.getHitRect(frame); 362 if (!mTracking && !frame.contains((int) x, (int) y)) { 363 return false; 364 } 365 366 if (action == MotionEvent.ACTION_DOWN) { 367 mTracking = true; 368 369 handle.setPressed(true); 370 // Must be called before prepareTracking() 371 prepareContent(); 372 373 // Must be called after prepareContent() 374 if (mOnDrawerScrollListener != null) { 375 mOnDrawerScrollListener.onScrollStarted(); 376 } 377 378 if (mVertical) { 379 final int top = mHandle.getTop(); 380 mTouchDelta = (int) y - top; 381 prepareTracking(top); 382 } else { 383 final int left = mHandle.getLeft(); 384 mTouchDelta = (int) x - left; 385 prepareTracking(left); 386 } 387 mVelocityTracker.addMovement(event); 388 } 389 390 return true; 391 } 392 393 @Override 394 public boolean onTouchEvent(MotionEvent event) { 395 if (mLocked) { 396 return true; 397 } 398 399 if (mTracking) { 400 mVelocityTracker.addMovement(event); 401 final int action = event.getAction(); 402 switch (action) { 403 case MotionEvent.ACTION_MOVE: 404 moveHandle((int) (mVertical ? event.getY() : event.getX()) - mTouchDelta); 405 break; 406 case MotionEvent.ACTION_UP: 407 case MotionEvent.ACTION_CANCEL: { 408 final VelocityTracker velocityTracker = mVelocityTracker; 409 velocityTracker.computeCurrentVelocity(mVelocityUnits); 410 411 float yVelocity = velocityTracker.getYVelocity(); 412 float xVelocity = velocityTracker.getXVelocity(); 413 boolean negative; 414 415 final boolean vertical = mVertical; 416 if (vertical) { 417 negative = yVelocity < 0; 418 if (xVelocity < 0) { 419 xVelocity = -xVelocity; 420 } 421 if (xVelocity > mMaximumMinorVelocity) { 422 xVelocity = mMaximumMinorVelocity; 423 } 424 } else { 425 negative = xVelocity < 0; 426 if (yVelocity < 0) { 427 yVelocity = -yVelocity; 428 } 429 if (yVelocity > mMaximumMinorVelocity) { 430 yVelocity = mMaximumMinorVelocity; 431 } 432 } 433 434 float velocity = (float) Math.hypot(xVelocity, yVelocity); 435 if (negative) { 436 velocity = -velocity; 437 } 438 439 final int top = mHandle.getTop(); 440 final int left = mHandle.getLeft(); 441 442 if (Math.abs(velocity) < mMaximumTapVelocity) { 443 if (vertical ? (mExpanded && top < mTapThreshold + mTopOffset) || 444 (!mExpanded && top > mBottomOffset + mBottom - mTop - 445 mHandleHeight - mTapThreshold) : 446 (mExpanded && left < mTapThreshold + mTopOffset) || 447 (!mExpanded && left > mBottomOffset + mRight - mLeft - 448 mHandleWidth - mTapThreshold)) { 449 450 if (mAllowSingleTap) { 451 playSoundEffect(SoundEffectConstants.CLICK); 452 453 if (mExpanded) { 454 animateClose(vertical ? top : left); 455 } else { 456 animateOpen(vertical ? top : left); 457 } 458 } else { 459 performFling(vertical ? top : left, velocity, false); 460 } 461 462 } else { 463 performFling(vertical ? top : left, velocity, false); 464 } 465 } else { 466 performFling(vertical ? top : left, velocity, false); 467 } 468 } 469 break; 470 } 471 } 472 473 return mTracking || mAnimating || super.onTouchEvent(event); 474 } 475 476 private void animateClose(int position) { 477 prepareTracking(position); 478 performFling(position, mMaximumAcceleration, true); 479 } 480 481 private void animateOpen(int position) { 482 prepareTracking(position); 483 performFling(position, -mMaximumAcceleration, true); 484 } 485 486 private void performFling(int position, float velocity, boolean always) { 487 mAnimationPosition = position; 488 mAnimatedVelocity = velocity; 489 490 if (mExpanded) { 491 if (always || (velocity > mMaximumMajorVelocity || 492 (position > mTopOffset + (mVertical ? mHandleHeight : mHandleWidth) && 493 velocity > -mMaximumMajorVelocity))) { 494 // We are expanded, but they didn't move sufficiently to cause 495 // us to retract. Animate back to the expanded position. 496 mAnimatedAcceleration = mMaximumAcceleration; 497 if (velocity < 0) { 498 mAnimatedVelocity = 0; 499 } 500 } else { 501 // We are expanded and are now going to animate away. 502 mAnimatedAcceleration = -mMaximumAcceleration; 503 if (velocity > 0) { 504 mAnimatedVelocity = 0; 505 } 506 } 507 } else { 508 if (!always && (velocity > mMaximumMajorVelocity || 509 (position > (mVertical ? getHeight() : getWidth()) / 2 && 510 velocity > -mMaximumMajorVelocity))) { 511 // We are collapsed, and they moved enough to allow us to expand. 512 mAnimatedAcceleration = mMaximumAcceleration; 513 if (velocity < 0) { 514 mAnimatedVelocity = 0; 515 } 516 } else { 517 // We are collapsed, but they didn't move sufficiently to cause 518 // us to retract. Animate back to the collapsed position. 519 mAnimatedAcceleration = -mMaximumAcceleration; 520 if (velocity > 0) { 521 mAnimatedVelocity = 0; 522 } 523 } 524 } 525 526 long now = SystemClock.uptimeMillis(); 527 mAnimationLastTime = now; 528 mCurrentAnimationTime = now + ANIMATION_FRAME_DURATION; 529 mAnimating = true; 530 mHandler.removeMessages(MSG_ANIMATE); 531 mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_ANIMATE), mCurrentAnimationTime); 532 stopTracking(); 533 } 534 535 private void prepareTracking(int position) { 536 mTracking = true; 537 mVelocityTracker = VelocityTracker.obtain(); 538 boolean opening = !mExpanded; 539 if (opening) { 540 mAnimatedAcceleration = mMaximumAcceleration; 541 mAnimatedVelocity = mMaximumMajorVelocity; 542 mAnimationPosition = mBottomOffset + 543 (mVertical ? getHeight() - mHandleHeight : getWidth() - mHandleWidth); 544 moveHandle((int) mAnimationPosition); 545 mAnimating = true; 546 mHandler.removeMessages(MSG_ANIMATE); 547 long now = SystemClock.uptimeMillis(); 548 mAnimationLastTime = now; 549 mCurrentAnimationTime = now + ANIMATION_FRAME_DURATION; 550 mAnimating = true; 551 } else { 552 if (mAnimating) { 553 mAnimating = false; 554 mHandler.removeMessages(MSG_ANIMATE); 555 } 556 moveHandle(position); 557 } 558 } 559 560 private void moveHandle(int position) { 561 final View handle = mHandle; 562 563 if (mVertical) { 564 if (position == EXPANDED_FULL_OPEN) { 565 handle.offsetTopAndBottom(mTopOffset - handle.getTop()); 566 invalidate(); 567 } else if (position == COLLAPSED_FULL_CLOSED) { 568 handle.offsetTopAndBottom(mBottomOffset + mBottom - mTop - 569 mHandleHeight - handle.getTop()); 570 invalidate(); 571 } else { 572 final int top = handle.getTop(); 573 int deltaY = position - top; 574 if (position < mTopOffset) { 575 deltaY = mTopOffset - top; 576 } else if (deltaY > mBottomOffset + mBottom - mTop - mHandleHeight - top) { 577 deltaY = mBottomOffset + mBottom - mTop - mHandleHeight - top; 578 } 579 handle.offsetTopAndBottom(deltaY); 580 581 final Rect frame = mFrame; 582 final Rect region = mInvalidate; 583 584 handle.getHitRect(frame); 585 region.set(frame); 586 587 region.union(frame.left, frame.top - deltaY, frame.right, frame.bottom - deltaY); 588 region.union(0, frame.bottom - deltaY, getWidth(), 589 frame.bottom - deltaY + mContent.getHeight()); 590 591 invalidate(region); 592 } 593 } else { 594 if (position == EXPANDED_FULL_OPEN) { 595 handle.offsetLeftAndRight(mTopOffset - handle.getLeft()); 596 invalidate(); 597 } else if (position == COLLAPSED_FULL_CLOSED) { 598 handle.offsetLeftAndRight(mBottomOffset + mRight - mLeft - 599 mHandleWidth - handle.getLeft()); 600 invalidate(); 601 } else { 602 final int left = handle.getLeft(); 603 int deltaX = position - left; 604 if (position < mTopOffset) { 605 deltaX = mTopOffset - left; 606 } else if (deltaX > mBottomOffset + mRight - mLeft - mHandleWidth - left) { 607 deltaX = mBottomOffset + mRight - mLeft - mHandleWidth - left; 608 } 609 handle.offsetLeftAndRight(deltaX); 610 611 final Rect frame = mFrame; 612 final Rect region = mInvalidate; 613 614 handle.getHitRect(frame); 615 region.set(frame); 616 617 region.union(frame.left - deltaX, frame.top, frame.right - deltaX, frame.bottom); 618 region.union(frame.right - deltaX, 0, 619 frame.right - deltaX + mContent.getWidth(), getHeight()); 620 621 invalidate(region); 622 } 623 } 624 } 625 626 private void prepareContent() { 627 if (mAnimating) { 628 return; 629 } 630 631 // Something changed in the content, we need to honor the layout request 632 // before creating the cached bitmap 633 final View content = mContent; 634 if (content.isLayoutRequested()) { 635 if (mVertical) { 636 final int childHeight = mHandleHeight; 637 int height = mBottom - mTop - childHeight - mTopOffset; 638 content.measure(MeasureSpec.makeMeasureSpec(mRight - mLeft, MeasureSpec.EXACTLY), 639 MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); 640 content.layout(0, mTopOffset + childHeight, content.getMeasuredWidth(), 641 mTopOffset + childHeight + content.getMeasuredHeight()); 642 } else { 643 final int childWidth = mHandle.getWidth(); 644 int width = mRight - mLeft - childWidth - mTopOffset; 645 content.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), 646 MeasureSpec.makeMeasureSpec(mBottom - mTop, MeasureSpec.EXACTLY)); 647 content.layout(childWidth + mTopOffset, 0, 648 mTopOffset + childWidth + content.getMeasuredWidth(), 649 content.getMeasuredHeight()); 650 } 651 } 652 // Try only once... we should really loop but it's not a big deal 653 // if the draw was cancelled, it will only be temporary anyway 654 content.getViewTreeObserver().dispatchOnPreDraw(); 655 content.buildDrawingCache(); 656 657 content.setVisibility(View.GONE); 658 } 659 660 private void stopTracking() { 661 mHandle.setPressed(false); 662 mTracking = false; 663 664 if (mOnDrawerScrollListener != null) { 665 mOnDrawerScrollListener.onScrollEnded(); 666 } 667 668 if (mVelocityTracker != null) { 669 mVelocityTracker.recycle(); 670 mVelocityTracker = null; 671 } 672 } 673 674 private void doAnimation() { 675 if (mAnimating) { 676 incrementAnimation(); 677 if (mAnimationPosition >= mBottomOffset + (mVertical ? getHeight() : getWidth()) - 1) { 678 mAnimating = false; 679 closeDrawer(); 680 } else if (mAnimationPosition < mTopOffset) { 681 mAnimating = false; 682 openDrawer(); 683 } else { 684 moveHandle((int) mAnimationPosition); 685 mCurrentAnimationTime += ANIMATION_FRAME_DURATION; 686 mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_ANIMATE), 687 mCurrentAnimationTime); 688 } 689 } 690 } 691 692 private void incrementAnimation() { 693 long now = SystemClock.uptimeMillis(); 694 float t = (now - mAnimationLastTime) / 1000.0f; // ms -> s 695 final float position = mAnimationPosition; 696 final float v = mAnimatedVelocity; // px/s 697 final float a = mAnimatedAcceleration; // px/s/s 698 mAnimationPosition = position + (v * t) + (0.5f * a * t * t); // px 699 mAnimatedVelocity = v + (a * t); // px/s 700 mAnimationLastTime = now; // ms 701 } 702 703 /** 704 * Toggles the drawer open and close. Takes effect immediately. 705 * 706 * @see #open() 707 * @see #close() 708 * @see #animateClose() 709 * @see #animateOpen() 710 * @see #animateToggle() 711 */ 712 public void toggle() { 713 if (!mExpanded) { 714 openDrawer(); 715 } else { 716 closeDrawer(); 717 } 718 invalidate(); 719 requestLayout(); 720 } 721 722 /** 723 * Toggles the drawer open and close with an animation. 724 * 725 * @see #open() 726 * @see #close() 727 * @see #animateClose() 728 * @see #animateOpen() 729 * @see #toggle() 730 */ 731 public void animateToggle() { 732 if (!mExpanded) { 733 animateOpen(); 734 } else { 735 animateClose(); 736 } 737 } 738 739 /** 740 * Opens the drawer immediately. 741 * 742 * @see #toggle() 743 * @see #close() 744 * @see #animateOpen() 745 */ 746 public void open() { 747 openDrawer(); 748 invalidate(); 749 requestLayout(); 750 751 sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); 752 } 753 754 /** 755 * Closes the drawer immediately. 756 * 757 * @see #toggle() 758 * @see #open() 759 * @see #animateClose() 760 */ 761 public void close() { 762 closeDrawer(); 763 invalidate(); 764 requestLayout(); 765 } 766 767 /** 768 * Closes the drawer with an animation. 769 * 770 * @see #close() 771 * @see #open() 772 * @see #animateOpen() 773 * @see #animateToggle() 774 * @see #toggle() 775 */ 776 public void animateClose() { 777 prepareContent(); 778 final OnDrawerScrollListener scrollListener = mOnDrawerScrollListener; 779 if (scrollListener != null) { 780 scrollListener.onScrollStarted(); 781 } 782 animateClose(mVertical ? mHandle.getTop() : mHandle.getLeft()); 783 784 if (scrollListener != null) { 785 scrollListener.onScrollEnded(); 786 } 787 } 788 789 /** 790 * Opens the drawer with an animation. 791 * 792 * @see #close() 793 * @see #open() 794 * @see #animateClose() 795 * @see #animateToggle() 796 * @see #toggle() 797 */ 798 public void animateOpen() { 799 prepareContent(); 800 final OnDrawerScrollListener scrollListener = mOnDrawerScrollListener; 801 if (scrollListener != null) { 802 scrollListener.onScrollStarted(); 803 } 804 animateOpen(mVertical ? mHandle.getTop() : mHandle.getLeft()); 805 806 sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); 807 808 if (scrollListener != null) { 809 scrollListener.onScrollEnded(); 810 } 811 } 812 813 private void closeDrawer() { 814 moveHandle(COLLAPSED_FULL_CLOSED); 815 mContent.setVisibility(View.GONE); 816 mContent.destroyDrawingCache(); 817 818 if (!mExpanded) { 819 return; 820 } 821 822 mExpanded = false; 823 if (mOnDrawerCloseListener != null) { 824 mOnDrawerCloseListener.onDrawerClosed(); 825 } 826 } 827 828 private void openDrawer() { 829 moveHandle(EXPANDED_FULL_OPEN); 830 mContent.setVisibility(View.VISIBLE); 831 832 if (mExpanded) { 833 return; 834 } 835 836 mExpanded = true; 837 838 if (mOnDrawerOpenListener != null) { 839 mOnDrawerOpenListener.onDrawerOpened(); 840 } 841 } 842 843 /** 844 * Sets the listener that receives a notification when the drawer becomes open. 845 * 846 * @param onDrawerOpenListener The listener to be notified when the drawer is opened. 847 */ 848 public void setOnDrawerOpenListener(OnDrawerOpenListener onDrawerOpenListener) { 849 mOnDrawerOpenListener = onDrawerOpenListener; 850 } 851 852 /** 853 * Sets the listener that receives a notification when the drawer becomes close. 854 * 855 * @param onDrawerCloseListener The listener to be notified when the drawer is closed. 856 */ 857 public void setOnDrawerCloseListener(OnDrawerCloseListener onDrawerCloseListener) { 858 mOnDrawerCloseListener = onDrawerCloseListener; 859 } 860 861 /** 862 * Sets the listener that receives a notification when the drawer starts or ends 863 * a scroll. A fling is considered as a scroll. A fling will also trigger a 864 * drawer opened or drawer closed event. 865 * 866 * @param onDrawerScrollListener The listener to be notified when scrolling 867 * starts or stops. 868 */ 869 public void setOnDrawerScrollListener(OnDrawerScrollListener onDrawerScrollListener) { 870 mOnDrawerScrollListener = onDrawerScrollListener; 871 } 872 873 /** 874 * Returns the handle of the drawer. 875 * 876 * @return The View reprenseting the handle of the drawer, identified by 877 * the "handle" id in XML. 878 */ 879 public View getHandle() { 880 return mHandle; 881 } 882 883 /** 884 * Returns the content of the drawer. 885 * 886 * @return The View reprenseting the content of the drawer, identified by 887 * the "content" id in XML. 888 */ 889 public View getContent() { 890 return mContent; 891 } 892 893 /** 894 * Unlocks the SlidingDrawer so that touch events are processed. 895 * 896 * @see #lock() 897 */ 898 public void unlock() { 899 mLocked = false; 900 } 901 902 /** 903 * Locks the SlidingDrawer so that touch events are ignores. 904 * 905 * @see #unlock() 906 */ 907 public void lock() { 908 mLocked = true; 909 } 910 911 /** 912 * Indicates whether the drawer is currently fully opened. 913 * 914 * @return True if the drawer is opened, false otherwise. 915 */ 916 public boolean isOpened() { 917 return mExpanded; 918 } 919 920 /** 921 * Indicates whether the drawer is scrolling or flinging. 922 * 923 * @return True if the drawer is scroller or flinging, false otherwise. 924 */ 925 public boolean isMoving() { 926 return mTracking || mAnimating; 927 } 928 929 private class DrawerToggler implements OnClickListener { 930 public void onClick(View v) { 931 if (mLocked) { 932 return; 933 } 934 // mAllowSingleTap isn't relevant here; you're *always* 935 // allowed to open/close the drawer by clicking with the 936 // trackball. 937 938 if (mAnimateOnClick) { 939 animateToggle(); 940 } else { 941 toggle(); 942 } 943 } 944 } 945 946 private class SlidingHandler extends Handler { 947 public void handleMessage(Message m) { 948 switch (m.what) { 949 case MSG_ANIMATE: 950 doAnimation(); 951 break; 952 } 953 } 954 } 955 } 956