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