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