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