| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License |
| */ |
| package com.android.settings.widget; |
| |
| import android.content.Context; |
| import android.content.res.Resources; |
| import android.content.res.TypedArray; |
| import android.graphics.Canvas; |
| import android.graphics.ColorFilter; |
| import android.graphics.Paint; |
| import android.graphics.PorterDuff; |
| import android.graphics.PorterDuffColorFilter; |
| import android.graphics.Typeface; |
| import android.icu.text.DecimalFormatSymbols; |
| import android.text.Layout; |
| import android.text.Spannable; |
| import android.text.SpannableString; |
| import android.text.Spanned; |
| import android.text.StaticLayout; |
| import android.text.TextPaint; |
| import android.text.TextUtils; |
| import android.text.style.RelativeSizeSpan; |
| import android.util.AttributeSet; |
| import android.view.View; |
| |
| import androidx.annotation.ColorRes; |
| import androidx.annotation.VisibleForTesting; |
| |
| import com.android.settings.R; |
| import com.android.settings.Utils; |
| |
| import java.util.Locale; |
| |
| /** |
| * DonutView represents a donut graph. It visualizes a certain percentage of fullness with a |
| * corresponding label with the fullness on the inside (i.e. "50%" inside of the donut). |
| */ |
| public class DonutView extends View { |
| private static final int TOP = -90; |
| // From manual testing, this is the longest we can go without visual errors. |
| private static final int LINE_CHARACTER_LIMIT = 10; |
| private float mStrokeWidth; |
| private double mPercent; |
| private Paint mBackgroundCircle; |
| private Paint mFilledArc; |
| private TextPaint mTextPaint; |
| private TextPaint mBigNumberPaint; |
| private String mPercentString; |
| private String mFullString; |
| private boolean mShowPercentString = true; |
| private int mMeterBackgroundColor; |
| private int mMeterConsumedColor; |
| |
| public DonutView(Context context) { |
| super(context); |
| } |
| |
| public DonutView(Context context, AttributeSet attrs) { |
| super(context, attrs); |
| mMeterBackgroundColor = context.getColor(R.color.meter_background_color); |
| mMeterConsumedColor = Utils.getColorStateListDefaultColor(mContext, |
| R.color.meter_consumed_color); |
| boolean applyColorAccent = true; |
| Resources resources = context.getResources(); |
| mStrokeWidth = resources.getDimension(R.dimen.storage_donut_thickness); |
| |
| if (attrs != null) { |
| TypedArray styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.DonutView); |
| mMeterBackgroundColor = styledAttrs.getColor(R.styleable.DonutView_meterBackgroundColor, |
| mMeterBackgroundColor); |
| mMeterConsumedColor = styledAttrs.getColor(R.styleable.DonutView_meterConsumedColor, |
| mMeterConsumedColor); |
| applyColorAccent = styledAttrs.getBoolean(R.styleable.DonutView_applyColorAccent, |
| true); |
| mShowPercentString = styledAttrs.getBoolean(R.styleable.DonutView_showPercentString, |
| true); |
| mStrokeWidth = styledAttrs.getDimensionPixelSize(R.styleable.DonutView_thickness, |
| (int) mStrokeWidth); |
| styledAttrs.recycle(); |
| } |
| |
| mBackgroundCircle = new Paint(); |
| mBackgroundCircle.setAntiAlias(true); |
| mBackgroundCircle.setStrokeCap(Paint.Cap.BUTT); |
| mBackgroundCircle.setStyle(Paint.Style.STROKE); |
| mBackgroundCircle.setStrokeWidth(mStrokeWidth); |
| mBackgroundCircle.setColor(mMeterBackgroundColor); |
| |
| mFilledArc = new Paint(); |
| mFilledArc.setAntiAlias(true); |
| mFilledArc.setStrokeCap(Paint.Cap.BUTT); |
| mFilledArc.setStyle(Paint.Style.STROKE); |
| mFilledArc.setStrokeWidth(mStrokeWidth); |
| mFilledArc.setColor(mMeterConsumedColor); |
| |
| if (applyColorAccent) { |
| final ColorFilter mAccentColorFilter = |
| new PorterDuffColorFilter( |
| Utils.getColorAttrDefaultColor(context, android.R.attr.colorAccent), |
| PorterDuff.Mode.SRC_IN); |
| mBackgroundCircle.setColorFilter(mAccentColorFilter); |
| mFilledArc.setColorFilter(mAccentColorFilter); |
| } |
| |
| final Locale locale = resources.getConfiguration().locale; |
| final int layoutDirection = TextUtils.getLayoutDirectionFromLocale(locale); |
| final int bidiFlags = (layoutDirection == LAYOUT_DIRECTION_LTR) |
| ? Paint.BIDI_LTR |
| : Paint.BIDI_RTL; |
| |
| mTextPaint = new TextPaint(); |
| mTextPaint.setColor(Utils.getColorAccentDefaultColor(getContext())); |
| mTextPaint.setAntiAlias(true); |
| mTextPaint.setTextSize( |
| resources.getDimension(R.dimen.storage_donut_view_label_text_size)); |
| mTextPaint.setTextAlign(Paint.Align.CENTER); |
| mTextPaint.setBidiFlags(bidiFlags); |
| |
| mBigNumberPaint = new TextPaint(); |
| mBigNumberPaint.setColor(Utils.getColorAccentDefaultColor(getContext())); |
| mBigNumberPaint.setAntiAlias(true); |
| mBigNumberPaint.setTextSize( |
| resources.getDimension(R.dimen.storage_donut_view_percent_text_size)); |
| mBigNumberPaint.setTypeface(Typeface.create( |
| context.getString(com.android.internal.R.string.config_headlineFontFamily), |
| Typeface.NORMAL)); |
| mBigNumberPaint.setBidiFlags(bidiFlags); |
| } |
| |
| @Override |
| protected void onDraw(Canvas canvas) { |
| super.onDraw(canvas); |
| drawDonut(canvas); |
| if (mShowPercentString) { |
| drawInnerText(canvas); |
| } |
| } |
| |
| private void drawDonut(Canvas canvas) { |
| canvas.drawArc( |
| 0 + mStrokeWidth, |
| 0 + mStrokeWidth, |
| getWidth() - mStrokeWidth, |
| getHeight() - mStrokeWidth, |
| TOP, |
| 360, |
| false, |
| mBackgroundCircle); |
| |
| canvas.drawArc( |
| 0 + mStrokeWidth, |
| 0 + mStrokeWidth, |
| getWidth() - mStrokeWidth, |
| getHeight() - mStrokeWidth, |
| TOP, |
| (360 * (float) mPercent), |
| false, |
| mFilledArc); |
| } |
| |
| private void drawInnerText(Canvas canvas) { |
| final float centerX = getWidth() / 2; |
| final float centerY = getHeight() / 2; |
| final float totalHeight = getTextHeight(mTextPaint) + getTextHeight(mBigNumberPaint); |
| final float startY = centerY + totalHeight / 2; |
| // Support from Android P |
| final String localizedPercentSign = new DecimalFormatSymbols().getPercentString(); |
| |
| // The first line y-coordinates start at (total height - all TextPaint height) / 2 |
| canvas.save(); |
| final Spannable percentStringSpan = |
| getPercentageStringSpannable(getResources(), mPercentString, localizedPercentSign); |
| final StaticLayout percentStringLayout = new StaticLayout(percentStringSpan, |
| mBigNumberPaint, getWidth(), Layout.Alignment.ALIGN_CENTER, 1, 0, false); |
| canvas.translate(0, (getHeight() - totalHeight) / 2); |
| percentStringLayout.draw(canvas); |
| canvas.restore(); |
| |
| // The second line starts at the bottom + room for the descender. |
| canvas.drawText(mFullString, centerX, startY - mTextPaint.descent(), mTextPaint); |
| } |
| |
| /** |
| * Set a percentage full to have the donut graph. |
| */ |
| public void setPercentage(double percent) { |
| mPercent = percent; |
| mPercentString = Utils.formatPercentage(mPercent); |
| mFullString = getContext().getString(R.string.storage_percent_full); |
| if (mFullString.length() > LINE_CHARACTER_LIMIT) { |
| mTextPaint.setTextSize( |
| getContext() |
| .getResources() |
| .getDimension( |
| R.dimen.storage_donut_view_shrunken_label_text_size)); |
| } |
| setContentDescription(getContext().getString( |
| R.string.join_two_unrelated_items, mPercentString, mFullString)); |
| invalidate(); |
| } |
| |
| @ColorRes |
| public int getMeterBackgroundColor() { |
| return mMeterBackgroundColor; |
| } |
| |
| public void setMeterBackgroundColor(@ColorRes int meterBackgroundColor) { |
| mMeterBackgroundColor = meterBackgroundColor; |
| mBackgroundCircle.setColor(meterBackgroundColor); |
| invalidate(); |
| } |
| |
| @ColorRes |
| public int getMeterConsumedColor() { |
| return mMeterConsumedColor; |
| } |
| |
| public void setMeterConsumedColor(@ColorRes int meterConsumedColor) { |
| mMeterConsumedColor = meterConsumedColor; |
| mFilledArc.setColor(meterConsumedColor); |
| invalidate(); |
| } |
| |
| @VisibleForTesting |
| static Spannable getPercentageStringSpannable( |
| Resources resources, String percentString, String percentageSignString) { |
| final float fontProportion = |
| resources.getDimension(R.dimen.storage_donut_view_percent_sign_size) |
| / resources.getDimension(R.dimen.storage_donut_view_percent_text_size); |
| final Spannable percentStringSpan = new SpannableString(percentString); |
| int startIndex = percentString.indexOf(percentageSignString); |
| int endIndex = startIndex + percentageSignString.length(); |
| |
| // Fallback to no small string if we can't find the percentage sign. |
| if (startIndex < 0) { |
| startIndex = 0; |
| endIndex = percentString.length(); |
| } |
| |
| percentStringSpan.setSpan( |
| new RelativeSizeSpan(fontProportion), |
| startIndex, |
| endIndex, |
| Spanned.SPAN_EXCLUSIVE_INCLUSIVE); |
| return percentStringSpan; |
| } |
| |
| private float getTextHeight(TextPaint paint) { |
| // Technically, this should be the cap height, but I can live with the descent - ascent. |
| return paint.descent() - paint.ascent(); |
| } |
| } |