1 /* 2 * Copyright (C) 2011 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 17 package com.android.settings.widget; 18 19 import android.content.Context; 20 import android.content.res.Resources; 21 import android.graphics.Canvas; 22 import android.graphics.Color; 23 import android.graphics.Matrix; 24 import android.graphics.Paint; 25 import android.graphics.Paint.Style; 26 import android.graphics.Path; 27 import android.graphics.Path.Direction; 28 import android.graphics.RadialGradient; 29 import android.graphics.RectF; 30 import android.graphics.Shader.TileMode; 31 import android.util.AttributeSet; 32 import android.util.Log; 33 import android.view.View; 34 35 import com.google.android.collect.Lists; 36 37 import java.util.ArrayList; 38 39 /** 40 * Pie chart with multiple items. 41 */ 42 public class PieChartView extends View { 43 public static final String TAG = "PieChartView"; 44 public static final boolean LOGD = false; 45 46 private static final boolean FILL_GRADIENT = false; 47 48 private ArrayList<Slice> mSlices = Lists.newArrayList(); 49 50 private int mOriginAngle; 51 private Matrix mMatrix = new Matrix(); 52 53 private Paint mPaintOutline = new Paint(); 54 55 private Path mPathSide = new Path(); 56 private Path mPathSideOutline = new Path(); 57 58 private Path mPathOutline = new Path(); 59 60 private int mSideWidth; 61 62 public class Slice { 63 public long value; 64 65 public Path path = new Path(); 66 public Path pathSide = new Path(); 67 public Path pathOutline = new Path(); 68 69 public Paint paint; 70 71 public Slice(long value, int color) { 72 this.value = value; 73 this.paint = buildFillPaint(color, getResources()); 74 } 75 } 76 77 public PieChartView(Context context) { 78 this(context, null); 79 } 80 81 public PieChartView(Context context, AttributeSet attrs) { 82 this(context, attrs, 0); 83 } 84 85 public PieChartView(Context context, AttributeSet attrs, int defStyle) { 86 super(context, attrs, defStyle); 87 88 mPaintOutline.setColor(Color.BLACK); 89 mPaintOutline.setStyle(Style.STROKE); 90 mPaintOutline.setStrokeWidth(3f * getResources().getDisplayMetrics().density); 91 mPaintOutline.setAntiAlias(true); 92 93 mSideWidth = (int) (20 * getResources().getDisplayMetrics().density); 94 95 setWillNotDraw(false); 96 } 97 98 private static Paint buildFillPaint(int color, Resources res) { 99 final Paint paint = new Paint(); 100 101 paint.setColor(color); 102 paint.setStyle(Style.FILL_AND_STROKE); 103 paint.setAntiAlias(true); 104 105 if (FILL_GRADIENT) { 106 final int width = (int) (280 * res.getDisplayMetrics().density); 107 paint.setShader(new RadialGradient(0, 0, width, color, darken(color), TileMode.MIRROR)); 108 } 109 110 return paint; 111 } 112 113 public void setOriginAngle(int originAngle) { 114 mOriginAngle = originAngle; 115 } 116 117 public void addSlice(long value, int color) { 118 mSlices.add(new Slice(value, color)); 119 } 120 121 public void removeAllSlices() { 122 mSlices.clear(); 123 } 124 125 @Override 126 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 127 final float centerX = getWidth() / 2; 128 final float centerY = getHeight() / 2; 129 130 mMatrix.reset(); 131 mMatrix.postScale(0.665f, 0.95f, centerX, centerY); 132 mMatrix.postRotate(-40, centerX, centerY); 133 134 generatePath(); 135 } 136 137 public void generatePath() { 138 if (LOGD) Log.d(TAG, "generatePath()"); 139 140 long total = 0; 141 for (Slice slice : mSlices) { 142 slice.path.reset(); 143 slice.pathSide.reset(); 144 slice.pathOutline.reset(); 145 total += slice.value; 146 } 147 148 mPathSide.reset(); 149 mPathSideOutline.reset(); 150 mPathOutline.reset(); 151 152 // bail when not enough stats to render 153 if (total == 0) { 154 invalidate(); 155 return; 156 } 157 158 final int width = getWidth(); 159 final int height = getHeight(); 160 161 final RectF rect = new RectF(0, 0, width, height); 162 final RectF rectSide = new RectF(); 163 rectSide.set(rect); 164 rectSide.offset(-mSideWidth, 0); 165 166 mPathSide.addOval(rectSide, Direction.CW); 167 mPathSideOutline.addOval(rectSide, Direction.CW); 168 mPathOutline.addOval(rect, Direction.CW); 169 170 int startAngle = mOriginAngle; 171 for (Slice slice : mSlices) { 172 final int sweepAngle = (int) (slice.value * 360 / total); 173 final int endAngle = startAngle + sweepAngle; 174 175 final float startAngleMod = startAngle % 360; 176 final float endAngleMod = endAngle % 360; 177 final boolean startSideVisible = startAngleMod > 90 && startAngleMod < 270; 178 final boolean endSideVisible = endAngleMod > 90 && endAngleMod < 270; 179 180 // draw slice 181 slice.path.moveTo(rect.centerX(), rect.centerY()); 182 slice.path.arcTo(rect, startAngle, sweepAngle); 183 slice.path.lineTo(rect.centerX(), rect.centerY()); 184 185 if (startSideVisible || endSideVisible) { 186 187 // when start is beyond horizon, push until visible 188 final float startAngleSide = startSideVisible ? startAngle : 450; 189 final float endAngleSide = endSideVisible ? endAngle : 270; 190 final float sweepAngleSide = endAngleSide - startAngleSide; 191 192 // draw slice side 193 slice.pathSide.moveTo(rect.centerX(), rect.centerY()); 194 slice.pathSide.arcTo(rect, startAngleSide, 0); 195 slice.pathSide.rLineTo(-mSideWidth, 0); 196 slice.pathSide.arcTo(rectSide, startAngleSide, sweepAngleSide); 197 slice.pathSide.rLineTo(mSideWidth, 0); 198 slice.pathSide.arcTo(rect, endAngleSide, -sweepAngleSide); 199 } 200 201 // draw slice outline 202 slice.pathOutline.moveTo(rect.centerX(), rect.centerY()); 203 slice.pathOutline.arcTo(rect, startAngle, 0); 204 if (startSideVisible) { 205 slice.pathOutline.rLineTo(-mSideWidth, 0); 206 } 207 slice.pathOutline.moveTo(rect.centerX(), rect.centerY()); 208 slice.pathOutline.arcTo(rect, startAngle + sweepAngle, 0); 209 if (endSideVisible) { 210 slice.pathOutline.rLineTo(-mSideWidth, 0); 211 } 212 213 startAngle += sweepAngle; 214 } 215 216 invalidate(); 217 } 218 219 @Override 220 protected void onDraw(Canvas canvas) { 221 222 canvas.concat(mMatrix); 223 224 for (Slice slice : mSlices) { 225 canvas.drawPath(slice.pathSide, slice.paint); 226 } 227 canvas.drawPath(mPathSideOutline, mPaintOutline); 228 229 for (Slice slice : mSlices) { 230 canvas.drawPath(slice.path, slice.paint); 231 canvas.drawPath(slice.pathOutline, mPaintOutline); 232 } 233 canvas.drawPath(mPathOutline, mPaintOutline); 234 } 235 236 public static int darken(int color) { 237 float[] hsv = new float[3]; 238 Color.colorToHSV(color, hsv); 239 hsv[2] /= 2; 240 hsv[1] /= 2; 241 return Color.HSVToColor(hsv); 242 } 243 244 } 245