FloatingActionButton.java revision a419ee1ef9aef8b567f1ccd8c29d01ec7bff4cc9
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.TypedArray; 23import android.graphics.PorterDuff; 24import android.graphics.Rect; 25import android.graphics.drawable.Drawable; 26import android.os.Build; 27import android.support.annotation.ColorInt; 28import android.support.annotation.Nullable; 29import android.support.design.R; 30import android.support.design.widget.FloatingActionButtonImpl.InternalVisibilityChangedListener; 31import android.support.v4.view.ViewCompat; 32import android.util.AttributeSet; 33import android.util.Log; 34import android.view.View; 35import android.widget.ImageView; 36 37import java.util.List; 38 39/** 40 * Floating action buttons are used for a special type of promoted action. They are distinguished 41 * by a circled icon floating above the UI and have special motion behaviors related to morphing, 42 * launching, and the transferring anchor point. 43 * 44 * <p>Floating action buttons come in two sizes: the default and the mini. The size can be 45 * controlled with the {@code fabSize} attribute.</p> 46 * 47 * <p>As this class descends from {@link ImageView}, you can control the icon which is displayed 48 * via {@link #setImageDrawable(Drawable)}.</p> 49 * 50 * <p>The background color of this view defaults to the your theme's {@code colorAccent}. If you 51 * wish to change this at runtime then you can do so via 52 * {@link #setBackgroundTintList(ColorStateList)}.</p> 53 * 54 * @attr ref android.support.design.R.styleable#FloatingActionButton_fabSize 55 */ 56@CoordinatorLayout.DefaultBehavior(FloatingActionButton.Behavior.class) 57public class FloatingActionButton extends VisibilityAwareImageButton { 58 59 private static final String LOG_TAG = "FloatingActionButton"; 60 61 /** 62 * Callback to be invoked when the visibility of a FloatingActionButton changes. 63 */ 64 public abstract static class OnVisibilityChangedListener { 65 /** 66 * Called when a FloatingActionButton has been 67 * {@link #show(OnVisibilityChangedListener) shown}. 68 * 69 * @param fab the FloatingActionButton that was shown. 70 */ 71 public void onShown(FloatingActionButton fab) {} 72 73 /** 74 * Called when a FloatingActionButton has been 75 * {@link #hide(OnVisibilityChangedListener) hidden}. 76 * 77 * @param fab the FloatingActionButton that was hidden. 78 */ 79 public void onHidden(FloatingActionButton fab) {} 80 } 81 82 // These values must match those in the attrs declaration 83 private static final int SIZE_MINI = 1; 84 private static final int SIZE_NORMAL = 0; 85 86 private ColorStateList mBackgroundTint; 87 private PorterDuff.Mode mBackgroundTintMode; 88 89 private int mBorderWidth; 90 private int mRippleColor; 91 private int mSize; 92 private int mContentPadding; 93 94 private final Rect mShadowPadding; 95 96 private final FloatingActionButtonImpl mImpl; 97 98 public FloatingActionButton(Context context) { 99 this(context, null); 100 } 101 102 public FloatingActionButton(Context context, AttributeSet attrs) { 103 this(context, attrs, 0); 104 } 105 106 public FloatingActionButton(Context context, AttributeSet attrs, int defStyleAttr) { 107 super(context, attrs, defStyleAttr); 108 109 ThemeUtils.checkAppCompatTheme(context); 110 111 mShadowPadding = new Rect(); 112 113 TypedArray a = context.obtainStyledAttributes(attrs, 114 R.styleable.FloatingActionButton, defStyleAttr, 115 R.style.Widget_Design_FloatingActionButton); 116 mBackgroundTint = a.getColorStateList(R.styleable.FloatingActionButton_backgroundTint); 117 mBackgroundTintMode = parseTintMode(a.getInt( 118 R.styleable.FloatingActionButton_backgroundTintMode, -1), null); 119 mRippleColor = a.getColor(R.styleable.FloatingActionButton_rippleColor, 0); 120 mSize = a.getInt(R.styleable.FloatingActionButton_fabSize, SIZE_NORMAL); 121 mBorderWidth = a.getDimensionPixelSize(R.styleable.FloatingActionButton_borderWidth, 0); 122 final float elevation = a.getDimension(R.styleable.FloatingActionButton_elevation, 0f); 123 final float pressedTranslationZ = a.getDimension( 124 R.styleable.FloatingActionButton_pressedTranslationZ, 0f); 125 a.recycle(); 126 127 final ShadowViewDelegate delegate = new ShadowViewDelegate() { 128 @Override 129 public float getRadius() { 130 return getSizeDimension() / 2f; 131 } 132 133 @Override 134 public void setShadowPadding(int left, int top, int right, int bottom) { 135 mShadowPadding.set(left, top, right, bottom); 136 137 setPadding(left + mContentPadding, top + mContentPadding, 138 right + mContentPadding, bottom + mContentPadding); 139 } 140 141 @Override 142 public void setBackgroundDrawable(Drawable background) { 143 FloatingActionButton.super.setBackgroundDrawable(background); 144 } 145 }; 146 147 final int sdk = Build.VERSION.SDK_INT; 148 if (sdk >= 21) { 149 mImpl = new FloatingActionButtonLollipop(this, delegate); 150 } else if (sdk >= 14) { 151 mImpl = new FloatingActionButtonIcs(this, delegate); 152 } else { 153 mImpl = new FloatingActionButtonEclairMr1(this, delegate); 154 } 155 156 final int maxContentSize = (int) getResources().getDimension( 157 R.dimen.design_fab_content_size); 158 mContentPadding = (getSizeDimension() - maxContentSize) / 2; 159 160 mImpl.setBackgroundDrawable(mBackgroundTint, mBackgroundTintMode, 161 mRippleColor, mBorderWidth); 162 mImpl.setElevation(elevation); 163 mImpl.setPressedTranslationZ(pressedTranslationZ); 164 } 165 166 @Override 167 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 168 final int preferredSize = getSizeDimension(); 169 170 final int w = resolveAdjustedSize(preferredSize, widthMeasureSpec); 171 final int h = resolveAdjustedSize(preferredSize, heightMeasureSpec); 172 173 // As we want to stay circular, we set both dimensions to be the 174 // smallest resolved dimension 175 final int d = Math.min(w, h); 176 177 // We add the shadow's padding to the measured dimension 178 setMeasuredDimension( 179 d + mShadowPadding.left + mShadowPadding.right, 180 d + mShadowPadding.top + mShadowPadding.bottom); 181 } 182 183 /** 184 * Set the ripple color for this {@link FloatingActionButton}. 185 * <p> 186 * When running on devices with KitKat or below, we draw a fill rather than a ripple. 187 * 188 * @param color ARGB color to use for the ripple. 189 */ 190 public void setRippleColor(@ColorInt int color) { 191 if (mRippleColor != color) { 192 mRippleColor = color; 193 mImpl.setRippleColor(color); 194 } 195 } 196 197 /** 198 * Return the tint applied to the background drawable, if specified. 199 * 200 * @return the tint applied to the background drawable 201 * @see #setBackgroundTintList(ColorStateList) 202 */ 203 @Nullable 204 @Override 205 public ColorStateList getBackgroundTintList() { 206 return mBackgroundTint; 207 } 208 209 /** 210 * Applies a tint to the background drawable. Does not modify the current tint 211 * mode, which is {@link PorterDuff.Mode#SRC_IN} by default. 212 * 213 * @param tint the tint to apply, may be {@code null} to clear tint 214 */ 215 public void setBackgroundTintList(@Nullable ColorStateList tint) { 216 if (mBackgroundTint != tint) { 217 mBackgroundTint = tint; 218 mImpl.setBackgroundTintList(tint); 219 } 220 } 221 222 223 /** 224 * Return the blending mode used to apply the tint to the background 225 * drawable, if specified. 226 * 227 * @return the blending mode used to apply the tint to the background 228 * drawable 229 * @see #setBackgroundTintMode(PorterDuff.Mode) 230 */ 231 @Nullable 232 @Override 233 public PorterDuff.Mode getBackgroundTintMode() { 234 return mBackgroundTintMode; 235 } 236 237 /** 238 * Specifies the blending mode used to apply the tint specified by 239 * {@link #setBackgroundTintList(ColorStateList)}} to the background 240 * drawable. The default mode is {@link PorterDuff.Mode#SRC_IN}. 241 * 242 * @param tintMode the blending mode used to apply the tint, may be 243 * {@code null} to clear tint 244 */ 245 public void setBackgroundTintMode(@Nullable PorterDuff.Mode tintMode) { 246 if (mBackgroundTintMode != tintMode) { 247 mBackgroundTintMode = tintMode; 248 mImpl.setBackgroundTintMode(tintMode); 249 } 250 } 251 252 @Override 253 public void setBackgroundDrawable(Drawable background) { 254 Log.i(LOG_TAG, "Setting a custom background is not supported."); 255 } 256 257 @Override 258 public void setBackgroundResource(int resid) { 259 Log.i(LOG_TAG, "Setting a custom background is not supported."); 260 } 261 262 @Override 263 public void setBackgroundColor(int color) { 264 Log.i(LOG_TAG, "Setting a custom background is not supported."); 265 } 266 267 /** 268 * Shows the button. 269 * <p>This method will animate the button show if the view has already been laid out.</p> 270 */ 271 public void show() { 272 show(null); 273 } 274 275 /** 276 * Shows the button. 277 * <p>This method will animate the button show if the view has already been laid out.</p> 278 * 279 * @param listener the listener to notify when this view is shown 280 */ 281 public void show(@Nullable final OnVisibilityChangedListener listener) { 282 show(listener, true); 283 } 284 285 private void show(OnVisibilityChangedListener listener, boolean fromUser) { 286 mImpl.show(wrapOnVisibilityChangedListener(listener), fromUser); 287 } 288 289 /** 290 * Hides the button. 291 * <p>This method will animate the button hide if the view has already been laid out.</p> 292 */ 293 public void hide() { 294 hide(null); 295 } 296 297 /** 298 * Hides the button. 299 * <p>This method will animate the button hide if the view has already been laid out.</p> 300 * 301 * @param listener the listener to notify when this view is hidden 302 */ 303 public void hide(@Nullable OnVisibilityChangedListener listener) { 304 hide(listener, true); 305 } 306 307 private void hide(@Nullable OnVisibilityChangedListener listener, boolean fromUser) { 308 mImpl.hide(wrapOnVisibilityChangedListener(listener), fromUser); 309 } 310 311 @Nullable 312 private InternalVisibilityChangedListener wrapOnVisibilityChangedListener( 313 @Nullable final OnVisibilityChangedListener listener) { 314 if (listener == null) { 315 return null; 316 } 317 318 return new InternalVisibilityChangedListener() { 319 @Override 320 public void onShown() { 321 listener.onShown(FloatingActionButton.this); 322 } 323 324 @Override 325 public void onHidden() { 326 listener.onHidden(FloatingActionButton.this); 327 } 328 }; 329 } 330 331 final int getSizeDimension() { 332 switch (mSize) { 333 case SIZE_MINI: 334 return getResources().getDimensionPixelSize(R.dimen.design_fab_size_mini); 335 case SIZE_NORMAL: 336 default: 337 return getResources().getDimensionPixelSize(R.dimen.design_fab_size_normal); 338 } 339 } 340 341 @Override 342 protected void onAttachedToWindow() { 343 super.onAttachedToWindow(); 344 mImpl.onAttachedToWindow(); 345 } 346 347 @Override 348 protected void onDetachedFromWindow() { 349 super.onDetachedFromWindow(); 350 mImpl.onDetachedFromWindow(); 351 } 352 353 @Override 354 protected void drawableStateChanged() { 355 super.drawableStateChanged(); 356 mImpl.onDrawableStateChanged(getDrawableState()); 357 } 358 359 @TargetApi(Build.VERSION_CODES.HONEYCOMB) 360 @Override 361 public void jumpDrawablesToCurrentState() { 362 super.jumpDrawablesToCurrentState(); 363 mImpl.jumpDrawableToCurrentState(); 364 } 365 366 private static int resolveAdjustedSize(int desiredSize, int measureSpec) { 367 int result = desiredSize; 368 int specMode = MeasureSpec.getMode(measureSpec); 369 int specSize = MeasureSpec.getSize(measureSpec); 370 switch (specMode) { 371 case MeasureSpec.UNSPECIFIED: 372 // Parent says we can be as big as we want. Just don't be larger 373 // than max size imposed on ourselves. 374 result = desiredSize; 375 break; 376 case MeasureSpec.AT_MOST: 377 // Parent says we can be as big as we want, up to specSize. 378 // Don't be larger than specSize, and don't be larger than 379 // the max size imposed on ourselves. 380 result = Math.min(desiredSize, specSize); 381 break; 382 case MeasureSpec.EXACTLY: 383 // No choice. Do what we are told. 384 result = specSize; 385 break; 386 } 387 return result; 388 } 389 390 static PorterDuff.Mode parseTintMode(int value, PorterDuff.Mode defaultMode) { 391 switch (value) { 392 case 3: 393 return PorterDuff.Mode.SRC_OVER; 394 case 5: 395 return PorterDuff.Mode.SRC_IN; 396 case 9: 397 return PorterDuff.Mode.SRC_ATOP; 398 case 14: 399 return PorterDuff.Mode.MULTIPLY; 400 case 15: 401 return PorterDuff.Mode.SCREEN; 402 default: 403 return defaultMode; 404 } 405 } 406 407 /** 408 * Behavior designed for use with {@link FloatingActionButton} instances. It's main function 409 * is to move {@link FloatingActionButton} views so that any displayed {@link Snackbar}s do 410 * not cover them. 411 */ 412 public static class Behavior extends CoordinatorLayout.Behavior<FloatingActionButton> { 413 // We only support the FAB <> Snackbar shift movement on Honeycomb and above. This is 414 // because we can use view translation properties which greatly simplifies the code. 415 private static final boolean SNACKBAR_BEHAVIOR_ENABLED = Build.VERSION.SDK_INT >= 11; 416 417 private ValueAnimatorCompat mFabTranslationYAnimator; 418 private float mFabTranslationY; 419 private Rect mTmpRect; 420 421 @Override 422 public boolean layoutDependsOn(CoordinatorLayout parent, 423 FloatingActionButton child, View dependency) { 424 // We're dependent on all SnackbarLayouts (if enabled) 425 return SNACKBAR_BEHAVIOR_ENABLED && dependency instanceof Snackbar.SnackbarLayout; 426 } 427 428 @Override 429 public boolean onDependentViewChanged(CoordinatorLayout parent, FloatingActionButton child, 430 View dependency) { 431 if (dependency instanceof Snackbar.SnackbarLayout) { 432 updateFabTranslationForSnackbar(parent, child, dependency); 433 } else if (dependency instanceof AppBarLayout) { 434 // If we're depending on an AppBarLayout we will show/hide it automatically 435 // if the FAB is anchored to the AppBarLayout 436 updateFabVisibility(parent, (AppBarLayout) dependency, child); 437 } 438 return false; 439 } 440 441 private boolean updateFabVisibility(CoordinatorLayout parent, 442 AppBarLayout appBarLayout, FloatingActionButton child) { 443 final CoordinatorLayout.LayoutParams lp = 444 (CoordinatorLayout.LayoutParams) child.getLayoutParams(); 445 if (lp.getAnchorId() != appBarLayout.getId()) { 446 // The anchor ID doesn't match the dependency, so we won't automatically 447 // show/hide the FAB 448 return false; 449 } 450 451 if (child.getUserSetVisibility() != VISIBLE) { 452 // The view isn't set to be visible so skip changing it's visibility 453 return false; 454 } 455 456 if (mTmpRect == null) { 457 mTmpRect = new Rect(); 458 } 459 460 // First, let's get the visible rect of the dependency 461 final Rect rect = mTmpRect; 462 ViewGroupUtils.getDescendantRect(parent, appBarLayout, rect); 463 464 if (rect.bottom <= appBarLayout.getMinimumHeightForVisibleOverlappingContent()) { 465 // If the anchor's bottom is below the seam, we'll animate our FAB out 466 child.hide(null, false); 467 } else { 468 // Else, we'll animate our FAB back in 469 child.show(null, false); 470 } 471 return true; 472 } 473 474 private void updateFabTranslationForSnackbar(CoordinatorLayout parent, 475 final FloatingActionButton fab, View snackbar) { 476 if (fab.getVisibility() != View.VISIBLE) { 477 return; 478 } 479 480 final float targetTransY = getFabTranslationYForSnackbar(parent, fab); 481 if (mFabTranslationY == targetTransY) { 482 // We're already at (or currently animating to) the target value, return... 483 return; 484 } 485 486 final float currentTransY = ViewCompat.getTranslationY(fab); 487 488 // Make sure that any current animation is cancelled 489 if (mFabTranslationYAnimator != null && mFabTranslationYAnimator.isRunning()) { 490 mFabTranslationYAnimator.cancel(); 491 } 492 493 if (Math.abs(currentTransY - targetTransY) > (fab.getHeight() * 0.667f)) { 494 // If the FAB will be travelling by more than 2/3 of it's height, let's animate 495 // it instead 496 if (mFabTranslationYAnimator == null) { 497 mFabTranslationYAnimator = ViewUtils.createAnimator(); 498 mFabTranslationYAnimator.setInterpolator( 499 AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR); 500 mFabTranslationYAnimator.setUpdateListener( 501 new ValueAnimatorCompat.AnimatorUpdateListener() { 502 @Override 503 public void onAnimationUpdate(ValueAnimatorCompat animator) { 504 ViewCompat.setTranslationY(fab, 505 animator.getAnimatedFloatValue()); 506 } 507 }); 508 } 509 mFabTranslationYAnimator.setFloatValues(currentTransY, targetTransY); 510 mFabTranslationYAnimator.start(); 511 } else { 512 // Now update the translation Y 513 ViewCompat.setTranslationY(fab, targetTransY); 514 } 515 516 mFabTranslationY = targetTransY; 517 } 518 519 private float getFabTranslationYForSnackbar(CoordinatorLayout parent, 520 FloatingActionButton fab) { 521 float minOffset = 0; 522 final List<View> dependencies = parent.getDependencies(fab); 523 for (int i = 0, z = dependencies.size(); i < z; i++) { 524 final View view = dependencies.get(i); 525 if (view instanceof Snackbar.SnackbarLayout && parent.doViewsOverlap(fab, view)) { 526 minOffset = Math.min(minOffset, 527 ViewCompat.getTranslationY(view) - view.getHeight()); 528 } 529 } 530 531 return minOffset; 532 } 533 534 @Override 535 public boolean onLayoutChild(CoordinatorLayout parent, FloatingActionButton child, 536 int layoutDirection) { 537 // First, lets make sure that the visibility of the FAB is consistent 538 final List<View> dependencies = parent.getDependencies(child); 539 for (int i = 0, count = dependencies.size(); i < count; i++) { 540 final View dependency = dependencies.get(i); 541 if (dependency instanceof AppBarLayout 542 && updateFabVisibility(parent, (AppBarLayout) dependency, child)) { 543 break; 544 } 545 } 546 // Now let the CoordinatorLayout lay out the FAB 547 parent.onLayoutChild(child, layoutDirection); 548 // Now offset it if needed 549 offsetIfNeeded(parent, child); 550 return true; 551 } 552 553 /** 554 * Pre-Lollipop we use padding so that the shadow has enough space to be drawn. This method 555 * offsets our layout position so that we're positioned correctly if we're on one of 556 * our parent's edges. 557 */ 558 private void offsetIfNeeded(CoordinatorLayout parent, FloatingActionButton fab) { 559 final Rect padding = fab.mShadowPadding; 560 561 if (padding != null && padding.centerX() > 0 && padding.centerY() > 0) { 562 final CoordinatorLayout.LayoutParams lp = 563 (CoordinatorLayout.LayoutParams) fab.getLayoutParams(); 564 565 int offsetTB = 0, offsetLR = 0; 566 567 if (fab.getRight() >= parent.getWidth() - lp.rightMargin) { 568 // If we're on the left edge, shift it the right 569 offsetLR = padding.right; 570 } else if (fab.getLeft() <= lp.leftMargin) { 571 // If we're on the left edge, shift it the left 572 offsetLR = -padding.left; 573 } 574 if (fab.getBottom() >= parent.getBottom() - lp.bottomMargin) { 575 // If we're on the bottom edge, shift it down 576 offsetTB = padding.bottom; 577 } else if (fab.getTop() <= lp.topMargin) { 578 // If we're on the top edge, shift it up 579 offsetTB = -padding.top; 580 } 581 582 fab.offsetTopAndBottom(offsetTB); 583 fab.offsetLeftAndRight(offsetLR); 584 } 585 } 586 } 587} 588