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