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