FloatingActionButton.java revision 097e80a3c5518c6bf2e9f3f9b55ed9f4b5cc37e8
1709a0978ae141198018ca9769f8d96292a8928e6Jason Sams/*
2709a0978ae141198018ca9769f8d96292a8928e6Jason Sams * Copyright (C) 2015 The Android Open Source Project
3709a0978ae141198018ca9769f8d96292a8928e6Jason Sams *
4709a0978ae141198018ca9769f8d96292a8928e6Jason Sams * Licensed under the Apache License, Version 2.0 (the "License");
5709a0978ae141198018ca9769f8d96292a8928e6Jason Sams * you may not use this file except in compliance with the License.
6709a0978ae141198018ca9769f8d96292a8928e6Jason Sams * You may obtain a copy of the License at
7709a0978ae141198018ca9769f8d96292a8928e6Jason Sams *
8709a0978ae141198018ca9769f8d96292a8928e6Jason Sams *      http://www.apache.org/licenses/LICENSE-2.0
9709a0978ae141198018ca9769f8d96292a8928e6Jason Sams *
10709a0978ae141198018ca9769f8d96292a8928e6Jason Sams * Unless required by applicable law or agreed to in writing, software
11709a0978ae141198018ca9769f8d96292a8928e6Jason Sams * distributed under the License is distributed on an "AS IS" BASIS,
12709a0978ae141198018ca9769f8d96292a8928e6Jason Sams * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13709a0978ae141198018ca9769f8d96292a8928e6Jason Sams * See the License for the specific language governing permissions and
14709a0978ae141198018ca9769f8d96292a8928e6Jason Sams * limitations under the License.
15709a0978ae141198018ca9769f8d96292a8928e6Jason Sams */
16709a0978ae141198018ca9769f8d96292a8928e6Jason Sams
17709a0978ae141198018ca9769f8d96292a8928e6Jason Samspackage android.support.design.widget;
18709a0978ae141198018ca9769f8d96292a8928e6Jason Sams
19709a0978ae141198018ca9769f8d96292a8928e6Jason Samsimport android.annotation.TargetApi;
20709a0978ae141198018ca9769f8d96292a8928e6Jason Samsimport android.content.Context;
21709a0978ae141198018ca9769f8d96292a8928e6Jason Samsimport android.content.res.ColorStateList;
22709a0978ae141198018ca9769f8d96292a8928e6Jason Samsimport android.content.res.TypedArray;
2325e3af55a43faddced1a9931574dfdc3cc8ad8fdStephen Hinesimport android.graphics.PorterDuff;
2429809d1f95d4cd4cbc6b2f9384b3321759691e13Tim Murrayimport android.graphics.Rect;
2525e3af55a43faddced1a9931574dfdc3cc8ad8fdStephen Hinesimport android.graphics.drawable.Drawable;
2625e3af55a43faddced1a9931574dfdc3cc8ad8fdStephen Hinesimport android.os.Build;
27709a0978ae141198018ca9769f8d96292a8928e6Jason Samsimport android.support.annotation.ColorInt;
28709a0978ae141198018ca9769f8d96292a8928e6Jason Samsimport android.support.annotation.NonNull;
29709a0978ae141198018ca9769f8d96292a8928e6Jason Samsimport android.support.annotation.Nullable;
30709a0978ae141198018ca9769f8d96292a8928e6Jason Samsimport android.support.design.R;
31709a0978ae141198018ca9769f8d96292a8928e6Jason Samsimport android.support.design.widget.FloatingActionButtonImpl.InternalVisibilityChangedListener;
32709a0978ae141198018ca9769f8d96292a8928e6Jason Samsimport android.support.v4.view.ViewCompat;
33709a0978ae141198018ca9769f8d96292a8928e6Jason Samsimport android.util.AttributeSet;
349ab5094dd32352b33e251e540934f6e814c5fa5bJean-Luc Brouilletimport android.util.Log;
359ab5094dd32352b33e251e540934f6e814c5fa5bJean-Luc Brouilletimport android.view.View;
369ab5094dd32352b33e251e540934f6e814c5fa5bJean-Luc Brouilletimport android.widget.ImageButton;
379ab5094dd32352b33e251e540934f6e814c5fa5bJean-Luc Brouilletimport android.widget.ImageView;
38709a0978ae141198018ca9769f8d96292a8928e6Jason Sams
39709a0978ae141198018ca9769f8d96292a8928e6Jason Samsimport java.util.List;
40709a0978ae141198018ca9769f8d96292a8928e6Jason Sams
412abfcc6d129fe3defddef4540aa95cc445c03a7aYang Ni/**
42709a0978ae141198018ca9769f8d96292a8928e6Jason Sams * Floating action buttons are used for a special type of promoted action. They are distinguished
43709a0978ae141198018ca9769f8d96292a8928e6Jason Sams * by a circled icon floating above the UI and have special motion behaviors related to morphing,
44709a0978ae141198018ca9769f8d96292a8928e6Jason Sams * launching, and the transferring anchor point.
45709a0978ae141198018ca9769f8d96292a8928e6Jason Sams *
46b0abb140ac51b93d1a85aadaa63fe057f2d29850David Gross * <p>Floating action buttons come in two sizes: the default and the mini. The size can be
47709a0978ae141198018ca9769f8d96292a8928e6Jason Sams * controlled with the {@code fabSize} attribute.</p>
489ed79105cc6a8dbfaf959875249f36022cc2c798Chris Wailes *
49dc0d8f7c0f1f43f25c34fbc04656ad578f6e953bPirama Arumuga Nainar * <p>As this class descends from {@link ImageView}, you can control the icon which is displayed
50110f181b7966212a36ef18016f9b81c7322d0a2fJason Sams * via {@link #setImageDrawable(Drawable)}.</p>
51110f181b7966212a36ef18016f9b81c7322d0a2fJason Sams *
52110f181b7966212a36ef18016f9b81c7322d0a2fJason Sams * <p>The background color of this view defaults to the your theme's {@code colorAccent}. If you
53dc0d8f7c0f1f43f25c34fbc04656ad578f6e953bPirama Arumuga Nainar * wish to change this at runtime then you can do so via
54110f181b7966212a36ef18016f9b81c7322d0a2fJason Sams * {@link #setBackgroundTintList(ColorStateList)}.</p>
55110f181b7966212a36ef18016f9b81c7322d0a2fJason Sams *
56709a0978ae141198018ca9769f8d96292a8928e6Jason Sams * @attr ref android.support.design.R.styleable#FloatingActionButton_fabSize
57709a0978ae141198018ca9769f8d96292a8928e6Jason Sams */
58005113297b19ed256b6db9d6bc293ed9266899fcStephen Hines@CoordinatorLayout.DefaultBehavior(FloatingActionButton.Behavior.class)
5944bef6fba6244292b751387f3d6c31cca96c28adChris Wailespublic class FloatingActionButton extends ImageButton {
60c060f1435e7b9405f3be8974417fa6f410f03753Stephen Hines
61709a0978ae141198018ca9769f8d96292a8928e6Jason Sams    private static final String LOG_TAG = "FloatingActionButton";
62c060f1435e7b9405f3be8974417fa6f410f03753Stephen Hines
63c060f1435e7b9405f3be8974417fa6f410f03753Stephen Hines    /**
64f37121300217d3b39ab66dd9c8881bcbcad932dfChris Wailes     * Callback to be invoked when the visibility of a FloatingActionButton changes.
65f37121300217d3b39ab66dd9c8881bcbcad932dfChris Wailes     */
6617e3cdc24776d8fdbf1ce16287b9b4dcd516708fJason Sams    public abstract static class OnVisibilityChangedListener {
67f37121300217d3b39ab66dd9c8881bcbcad932dfChris Wailes        /**
68f37121300217d3b39ab66dd9c8881bcbcad932dfChris Wailes         * Called when a FloatingActionButton has been
69f37121300217d3b39ab66dd9c8881bcbcad932dfChris Wailes         * {@link #show(OnVisibilityChangedListener) shown}.
70f37121300217d3b39ab66dd9c8881bcbcad932dfChris Wailes         *
71f37121300217d3b39ab66dd9c8881bcbcad932dfChris Wailes         * @param fab the FloatingActionButton that was shown.
72c060f1435e7b9405f3be8974417fa6f410f03753Stephen Hines         */
73c060f1435e7b9405f3be8974417fa6f410f03753Stephen Hines        public void onShown(FloatingActionButton fab) {}
74c060f1435e7b9405f3be8974417fa6f410f03753Stephen Hines
75c060f1435e7b9405f3be8974417fa6f410f03753Stephen Hines        /**
76c060f1435e7b9405f3be8974417fa6f410f03753Stephen Hines         * Called when a FloatingActionButton has been
77c060f1435e7b9405f3be8974417fa6f410f03753Stephen Hines         * {@link #hide(OnVisibilityChangedListener) hidden}.
78c060f1435e7b9405f3be8974417fa6f410f03753Stephen Hines         *
79c060f1435e7b9405f3be8974417fa6f410f03753Stephen Hines         * @param fab the FloatingActionButton that was hidden.
80c060f1435e7b9405f3be8974417fa6f410f03753Stephen Hines         */
81c060f1435e7b9405f3be8974417fa6f410f03753Stephen Hines        public void onHidden(FloatingActionButton fab) {}
82c060f1435e7b9405f3be8974417fa6f410f03753Stephen Hines    }
83c060f1435e7b9405f3be8974417fa6f410f03753Stephen Hines
84c060f1435e7b9405f3be8974417fa6f410f03753Stephen Hines    // These values must match those in the attrs declaration
85c060f1435e7b9405f3be8974417fa6f410f03753Stephen Hines    private static final int SIZE_MINI = 1;
86c060f1435e7b9405f3be8974417fa6f410f03753Stephen Hines    private static final int SIZE_NORMAL = 0;
87c060f1435e7b9405f3be8974417fa6f410f03753Stephen Hines
88c060f1435e7b9405f3be8974417fa6f410f03753Stephen Hines    private ColorStateList mBackgroundTint;
89c060f1435e7b9405f3be8974417fa6f410f03753Stephen Hines    private PorterDuff.Mode mBackgroundTintMode;
90709a0978ae141198018ca9769f8d96292a8928e6Jason Sams
91062c287f573ecc06c38ee4295e5627e12c52ac3dYang Ni    private int mBorderWidth;
92709a0978ae141198018ca9769f8d96292a8928e6Jason Sams    private int mRippleColor;
93c060f1435e7b9405f3be8974417fa6f410f03753Stephen Hines    private int mSize;
94709a0978ae141198018ca9769f8d96292a8928e6Jason Sams    private int mContentPadding;
95709a0978ae141198018ca9769f8d96292a8928e6Jason Sams
96709a0978ae141198018ca9769f8d96292a8928e6Jason Sams    private final Rect mShadowPadding;
97709a0978ae141198018ca9769f8d96292a8928e6Jason Sams
98bf2111d3b3de310932099514f06924e48fa1d7b2Jason Sams    private final FloatingActionButtonImpl mImpl;
994b3c34e6833e39bc89c2128002806b654b8e623dChris Wailes
1004b3c34e6833e39bc89c2128002806b654b8e623dChris Wailes    public FloatingActionButton(Context context) {
1014b3c34e6833e39bc89c2128002806b654b8e623dChris Wailes        this(context, null);
102709a0978ae141198018ca9769f8d96292a8928e6Jason Sams    }
103709a0978ae141198018ca9769f8d96292a8928e6Jason Sams
104709a0978ae141198018ca9769f8d96292a8928e6Jason Sams    public FloatingActionButton(Context context, AttributeSet attrs) {
105709a0978ae141198018ca9769f8d96292a8928e6Jason Sams        this(context, attrs, 0);
106709a0978ae141198018ca9769f8d96292a8928e6Jason Sams    }
107709a0978ae141198018ca9769f8d96292a8928e6Jason Sams
108c060f1435e7b9405f3be8974417fa6f410f03753Stephen Hines    public FloatingActionButton(Context context, AttributeSet attrs, int defStyleAttr) {
109dc0d8f7c0f1f43f25c34fbc04656ad578f6e953bPirama Arumuga Nainar        super(context, attrs, defStyleAttr);
110709a0978ae141198018ca9769f8d96292a8928e6Jason Sams
1118409d6414dd4a42aa59779fcfe9fce18648cb135Stephen Hines        ThemeUtils.checkAppCompatTheme(context);
1128409d6414dd4a42aa59779fcfe9fce18648cb135Stephen Hines
1138409d6414dd4a42aa59779fcfe9fce18648cb135Stephen Hines        mShadowPadding = new Rect();
1148409d6414dd4a42aa59779fcfe9fce18648cb135Stephen Hines
1155aa018cc36e589b07674957714d27ae3d1fa1c4eStephen Hines        TypedArray a = context.obtainStyledAttributes(attrs,
1168409d6414dd4a42aa59779fcfe9fce18648cb135Stephen Hines                R.styleable.FloatingActionButton, defStyleAttr,
117709a0978ae141198018ca9769f8d96292a8928e6Jason Sams                R.style.Widget_Design_FloatingActionButton);
118709a0978ae141198018ca9769f8d96292a8928e6Jason Sams        mBackgroundTint = a.getColorStateList(R.styleable.FloatingActionButton_backgroundTint);
119709a0978ae141198018ca9769f8d96292a8928e6Jason Sams        mBackgroundTintMode = parseTintMode(a.getInt(
120dc0d8f7c0f1f43f25c34fbc04656ad578f6e953bPirama Arumuga Nainar                R.styleable.FloatingActionButton_backgroundTintMode, -1), null);
121709a0978ae141198018ca9769f8d96292a8928e6Jason Sams        mRippleColor = a.getColor(R.styleable.FloatingActionButton_rippleColor, 0);
122110f181b7966212a36ef18016f9b81c7322d0a2fJason Sams        mSize = a.getInt(R.styleable.FloatingActionButton_fabSize, SIZE_NORMAL);
12340e35cdbe217ec8bf9fc3c69873c7d62fc14158fJean-Luc Brouillet        mBorderWidth = a.getDimensionPixelSize(R.styleable.FloatingActionButton_borderWidth, 0);
12440e35cdbe217ec8bf9fc3c69873c7d62fc14158fJean-Luc Brouillet        final float elevation = a.getDimension(R.styleable.FloatingActionButton_elevation, 0f);
1259ab5094dd32352b33e251e540934f6e814c5fa5bJean-Luc Brouillet        final float pressedTranslationZ = a.getDimension(
12629809d1f95d4cd4cbc6b2f9384b3321759691e13Tim Murray                R.styleable.FloatingActionButton_pressedTranslationZ, 0f);
127709a0978ae141198018ca9769f8d96292a8928e6Jason Sams        a.recycle();
12845e753a46e587c69b3b0d0c5138e88715a24a29aStephen Hines
12945e753a46e587c69b3b0d0c5138e88715a24a29aStephen Hines        final ShadowViewDelegate delegate = new ShadowViewDelegate() {
130110f181b7966212a36ef18016f9b81c7322d0a2fJason Sams            @Override
131110f181b7966212a36ef18016f9b81c7322d0a2fJason Sams            public float getRadius() {
132110f181b7966212a36ef18016f9b81c7322d0a2fJason Sams                return getSizeDimension() / 2f;
133110f181b7966212a36ef18016f9b81c7322d0a2fJason Sams            }
134d9bae689c1b8c3f2ed1a5f2b374dc9393584b8ddYang Ni
135709a0978ae141198018ca9769f8d96292a8928e6Jason Sams            @Override
136709a0978ae141198018ca9769f8d96292a8928e6Jason Sams            public void setShadowPadding(int left, int top, int right, int bottom) {
137709a0978ae141198018ca9769f8d96292a8928e6Jason Sams                mShadowPadding.set(left, top, right, bottom);
138709a0978ae141198018ca9769f8d96292a8928e6Jason Sams
139da0f069871343119251d6b0586be356dc2146a62Yang Ni                setPadding(left + mContentPadding, top + mContentPadding,
1402abfcc6d129fe3defddef4540aa95cc445c03a7aYang Ni                        right + mContentPadding, bottom + mContentPadding);
1412abfcc6d129fe3defddef4540aa95cc445c03a7aYang Ni            }
1422abfcc6d129fe3defddef4540aa95cc445c03a7aYang Ni
143da0f069871343119251d6b0586be356dc2146a62Yang Ni            @Override
1442abfcc6d129fe3defddef4540aa95cc445c03a7aYang Ni            public void setBackgroundDrawable(Drawable background) {
1452abfcc6d129fe3defddef4540aa95cc445c03a7aYang Ni                FloatingActionButton.super.setBackgroundDrawable(background);
146cb17015fed6b11a5028f31cc804a3847e379945dYang Ni            }
147aa6757ffc1b23d771566439c3179fdbc1e5ba569Pirama Arumuga Nainar        };
148709a0978ae141198018ca9769f8d96292a8928e6Jason Sams
149709a0978ae141198018ca9769f8d96292a8928e6Jason Sams        final int sdk = Build.VERSION.SDK_INT;
150709a0978ae141198018ca9769f8d96292a8928e6Jason Sams        if (sdk >= 21) {
151709a0978ae141198018ca9769f8d96292a8928e6Jason Sams            mImpl = new FloatingActionButtonLollipop(this, delegate);
152709a0978ae141198018ca9769f8d96292a8928e6Jason Sams        } else if (sdk >= 14) {
153709a0978ae141198018ca9769f8d96292a8928e6Jason Sams            mImpl = new FloatingActionButtonIcs(this, delegate);
154709a0978ae141198018ca9769f8d96292a8928e6Jason Sams        } else {
155cb17015fed6b11a5028f31cc804a3847e379945dYang Ni            mImpl = new FloatingActionButtonEclairMr1(this, delegate);
156cb17015fed6b11a5028f31cc804a3847e379945dYang Ni        }
157cb17015fed6b11a5028f31cc804a3847e379945dYang Ni
158f02a2b0a2749d4a4f07edbc23eddff2e51d11b72Yang Ni        final int maxContentSize = (int) getResources().getDimension(
159709a0978ae141198018ca9769f8d96292a8928e6Jason Sams                R.dimen.design_fab_content_size);
160d9bae689c1b8c3f2ed1a5f2b374dc9393584b8ddYang Ni        mContentPadding = (getSizeDimension() - maxContentSize) / 2;
161da0f069871343119251d6b0586be356dc2146a62Yang Ni
162da0f069871343119251d6b0586be356dc2146a62Yang Ni        mImpl.setBackgroundDrawable(mBackgroundTint, mBackgroundTintMode,
163682672e36b05349bc4d9dee74e9fab73ce804183Pirama Arumuga Nainar                mRippleColor, mBorderWidth);
164da0f069871343119251d6b0586be356dc2146a62Yang Ni        mImpl.setElevation(elevation);
165da0f069871343119251d6b0586be356dc2146a62Yang Ni        mImpl.setPressedTranslationZ(pressedTranslationZ);
166682672e36b05349bc4d9dee74e9fab73ce804183Pirama Arumuga Nainar    }
167da0f069871343119251d6b0586be356dc2146a62Yang Ni
168da0f069871343119251d6b0586be356dc2146a62Yang Ni    @Override
169709a0978ae141198018ca9769f8d96292a8928e6Jason Sams    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
170709a0978ae141198018ca9769f8d96292a8928e6Jason Sams        final int preferredSize = getSizeDimension();
171709a0978ae141198018ca9769f8d96292a8928e6Jason Sams
172        final int w = resolveAdjustedSize(preferredSize, widthMeasureSpec);
173        final int h = resolveAdjustedSize(preferredSize, heightMeasureSpec);
174
175        // As we want to stay circular, we set both dimensions to be the
176        // smallest resolved dimension
177        final int d = Math.min(w, h);
178
179        // We add the shadow's padding to the measured dimension
180        setMeasuredDimension(
181                d + mShadowPadding.left + mShadowPadding.right,
182                d + mShadowPadding.top + mShadowPadding.bottom);
183    }
184
185    /**
186     * Set the ripple color for this {@link FloatingActionButton}.
187     * <p>
188     * When running on devices with KitKat or below, we draw a fill rather than a ripple.
189     *
190     * @param color ARGB color to use for the ripple.
191     */
192    public void setRippleColor(@ColorInt int color) {
193        if (mRippleColor != color) {
194            mRippleColor = color;
195            mImpl.setRippleColor(color);
196        }
197    }
198
199    /**
200     * Return the tint applied to the background drawable, if specified.
201     *
202     * @return the tint applied to the background drawable
203     * @see #setBackgroundTintList(ColorStateList)
204     */
205    @Nullable
206    @Override
207    public ColorStateList getBackgroundTintList() {
208        return mBackgroundTint;
209    }
210
211    /**
212     * Applies a tint to the background drawable. Does not modify the current tint
213     * mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
214     *
215     * @param tint the tint to apply, may be {@code null} to clear tint
216     */
217    public void setBackgroundTintList(@Nullable ColorStateList tint) {
218        if (mBackgroundTint != tint) {
219            mBackgroundTint = tint;
220            mImpl.setBackgroundTintList(tint);
221        }
222    }
223
224
225    /**
226     * Return the blending mode used to apply the tint to the background
227     * drawable, if specified.
228     *
229     * @return the blending mode used to apply the tint to the background
230     *         drawable
231     * @see #setBackgroundTintMode(PorterDuff.Mode)
232     */
233    @Nullable
234    @Override
235    public PorterDuff.Mode getBackgroundTintMode() {
236        return mBackgroundTintMode;
237    }
238
239    /**
240     * Specifies the blending mode used to apply the tint specified by
241     * {@link #setBackgroundTintList(ColorStateList)}} to the background
242     * drawable. The default mode is {@link PorterDuff.Mode#SRC_IN}.
243     *
244     * @param tintMode the blending mode used to apply the tint, may be
245     *                 {@code null} to clear tint
246     */
247    public void setBackgroundTintMode(@Nullable PorterDuff.Mode tintMode) {
248        if (mBackgroundTintMode != tintMode) {
249            mBackgroundTintMode = tintMode;
250            mImpl.setBackgroundTintMode(tintMode);
251        }
252    }
253
254    @Override
255    public void setBackgroundDrawable(Drawable background) {
256        Log.i(LOG_TAG, "Setting a custom background is not supported.");
257    }
258
259    @Override
260    public void setBackgroundResource(int resid) {
261        Log.i(LOG_TAG, "Setting a custom background is not supported.");
262    }
263
264    @Override
265    public void setBackgroundColor(int color) {
266        Log.i(LOG_TAG, "Setting a custom background is not supported.");
267    }
268
269    /**
270     * Shows the button.
271     * <p>This method will animate the button show if the view has already been laid out.</p>
272     */
273    public void show() {
274        mImpl.show(null);
275    }
276
277    /**
278     * Shows the button.
279     * <p>This method will animate the button show if the view has already been laid out.</p>
280     *
281     * @param listener the listener to notify when this view is shown
282     */
283    public void show(@Nullable final OnVisibilityChangedListener listener) {
284        mImpl.show(wrapOnVisibilityChangedListener(listener));
285    }
286
287    /**
288     * Hides the button.
289     * <p>This method will animate the button hide if the view has already been laid out.</p>
290     */
291    public void hide() {
292        mImpl.hide(null);
293    }
294
295    /**
296     * Hides the button.
297     * <p>This method will animate the button hide if the view has already been laid out.</p>
298     *
299     * @param listener the listener to notify when this view is hidden
300     */
301    public void hide(@Nullable OnVisibilityChangedListener listener) {
302        mImpl.hide(wrapOnVisibilityChangedListener(listener));
303    }
304
305    @Nullable
306    private InternalVisibilityChangedListener wrapOnVisibilityChangedListener(
307            @Nullable final OnVisibilityChangedListener listener) {
308        if (listener == null) {
309            return null;
310        }
311
312        return new InternalVisibilityChangedListener() {
313            @Override
314            public void onShown() {
315                listener.onShown(FloatingActionButton.this);
316            }
317
318            @Override
319            public void onHidden() {
320                listener.onHidden(FloatingActionButton.this);
321            }
322        };
323    }
324
325    final int getSizeDimension() {
326        switch (mSize) {
327            case SIZE_MINI:
328                return getResources().getDimensionPixelSize(R.dimen.design_fab_size_mini);
329            case SIZE_NORMAL:
330            default:
331                return getResources().getDimensionPixelSize(R.dimen.design_fab_size_normal);
332        }
333    }
334
335    @Override
336    protected void onAttachedToWindow() {
337        super.onAttachedToWindow();
338        mImpl.onAttachedToWindow();
339    }
340
341    @Override
342    protected void onDetachedFromWindow() {
343        super.onDetachedFromWindow();
344        mImpl.onDetachedFromWindow();
345    }
346
347    @Override
348    protected void drawableStateChanged() {
349        super.drawableStateChanged();
350        mImpl.onDrawableStateChanged(getDrawableState());
351    }
352
353    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
354    @Override
355    public void jumpDrawablesToCurrentState() {
356        super.jumpDrawablesToCurrentState();
357        mImpl.jumpDrawableToCurrentState();
358    }
359
360    private static int resolveAdjustedSize(int desiredSize, int measureSpec) {
361        int result = desiredSize;
362        int specMode = MeasureSpec.getMode(measureSpec);
363        int specSize = MeasureSpec.getSize(measureSpec);
364        switch (specMode) {
365            case MeasureSpec.UNSPECIFIED:
366                // Parent says we can be as big as we want. Just don't be larger
367                // than max size imposed on ourselves.
368                result = desiredSize;
369                break;
370            case MeasureSpec.AT_MOST:
371                // Parent says we can be as big as we want, up to specSize.
372                // Don't be larger than specSize, and don't be larger than
373                // the max size imposed on ourselves.
374                result = Math.min(desiredSize, specSize);
375                break;
376            case MeasureSpec.EXACTLY:
377                // No choice. Do what we are told.
378                result = specSize;
379                break;
380        }
381        return result;
382    }
383
384    static PorterDuff.Mode parseTintMode(int value, PorterDuff.Mode defaultMode) {
385        switch (value) {
386            case 3:
387                return PorterDuff.Mode.SRC_OVER;
388            case 5:
389                return PorterDuff.Mode.SRC_IN;
390            case 9:
391                return PorterDuff.Mode.SRC_ATOP;
392            case 14:
393                return PorterDuff.Mode.MULTIPLY;
394            case 15:
395                return PorterDuff.Mode.SCREEN;
396            default:
397                return defaultMode;
398        }
399    }
400
401    /**
402     * Behavior designed for use with {@link FloatingActionButton} instances. It's main function
403     * is to move {@link FloatingActionButton} views so that any displayed {@link Snackbar}s do
404     * not cover them.
405     */
406    public static class Behavior extends CoordinatorLayout.Behavior<FloatingActionButton> {
407        // We only support the FAB <> Snackbar shift movement on Honeycomb and above. This is
408        // because we can use view translation properties which greatly simplifies the code.
409        private static final boolean SNACKBAR_BEHAVIOR_ENABLED = Build.VERSION.SDK_INT >= 11;
410
411        private float mFabTranslationY;
412        private Rect mTmpRect;
413
414        @Override
415        public boolean layoutDependsOn(CoordinatorLayout parent,
416                FloatingActionButton child, View dependency) {
417            // We're dependent on all SnackbarLayouts (if enabled)
418            return SNACKBAR_BEHAVIOR_ENABLED && dependency instanceof Snackbar.SnackbarLayout;
419        }
420
421        @Override
422        public boolean onDependentViewChanged(CoordinatorLayout parent, FloatingActionButton child,
423                View dependency) {
424            if (dependency instanceof Snackbar.SnackbarLayout) {
425                updateFabTranslationForSnackbar(parent, child, dependency);
426            } else if (dependency instanceof AppBarLayout) {
427                // If we're depending on an AppBarLayout we will show/hide it automatically
428                // if the FAB is anchored to the AppBarLayout
429                updateFabVisibility(parent, (AppBarLayout) dependency, child);
430            }
431            return false;
432        }
433
434        private boolean updateFabVisibility(CoordinatorLayout parent,
435                AppBarLayout appBarLayout, FloatingActionButton child) {
436            final CoordinatorLayout.LayoutParams lp =
437                    (CoordinatorLayout.LayoutParams) child.getLayoutParams();
438            if (lp.getAnchorId() != appBarLayout.getId()) {
439                // The anchor ID doesn't match the dependency, so we won't automatically
440                // show/hide the FAB
441                return false;
442            }
443
444            if (mTmpRect == null) {
445                mTmpRect = new Rect();
446            }
447
448            // First, let's get the visible rect of the dependency
449            final Rect rect = mTmpRect;
450            ViewGroupUtils.getDescendantRect(parent, appBarLayout, rect);
451
452            if (rect.bottom <= appBarLayout.getMinimumHeightForVisibleOverlappingContent()) {
453                // If the anchor's bottom is below the seam, we'll animate our FAB out
454                child.hide();
455            } else {
456                // Else, we'll animate our FAB back in
457                child.show();
458            }
459            return true;
460        }
461
462        private void updateFabTranslationForSnackbar(CoordinatorLayout parent,
463                FloatingActionButton fab, View snackbar) {
464            if (fab.getVisibility() != View.VISIBLE) {
465                return;
466            }
467
468            final float targetTransY = getFabTranslationYForSnackbar(parent, fab);
469            if (mFabTranslationY == targetTransY) {
470                // We're already at (or currently animating to) the target value, return...
471                return;
472            }
473
474            mFabTranslationY = targetTransY;
475            final float currentTransY = ViewCompat.getTranslationY(fab);
476            final float dy = currentTransY - targetTransY;
477
478            if (Math.abs(dy) > (fab.getHeight() * 0.667f)) {
479                // If the FAB will be travelling by more than 2/3 of it's height, let's animate
480                // it instead
481                ViewCompat.animate(fab)
482                        .translationY(targetTransY)
483                        .scaleX(1f)
484                        .scaleY(1f)
485                        .alpha(1f)
486                        .setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR)
487                        .setListener(null);
488            } else {
489                // Make sure that any current animation is cancelled
490                ViewCompat.animate(fab).cancel();
491                // Now update the translation Y
492                ViewCompat.setTranslationY(fab, targetTransY);
493            }
494        }
495
496        private float getFabTranslationYForSnackbar(CoordinatorLayout parent,
497                FloatingActionButton fab) {
498            float minOffset = 0;
499            final List<View> dependencies = parent.getDependencies(fab);
500            for (int i = 0, z = dependencies.size(); i < z; i++) {
501                final View view = dependencies.get(i);
502                if (view instanceof Snackbar.SnackbarLayout && parent.doViewsOverlap(fab, view)) {
503                    minOffset = Math.min(minOffset,
504                            ViewCompat.getTranslationY(view) - view.getHeight());
505                }
506            }
507
508            return minOffset;
509        }
510
511        @Override
512        public boolean onLayoutChild(CoordinatorLayout parent, FloatingActionButton child,
513                int layoutDirection) {
514            // First, lets make sure that the visibility of the FAB is consistent
515            final List<View> dependencies = parent.getDependencies(child);
516            for (int i = 0, count = dependencies.size(); i < count; i++) {
517                final View dependency = dependencies.get(i);
518                if (dependency instanceof AppBarLayout
519                        && updateFabVisibility(parent, (AppBarLayout) dependency, child)) {
520                    break;
521                }
522            }
523            // Now let the CoordinatorLayout lay out the FAB
524            parent.onLayoutChild(child, layoutDirection);
525            // Now offset it if needed
526            offsetIfNeeded(parent, child);
527            return true;
528        }
529
530        /**
531         * Pre-Lollipop we use padding so that the shadow has enough space to be drawn. This method
532         * offsets our layout position so that we're positioned correctly if we're on one of
533         * our parent's edges.
534         */
535        private void offsetIfNeeded(CoordinatorLayout parent, FloatingActionButton fab) {
536            final Rect padding = fab.mShadowPadding;
537
538            if (padding != null && padding.centerX() > 0 && padding.centerY() > 0) {
539                final CoordinatorLayout.LayoutParams lp =
540                        (CoordinatorLayout.LayoutParams) fab.getLayoutParams();
541
542                int offsetTB = 0, offsetLR = 0;
543
544                if (fab.getRight() >= parent.getWidth() - lp.rightMargin) {
545                    // If we're on the left edge, shift it the right
546                    offsetLR = padding.right;
547                } else if (fab.getLeft() <= lp.leftMargin) {
548                    // If we're on the left edge, shift it the left
549                    offsetLR = -padding.left;
550                }
551                if (fab.getBottom() >= parent.getBottom() - lp.bottomMargin) {
552                    // If we're on the bottom edge, shift it down
553                    offsetTB = padding.bottom;
554                } else if (fab.getTop() <= lp.topMargin) {
555                    // If we're on the top edge, shift it up
556                    offsetTB = -padding.top;
557                }
558
559                fab.offsetTopAndBottom(offsetTB);
560                fab.offsetLeftAndRight(offsetLR);
561            }
562        }
563    }
564}
565