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