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