FloatingActionButton.java revision a6a508b2296730ca6954aaebcca52a9962a5cb55
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.widget.ImageView; 34 35import java.util.List; 36 37/** 38 * Floating action buttons are used for a special type of promoted action. They are distinguished 39 * by a circled icon floating above the UI and have special motion behaviors related to morphing, 40 * launching, and the transferring anchor point. 41 * 42 * Floating action buttons come in two sizes: the default, which should be used in most cases, and 43 * the mini, which should only be used to create visual continuity with other elements on the 44 * screen. 45 */ 46@CoordinatorLayout.DefaultBehavior(FloatingActionButton.Behavior.class) 47public class FloatingActionButton extends ImageView { 48 49 // These values must match those in the attrs declaration 50 private static final int SIZE_MINI = 1; 51 private static final int SIZE_NORMAL = 0; 52 53 private ColorStateList mBackgroundTint; 54 private PorterDuff.Mode mBackgroundTintMode; 55 56 private int mRippleColor; 57 private int mSize; 58 private int mContentPadding; 59 60 private final Rect mShadowPadding; 61 62 private final FloatingActionButtonImpl mImpl; 63 64 public FloatingActionButton(Context context) { 65 this(context, null); 66 } 67 68 public FloatingActionButton(Context context, AttributeSet attrs) { 69 this(context, attrs, 0); 70 } 71 72 public FloatingActionButton(Context context, AttributeSet attrs, int defStyleAttr) { 73 super(context, attrs, defStyleAttr); 74 75 mShadowPadding = new Rect(); 76 77 TypedArray a = context.obtainStyledAttributes(attrs, 78 R.styleable.FloatingActionButton, defStyleAttr, 79 R.style.Widget_Design_FloatingActionButton); 80 Drawable background = a.getDrawable(R.styleable.FloatingActionButton_android_background); 81 mBackgroundTint = a.getColorStateList(R.styleable.FloatingActionButton_backgroundTint); 82 mBackgroundTintMode = parseTintMode(a.getInt( 83 R.styleable.FloatingActionButton_backgroundTintMode, -1), null); 84 mRippleColor = a.getColor(R.styleable.FloatingActionButton_rippleColor, 0); 85 mSize = a.getInt(R.styleable.FloatingActionButton_fabSize, SIZE_NORMAL); 86 final float elevation = a.getDimension(R.styleable.FloatingActionButton_elevation, 0f); 87 final float pressedTranslationZ = a.getDimension( 88 R.styleable.FloatingActionButton_pressedTranslationZ, 0f); 89 a.recycle(); 90 91 final ShadowViewDelegate delegate = new ShadowViewDelegate() { 92 @Override 93 public float getRadius() { 94 return getSizeDimension() / 2f; 95 } 96 97 @Override 98 public void setShadowPadding(int left, int top, int right, int bottom) { 99 mShadowPadding.set(left, top, right, bottom); 100 101 setPadding(left + mContentPadding, top + mContentPadding, 102 right + mContentPadding, bottom + mContentPadding); 103 } 104 105 @Override 106 public void setBackgroundDrawable(Drawable background) { 107 FloatingActionButton.super.setBackgroundDrawable(background); 108 } 109 }; 110 111 if (Build.VERSION.SDK_INT >= 21) { 112 mImpl = new FloatingActionButtonLollipop(this, delegate); 113 } else { 114 mImpl = new FloatingActionButtonEclairMr1(this, delegate); 115 } 116 117 final int maxContentSize = (int) getResources().getDimension(R.dimen.fab_content_size); 118 mContentPadding = (getSizeDimension() - maxContentSize) / 2; 119 120 mImpl.setBackgroundDrawable(background, mBackgroundTint, 121 mBackgroundTintMode, mRippleColor); 122 mImpl.setElevation(elevation); 123 mImpl.setPressedTranslationZ(pressedTranslationZ); 124 } 125 126 @Override 127 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 128 final int preferredSize = getSizeDimension(); 129 130 final int w = resolveAdjustedSize(preferredSize, widthMeasureSpec); 131 final int h = resolveAdjustedSize(preferredSize, heightMeasureSpec); 132 133 // As we want to stay circular, we set both dimensions to be the 134 // smallest resolved dimension 135 final int d = Math.min(w, h); 136 137 // We add the shadow's padding to the measured dimension 138 setMeasuredDimension( 139 d + mShadowPadding.left + mShadowPadding.right, 140 d + mShadowPadding.top + mShadowPadding.bottom); 141 } 142 143 /** 144 * Set the ripple color for this {@link FloatingActionButton}. 145 * <p> 146 * When running on devices with KitKat or below, we draw a fill rather than a ripple. 147 * 148 * @param color ARGB color to use for the ripple. 149 */ 150 public void setRippleColor(int color) { 151 if (mRippleColor != color) { 152 mRippleColor = color; 153 mImpl.setRippleColor(color); 154 } 155 } 156 157 /** 158 * Return the tint applied to the background drawable, if specified. 159 * 160 * @return the tint applied to the background drawable 161 * @see #setBackgroundTintList(ColorStateList) 162 */ 163 @Nullable 164 @Override 165 public ColorStateList getBackgroundTintList() { 166 return mBackgroundTint; 167 } 168 169 /** 170 * Applies a tint to the background drawable. Does not modify the current tint 171 * mode, which is {@link PorterDuff.Mode#SRC_IN} by default. 172 * 173 * @param tint the tint to apply, may be {@code null} to clear tint 174 */ 175 public void setBackgroundTintList(@Nullable ColorStateList tint) { 176 mImpl.setBackgroundTintList(tint); 177 } 178 179 180 /** 181 * Return the blending mode used to apply the tint to the background 182 * drawable, if specified. 183 * 184 * @return the blending mode used to apply the tint to the background 185 * drawable 186 * @see #setBackgroundTintMode(PorterDuff.Mode) 187 */ 188 @Nullable 189 @Override 190 public PorterDuff.Mode getBackgroundTintMode() { 191 return mBackgroundTintMode; 192 } 193 194 /** 195 * Specifies the blending mode used to apply the tint specified by 196 * {@link #setBackgroundTintList(ColorStateList)}} to the background 197 * drawable. The default mode is {@link PorterDuff.Mode#SRC_IN}. 198 * 199 * @param tintMode the blending mode used to apply the tint, may be 200 * {@code null} to clear tint 201 */ 202 public void setBackgroundTintMode(@Nullable PorterDuff.Mode tintMode) { 203 mImpl.setBackgroundTintMode(tintMode); 204 } 205 206 @Override 207 public void setBackgroundDrawable(Drawable background) { 208 mImpl.setBackgroundDrawable(background, mBackgroundTint, mBackgroundTintMode, mRippleColor); 209 } 210 211 final int getSizeDimension() { 212 switch (mSize) { 213 case SIZE_MINI: 214 return getResources().getDimensionPixelSize(R.dimen.fab_size_mini); 215 case SIZE_NORMAL: 216 default: 217 return getResources().getDimensionPixelSize(R.dimen.fab_size_normal); 218 } 219 } 220 221 @Override 222 protected void drawableStateChanged() { 223 super.drawableStateChanged(); 224 mImpl.onDrawableStateChanged(getDrawableState()); 225 } 226 227 @TargetApi(Build.VERSION_CODES.HONEYCOMB) 228 @Override 229 public void jumpDrawablesToCurrentState() { 230 super.jumpDrawablesToCurrentState(); 231 mImpl.jumpDrawableToCurrentState(); 232 } 233 234 private static int resolveAdjustedSize(int desiredSize, int measureSpec) { 235 int result = desiredSize; 236 int specMode = MeasureSpec.getMode(measureSpec); 237 int specSize = MeasureSpec.getSize(measureSpec); 238 switch (specMode) { 239 case MeasureSpec.UNSPECIFIED: 240 // Parent says we can be as big as we want. Just don't be larger 241 // than max size imposed on ourselves. 242 result = desiredSize; 243 break; 244 case MeasureSpec.AT_MOST: 245 // Parent says we can be as big as we want, up to specSize. 246 // Don't be larger than specSize, and don't be larger than 247 // the max size imposed on ourselves. 248 result = Math.min(desiredSize, specSize); 249 break; 250 case MeasureSpec.EXACTLY: 251 // No choice. Do what we are told. 252 result = specSize; 253 break; 254 } 255 return result; 256 } 257 258 static PorterDuff.Mode parseTintMode(int value, PorterDuff.Mode defaultMode) { 259 switch (value) { 260 case 3: 261 return PorterDuff.Mode.SRC_OVER; 262 case 5: 263 return PorterDuff.Mode.SRC_IN; 264 case 9: 265 return PorterDuff.Mode.SRC_ATOP; 266 case 14: 267 return PorterDuff.Mode.MULTIPLY; 268 case 15: 269 return PorterDuff.Mode.SCREEN; 270 default: 271 return defaultMode; 272 } 273 } 274 275 /** 276 * Behavior designed for use with {@link FloatingActionButton} instances. It's main function 277 * is to move {@link FloatingActionButton} views so that any displayed {@link Snackbar}s do 278 * not cover them. 279 */ 280 public static class Behavior extends CoordinatorLayout.Behavior<FloatingActionButton> { 281 // We only support the FAB <> Snackbar shift movement on Honeycomb and above. This is 282 // because we can use view translation properties which greatly simplifies the code. 283 private static final boolean SNACKBAR_BEHAVIOR_ENABLED = Build.VERSION.SDK_INT >= 11; 284 285 private Rect mTmpRect; 286 private boolean mIsAnimatingOut; 287 private float mTranslationY; 288 289 @Override 290 public boolean layoutDependsOn(CoordinatorLayout parent, 291 FloatingActionButton child, 292 View dependency) { 293 // We're dependent on all SnackbarLayouts (if enabled) 294 return SNACKBAR_BEHAVIOR_ENABLED && dependency instanceof Snackbar.SnackbarLayout; 295 } 296 297 @Override 298 public boolean onDependentViewChanged(CoordinatorLayout parent, FloatingActionButton child, 299 View dependency) { 300 if (dependency instanceof Snackbar.SnackbarLayout) { 301 updateFabTranslationForSnackbar(parent, child, dependency); 302 } else if (dependency instanceof AppBarLayout) { 303 final AppBarLayout appBarLayout = (AppBarLayout) dependency; 304 if (mTmpRect == null) { 305 mTmpRect = new Rect(); 306 } 307 308 // First, let's get the visible rect of the dependency 309 final Rect rect = mTmpRect; 310 ViewGroupUtils.getDescendantRect(parent, dependency, rect); 311 312 if (rect.bottom <= appBarLayout.getMinimumHeightForVisibleOverlappingContent()) { 313 // If the anchor's bottom is below the seam, we'll animate our FAB out 314 if (!mIsAnimatingOut && child.getVisibility() == View.VISIBLE) { 315 animateOut(child); 316 } 317 } else { 318 // Else, we'll animate our FAB back in 319 if (child.getVisibility() != View.VISIBLE) { 320 animateIn(child); 321 } 322 } 323 } 324 return false; 325 } 326 327 private void updateFabTranslationForSnackbar(CoordinatorLayout parent, 328 FloatingActionButton fab, View snackbar) { 329 final float translationY = getFabTranslationYForSnackbar(parent, fab); 330 if (translationY != mTranslationY) { 331 // First, cancel any current animation 332 ViewCompat.animate(fab).cancel(); 333 334 if (Math.abs(translationY - mTranslationY) == snackbar.getHeight()) { 335 // If we're travelling by the height of the Snackbar then we probably need to 336 // animate to the value 337 ViewCompat.animate(fab) 338 .translationY(translationY) 339 .setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR) 340 .setListener(null); 341 } else { 342 // Else we'll set use setTranslationY 343 ViewCompat.setTranslationY(fab, translationY); 344 } 345 mTranslationY = translationY; 346 } 347 } 348 349 private float getFabTranslationYForSnackbar(CoordinatorLayout parent, 350 FloatingActionButton fab) { 351 float minOffset = 0; 352 final List<View> dependencies = parent.getDependencies(fab); 353 for (int i = 0, z = dependencies.size(); i < z; i++) { 354 final View view = dependencies.get(i); 355 if (view instanceof Snackbar.SnackbarLayout && parent.doViewsOverlap(fab, view)) { 356 minOffset = Math.min(minOffset, 357 ViewCompat.getTranslationY(view) - view.getHeight()); 358 } 359 } 360 361 return minOffset; 362 } 363 364 private void animateIn(FloatingActionButton button) { 365 button.setVisibility(View.VISIBLE); 366 367 ViewCompat.animate(button) 368 .scaleX(1f) 369 .scaleY(1f) 370 .alpha(1f) 371 .setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR) 372 .withLayer() 373 .setListener(null) 374 .start(); 375 } 376 377 private void animateOut(FloatingActionButton button) { 378 ViewCompat.animate(button) 379 .scaleX(0f) 380 .scaleY(0f) 381 .alpha(0f) 382 .setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR) 383 .withLayer() 384 .setListener(new ViewPropertyAnimatorListener() { 385 @Override 386 public void onAnimationStart(View view) { 387 mIsAnimatingOut = true; 388 } 389 390 @Override 391 public void onAnimationCancel(View view) { 392 mIsAnimatingOut = false; 393 } 394 395 @Override 396 public void onAnimationEnd(View view) { 397 mIsAnimatingOut = false; 398 view.setVisibility(View.GONE); 399 } 400 }).start(); 401 } 402 } 403} 404