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