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