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