Home | History | Annotate | Download | only in common
      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  */
     16 package com.android.car.apps.common;
     17 
     18 import android.animation.ValueAnimator;
     19 import android.content.Context;
     20 import android.graphics.Canvas;
     21 import android.graphics.Color;
     22 import android.graphics.ColorFilter;
     23 import android.graphics.Outline;
     24 import android.graphics.Paint;
     25 import android.graphics.PixelFormat;
     26 import android.graphics.Rect;
     27 import android.graphics.drawable.Drawable;
     28 import 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  */
     43 public 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