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.settings.widget; 17 18 import android.content.Context; 19 import android.content.res.Resources; 20 import android.content.res.TypedArray; 21 import android.graphics.Canvas; 22 import android.graphics.ColorFilter; 23 import android.graphics.Paint; 24 import android.graphics.PorterDuff; 25 import android.graphics.PorterDuffColorFilter; 26 import android.graphics.Typeface; 27 import android.icu.text.DecimalFormatSymbols; 28 import android.support.annotation.ColorRes; 29 import android.text.Layout; 30 import android.text.Spannable; 31 import android.text.SpannableString; 32 import android.text.Spanned; 33 import android.text.StaticLayout; 34 import android.text.TextPaint; 35 import android.text.TextUtils; 36 import android.text.style.RelativeSizeSpan; 37 import android.util.AttributeSet; 38 import android.view.View; 39 40 import com.android.internal.annotations.VisibleForTesting; 41 import com.android.settings.R; 42 import com.android.settings.Utils; 43 44 import java.util.Locale; 45 46 /** 47 * DonutView represents a donut graph. It visualizes a certain percentage of fullness with a 48 * corresponding label with the fullness on the inside (i.e. "50%" inside of the donut). 49 */ 50 public class DonutView extends View { 51 private static final int TOP = -90; 52 // From manual testing, this is the longest we can go without visual errors. 53 private static final int LINE_CHARACTER_LIMIT = 10; 54 private float mStrokeWidth; 55 private double mPercent; 56 private Paint mBackgroundCircle; 57 private Paint mFilledArc; 58 private TextPaint mTextPaint; 59 private TextPaint mBigNumberPaint; 60 private String mPercentString; 61 private String mFullString; 62 private boolean mShowPercentString = true; 63 private int mMeterBackgroundColor; 64 private int mMeterConsumedColor; 65 DonutView(Context context)66 public DonutView(Context context) { 67 super(context); 68 } 69 DonutView(Context context, AttributeSet attrs)70 public DonutView(Context context, AttributeSet attrs) { 71 super(context, attrs); 72 mMeterBackgroundColor = context.getColor(R.color.meter_background_color); 73 mMeterConsumedColor = Utils.getDefaultColor(mContext, R.color.meter_consumed_color); 74 boolean applyColorAccent = true; 75 Resources resources = context.getResources(); 76 mStrokeWidth = resources.getDimension(R.dimen.storage_donut_thickness); 77 78 if (attrs != null) { 79 TypedArray styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.DonutView); 80 mMeterBackgroundColor = styledAttrs.getColor(R.styleable.DonutView_meterBackgroundColor, 81 mMeterBackgroundColor); 82 mMeterConsumedColor = styledAttrs.getColor(R.styleable.DonutView_meterConsumedColor, 83 mMeterConsumedColor); 84 applyColorAccent = styledAttrs.getBoolean(R.styleable.DonutView_applyColorAccent, 85 true); 86 mShowPercentString = styledAttrs.getBoolean(R.styleable.DonutView_showPercentString, 87 true); 88 mStrokeWidth = styledAttrs.getDimensionPixelSize(R.styleable.DonutView_thickness, 89 (int) mStrokeWidth); 90 styledAttrs.recycle(); 91 } 92 93 mBackgroundCircle = new Paint(); 94 mBackgroundCircle.setAntiAlias(true); 95 mBackgroundCircle.setStrokeCap(Paint.Cap.BUTT); 96 mBackgroundCircle.setStyle(Paint.Style.STROKE); 97 mBackgroundCircle.setStrokeWidth(mStrokeWidth); 98 mBackgroundCircle.setColor(mMeterBackgroundColor); 99 100 mFilledArc = new Paint(); 101 mFilledArc.setAntiAlias(true); 102 mFilledArc.setStrokeCap(Paint.Cap.BUTT); 103 mFilledArc.setStyle(Paint.Style.STROKE); 104 mFilledArc.setStrokeWidth(mStrokeWidth); 105 mFilledArc.setColor(mMeterConsumedColor); 106 107 if (applyColorAccent) { 108 final ColorFilter mAccentColorFilter = 109 new PorterDuffColorFilter( 110 Utils.getColorAttr(context, android.R.attr.colorAccent), 111 PorterDuff.Mode.SRC_IN); 112 mBackgroundCircle.setColorFilter(mAccentColorFilter); 113 mFilledArc.setColorFilter(mAccentColorFilter); 114 } 115 116 final Locale locale = resources.getConfiguration().locale; 117 final int layoutDirection = TextUtils.getLayoutDirectionFromLocale(locale); 118 final int bidiFlags = (layoutDirection == LAYOUT_DIRECTION_LTR) 119 ? Paint.BIDI_LTR 120 : Paint.BIDI_RTL; 121 122 mTextPaint = new TextPaint(); 123 mTextPaint.setColor(Utils.getColorAccent(getContext())); 124 mTextPaint.setAntiAlias(true); 125 mTextPaint.setTextSize( 126 resources.getDimension(R.dimen.storage_donut_view_label_text_size)); 127 mTextPaint.setTextAlign(Paint.Align.CENTER); 128 mTextPaint.setBidiFlags(bidiFlags); 129 130 mBigNumberPaint = new TextPaint(); 131 mBigNumberPaint.setColor(Utils.getColorAccent(getContext())); 132 mBigNumberPaint.setAntiAlias(true); 133 mBigNumberPaint.setTextSize( 134 resources.getDimension(R.dimen.storage_donut_view_percent_text_size)); 135 mBigNumberPaint.setTypeface(Typeface.create( 136 context.getString(com.android.internal.R.string.config_headlineFontFamily), 137 Typeface.NORMAL)); 138 mBigNumberPaint.setBidiFlags(bidiFlags); 139 } 140 141 @Override onDraw(Canvas canvas)142 protected void onDraw(Canvas canvas) { 143 super.onDraw(canvas); 144 drawDonut(canvas); 145 if (mShowPercentString) { 146 drawInnerText(canvas); 147 } 148 } 149 drawDonut(Canvas canvas)150 private void drawDonut(Canvas canvas) { 151 canvas.drawArc( 152 0 + mStrokeWidth, 153 0 + mStrokeWidth, 154 getWidth() - mStrokeWidth, 155 getHeight() - mStrokeWidth, 156 TOP, 157 360, 158 false, 159 mBackgroundCircle); 160 161 canvas.drawArc( 162 0 + mStrokeWidth, 163 0 + mStrokeWidth, 164 getWidth() - mStrokeWidth, 165 getHeight() - mStrokeWidth, 166 TOP, 167 (360 * (float) mPercent), 168 false, 169 mFilledArc); 170 } 171 drawInnerText(Canvas canvas)172 private void drawInnerText(Canvas canvas) { 173 final float centerX = getWidth() / 2; 174 final float centerY = getHeight() / 2; 175 final float totalHeight = getTextHeight(mTextPaint) + getTextHeight(mBigNumberPaint); 176 final float startY = centerY + totalHeight / 2; 177 // Support from Android P 178 final String localizedPercentSign = new DecimalFormatSymbols().getPercentString(); 179 180 // The first line y-coordinates start at (total height - all TextPaint height) / 2 181 canvas.save(); 182 final Spannable percentStringSpan = 183 getPercentageStringSpannable(getResources(), mPercentString, localizedPercentSign); 184 final StaticLayout percentStringLayout = new StaticLayout(percentStringSpan, 185 mBigNumberPaint, getWidth(), Layout.Alignment.ALIGN_CENTER, 1, 0, false); 186 canvas.translate(0, (getHeight() - totalHeight) / 2); 187 percentStringLayout.draw(canvas); 188 canvas.restore(); 189 190 // The second line starts at the bottom + room for the descender. 191 canvas.drawText(mFullString, centerX, startY - mTextPaint.descent(), mTextPaint); 192 } 193 194 /** 195 * Set a percentage full to have the donut graph. 196 */ setPercentage(double percent)197 public void setPercentage(double percent) { 198 mPercent = percent; 199 mPercentString = Utils.formatPercentage(mPercent); 200 mFullString = getContext().getString(R.string.storage_percent_full); 201 if (mFullString.length() > LINE_CHARACTER_LIMIT) { 202 mTextPaint.setTextSize( 203 getContext() 204 .getResources() 205 .getDimension( 206 R.dimen.storage_donut_view_shrunken_label_text_size)); 207 } 208 setContentDescription(getContext().getString( 209 R.string.join_many_items_middle, mPercentString, mFullString)); 210 invalidate(); 211 } 212 213 @ColorRes getMeterBackgroundColor()214 public int getMeterBackgroundColor() { 215 return mMeterBackgroundColor; 216 } 217 setMeterBackgroundColor(@olorRes int meterBackgroundColor)218 public void setMeterBackgroundColor(@ColorRes int meterBackgroundColor) { 219 mMeterBackgroundColor = meterBackgroundColor; 220 mBackgroundCircle.setColor(meterBackgroundColor); 221 invalidate(); 222 } 223 224 @ColorRes getMeterConsumedColor()225 public int getMeterConsumedColor() { 226 return mMeterConsumedColor; 227 } 228 setMeterConsumedColor(@olorRes int meterConsumedColor)229 public void setMeterConsumedColor(@ColorRes int meterConsumedColor) { 230 mMeterConsumedColor = meterConsumedColor; 231 mFilledArc.setColor(meterConsumedColor); 232 invalidate(); 233 } 234 235 @VisibleForTesting getPercentageStringSpannable( Resources resources, String percentString, String percentageSignString)236 static Spannable getPercentageStringSpannable( 237 Resources resources, String percentString, String percentageSignString) { 238 final float fontProportion = 239 resources.getDimension(R.dimen.storage_donut_view_percent_sign_size) 240 / resources.getDimension(R.dimen.storage_donut_view_percent_text_size); 241 final Spannable percentStringSpan = new SpannableString(percentString); 242 int startIndex = percentString.indexOf(percentageSignString); 243 int endIndex = startIndex + percentageSignString.length(); 244 245 // Fallback to no small string if we can't find the percentage sign. 246 if (startIndex < 0) { 247 startIndex = 0; 248 endIndex = percentString.length(); 249 } 250 251 percentStringSpan.setSpan( 252 new RelativeSizeSpan(fontProportion), 253 startIndex, 254 endIndex, 255 Spanned.SPAN_EXCLUSIVE_INCLUSIVE); 256 return percentStringSpan; 257 } 258 getTextHeight(TextPaint paint)259 private float getTextHeight(TextPaint paint) { 260 // Technically, this should be the cap height, but I can live with the descent - ascent. 261 return paint.descent() - paint.ascent(); 262 } 263 } 264