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