FloatingActionButton.java revision e882ef3492de3d2bb687b454e08b870b06d8f4e2
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.support.v4.view.ViewPropertyAnimatorListener; 31import android.util.AttributeSet; 32import android.view.View; 33import android.view.animation.Animation; 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 if (Build.VERSION.SDK_INT >= 21) { 123 mImpl = new FloatingActionButtonLollipop(this, delegate); 124 } else { 125 mImpl = new FloatingActionButtonEclairMr1(this, delegate); 126 } 127 128 final int maxContentSize = (int) getResources().getDimension(R.dimen.fab_content_size); 129 mContentPadding = (getSizeDimension() - maxContentSize) / 2; 130 131 mImpl.setBackgroundDrawable(background, mBackgroundTint, 132 mBackgroundTintMode, mRippleColor, mBorderWidth); 133 mImpl.setElevation(elevation); 134 mImpl.setPressedTranslationZ(pressedTranslationZ); 135 136 setClickable(true); 137 } 138 139 @Override 140 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 141 final int preferredSize = getSizeDimension(); 142 143 final int w = resolveAdjustedSize(preferredSize, widthMeasureSpec); 144 final int h = resolveAdjustedSize(preferredSize, heightMeasureSpec); 145 146 // As we want to stay circular, we set both dimensions to be the 147 // smallest resolved dimension 148 final int d = Math.min(w, h); 149 150 // We add the shadow's padding to the measured dimension 151 setMeasuredDimension( 152 d + mShadowPadding.left + mShadowPadding.right, 153 d + mShadowPadding.top + mShadowPadding.bottom); 154 } 155 156 /** 157 * Set the ripple color for this {@link FloatingActionButton}. 158 * <p> 159 * When running on devices with KitKat or below, we draw a fill rather than a ripple. 160 * 161 * @param color ARGB color to use for the ripple. 162 */ 163 public void setRippleColor(int color) { 164 if (mRippleColor != color) { 165 mRippleColor = color; 166 mImpl.setRippleColor(color); 167 } 168 } 169 170 /** 171 * Return the tint applied to the background drawable, if specified. 172 * 173 * @return the tint applied to the background drawable 174 * @see #setBackgroundTintList(ColorStateList) 175 */ 176 @Nullable 177 @Override 178 public ColorStateList getBackgroundTintList() { 179 return mBackgroundTint; 180 } 181 182 /** 183 * Applies a tint to the background drawable. Does not modify the current tint 184 * mode, which is {@link PorterDuff.Mode#SRC_IN} by default. 185 * 186 * @param tint the tint to apply, may be {@code null} to clear tint 187 */ 188 public void setBackgroundTintList(@Nullable ColorStateList tint) { 189 mImpl.setBackgroundTintList(tint); 190 } 191 192 193 /** 194 * Return the blending mode used to apply the tint to the background 195 * drawable, if specified. 196 * 197 * @return the blending mode used to apply the tint to the background 198 * drawable 199 * @see #setBackgroundTintMode(PorterDuff.Mode) 200 */ 201 @Nullable 202 @Override 203 public PorterDuff.Mode getBackgroundTintMode() { 204 return mBackgroundTintMode; 205 } 206 207 /** 208 * Specifies the blending mode used to apply the tint specified by 209 * {@link #setBackgroundTintList(ColorStateList)}} to the background 210 * drawable. The default mode is {@link PorterDuff.Mode#SRC_IN}. 211 * 212 * @param tintMode the blending mode used to apply the tint, may be 213 * {@code null} to clear tint 214 */ 215 public void setBackgroundTintMode(@Nullable PorterDuff.Mode tintMode) { 216 mImpl.setBackgroundTintMode(tintMode); 217 } 218 219 @Override 220 public void setBackgroundDrawable(Drawable background) { 221 if (mImpl != null) { 222 mImpl.setBackgroundDrawable( 223 background, mBackgroundTint, mBackgroundTintMode, mRippleColor, mBorderWidth); 224 } 225 } 226 227 final int getSizeDimension() { 228 switch (mSize) { 229 case SIZE_MINI: 230 return getResources().getDimensionPixelSize(R.dimen.fab_size_mini); 231 case SIZE_NORMAL: 232 default: 233 return getResources().getDimensionPixelSize(R.dimen.fab_size_normal); 234 } 235 } 236 237 @Override 238 protected void drawableStateChanged() { 239 super.drawableStateChanged(); 240 mImpl.onDrawableStateChanged(getDrawableState()); 241 } 242 243 @TargetApi(Build.VERSION_CODES.HONEYCOMB) 244 @Override 245 public void jumpDrawablesToCurrentState() { 246 super.jumpDrawablesToCurrentState(); 247 mImpl.jumpDrawableToCurrentState(); 248 } 249 250 private static int resolveAdjustedSize(int desiredSize, int measureSpec) { 251 int result = desiredSize; 252 int specMode = MeasureSpec.getMode(measureSpec); 253 int specSize = MeasureSpec.getSize(measureSpec); 254 switch (specMode) { 255 case MeasureSpec.UNSPECIFIED: 256 // Parent says we can be as big as we want. Just don't be larger 257 // than max size imposed on ourselves. 258 result = desiredSize; 259 break; 260 case MeasureSpec.AT_MOST: 261 // Parent says we can be as big as we want, up to specSize. 262 // Don't be larger than specSize, and don't be larger than 263 // the max size imposed on ourselves. 264 result = Math.min(desiredSize, specSize); 265 break; 266 case MeasureSpec.EXACTLY: 267 // No choice. Do what we are told. 268 result = specSize; 269 break; 270 } 271 return result; 272 } 273 274 static PorterDuff.Mode parseTintMode(int value, PorterDuff.Mode defaultMode) { 275 switch (value) { 276 case 3: 277 return PorterDuff.Mode.SRC_OVER; 278 case 5: 279 return PorterDuff.Mode.SRC_IN; 280 case 9: 281 return PorterDuff.Mode.SRC_ATOP; 282 case 14: 283 return PorterDuff.Mode.MULTIPLY; 284 case 15: 285 return PorterDuff.Mode.SCREEN; 286 default: 287 return defaultMode; 288 } 289 } 290 291 /** 292 * Behavior designed for use with {@link FloatingActionButton} instances. It's main function 293 * is to move {@link FloatingActionButton} views so that any displayed {@link Snackbar}s do 294 * not cover them. 295 */ 296 public static class Behavior extends CoordinatorLayout.Behavior<FloatingActionButton> { 297 // We only support the FAB <> Snackbar shift movement on Honeycomb and above. This is 298 // because we can use view translation properties which greatly simplifies the code. 299 private static final boolean SNACKBAR_BEHAVIOR_ENABLED = Build.VERSION.SDK_INT >= 11; 300 301 private Rect mTmpRect; 302 private boolean mIsAnimatingOut; 303 private float mTranslationY; 304 305 @Override 306 public boolean layoutDependsOn(CoordinatorLayout parent, 307 FloatingActionButton child, 308 View dependency) { 309 // We're dependent on all SnackbarLayouts (if enabled) 310 return SNACKBAR_BEHAVIOR_ENABLED && dependency instanceof Snackbar.SnackbarLayout; 311 } 312 313 @Override 314 public boolean onDependentViewChanged(CoordinatorLayout parent, FloatingActionButton child, 315 View dependency) { 316 if (dependency instanceof Snackbar.SnackbarLayout) { 317 updateFabTranslationForSnackbar(parent, child, dependency); 318 } else if (dependency instanceof AppBarLayout) { 319 final AppBarLayout appBarLayout = (AppBarLayout) dependency; 320 if (mTmpRect == null) { 321 mTmpRect = new Rect(); 322 } 323 324 // First, let's get the visible rect of the dependency 325 final Rect rect = mTmpRect; 326 ViewGroupUtils.getDescendantRect(parent, dependency, rect); 327 328 if (rect.bottom <= appBarLayout.getMinimumHeightForVisibleOverlappingContent()) { 329 // If the anchor's bottom is below the seam, we'll animate our FAB out 330 if (!mIsAnimatingOut && child.getVisibility() == View.VISIBLE) { 331 animateOut(child); 332 } 333 } else { 334 // Else, we'll animate our FAB back in 335 if (child.getVisibility() != View.VISIBLE) { 336 animateIn(child); 337 } 338 } 339 } 340 return false; 341 } 342 343 private void updateFabTranslationForSnackbar(CoordinatorLayout parent, 344 FloatingActionButton fab, View snackbar) { 345 final float translationY = getFabTranslationYForSnackbar(parent, fab); 346 if (translationY != mTranslationY) { 347 // First, cancel any current animation 348 ViewCompat.animate(fab).cancel(); 349 350 if (Math.abs(translationY - mTranslationY) == snackbar.getHeight()) { 351 // If we're travelling by the height of the Snackbar then we probably need to 352 // animate to the value 353 ViewCompat.animate(fab) 354 .translationY(translationY) 355 .setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR) 356 .setListener(null); 357 } else { 358 // Else we'll set use setTranslationY 359 ViewCompat.setTranslationY(fab, translationY); 360 } 361 mTranslationY = translationY; 362 } 363 } 364 365 private float getFabTranslationYForSnackbar(CoordinatorLayout parent, 366 FloatingActionButton fab) { 367 float minOffset = 0; 368 final List<View> dependencies = parent.getDependencies(fab); 369 for (int i = 0, z = dependencies.size(); i < z; i++) { 370 final View view = dependencies.get(i); 371 if (view instanceof Snackbar.SnackbarLayout && parent.doViewsOverlap(fab, view)) { 372 minOffset = Math.min(minOffset, 373 ViewCompat.getTranslationY(view) - view.getHeight()); 374 } 375 } 376 377 return minOffset; 378 } 379 380 private void animateIn(FloatingActionButton button) { 381 button.setVisibility(View.VISIBLE); 382 383 if (Build.VERSION.SDK_INT >= 14) { 384 ViewCompat.animate(button) 385 .scaleX(1f) 386 .scaleY(1f) 387 .alpha(1f) 388 .setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR) 389 .withLayer() 390 .setListener(null) 391 .start(); 392 } else { 393 Animation anim = android.view.animation.AnimationUtils.loadAnimation( 394 button.getContext(), R.anim.fab_in); 395 anim.setDuration(200); 396 anim.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR); 397 button.startAnimation(anim); 398 } 399 } 400 401 @Override 402 public boolean onLayoutChild(CoordinatorLayout parent, FloatingActionButton child, 403 int layoutDirection) { 404 // Let the CoordinatorLayout lay out the FAB 405 parent.onLayoutChild(child, layoutDirection); 406 // Now offset it if needed 407 offsetIfNeeded(parent, child); 408 return true; 409 } 410 411 private void animateOut(final FloatingActionButton button) { 412 if (Build.VERSION.SDK_INT >= 14) { 413 ViewCompat.animate(button) 414 .scaleX(0f) 415 .scaleY(0f) 416 .alpha(0f) 417 .setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR) 418 .withLayer() 419 .setListener(new ViewPropertyAnimatorListener() { 420 @Override 421 public void onAnimationStart(View view) { 422 mIsAnimatingOut = true; 423 } 424 425 @Override 426 public void onAnimationCancel(View view) { 427 mIsAnimatingOut = false; 428 } 429 430 @Override 431 public void onAnimationEnd(View view) { 432 mIsAnimatingOut = false; 433 view.setVisibility(View.GONE); 434 } 435 }).start(); 436 } else { 437 Animation anim = android.view.animation.AnimationUtils.loadAnimation( 438 button.getContext(), R.anim.fab_out); 439 anim.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR); 440 anim.setDuration(200); 441 anim.setAnimationListener(new AnimationUtils.AnimationListenerAdapter() { 442 @Override 443 public void onAnimationStart(Animation animation) { 444 mIsAnimatingOut = true; 445 } 446 447 @Override 448 public void onAnimationEnd(Animation animation) { 449 mIsAnimatingOut = false; 450 button.setVisibility(View.GONE); 451 } 452 }); 453 button.startAnimation(anim); 454 } 455 } 456 457 /** 458 * Pre-Lollipop we use padding so that the shadow has enough space to be drawn. This method 459 * offsets our layout position so that we're positioned correctly if we're on one of 460 * our parent's edges. 461 */ 462 private void offsetIfNeeded(CoordinatorLayout parent, FloatingActionButton fab) { 463 final Rect padding = fab.mShadowPadding; 464 465 if (padding != null && padding.centerX() > 0 && padding.centerY() > 0) { 466 final CoordinatorLayout.LayoutParams lp = 467 (CoordinatorLayout.LayoutParams) fab.getLayoutParams(); 468 469 int offsetTB = 0, offsetLR = 0; 470 471 if (fab.getRight() >= parent.getWidth() - lp.rightMargin) { 472 // If we're on the left edge, shift it the right 473 offsetLR = padding.right; 474 } else if (fab.getLeft() <= lp.leftMargin) { 475 // If we're on the left edge, shift it the left 476 offsetLR = -padding.left; 477 } 478 if (fab.getBottom() >= parent.getBottom() - lp.bottomMargin) { 479 // If we're on the bottom edge, shift it down 480 offsetTB = padding.bottom; 481 } else if (fab.getTop() <= lp.topMargin) { 482 // If we're on the top edge, shift it up 483 offsetTB = -padding.top; 484 } 485 486 fab.offsetTopAndBottom(offsetTB); 487 fab.offsetLeftAndRight(offsetLR); 488 } 489 } 490 } 491} 492