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
17package com.android.settings.widget;
18
19import android.content.Context;
20import android.content.res.Resources;
21import android.graphics.Canvas;
22import android.graphics.Color;
23import android.graphics.Matrix;
24import android.graphics.Paint;
25import android.graphics.Paint.Style;
26import android.graphics.Path;
27import android.graphics.Path.Direction;
28import android.graphics.RadialGradient;
29import android.graphics.RectF;
30import android.graphics.Shader.TileMode;
31import android.util.AttributeSet;
32import android.util.Log;
33import android.view.View;
34
35import com.google.common.collect.Lists;
36
37import java.util.ArrayList;
38
39/**
40 * Pie chart with multiple items.
41 */
42public 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