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