1/*
2 * Copyright (C) 2017 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 */
16package com.android.car.apps.common;
17
18import android.animation.ValueAnimator;
19import android.content.Context;
20import android.graphics.Canvas;
21import android.graphics.Color;
22import android.graphics.ColorFilter;
23import android.graphics.Outline;
24import android.graphics.Paint;
25import android.graphics.PixelFormat;
26import android.graphics.Rect;
27import android.graphics.drawable.Drawable;
28import android.view.animation.DecelerateInterpolator;
29
30/**
31 * Custom drawable that can be used as the background for fabs.
32 *
33 * When not focused or pressed, the fab will be a solid circle of the color specified with
34 * {@link #setFabColor(int)}. When it is pressed or focused, the fab will grow or shrink
35 * and it will gain a stroke that has the color specified with {@link #setStrokeColor(int)}.
36 *
37 * {@link #FabDrawable(android.content.Context)} provides a quick way to use fab drawable using
38 * default values for size and animation values provided for consistency.
39 *
40 * {@link #FabDrawable(int, int, int)} can also be used for added customization.
41 * @hide
42 */
43public class FabDrawable extends Drawable {
44    private final int mFabGrowth;
45    private final int mStrokeWidth;
46    private final Paint mFabPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
47    private final Paint mStrokePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
48    private final ValueAnimator mStrokeAnimator;
49
50    private boolean mStrokeAnimatorIsReversing;
51    private int mFabRadius;
52    private int mStrokeRadius;
53    private Outline mOutline;
54
55    /**
56     * Default constructor to provide consistent fab values across uses.
57     */
58    public FabDrawable(Context context) {
59        this(context.getResources().getDimensionPixelSize(R.dimen.car_fab_focused_growth),
60                context.getResources().getDimensionPixelSize(R.dimen.car_fab_focused_stroke_width),
61                context.getResources().getInteger(R.integer.car_fab_animation_duration));
62    }
63
64    /**
65     * Custom constructor allows extra customization of the fab's behavior.
66     *
67     * @param fabGrowth The amount that the fab should change by when it is focused in pixels.
68     * @param strokeWidth The width of the stroke when the fab is focused in pixels.
69     * @param duration The animation duration for the growth of the fab and stroke.
70     */
71    public FabDrawable(int fabGrowth, int strokeWidth, int duration) {
72        if (fabGrowth < 0) {
73            throw new IllegalArgumentException("Fab growth must be >= 0.");
74        } else if (fabGrowth > strokeWidth) {
75            throw new IllegalArgumentException("Fab growth must be <= strokeWidth.");
76        } else if (strokeWidth < 0) {
77            throw new IllegalArgumentException("Stroke width must be >= 0.");
78        }
79        mFabGrowth = fabGrowth;
80        mStrokeWidth = strokeWidth;
81        mStrokeAnimator = ValueAnimator.ofFloat(0f, 1f).setDuration(duration);
82        mStrokeAnimator.setInterpolator(new DecelerateInterpolator());
83        mStrokeAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
84            @Override
85            public void onAnimationUpdate(ValueAnimator valueAnimator) {
86                updateRadius();
87            }
88        });
89    }
90
91    /**
92     * @param color The primary color of the fab. It will be the entire fab color when not selected
93     *              or pressed and will be the color of the interior circle when selected
94     *              or pressed.
95     */
96    public void setFabColor(int color) {
97        mFabPaint.setColor(color);
98    }
99
100    /**
101     * @param color The color of the stroke on the fab that appears when the fab is selected
102     *              or pressed.
103     */
104    public void setStrokeColor(int color) {
105        mStrokePaint.setColor(color);
106    }
107
108    /**
109     * Default implementation of {@link #setFabAndStrokeColor(int, float)} with valueMultiplier
110     * set to 0.9.
111     */
112    public void setFabAndStrokeColor(int color) {
113        setFabAndStrokeColor(color, 0.9f);
114    }
115
116    /**
117     * @param color The primary color of the fab.
118     * @param valueMultiplier The hsv value multiplier that will be set as the stroke color.
119     */
120    public void setFabAndStrokeColor(int color, float valueMultiplier) {
121        setFabColor(color);
122        float[] hsv = new float[3];
123        Color.colorToHSV(color, hsv);
124        hsv[2] *= valueMultiplier;
125        setStrokeColor(Color.HSVToColor(hsv));
126    }
127
128    @Override
129    protected boolean onStateChange(int[] stateSet) {
130        boolean superChanged = super.onStateChange(stateSet);
131
132        boolean focused = false;
133        boolean pressed = false;
134
135        for (int state : stateSet) {
136            if (state == android.R.attr.state_focused) {
137                focused = true;
138            } else if (state == android.R.attr.state_pressed) {
139                pressed = true;
140            }
141        }
142
143        if ((focused || pressed) && mStrokeAnimatorIsReversing) {
144            mStrokeAnimator.start();
145            mStrokeAnimatorIsReversing = false;
146        } else if (!(focused || pressed) && !mStrokeAnimatorIsReversing) {
147            mStrokeAnimator.reverse();
148            mStrokeAnimatorIsReversing = true;
149        }
150
151        return superChanged || focused;
152    }
153
154    @Override
155    public void draw(Canvas canvas) {
156        int cx = canvas.getWidth() / 2;
157        int cy = canvas.getHeight() / 2;
158
159        canvas.drawCircle(cx, cy, mStrokeRadius, mStrokePaint);
160        canvas.drawCircle(cx, cy, mFabRadius, mFabPaint);
161    }
162
163    @Override
164    protected void onBoundsChange(Rect bounds) {
165        updateRadius();
166    }
167
168    @Override
169    public void setAlpha(int alpha) {
170        mFabPaint.setAlpha(alpha);
171        mStrokePaint.setAlpha(alpha);
172    }
173
174    @Override
175    public void setColorFilter(ColorFilter colorFilter) {
176        mFabPaint.setColorFilter(colorFilter);
177        mStrokePaint.setColorFilter(colorFilter);
178    }
179
180    @Override
181    public int getOpacity() {
182        return PixelFormat.OPAQUE;
183    }
184
185    @Override
186    public void getOutline(Outline outline) {
187        mOutline = outline;
188        updateOutline();
189    }
190
191    @Override
192    public boolean isStateful() {
193        return true;
194    }
195
196    private void updateRadius() {
197        int normalRadius = Math.min(getBounds().width(), getBounds().height()) / 2 - mStrokeWidth;
198        float fraction = mStrokeAnimator.getAnimatedFraction();
199        mStrokeRadius = (int) (normalRadius + (mStrokeWidth * fraction));
200        mFabRadius = (int) (normalRadius + (mFabGrowth * fraction));
201        updateOutline();
202        invalidateSelf();
203    }
204
205    private void updateOutline() {
206        int cx = getBounds().width() / 2;
207        int cy = getBounds().height() / 2;
208        if (mOutline != null) {
209            mOutline.setRoundRect(
210                    cx - mStrokeRadius,
211                    cy - mStrokeRadius,
212                    cx + mStrokeRadius,
213                    cy + mStrokeRadius,
214                    mStrokeRadius);
215        }
216    }
217}
218