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