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