FloatingActionButton.java revision 9840efe3dbdc7026521da8576574c55120782f6c
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.util.AttributeSet; 30import android.widget.Checkable; 31import android.widget.ImageView; 32 33/** 34 * Floating action buttons are used for a special type of promoted action. They are distinguished 35 * by 36 * a circled icon floating above the UI and have special motion behaviors related to morphing, 37 * launching, and the transferring anchor point. 38 * 39 * Floating action buttons come in two sizes: the default, which should be used in most cases, and 40 * the mini, which should only be used to create visual continuity with other elements on the 41 * screen. 42 */ 43public class FloatingActionButton extends ImageView { 44 45 // These values must match those in the attrs declaration 46 private static final int SIZE_MINI = 1; 47 private static final int SIZE_NORMAL = 0; 48 49 private ColorStateList mBackgroundTint; 50 private PorterDuff.Mode mBackgroundTintMode; 51 52 private int mRippleColor; 53 private int mSize; 54 private int mContentPadding; 55 56 private final Rect mShadowPadding; 57 58 private final FloatingActionButtonImpl mImpl; 59 60 public FloatingActionButton(Context context) { 61 this(context, null); 62 } 63 64 public FloatingActionButton(Context context, AttributeSet attrs) { 65 this(context, attrs, 0); 66 } 67 68 public FloatingActionButton(Context context, AttributeSet attrs, int defStyleAttr) { 69 super(context, attrs, defStyleAttr); 70 71 mShadowPadding = new Rect(); 72 73 TypedArray a = context.obtainStyledAttributes(attrs, 74 R.styleable.FloatingActionButton, defStyleAttr, 75 R.style.Widget_Design_FloatingActionButton); 76 Drawable background = a.getDrawable(R.styleable.FloatingActionButton_android_background); 77 mBackgroundTint = a.getColorStateList(R.styleable.FloatingActionButton_backgroundTint); 78 mBackgroundTintMode = parseTintMode(a.getInt( 79 R.styleable.FloatingActionButton_backgroundTintMode, -1), null); 80 mRippleColor = a.getColor(R.styleable.FloatingActionButton_rippleColor, 0); 81 mSize = a.getInt(R.styleable.FloatingActionButton_fabSize, SIZE_NORMAL); 82 final float elevation = a.getDimension(R.styleable.FloatingActionButton_elevation, 0f); 83 final float pressedTranslationZ = a.getDimension( 84 R.styleable.FloatingActionButton_pressedTranslationZ, 0f); 85 a.recycle(); 86 87 final ShadowViewDelegate delegate = new ShadowViewDelegate() { 88 @Override 89 public float getRadius() { 90 return getSizeDimension() / 2f; 91 } 92 93 @Override 94 public void setShadowPadding(int left, int top, int right, int bottom) { 95 mShadowPadding.set(left, top, right, bottom); 96 97 setPadding(left + mContentPadding, top + mContentPadding, 98 right + mContentPadding, bottom + mContentPadding); 99 } 100 101 @Override 102 public void setBackgroundDrawable(Drawable background) { 103 FloatingActionButton.super.setBackgroundDrawable(background); 104 } 105 }; 106 107 if (Build.VERSION.SDK_INT >= 21) { 108 mImpl = new FloatingActionButtonLollipop(this, delegate); 109 } else { 110 mImpl = new FloatingActionButtonEclairMr1(this, delegate); 111 } 112 113 final int maxContentSize = (int) getResources().getDimension(R.dimen.fab_content_size); 114 mContentPadding = (getSizeDimension() - maxContentSize) / 2; 115 116 mImpl.setBackgroundDrawable(background, mBackgroundTint, 117 mBackgroundTintMode, mRippleColor); 118 mImpl.setElevation(elevation); 119 mImpl.setPressedTranslationZ(pressedTranslationZ); 120 } 121 122 @Override 123 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 124 final int preferredSize = getSizeDimension(); 125 126 final int w = resolveAdjustedSize(preferredSize, widthMeasureSpec); 127 final int h = resolveAdjustedSize(preferredSize, heightMeasureSpec); 128 129 // As we want to stay circular, we set both dimensions to be the 130 // smallest resolved dimension 131 final int d = Math.min(w, h); 132 133 // We add the shadow's padding to the measured dimension 134 setMeasuredDimension( 135 d + mShadowPadding.left + mShadowPadding.right, 136 d + mShadowPadding.top + mShadowPadding.bottom); 137 } 138 139 /** 140 * Set the ripple color for this {@link FloatingActionButton}. 141 * <p> 142 * When running on devices with KitKat or below, we draw a fill rather than a ripple. 143 * 144 * @param color ARGB color to use for the ripple. 145 */ 146 public void setRippleColor(int color) { 147 if (mRippleColor != color) { 148 mRippleColor = color; 149 mImpl.setRippleColor(color); 150 } 151 } 152 153 /** 154 * Return the tint applied to the background drawable, if specified. 155 * 156 * @return the tint applied to the background drawable 157 * @see #setBackgroundTintList(ColorStateList) 158 */ 159 @Nullable 160 @Override 161 public ColorStateList getBackgroundTintList() { 162 return mBackgroundTint; 163 } 164 165 /** 166 * Applies a tint to the background drawable. Does not modify the current tint 167 * mode, which is {@link PorterDuff.Mode#SRC_IN} by default. 168 * 169 * @param tint the tint to apply, may be {@code null} to clear tint 170 */ 171 public void setBackgroundTintList(@Nullable ColorStateList tint) { 172 mImpl.setBackgroundTintList(tint); 173 } 174 175 176 /** 177 * Return the blending mode used to apply the tint to the background 178 * drawable, if specified. 179 * 180 * @return the blending mode used to apply the tint to the background 181 * drawable 182 * @see #setBackgroundTintMode(PorterDuff.Mode) 183 */ 184 @Nullable 185 @Override 186 public PorterDuff.Mode getBackgroundTintMode() { 187 return mBackgroundTintMode; 188 } 189 190 /** 191 * Specifies the blending mode used to apply the tint specified by 192 * {@link #setBackgroundTintList(ColorStateList)}} to the background 193 * drawable. The default mode is {@link PorterDuff.Mode#SRC_IN}. 194 * 195 * @param tintMode the blending mode used to apply the tint, may be 196 * {@code null} to clear tint 197 */ 198 public void setBackgroundTintMode(@Nullable PorterDuff.Mode tintMode) { 199 mImpl.setBackgroundTintMode(tintMode); 200 } 201 202 @Override 203 public void setBackgroundDrawable(Drawable background) { 204 mImpl.setBackgroundDrawable(background, mBackgroundTint, mBackgroundTintMode, mRippleColor); 205 } 206 207 final int getSizeDimension() { 208 switch (mSize) { 209 case SIZE_MINI: 210 return getResources().getDimensionPixelSize(R.dimen.fab_size_mini); 211 case SIZE_NORMAL: 212 default: 213 return getResources().getDimensionPixelSize(R.dimen.fab_size_normal); 214 } 215 } 216 217 @Override 218 protected void drawableStateChanged() { 219 super.drawableStateChanged(); 220 mImpl.onDrawableStateChanged(getDrawableState()); 221 } 222 223 @TargetApi(Build.VERSION_CODES.HONEYCOMB) 224 @Override 225 public void jumpDrawablesToCurrentState() { 226 super.jumpDrawablesToCurrentState(); 227 mImpl.jumpDrawableToCurrentState(); 228 } 229 230 private static int resolveAdjustedSize(int desiredSize, int measureSpec) { 231 int result = desiredSize; 232 int specMode = MeasureSpec.getMode(measureSpec); 233 int specSize = MeasureSpec.getSize(measureSpec); 234 switch (specMode) { 235 case MeasureSpec.UNSPECIFIED: 236 // Parent says we can be as big as we want. Just don't be larger 237 // than max size imposed on ourselves. 238 result = desiredSize; 239 break; 240 case MeasureSpec.AT_MOST: 241 // Parent says we can be as big as we want, up to specSize. 242 // Don't be larger than specSize, and don't be larger than 243 // the max size imposed on ourselves. 244 result = Math.min(desiredSize, specSize); 245 break; 246 case MeasureSpec.EXACTLY: 247 // No choice. Do what we are told. 248 result = specSize; 249 break; 250 } 251 return result; 252 } 253 254 static PorterDuff.Mode parseTintMode(int value, PorterDuff.Mode defaultMode) { 255 switch (value) { 256 case 3: 257 return PorterDuff.Mode.SRC_OVER; 258 case 5: 259 return PorterDuff.Mode.SRC_IN; 260 case 9: 261 return PorterDuff.Mode.SRC_ATOP; 262 case 14: 263 return PorterDuff.Mode.MULTIPLY; 264 case 15: 265 return PorterDuff.Mode.SCREEN; 266 default: 267 return defaultMode; 268 } 269 } 270} 271