• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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