FloatingActionButton.java revision de373c43e108bc24b46c947c7a4da26774cdb874
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 setClickable(true); 126 } 127 128 @Override 129 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 130 final int preferredSize = getSizeDimension(); 131 132 final int w = resolveAdjustedSize(preferredSize, widthMeasureSpec); 133 final int h = resolveAdjustedSize(preferredSize, heightMeasureSpec); 134 135 // As we want to stay circular, we set both dimensions to be the 136 // smallest resolved dimension 137 final int d = Math.min(w, h); 138 139 // We add the shadow's padding to the measured dimension 140 setMeasuredDimension( 141 d + mShadowPadding.left + mShadowPadding.right, 142 d + mShadowPadding.top + mShadowPadding.bottom); 143 } 144 145 /** 146 * Set the ripple color for this {@link FloatingActionButton}. 147 * <p> 148 * When running on devices with KitKat or below, we draw a fill rather than a ripple. 149 * 150 * @param color ARGB color to use for the ripple. 151 */ 152 public void setRippleColor(int color) { 153 if (mRippleColor != color) { 154 mRippleColor = color; 155 mImpl.setRippleColor(color); 156 } 157 } 158 159 /** 160 * Return the tint applied to the background drawable, if specified. 161 * 162 * @return the tint applied to the background drawable 163 * @see #setBackgroundTintList(ColorStateList) 164 */ 165 @Nullable 166 @Override 167 public ColorStateList getBackgroundTintList() { 168 return mBackgroundTint; 169 } 170 171 /** 172 * Applies a tint to the background drawable. Does not modify the current tint 173 * mode, which is {@link PorterDuff.Mode#SRC_IN} by default. 174 * 175 * @param tint the tint to apply, may be {@code null} to clear tint 176 */ 177 public void setBackgroundTintList(@Nullable ColorStateList tint) { 178 mImpl.setBackgroundTintList(tint); 179 } 180 181 182 /** 183 * Return the blending mode used to apply the tint to the background 184 * drawable, if specified. 185 * 186 * @return the blending mode used to apply the tint to the background 187 * drawable 188 * @see #setBackgroundTintMode(PorterDuff.Mode) 189 */ 190 @Nullable 191 @Override 192 public PorterDuff.Mode getBackgroundTintMode() { 193 return mBackgroundTintMode; 194 } 195 196 /** 197 * Specifies the blending mode used to apply the tint specified by 198 * {@link #setBackgroundTintList(ColorStateList)}} to the background 199 * drawable. The default mode is {@link PorterDuff.Mode#SRC_IN}. 200 * 201 * @param tintMode the blending mode used to apply the tint, may be 202 * {@code null} to clear tint 203 */ 204 public void setBackgroundTintMode(@Nullable PorterDuff.Mode tintMode) { 205 mImpl.setBackgroundTintMode(tintMode); 206 } 207 208 @Override 209 public void setBackgroundDrawable(Drawable background) { 210 if (mImpl != null) { 211 mImpl.setBackgroundDrawable( 212 background, mBackgroundTint, mBackgroundTintMode, mRippleColor); 213 } 214 } 215 216 final int getSizeDimension() { 217 switch (mSize) { 218 case SIZE_MINI: 219 return getResources().getDimensionPixelSize(R.dimen.fab_size_mini); 220 case SIZE_NORMAL: 221 default: 222 return getResources().getDimensionPixelSize(R.dimen.fab_size_normal); 223 } 224 } 225 226 @Override 227 protected void drawableStateChanged() { 228 super.drawableStateChanged(); 229 mImpl.onDrawableStateChanged(getDrawableState()); 230 } 231 232 @TargetApi(Build.VERSION_CODES.HONEYCOMB) 233 @Override 234 public void jumpDrawablesToCurrentState() { 235 super.jumpDrawablesToCurrentState(); 236 mImpl.jumpDrawableToCurrentState(); 237 } 238 239 private static int resolveAdjustedSize(int desiredSize, int measureSpec) { 240 int result = desiredSize; 241 int specMode = MeasureSpec.getMode(measureSpec); 242 int specSize = MeasureSpec.getSize(measureSpec); 243 switch (specMode) { 244 case MeasureSpec.UNSPECIFIED: 245 // Parent says we can be as big as we want. Just don't be larger 246 // than max size imposed on ourselves. 247 result = desiredSize; 248 break; 249 case MeasureSpec.AT_MOST: 250 // Parent says we can be as big as we want, up to specSize. 251 // Don't be larger than specSize, and don't be larger than 252 // the max size imposed on ourselves. 253 result = Math.min(desiredSize, specSize); 254 break; 255 case MeasureSpec.EXACTLY: 256 // No choice. Do what we are told. 257 result = specSize; 258 break; 259 } 260 return result; 261 } 262 263 static PorterDuff.Mode parseTintMode(int value, PorterDuff.Mode defaultMode) { 264 switch (value) { 265 case 3: 266 return PorterDuff.Mode.SRC_OVER; 267 case 5: 268 return PorterDuff.Mode.SRC_IN; 269 case 9: 270 return PorterDuff.Mode.SRC_ATOP; 271 case 14: 272 return PorterDuff.Mode.MULTIPLY; 273 case 15: 274 return PorterDuff.Mode.SCREEN; 275 default: 276 return defaultMode; 277 } 278 } 279 280 /** 281 * Behavior designed for use with {@link FloatingActionButton} instances. It's main function 282 * is to move {@link FloatingActionButton} views so that any displayed {@link Snackbar}s do 283 * not cover them. 284 */ 285 public static class Behavior extends CoordinatorLayout.Behavior<FloatingActionButton> { 286 // We only support the FAB <> Snackbar shift movement on Honeycomb and above. This is 287 // because we can use view translation properties which greatly simplifies the code. 288 private static final boolean SNACKBAR_BEHAVIOR_ENABLED = Build.VERSION.SDK_INT >= 11; 289 290 private Rect mTmpRect; 291 private boolean mIsAnimatingOut; 292 private float mTranslationY; 293 294 @Override 295 public boolean layoutDependsOn(CoordinatorLayout parent, 296 FloatingActionButton child, 297 View dependency) { 298 // We're dependent on all SnackbarLayouts (if enabled) 299 return SNACKBAR_BEHAVIOR_ENABLED && dependency instanceof Snackbar.SnackbarLayout; 300 } 301 302 @Override 303 public boolean onDependentViewChanged(CoordinatorLayout parent, FloatingActionButton child, 304 View dependency) { 305 if (dependency instanceof Snackbar.SnackbarLayout) { 306 updateFabTranslationForSnackbar(parent, child, dependency); 307 } else if (dependency instanceof AppBarLayout) { 308 final AppBarLayout appBarLayout = (AppBarLayout) dependency; 309 if (mTmpRect == null) { 310 mTmpRect = new Rect(); 311 } 312 313 // First, let's get the visible rect of the dependency 314 final Rect rect = mTmpRect; 315 ViewGroupUtils.getDescendantRect(parent, dependency, rect); 316 317 if (rect.bottom <= appBarLayout.getMinimumHeightForVisibleOverlappingContent()) { 318 // If the anchor's bottom is below the seam, we'll animate our FAB out 319 if (!mIsAnimatingOut && child.getVisibility() == View.VISIBLE) { 320 animateOut(child); 321 } 322 } else { 323 // Else, we'll animate our FAB back in 324 if (child.getVisibility() != View.VISIBLE) { 325 animateIn(child); 326 } 327 } 328 } 329 return false; 330 } 331 332 private void updateFabTranslationForSnackbar(CoordinatorLayout parent, 333 FloatingActionButton fab, View snackbar) { 334 final float translationY = getFabTranslationYForSnackbar(parent, fab); 335 if (translationY != mTranslationY) { 336 // First, cancel any current animation 337 ViewCompat.animate(fab).cancel(); 338 339 if (Math.abs(translationY - mTranslationY) == snackbar.getHeight()) { 340 // If we're travelling by the height of the Snackbar then we probably need to 341 // animate to the value 342 ViewCompat.animate(fab) 343 .translationY(translationY) 344 .setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR) 345 .setListener(null); 346 } else { 347 // Else we'll set use setTranslationY 348 ViewCompat.setTranslationY(fab, translationY); 349 } 350 mTranslationY = translationY; 351 } 352 } 353 354 private float getFabTranslationYForSnackbar(CoordinatorLayout parent, 355 FloatingActionButton fab) { 356 float minOffset = 0; 357 final List<View> dependencies = parent.getDependencies(fab); 358 for (int i = 0, z = dependencies.size(); i < z; i++) { 359 final View view = dependencies.get(i); 360 if (view instanceof Snackbar.SnackbarLayout && parent.doViewsOverlap(fab, view)) { 361 minOffset = Math.min(minOffset, 362 ViewCompat.getTranslationY(view) - view.getHeight()); 363 } 364 } 365 366 return minOffset; 367 } 368 369 private void animateIn(FloatingActionButton button) { 370 button.setVisibility(View.VISIBLE); 371 372 ViewCompat.animate(button) 373 .scaleX(1f) 374 .scaleY(1f) 375 .alpha(1f) 376 .setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR) 377 .withLayer() 378 .setListener(null) 379 .start(); 380 } 381 382 private void animateOut(FloatingActionButton button) { 383 ViewCompat.animate(button) 384 .scaleX(0f) 385 .scaleY(0f) 386 .alpha(0f) 387 .setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR) 388 .withLayer() 389 .setListener(new ViewPropertyAnimatorListener() { 390 @Override 391 public void onAnimationStart(View view) { 392 mIsAnimatingOut = true; 393 } 394 395 @Override 396 public void onAnimationCancel(View view) { 397 mIsAnimatingOut = false; 398 } 399 400 @Override 401 public void onAnimationEnd(View view) { 402 mIsAnimatingOut = false; 403 view.setVisibility(View.GONE); 404 } 405 }).start(); 406 } 407 } 408} 409