FloatingActionButton.java revision b6e7e4c2bcefc6bba852f9e36a02c6c2519f6466
1/* 2 * Copyright (C) 2015 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.support.design.widget; 18 19import android.annotation.TargetApi; 20import android.content.Context; 21import android.content.res.ColorStateList; 22import android.content.res.Resources; 23import android.content.res.TypedArray; 24import android.graphics.PorterDuff; 25import android.graphics.Rect; 26import android.graphics.drawable.Drawable; 27import android.os.Build; 28import android.support.annotation.ColorInt; 29import android.support.annotation.DrawableRes; 30import android.support.annotation.IntDef; 31import android.support.annotation.NonNull; 32import android.support.annotation.Nullable; 33import android.support.annotation.VisibleForTesting; 34import android.support.design.R; 35import android.support.design.widget.FloatingActionButtonImpl.InternalVisibilityChangedListener; 36import android.support.v4.content.res.ConfigurationHelper; 37import android.support.v4.view.ViewCompat; 38import android.support.v7.widget.AppCompatImageHelper; 39import android.util.AttributeSet; 40import android.util.Log; 41import android.view.Gravity; 42import android.view.MotionEvent; 43import android.view.View; 44import android.widget.ImageView; 45 46import java.lang.annotation.Retention; 47import java.lang.annotation.RetentionPolicy; 48import java.util.List; 49 50/** 51 * Floating action buttons are used for a special type of promoted action. They are distinguished 52 * by a circled icon floating above the UI and have special motion behaviors related to morphing, 53 * launching, and the transferring anchor point. 54 * 55 * <p>Floating action buttons come in two sizes: the default and the mini. The size can be 56 * controlled with the {@code fabSize} attribute.</p> 57 * 58 * <p>As this class descends from {@link ImageView}, you can control the icon which is displayed 59 * via {@link #setImageDrawable(Drawable)}.</p> 60 * 61 * <p>The background color of this view defaults to the your theme's {@code colorAccent}. If you 62 * wish to change this at runtime then you can do so via 63 * {@link #setBackgroundTintList(ColorStateList)}.</p> 64 */ 65@CoordinatorLayout.DefaultBehavior(FloatingActionButton.Behavior.class) 66public class FloatingActionButton extends VisibilityAwareImageButton { 67 68 private static final String LOG_TAG = "FloatingActionButton"; 69 70 /** 71 * Callback to be invoked when the visibility of a FloatingActionButton changes. 72 */ 73 public abstract static class OnVisibilityChangedListener { 74 /** 75 * Called when a FloatingActionButton has been 76 * {@link #show(OnVisibilityChangedListener) shown}. 77 * 78 * @param fab the FloatingActionButton that was shown. 79 */ 80 public void onShown(FloatingActionButton fab) {} 81 82 /** 83 * Called when a FloatingActionButton has been 84 * {@link #hide(OnVisibilityChangedListener) hidden}. 85 * 86 * @param fab the FloatingActionButton that was hidden. 87 */ 88 public void onHidden(FloatingActionButton fab) {} 89 } 90 91 // These values must match those in the attrs declaration 92 93 /** 94 * The mini sized button. Will always been smaller than {@link #SIZE_NORMAL}. 95 * 96 * @see #setSize(int) 97 */ 98 public static final int SIZE_MINI = 1; 99 100 /** 101 * The normal sized button. Will always been larger than {@link #SIZE_MINI}. 102 * 103 * @see #setSize(int) 104 */ 105 public static final int SIZE_NORMAL = 0; 106 107 /** 108 * Size which will change based on the window size. For small sized windows 109 * (largest screen dimension < 470dp) this will select a small sized button, and for 110 * larger sized windows it will select a larger size. 111 * 112 * @see #setSize(int) 113 */ 114 public static final int SIZE_AUTO = -1; 115 116 /** 117 * The switch point for the largest screen edge where SIZE_AUTO switches from mini to normal. 118 */ 119 private static final int AUTO_MINI_LARGEST_SCREEN_WIDTH = 470; 120 121 /** @hide */ 122 @Retention(RetentionPolicy.SOURCE) 123 @IntDef({SIZE_MINI, SIZE_NORMAL, SIZE_AUTO}) 124 public @interface Size {} 125 126 private ColorStateList mBackgroundTint; 127 private PorterDuff.Mode mBackgroundTintMode; 128 129 private int mBorderWidth; 130 private int mRippleColor; 131 private int mSize; 132 private int mImagePadding; 133 private int mMaxImageSize; 134 135 private boolean mCompatPadding; 136 private final Rect mShadowPadding = new Rect(); 137 private final Rect mTouchArea = new Rect(); 138 139 private AppCompatImageHelper mImageHelper; 140 141 private FloatingActionButtonImpl mImpl; 142 143 public FloatingActionButton(Context context) { 144 this(context, null); 145 } 146 147 public FloatingActionButton(Context context, AttributeSet attrs) { 148 this(context, attrs, 0); 149 } 150 151 public FloatingActionButton(Context context, AttributeSet attrs, int defStyleAttr) { 152 super(context, attrs, defStyleAttr); 153 154 ThemeUtils.checkAppCompatTheme(context); 155 156 TypedArray a = context.obtainStyledAttributes(attrs, 157 R.styleable.FloatingActionButton, defStyleAttr, 158 R.style.Widget_Design_FloatingActionButton); 159 mBackgroundTint = a.getColorStateList(R.styleable.FloatingActionButton_backgroundTint); 160 mBackgroundTintMode = ViewUtils.parseTintMode(a.getInt( 161 R.styleable.FloatingActionButton_backgroundTintMode, -1), null); 162 mRippleColor = a.getColor(R.styleable.FloatingActionButton_rippleColor, 0); 163 mSize = a.getInt(R.styleable.FloatingActionButton_fabSize, SIZE_AUTO); 164 mBorderWidth = a.getDimensionPixelSize(R.styleable.FloatingActionButton_borderWidth, 0); 165 final float elevation = a.getDimension(R.styleable.FloatingActionButton_elevation, 0f); 166 final float pressedTranslationZ = a.getDimension( 167 R.styleable.FloatingActionButton_pressedTranslationZ, 0f); 168 mCompatPadding = a.getBoolean(R.styleable.FloatingActionButton_useCompatPadding, false); 169 a.recycle(); 170 171 mImageHelper = new AppCompatImageHelper(this); 172 mImageHelper.loadFromAttributes(attrs, defStyleAttr); 173 174 mMaxImageSize = (int) getResources().getDimension(R.dimen.design_fab_image_size); 175 176 getImpl().setBackgroundDrawable(mBackgroundTint, mBackgroundTintMode, 177 mRippleColor, mBorderWidth); 178 getImpl().setElevation(elevation); 179 getImpl().setPressedTranslationZ(pressedTranslationZ); 180 } 181 182 @Override 183 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 184 final int preferredSize = getSizeDimension(); 185 186 mImagePadding = (preferredSize - mMaxImageSize) / 2; 187 getImpl().updatePadding(); 188 189 final int w = resolveAdjustedSize(preferredSize, widthMeasureSpec); 190 final int h = resolveAdjustedSize(preferredSize, heightMeasureSpec); 191 192 // As we want to stay circular, we set both dimensions to be the 193 // smallest resolved dimension 194 final int d = Math.min(w, h); 195 196 // We add the shadow's padding to the measured dimension 197 setMeasuredDimension( 198 d + mShadowPadding.left + mShadowPadding.right, 199 d + mShadowPadding.top + mShadowPadding.bottom); 200 } 201 202 /** 203 * Returns the ripple color for this button. 204 * 205 * @return the ARGB color used for the ripple 206 * @see #setRippleColor(int) 207 */ 208 @ColorInt 209 public int getRippleColor() { 210 return mRippleColor; 211 } 212 213 /** 214 * Sets the ripple color for this button. 215 * 216 * <p>When running on devices with KitKat or below, we draw this color as a filled circle 217 * rather than a ripple.</p> 218 * 219 * @param color ARGB color to use for the ripple 220 * @attr ref android.support.design.R.styleable#FloatingActionButton_rippleColor 221 * @see #getRippleColor() 222 */ 223 public void setRippleColor(@ColorInt int color) { 224 if (mRippleColor != color) { 225 mRippleColor = color; 226 getImpl().setRippleColor(color); 227 } 228 } 229 230 /** 231 * Returns the tint applied to the background drawable, if specified. 232 * 233 * @return the tint applied to the background drawable 234 * @see #setBackgroundTintList(ColorStateList) 235 */ 236 @Nullable 237 @Override 238 public ColorStateList getBackgroundTintList() { 239 return mBackgroundTint; 240 } 241 242 /** 243 * Applies a tint to the background drawable. Does not modify the current tint 244 * mode, which is {@link PorterDuff.Mode#SRC_IN} by default. 245 * 246 * @param tint the tint to apply, may be {@code null} to clear tint 247 */ 248 public void setBackgroundTintList(@Nullable ColorStateList tint) { 249 if (mBackgroundTint != tint) { 250 mBackgroundTint = tint; 251 getImpl().setBackgroundTintList(tint); 252 } 253 } 254 255 /** 256 * Returns the blending mode used to apply the tint to the background 257 * drawable, if specified. 258 * 259 * @return the blending mode used to apply the tint to the background 260 * drawable 261 * @see #setBackgroundTintMode(PorterDuff.Mode) 262 */ 263 @Nullable 264 @Override 265 public PorterDuff.Mode getBackgroundTintMode() { 266 return mBackgroundTintMode; 267 } 268 269 /** 270 * Specifies the blending mode used to apply the tint specified by 271 * {@link #setBackgroundTintList(ColorStateList)}} to the background 272 * drawable. The default mode is {@link PorterDuff.Mode#SRC_IN}. 273 * 274 * @param tintMode the blending mode used to apply the tint, may be 275 * {@code null} to clear tint 276 */ 277 public void setBackgroundTintMode(@Nullable PorterDuff.Mode tintMode) { 278 if (mBackgroundTintMode != tintMode) { 279 mBackgroundTintMode = tintMode; 280 getImpl().setBackgroundTintMode(tintMode); 281 } 282 } 283 284 @Override 285 public void setBackgroundDrawable(Drawable background) { 286 Log.i(LOG_TAG, "Setting a custom background is not supported."); 287 } 288 289 @Override 290 public void setBackgroundResource(int resid) { 291 Log.i(LOG_TAG, "Setting a custom background is not supported."); 292 } 293 294 @Override 295 public void setBackgroundColor(int color) { 296 Log.i(LOG_TAG, "Setting a custom background is not supported."); 297 } 298 299 @Override 300 public void setImageResource(@DrawableRes int resId) { 301 // Intercept this call and instead retrieve the Drawable via the image helper 302 mImageHelper.setImageResource(resId); 303 } 304 305 /** 306 * Shows the button. 307 * <p>This method will animate the button show if the view has already been laid out.</p> 308 */ 309 public void show() { 310 show(null); 311 } 312 313 /** 314 * Shows the button. 315 * <p>This method will animate the button show if the view has already been laid out.</p> 316 * 317 * @param listener the listener to notify when this view is shown 318 */ 319 public void show(@Nullable final OnVisibilityChangedListener listener) { 320 show(listener, true); 321 } 322 323 private void show(OnVisibilityChangedListener listener, boolean fromUser) { 324 getImpl().show(wrapOnVisibilityChangedListener(listener), fromUser); 325 } 326 327 /** 328 * Hides the button. 329 * <p>This method will animate the button hide if the view has already been laid out.</p> 330 */ 331 public void hide() { 332 hide(null); 333 } 334 335 /** 336 * Hides the button. 337 * <p>This method will animate the button hide if the view has already been laid out.</p> 338 * 339 * @param listener the listener to notify when this view is hidden 340 */ 341 public void hide(@Nullable OnVisibilityChangedListener listener) { 342 hide(listener, true); 343 } 344 345 private void hide(@Nullable OnVisibilityChangedListener listener, boolean fromUser) { 346 getImpl().hide(wrapOnVisibilityChangedListener(listener), fromUser); 347 } 348 349 /** 350 * Set whether FloatingActionButton should add inner padding on platforms Lollipop and after, 351 * to ensure consistent dimensions on all platforms. 352 * 353 * @param useCompatPadding true if FloatingActionButton is adding inner padding on platforms 354 * Lollipop and after, to ensure consistent dimensions on all platforms. 355 * 356 * @attr ref android.support.design.R.styleable#FloatingActionButton_useCompatPadding 357 * @see #getUseCompatPadding() 358 */ 359 public void setUseCompatPadding(boolean useCompatPadding) { 360 if (mCompatPadding != useCompatPadding) { 361 mCompatPadding = useCompatPadding; 362 getImpl().onCompatShadowChanged(); 363 } 364 } 365 366 /** 367 * Returns whether FloatingActionButton will add inner padding on platforms Lollipop and after. 368 * 369 * @return true if FloatingActionButton is adding inner padding on platforms Lollipop and after, 370 * to ensure consistent dimensions on all platforms. 371 * 372 * @attr ref android.support.design.R.styleable#FloatingActionButton_useCompatPadding 373 * @see #setUseCompatPadding(boolean) 374 */ 375 public boolean getUseCompatPadding() { 376 return mCompatPadding; 377 } 378 379 /** 380 * Sets the size of the button. 381 * 382 * <p>The options relate to the options available on the material design specification. 383 * {@link #SIZE_NORMAL} is larger than {@link #SIZE_MINI}. {@link #SIZE_AUTO} will choose 384 * an appropriate size based on the screen size.</p> 385 * 386 * @param size one of {@link #SIZE_NORMAL}, {@link #SIZE_MINI} or {@link #SIZE_AUTO} 387 * 388 * @attr ref android.support.design.R.styleable#FloatingActionButton_fabSize 389 */ 390 public void setSize(@Size int size) { 391 if (size != mSize) { 392 mSize = size; 393 requestLayout(); 394 } 395 } 396 397 /** 398 * Returns the chosen size for this button. 399 * 400 * @return one of {@link #SIZE_NORMAL}, {@link #SIZE_MINI} or {@link #SIZE_AUTO} 401 * @see #setSize(int) 402 */ 403 @Size 404 public int getSize() { 405 return mSize; 406 } 407 408 @Nullable 409 private InternalVisibilityChangedListener wrapOnVisibilityChangedListener( 410 @Nullable final OnVisibilityChangedListener listener) { 411 if (listener == null) { 412 return null; 413 } 414 415 return new InternalVisibilityChangedListener() { 416 @Override 417 public void onShown() { 418 listener.onShown(FloatingActionButton.this); 419 } 420 421 @Override 422 public void onHidden() { 423 listener.onHidden(FloatingActionButton.this); 424 } 425 }; 426 } 427 428 private int getSizeDimension() { 429 return getSizeDimension(mSize); 430 } 431 432 private int getSizeDimension(@Size final int size) { 433 final Resources res = getResources(); 434 switch (size) { 435 case SIZE_AUTO: 436 // If we're set to auto, grab the size from resources and refresh 437 final int width = ConfigurationHelper.getScreenWidthDp(res); 438 final int height = ConfigurationHelper.getScreenHeightDp(res); 439 return Math.max(width, height) < AUTO_MINI_LARGEST_SCREEN_WIDTH 440 ? getSizeDimension(SIZE_MINI) 441 : getSizeDimension(SIZE_NORMAL); 442 case SIZE_MINI: 443 return res.getDimensionPixelSize(R.dimen.design_fab_size_mini); 444 case SIZE_NORMAL: 445 default: 446 return res.getDimensionPixelSize(R.dimen.design_fab_size_normal); 447 } 448 } 449 450 @Override 451 protected void onAttachedToWindow() { 452 super.onAttachedToWindow(); 453 getImpl().onAttachedToWindow(); 454 } 455 456 @Override 457 protected void onDetachedFromWindow() { 458 super.onDetachedFromWindow(); 459 getImpl().onDetachedFromWindow(); 460 } 461 462 @Override 463 protected void drawableStateChanged() { 464 super.drawableStateChanged(); 465 getImpl().onDrawableStateChanged(getDrawableState()); 466 } 467 468 @TargetApi(Build.VERSION_CODES.HONEYCOMB) 469 @Override 470 public void jumpDrawablesToCurrentState() { 471 super.jumpDrawablesToCurrentState(); 472 getImpl().jumpDrawableToCurrentState(); 473 } 474 475 /** 476 * Return in {@code rect} the bounds of the actual floating action button content in view-local 477 * coordinates. This is defined as anything within any visible shadow. 478 * 479 * @return true if this view actually has been laid out and has a content rect, else false. 480 */ 481 public boolean getContentRect(@NonNull Rect rect) { 482 if (ViewCompat.isLaidOut(this)) { 483 rect.set(0, 0, getWidth(), getHeight()); 484 rect.left += mShadowPadding.left; 485 rect.top += mShadowPadding.top; 486 rect.right -= mShadowPadding.right; 487 rect.bottom -= mShadowPadding.bottom; 488 return true; 489 } else { 490 return false; 491 } 492 } 493 494 /** 495 * Returns the FloatingActionButton's background, minus any compatible shadow implementation. 496 */ 497 @NonNull 498 public Drawable getContentBackground() { 499 return getImpl().getContentBackground(); 500 } 501 502 private static int resolveAdjustedSize(int desiredSize, int measureSpec) { 503 int result = desiredSize; 504 int specMode = MeasureSpec.getMode(measureSpec); 505 int specSize = MeasureSpec.getSize(measureSpec); 506 switch (specMode) { 507 case MeasureSpec.UNSPECIFIED: 508 // Parent says we can be as big as we want. Just don't be larger 509 // than max size imposed on ourselves. 510 result = desiredSize; 511 break; 512 case MeasureSpec.AT_MOST: 513 // Parent says we can be as big as we want, up to specSize. 514 // Don't be larger than specSize, and don't be larger than 515 // the max size imposed on ourselves. 516 result = Math.min(desiredSize, specSize); 517 break; 518 case MeasureSpec.EXACTLY: 519 // No choice. Do what we are told. 520 result = specSize; 521 break; 522 } 523 return result; 524 } 525 526 @Override 527 public boolean onTouchEvent(MotionEvent ev) { 528 if(getContentRect(mTouchArea) && !mTouchArea.contains((int) ev.getX(), (int) ev.getY())) { 529 return false; 530 } 531 532 return super.onTouchEvent(ev); 533 } 534 535 /** 536 * Behavior designed for use with {@link FloatingActionButton} instances. Its main function 537 * is to move {@link FloatingActionButton} views so that any displayed {@link Snackbar}s do 538 * not cover them. 539 */ 540 public static class Behavior extends CoordinatorLayout.Behavior<FloatingActionButton> { 541 private static final boolean AUTO_HIDE_DEFAULT = true; 542 543 private Rect mTmpRect; 544 private OnVisibilityChangedListener mInternalAutoHideListener; 545 private boolean mAutoHideEnabled; 546 547 public Behavior() { 548 super(); 549 mAutoHideEnabled = AUTO_HIDE_DEFAULT; 550 } 551 552 public Behavior(Context context, AttributeSet attrs) { 553 super(context, attrs); 554 TypedArray a = context.obtainStyledAttributes(attrs, 555 R.styleable.FloatingActionButton_Behavior_Layout); 556 mAutoHideEnabled = a.getBoolean( 557 R.styleable.FloatingActionButton_Behavior_Layout_behavior_autoHide, 558 AUTO_HIDE_DEFAULT); 559 a.recycle(); 560 } 561 562 /** 563 * Sets whether the associated FloatingActionButton automatically hides when there is 564 * not enough space to be displayed. This works with {@link AppBarLayout} 565 * and {@link BottomSheetBehavior}. 566 * 567 * @attr ref android.support.design.R.styleable#FloatingActionButton_Behavior_Layout_behavior_autoHide 568 * @param autoHide true to enable automatic hiding 569 */ 570 public void setAutoHideEnabled(boolean autoHide) { 571 mAutoHideEnabled = autoHide; 572 } 573 574 /** 575 * Returns whether the associated FloatingActionButton automatically hides when there is 576 * not enough space to be displayed. 577 * 578 * @attr ref android.support.design.R.styleable#FloatingActionButton_Behavior_Layout_behavior_autoHide 579 * @return true if enabled 580 */ 581 public boolean isAutoHideEnabled() { 582 return mAutoHideEnabled; 583 } 584 585 @Override 586 public void onAttachedToLayoutParams(@NonNull CoordinatorLayout.LayoutParams lp) { 587 if (lp.dodgeInsetEdges == Gravity.NO_GRAVITY) { 588 // If the developer hasn't set dodgeInsetEdges, lets set it to BOTTOM so that 589 // we dodge any Snackbars 590 lp.dodgeInsetEdges = Gravity.BOTTOM; 591 } 592 } 593 594 @Override 595 public boolean onDependentViewChanged(CoordinatorLayout parent, FloatingActionButton child, 596 View dependency) { 597 if (dependency instanceof AppBarLayout) { 598 // If we're depending on an AppBarLayout we will show/hide it automatically 599 // if the FAB is anchored to the AppBarLayout 600 updateFabVisibilityForAppBarLayout(parent, (AppBarLayout) dependency, child); 601 } else if (isBottomSheet(dependency)) { 602 updateFabVisibilityForBottomSheet(dependency, child); 603 } 604 return false; 605 } 606 607 private static boolean isBottomSheet(View view) { 608 CoordinatorLayout.LayoutParams lp = 609 (CoordinatorLayout.LayoutParams) view.getLayoutParams(); 610 return lp != null && lp.getBehavior() instanceof BottomSheetBehavior; 611 } 612 613 @VisibleForTesting 614 void setInternalAutoHideListener(OnVisibilityChangedListener listener) { 615 mInternalAutoHideListener = listener; 616 } 617 618 private boolean shouldUpdateVisibility(View dependency, FloatingActionButton child) { 619 final CoordinatorLayout.LayoutParams lp = 620 (CoordinatorLayout.LayoutParams) child.getLayoutParams(); 621 if (!mAutoHideEnabled) { 622 return false; 623 } 624 625 if (lp.getAnchorId() != dependency.getId()) { 626 // The anchor ID doesn't match the dependency, so we won't automatically 627 // show/hide the FAB 628 return false; 629 } 630 631 //noinspection RedundantIfStatement 632 if (child.getUserSetVisibility() != VISIBLE) { 633 // The view isn't set to be visible so skip changing its visibility 634 return false; 635 } 636 637 return true; 638 } 639 640 private boolean updateFabVisibilityForAppBarLayout(CoordinatorLayout parent, 641 AppBarLayout appBarLayout, FloatingActionButton child) { 642 if (!shouldUpdateVisibility(appBarLayout, child)) { 643 return false; 644 } 645 646 if (mTmpRect == null) { 647 mTmpRect = new Rect(); 648 } 649 650 // First, let's get the visible rect of the dependency 651 final Rect rect = mTmpRect; 652 ViewGroupUtils.getDescendantRect(parent, appBarLayout, rect); 653 654 if (rect.bottom <= appBarLayout.getMinimumHeightForVisibleOverlappingContent()) { 655 // If the anchor's bottom is below the seam, we'll animate our FAB out 656 child.hide(mInternalAutoHideListener, false); 657 } else { 658 // Else, we'll animate our FAB back in 659 child.show(mInternalAutoHideListener, false); 660 } 661 return true; 662 } 663 664 private boolean updateFabVisibilityForBottomSheet(View bottomSheet, 665 FloatingActionButton child) { 666 if (!shouldUpdateVisibility(bottomSheet, child)) { 667 return false; 668 } 669 CoordinatorLayout.LayoutParams lp = 670 (CoordinatorLayout.LayoutParams) child.getLayoutParams(); 671 if (bottomSheet.getTop() < child.getHeight() / 2 + lp.topMargin) { 672 child.hide(mInternalAutoHideListener, false); 673 } else { 674 child.show(mInternalAutoHideListener, false); 675 } 676 return true; 677 } 678 679 @Override 680 public boolean onLayoutChild(CoordinatorLayout parent, FloatingActionButton child, 681 int layoutDirection) { 682 // First, let's make sure that the visibility of the FAB is consistent 683 final List<View> dependencies = parent.getDependencies(child); 684 for (int i = 0, count = dependencies.size(); i < count; i++) { 685 final View dependency = dependencies.get(i); 686 if (dependency instanceof AppBarLayout) { 687 if (updateFabVisibilityForAppBarLayout( 688 parent, (AppBarLayout) dependency, child)) { 689 break; 690 } 691 } else if (isBottomSheet(dependency)) { 692 if (updateFabVisibilityForBottomSheet(dependency, child)) { 693 break; 694 } 695 } 696 } 697 // Now let the CoordinatorLayout lay out the FAB 698 parent.onLayoutChild(child, layoutDirection); 699 // Now offset it if needed 700 offsetIfNeeded(parent, child); 701 return true; 702 } 703 704 @Override 705 public boolean getInsetDodgeRect(@NonNull CoordinatorLayout parent, 706 @NonNull FloatingActionButton child, @NonNull Rect rect) { 707 // Since we offset so that any internal shadow padding isn't shown, we need to make 708 // sure that the shadow isn't used for any dodge inset calculations 709 final Rect shadowPadding = child.mShadowPadding; 710 rect.set(child.getLeft() + shadowPadding.left, 711 child.getTop() + shadowPadding.top, 712 child.getRight() - shadowPadding.right, 713 child.getBottom() - shadowPadding.bottom); 714 return true; 715 } 716 717 /** 718 * Pre-Lollipop we use padding so that the shadow has enough space to be drawn. This method 719 * offsets our layout position so that we're positioned correctly if we're on one of 720 * our parent's edges. 721 */ 722 private void offsetIfNeeded(CoordinatorLayout parent, FloatingActionButton fab) { 723 final Rect padding = fab.mShadowPadding; 724 725 if (padding != null && padding.centerX() > 0 && padding.centerY() > 0) { 726 final CoordinatorLayout.LayoutParams lp = 727 (CoordinatorLayout.LayoutParams) fab.getLayoutParams(); 728 729 int offsetTB = 0, offsetLR = 0; 730 731 if (fab.getRight() >= parent.getWidth() - lp.rightMargin) { 732 // If we're on the right edge, shift it the right 733 offsetLR = padding.right; 734 } else if (fab.getLeft() <= lp.leftMargin) { 735 // If we're on the left edge, shift it the left 736 offsetLR = -padding.left; 737 } 738 if (fab.getBottom() >= parent.getHeight() - lp.bottomMargin) { 739 // If we're on the bottom edge, shift it down 740 offsetTB = padding.bottom; 741 } else if (fab.getTop() <= lp.topMargin) { 742 // If we're on the top edge, shift it up 743 offsetTB = -padding.top; 744 } 745 746 if (offsetTB != 0) { 747 ViewCompat.offsetTopAndBottom(fab, offsetTB); 748 } 749 if (offsetLR != 0) { 750 ViewCompat.offsetLeftAndRight(fab, offsetLR); 751 } 752 } 753 } 754 } 755 756 /** 757 * Returns the backward compatible elevation of the FloatingActionButton. 758 * 759 * @return the backward compatible elevation in pixels. 760 * @attr ref android.support.design.R.styleable#FloatingActionButton_elevation 761 * @see #setCompatElevation(float) 762 */ 763 public float getCompatElevation() { 764 return getImpl().getElevation(); 765 } 766 767 /** 768 * Updates the backward compatible elevation of the FloatingActionButton. 769 * 770 * @param elevation The backward compatible elevation in pixels. 771 * @attr ref android.support.design.R.styleable#FloatingActionButton_elevation 772 * @see #getCompatElevation() 773 * @see #setUseCompatPadding(boolean) 774 */ 775 public void setCompatElevation(float elevation) { 776 getImpl().setElevation(elevation); 777 } 778 779 private FloatingActionButtonImpl getImpl() { 780 if (mImpl == null) { 781 mImpl = createImpl(); 782 } 783 return mImpl; 784 } 785 786 private FloatingActionButtonImpl createImpl() { 787 final int sdk = Build.VERSION.SDK_INT; 788 if (sdk >= 21) { 789 return new FloatingActionButtonLollipop(this, new ShadowDelegateImpl(), 790 ViewUtils.DEFAULT_ANIMATOR_CREATOR); 791 } else if (sdk >= 14) { 792 return new FloatingActionButtonIcs(this, new ShadowDelegateImpl(), 793 ViewUtils.DEFAULT_ANIMATOR_CREATOR); 794 } else { 795 return new FloatingActionButtonGingerbread(this, new ShadowDelegateImpl(), 796 ViewUtils.DEFAULT_ANIMATOR_CREATOR); 797 } 798 } 799 800 private class ShadowDelegateImpl implements ShadowViewDelegate { 801 @Override 802 public float getRadius() { 803 return getSizeDimension() / 2f; 804 } 805 806 @Override 807 public void setShadowPadding(int left, int top, int right, int bottom) { 808 mShadowPadding.set(left, top, right, bottom); 809 setPadding(left + mImagePadding, top + mImagePadding, 810 right + mImagePadding, bottom + mImagePadding); 811 } 812 813 @Override 814 public void setBackgroundDrawable(Drawable background) { 815 FloatingActionButton.super.setBackgroundDrawable(background); 816 } 817 818 @Override 819 public boolean isCompatPaddingEnabled() { 820 return mCompatPadding; 821 } 822 } 823} 824