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