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 mImpl.show(); 243 } 244 245 /** 246 * Hides the button. 247 * <p>This method will animate the button hide if the view has already been laid out.</p> 248 */ 249 public void hide() { 250 mImpl.hide(); 251 } 252 253 final int getSizeDimension() { 254 switch (mSize) { 255 case SIZE_MINI: 256 return getResources().getDimensionPixelSize(R.dimen.design_fab_size_mini); 257 case SIZE_NORMAL: 258 default: 259 return getResources().getDimensionPixelSize(R.dimen.design_fab_size_normal); 260 } 261 } 262 263 @Override 264 protected void drawableStateChanged() { 265 super.drawableStateChanged(); 266 mImpl.onDrawableStateChanged(getDrawableState()); 267 } 268 269 @TargetApi(Build.VERSION_CODES.HONEYCOMB) 270 @Override 271 public void jumpDrawablesToCurrentState() { 272 super.jumpDrawablesToCurrentState(); 273 mImpl.jumpDrawableToCurrentState(); 274 } 275 276 private static int resolveAdjustedSize(int desiredSize, int measureSpec) { 277 int result = desiredSize; 278 int specMode = MeasureSpec.getMode(measureSpec); 279 int specSize = MeasureSpec.getSize(measureSpec); 280 switch (specMode) { 281 case MeasureSpec.UNSPECIFIED: 282 // Parent says we can be as big as we want. Just don't be larger 283 // than max size imposed on ourselves. 284 result = desiredSize; 285 break; 286 case MeasureSpec.AT_MOST: 287 // Parent says we can be as big as we want, up to specSize. 288 // Don't be larger than specSize, and don't be larger than 289 // the max size imposed on ourselves. 290 result = Math.min(desiredSize, specSize); 291 break; 292 case MeasureSpec.EXACTLY: 293 // No choice. Do what we are told. 294 result = specSize; 295 break; 296 } 297 return result; 298 } 299 300 static PorterDuff.Mode parseTintMode(int value, PorterDuff.Mode defaultMode) { 301 switch (value) { 302 case 3: 303 return PorterDuff.Mode.SRC_OVER; 304 case 5: 305 return PorterDuff.Mode.SRC_IN; 306 case 9: 307 return PorterDuff.Mode.SRC_ATOP; 308 case 14: 309 return PorterDuff.Mode.MULTIPLY; 310 case 15: 311 return PorterDuff.Mode.SCREEN; 312 default: 313 return defaultMode; 314 } 315 } 316 317 /** 318 * Behavior designed for use with {@link FloatingActionButton} instances. It's main function 319 * is to move {@link FloatingActionButton} views so that any displayed {@link Snackbar}s do 320 * not cover them. 321 */ 322 public static class Behavior extends CoordinatorLayout.Behavior<FloatingActionButton> { 323 // We only support the FAB <> Snackbar shift movement on Honeycomb and above. This is 324 // because we can use view translation properties which greatly simplifies the code. 325 private static final boolean SNACKBAR_BEHAVIOR_ENABLED = Build.VERSION.SDK_INT >= 11; 326 327 private Rect mTmpRect; 328 329 @Override 330 public boolean layoutDependsOn(CoordinatorLayout parent, 331 FloatingActionButton child, View dependency) { 332 // We're dependent on all SnackbarLayouts (if enabled) 333 return SNACKBAR_BEHAVIOR_ENABLED && dependency instanceof Snackbar.SnackbarLayout; 334 } 335 336 @Override 337 public boolean onDependentViewChanged(CoordinatorLayout parent, FloatingActionButton child, 338 View dependency) { 339 if (dependency instanceof Snackbar.SnackbarLayout) { 340 updateFabTranslationForSnackbar(parent, child, dependency); 341 } else if (dependency instanceof AppBarLayout) { 342 // If we're depending on an AppBarLayout we will show/hide it automatically 343 // if the FAB is anchored to the AppBarLayout 344 updateFabVisibility(parent, (AppBarLayout) dependency, child); 345 } 346 return false; 347 } 348 349 @Override 350 public void onDependentViewRemoved(CoordinatorLayout parent, FloatingActionButton child, 351 View dependency) { 352 if (dependency instanceof Snackbar.SnackbarLayout) { 353 // If the removed view is a SnackbarLayout, we will animate back to our normal 354 // position 355 if (ViewCompat.getTranslationY(child) != 0f) { 356 ViewCompat.animate(child) 357 .translationY(0f) 358 .scaleX(1f) 359 .scaleY(1f) 360 .alpha(1f) 361 .setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR) 362 .setListener(null); 363 } 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 ViewCompat.setTranslationY(fab, translationY); 403 } 404 405 private float getFabTranslationYForSnackbar(CoordinatorLayout parent, 406 FloatingActionButton fab) { 407 float minOffset = 0; 408 final List<View> dependencies = parent.getDependencies(fab); 409 for (int i = 0, z = dependencies.size(); i < z; i++) { 410 final View view = dependencies.get(i); 411 if (view instanceof Snackbar.SnackbarLayout && parent.doViewsOverlap(fab, view)) { 412 minOffset = Math.min(minOffset, 413 ViewCompat.getTranslationY(view) - view.getHeight()); 414 } 415 } 416 417 return minOffset; 418 } 419 420 @Override 421 public boolean onLayoutChild(CoordinatorLayout parent, FloatingActionButton child, 422 int layoutDirection) { 423 // First, lets make sure that the visibility of the FAB is consistent 424 final List<View> dependencies = parent.getDependencies(child); 425 for (int i = 0, count = dependencies.size(); i < count; i++) { 426 final View dependency = dependencies.get(i); 427 if (dependency instanceof AppBarLayout 428 && updateFabVisibility(parent, (AppBarLayout) dependency, child)) { 429 break; 430 } 431 } 432 // Now let the CoordinatorLayout lay out the FAB 433 parent.onLayoutChild(child, layoutDirection); 434 // Now offset it if needed 435 offsetIfNeeded(parent, child); 436 return true; 437 } 438 439 /** 440 * Pre-Lollipop we use padding so that the shadow has enough space to be drawn. This method 441 * offsets our layout position so that we're positioned correctly if we're on one of 442 * our parent's edges. 443 */ 444 private void offsetIfNeeded(CoordinatorLayout parent, FloatingActionButton fab) { 445 final Rect padding = fab.mShadowPadding; 446 447 if (padding != null && padding.centerX() > 0 && padding.centerY() > 0) { 448 final CoordinatorLayout.LayoutParams lp = 449 (CoordinatorLayout.LayoutParams) fab.getLayoutParams(); 450 451 int offsetTB = 0, offsetLR = 0; 452 453 if (fab.getRight() >= parent.getWidth() - lp.rightMargin) { 454 // If we're on the left edge, shift it the right 455 offsetLR = padding.right; 456 } else if (fab.getLeft() <= lp.leftMargin) { 457 // If we're on the left edge, shift it the left 458 offsetLR = -padding.left; 459 } 460 if (fab.getBottom() >= parent.getBottom() - lp.bottomMargin) { 461 // If we're on the bottom edge, shift it down 462 offsetTB = padding.bottom; 463 } else if (fab.getTop() <= lp.topMargin) { 464 // If we're on the top edge, shift it up 465 offsetTB = -padding.top; 466 } 467 468 fab.offsetTopAndBottom(offsetTB); 469 fab.offsetLeftAndRight(offsetLR); 470 } 471 } 472 } 473} 474