Copy this link when reproducing:
http://www.casperlee.com/en/y/blog/204
To create a user-defined widget in Android application, there are 2 ways at least:
1. Extends a known widget and enriches its functions.
2. Extends the View class and creates the widget by drawing it totally.
For the PieView widget, I'll use the second method.
To create the PieView widget, it involves the following steps:
- create data classes for the widget
- create a class named "PieView" which extends the View class
- create an adapter class to handle the data
- Override the onMesure function of the PieView class to identify the size of the canvas
- Override the onDraw function of the PieView class to draw the widget
- Create event definitions for the widget
Here are the detailed steps:
1. Add a new class named "PieViewDataItem" in the folder "app -> java -> com.casperlee.personalexpense -> widgets", and put the following code in:
package com.casperlee.personalexpense.widgets;
public class PieViewDataItem {
public String Name;
public double Value;
public PieViewDataItem(String aName, double aValue) {
this.Name = aName;
this.Value = aValue;
}
}
2. Add a new class named "PieViewBarTable" in the folder "app -> java -> com.casperlee.personalexpense -> widgets", and put the following code in:
package com.casperlee.personalexpense.widgets;
public class PieViewBarTable {
public int Rows = 1;
public int Cols = 1;
public PieViewBarTable() {
}
public PieViewBarTable(int aRows, int aCols) {
this.Rows = aRows;
this.Cols = aCols;
}
}
3. Add a new class named "PieViewParams" in the folder "app -> java -> com.casperlee.personalexpense -> widgets", and put the following code in:
package com.casperlee.personalexpense.widgets;
import android.graphics.Color;
public class PieViewParams {
// Constructors
public PieViewParams() {
this.getDefaultParams();
}
// Enum
public enum BarPosition {
Left,
Right,
Top,
Bottom
};
// Properties
// Title
private int titleFontSize;
public int getTitleFontSize() {
return titleFontSize;
}
public void setTitleFontSize(int titleFontSize) {
this.titleFontSize = titleFontSize;
}
private int titleFontColor;
public int getTitleFontColor() {
return titleFontColor;
}
public void setTitleFontColor(int titleFontColor) {
this.titleFontColor = titleFontColor;
}
private int titleUnderMargin;
public int getTitleUnderMargin() {
return titleUnderMargin;
}
public void setTitleUnderMargin(int titleUnderMargin) {
this.titleUnderMargin = titleUnderMargin;
}
// Bar
private BarPosition barPosition;
public BarPosition getBarPosition() {
return this.barPosition;
}
public void setBarPosition(BarPosition bp) {
this.barPosition = bp;
}
private int barRegionPaddingTop;
public int getBarRegionPaddingTop() {
return barRegionPaddingTop;
}
public void setBarRegionPaddingTop(int barRegionPaddingTop) {
this.barRegionPaddingTop = barRegionPaddingTop;
}
private int barRegionPaddingBottom;
public int getBarRegionPaddingBottom() {
return barRegionPaddingBottom;
}
public void setBarRegionPaddingBottom(int barRegionPaddingBottom) {
this.barRegionPaddingBottom = barRegionPaddingBottom;
}
private int barRegionPaddingLeft;
public int getBarRegionPaddingLeft() {
return barRegionPaddingLeft;
}
public void setBarRegionPaddingLeft(int barRegionPaddingLeft) {
this.barRegionPaddingLeft = barRegionPaddingLeft;
}
private int barRegionPaddingRight;
public int getBarRegionPaddingRight() {
return barRegionPaddingRight;
}
public void setBarRegionPaddingRight(int barRegionPaddingRight) {
this.barRegionPaddingRight = barRegionPaddingRight;
}
private int barItemGapVertical;
public int getBarItemGapVertical() {
return barItemGapVertical;
}
public void setBarItemGapVertical(int barItemGapVertical) {
this.barItemGapVertical = barItemGapVertical;
}
private int barItemGapHorizontal;
public int getBarItemGapHorizontal() {
return barItemGapHorizontal;
}
public void setItemGapHorizontal(int barItemGapHorizontal) {
this.barItemGapHorizontal = barItemGapHorizontal;
}
private int barItemGapBetweenColorAndText;
public int getBarItemGapBetweenColorAndText() {
return barItemGapBetweenColorAndText;
}
public void setBarItemGapBetweenColorAndText(int barItemGapBetweenColorAndText) {
this.barItemGapBetweenColorAndText = barItemGapBetweenColorAndText;
}
private int barItemTextSize;
public int getBarItemTextSize() {
return barItemTextSize;
}
public void setBarItemTextSize(int barItemTextSize) {
this.barItemTextSize = barItemTextSize;
}
private int barItemTextColor;
public int getBarItemTextColor() {
return barItemTextColor;
}
public void setBarItemTextColor(int barItemTextColor) {
this.barItemTextColor = barItemTextColor;
}
// Pie
private int[] sectionColors;
public int[] getSectionColors() {
return sectionColors;
}
public void setSectionColors(int[] sectionColors) {
this.sectionColors = sectionColors;
}
// Private methods
private void getDefaultParams() {
this.titleFontColor = Color.WHITE;
this.titleFontSize = 20;
this.titleUnderMargin = 16;
this.barPosition = BarPosition.Right;
this.barRegionPaddingTop = 16;
this.barRegionPaddingBottom = 16;
this.barRegionPaddingLeft = 16;
this.barRegionPaddingRight = 16;
this.barItemGapVertical = 6;
this.barItemGapHorizontal = 12;
this.barItemGapBetweenColorAndText = 6;
this.barItemTextSize = 16;
this.barItemTextColor = Color.WHITE;
this.sectionColors = new int[1];
this.sectionColors[0] = Color.RED;
}
}
4. Add a new class named "PieViewAdapter" in the folder "app -> java -> com.casperlee.personalexpense -> widgets", and put the following code in:
package com.casperlee.personalexpense.widgets;
import android.graphics.Color;
import java.util.List;
public class PieViewAdapter {
// Fields
private OnDatasetChangedListener listener;
// Entrances
public PieViewAdapter(List<PieViewDataItem> data) {
this.data = data;
this.getDefaultOptions();
}
// Interfaces
public interface OnDatasetChangedListener {
void OnDatasetChanged();
};
// Properties
private String title;
public String getTitle() {
return this.title;
}
public void setTitle(String title) {
this.title = title;
}
private List<PieViewDataItem> data;
public List<PieViewDataItem> getData() {
return data;
}
public void setData(List<PieViewDataItem> data) {
this.data = data;
this.setColorsForSections();
}
private PieViewParams options;
public PieViewParams getOptions() {
return options;
}
public void setOptions(PieViewParams options) {
this.options = options;
}
// Public methods
public void setListener(OnDatasetChangedListener l) {
this.listener = l;
}
public void notifyDatasetChanged() {
if (this.listener != null) {
this.listener.OnDatasetChanged();
}
}
// Private methods
private void getDefaultOptions() {
this.title = "";
this.options = new PieViewParams();
this.setColorsForSections();
}
private void setColorsForSections() {
if (this.data == null || this.data.size() == 0 || this.options == null) {
return;
}
int colorCount = this.data.size();
this.setColorsForSections(colorCount);
}
private void setColorsForSections(int colorCount) {
if (this.options == null || colorCount <= 0 || colorCount > 360) {
return;
}
int[] sectionColors = new int[colorCount];
float[] hsvColor = new float[3];
for (int i = 0; i < colorCount; i++) {
hsvColor[0] = (float) (360.0 * (colorCount - i) / colorCount);
hsvColor[1] = 1.0f;
hsvColor[2] = 1.0f;
sectionColors[i] = Color.HSVToColor(hsvColor);
}
this.options.setSectionColors(sectionColors);
}
}
Note: when generating a color for a section, I chose the function "Color.HSVToColor" so that each color that have been generated can be very different for human eyes.
5. Open the file "app -> java -> com.casperlee.personalexpense -> global -> GlobalFuncs", add the following code:
public static double getDPFromPixels(Activity activity, double pixels) {
DisplayMetrics metrics = new DisplayMetrics();
activity.getWindowManager().getDefaultDisplay().getMetrics(metrics);
switch(metrics.densityDpi){
case DisplayMetrics.DENSITY_LOW:
pixels = pixels * 0.75;
break;
case DisplayMetrics.DENSITY_MEDIUM:
//pixels = pixels * 1;
break;
case DisplayMetrics.DENSITY_HIGH:
pixels = pixels * 1.5;
break;
}
return pixels;
}
6. Add a new class named "PieView" in the folder "app -> java -> com.casperlee.personalexpense -> widgets", and put the following code in:
package com.casperlee.personalexpense.widgets;
import com.casperlee.personalexpense.global.GlobalFuncs;
import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.view.View;
import java.util.List;
public class PieView extends View {
// Fields
private Context context;
public static final int MESSAGE_DATASET_CHANGED = 0x1000;
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
private String title;
private List<PieViewDataItem> data;
private PieViewParams options;
// Entrances
public PieView(Context context, AttributeSet attrs) {
super(context, attrs);
this.initFields(context);
}
public Handler PieViewHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case PieView.MESSAGE_DATASET_CHANGED:
PieView pv = (PieView)msg.obj;
pv.invalidate();
break;
}
}
};
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = 286;
if (MeasureSpec.UNSPECIFIED != MeasureSpec.getMode(widthMeasureSpec)) {
width = MeasureSpec.getSize(widthMeasureSpec);
}
int height = 64;
if (MeasureSpec.UNSPECIFIED != MeasureSpec.getMode(heightMeasureSpec)) {
height = MeasureSpec.getSize(heightMeasureSpec);
}
setMeasuredDimension(width, height);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Rect rect = this.getWholeRegion();
this.drawTitle(canvas, rect);
this.drawBarItems(canvas, rect);
this.drawPie(canvas, rect);
}
// Properties
private PieViewAdapter adapter;
public void setAdapter(PieViewAdapter adapter) {
this.adapter = adapter;
this.adapter.setListener(new OnMyDatasetChangedListener());
this.refreshDataset();
}
public PieViewAdapter getAdapter() {
return this.adapter;
}
// Private methods
private void initFields(Context c) {
this.adapter = null;
this.context = c;
this.title = "";
this.options = new PieViewParams();
this.data = null;
}
private void refreshDataset() {
this.title = this.adapter.getTitle();
this.data = this.adapter.getData();
this.options = this.adapter.getOptions();
this.calcPercent();
}
private void calcPercent() {
if (this.data == null || this.data.size() == 0) {
return;
}
double total = 0;
for (int i = 0; i < data.size(); i++) {
total += data.get(i).Value;
}
if (total == 0) {
return;
}
double newTotal = 0;
for (int i = 0; i < data.size(); i++) {
data.get(i).Value = data.get(i).Value * 100.0 / total;
newTotal += data.get(i).Value;
}
if (newTotal != 100) {
data.get(data.size() - 1).Value -= newTotal - 100;
}
}
private Rect getWholeRegion() {
Rect rect = new Rect();
rect.left = this.getPaddingLeft();
rect.right = this.getWidth() - this.getPaddingRight();
rect.top = this.getPaddingTop();
rect.bottom = this.getHeight() - this.getPaddingBottom();
return rect;
}
private void drawTitle(Canvas canvas, Rect rect) {
if (this.title.equals("")) {
return;
}
int centerX = (int)(((double)rect.left + (double)rect.right) / 2.0);
int textSize = (int)GlobalFuncs.getDPFromPixels((Activity)this.context, this.options.getTitleFontSize());
paint.setColor(this.options.getTitleFontColor());
paint.setTextAlign(Align.CENTER);
paint.setTextSize(textSize);
canvas.drawText(this.title, centerX, rect.top + textSize / 2, paint);
rect.top += textSize + this.options.getTitleUnderMargin();
}
private void drawBarItems(Canvas canvas, Rect rect) {
if (this.data == null || this.data.size() == 0 || this.adapter == null) {
return;
}
PieViewBarTable barTable = new PieViewBarTable();
Rect barRect = this.getBarRegion(rect, barTable);
Rect barBlockRect = new Rect();
int itemHeight = this.getBarItemHeight();
int itemWidth = this.getLongestItemWidth();
int itemGapVertical = this.options.getBarItemGapVertical();
int itemGapHorizontal = this.options.getBarItemGapHorizontal();
int left;
int top = barRect.top;
for (int row = 0; row < barTable.Rows; row++) {
left = barRect.left;
for (int col = 0; col < barTable.Cols; col++) {
int i = row + col * barTable.Rows;
if (i < this.data.size()) {
// draw block
barBlockRect.set(left, top, left + itemHeight, top + itemHeight);
this.paint.setColor(this.options.getSectionColors()[i]);
canvas.drawRect(barBlockRect, this.paint);
// draw text
this.paint.setColor(this.options.getBarItemTextColor());
this.paint.setTextAlign(Align.LEFT);
canvas.drawText(this.data.get(i).Name, left + itemHeight + this.options.getBarItemGapBetweenColorAndText(), top + itemHeight / 2, paint);
}
left += itemWidth + itemGapHorizontal;
}
top += (itemHeight + itemGapVertical);
}
}
private void drawPie(Canvas canvas, Rect rect) {
// get square RectF
RectF circle = this.getCircleRect(rect);
if (this.data == null || this.data.size() == 0 || this.adapter == null) {
paint.setColor(Color.BLUE);
canvas.drawArc(circle, 0, 360, true, paint);
return;
}
float startAngle = 0;
float stopAngle = 0;
for (int i = 0; i < data.size(); i++) {
stopAngle += data.get(i).Value * 360.0 / 100.0;
if (stopAngle > 360 || i == data.size() - 1) {
stopAngle = 360;
}
paint.setColor(this.options.getSectionColors()[i]);
canvas.drawArc(circle, startAngle, stopAngle - startAngle, true, paint);
startAngle = stopAngle;
if (stopAngle == 360) {
break;
}
}
}
private Rect getBarRegion(Rect rect, PieViewBarTable barTable) {
Rect ret;
switch (this.options.getBarPosition()) {
case Left:
ret = this.getBarRegionLeft(rect, barTable);
break;
case Right:
ret = this.getBarRegionRight(rect, barTable);
break;
case Top:
ret = this.getBarRegionTop(rect, barTable);
break;
case Bottom:
ret = this.getBarRegionBottom(rect, barTable);
break;
default:
ret = this.getBarRegionRight(rect, barTable);
break;
}
return ret;
}
private Rect getBarRegionLeft(Rect rect, PieViewBarTable barTable) {
Rect ret = new Rect();
ret.set(rect.left, rect.top, rect.right - this.options.getBarRegionPaddingRight(), rect.bottom);
this.calcRowsAndCols(ret, barTable, true);
int regionWidth = barTable.Cols * this.getLongestItemWidth() + (barTable.Cols - 1) * this.options.getBarItemGapHorizontal();
int regionHeight = barTable.Rows * this.getBarItemHeight() + (barTable.Rows - 1) * this.options.getBarItemGapVertical();
ret.set(rect.left,
rect.top + (rect.bottom - rect.top - regionHeight) / 2,
rect.left + regionWidth + this.options.getBarRegionPaddingRight(),
rect.bottom - (rect.bottom - rect.top - regionHeight) / 2);
rect.set(ret.right, rect.top, rect.right, rect.bottom);
return ret;
}
private Rect getBarRegionRight(Rect rect, PieViewBarTable barTable) {
Rect ret = new Rect();
ret.set(rect.left + this.options.getBarRegionPaddingLeft(), rect.top, rect.right, rect.bottom);
this.calcRowsAndCols(ret, barTable, true);
int regionWidth = barTable.Cols * this.getLongestItemWidth() + (barTable.Cols - 1) * this.options.getBarItemGapHorizontal();
int regionHeight = barTable.Rows * this.getBarItemHeight() + (barTable.Rows - 1) * this.options.getBarItemGapVertical();
ret.set(rect.right - regionWidth,
rect.top + (rect.bottom - rect.top - regionHeight) / 2,
rect.right,
rect.bottom - (rect.bottom - rect.top - regionHeight) / 2);
rect.set(rect.left, rect.top, ret.left - this.options.getBarRegionPaddingLeft(), rect.bottom);
return ret;
}
private Rect getBarRegionTop(Rect rect, PieViewBarTable barTable) {
Rect ret = new Rect();
ret.set(rect.left, rect.top, rect.right, rect.bottom - this.options.getBarRegionPaddingBottom());
this.calcRowsAndCols(ret, barTable, false);
int regionWidth = barTable.Cols * this.getLongestItemWidth() + (barTable.Cols - 1) * this.options.getBarItemGapHorizontal();
int regionHeight = barTable.Rows * this.getBarItemHeight() + (barTable.Rows - 1) * this.options.getBarItemGapVertical();
ret.set(rect.left + (rect.right - rect.left - regionWidth) / 2,
rect.top,
rect.right - (rect.right - rect.left - regionWidth) / 2,
rect.top + regionHeight + this.options.getBarRegionPaddingBottom());
rect.set(rect.left, ret.bottom, rect.right, rect.bottom);
return ret;
}
private Rect getBarRegionBottom(Rect rect, PieViewBarTable barTable) {
Rect ret = new Rect();
ret.set(rect.left, rect.top + this.options.getBarRegionPaddingTop(), rect.right, rect.bottom);
this.calcRowsAndCols(ret, barTable, false);
int regionWidth = barTable.Cols * this.getLongestItemWidth() + (barTable.Cols - 1) * this.options.getBarItemGapHorizontal();
int regionHeight = barTable.Rows * this.getBarItemHeight() + (barTable.Rows - 1) * this.options.getBarItemGapVertical();
ret.set(rect.left + (rect.right - rect.left - regionWidth) / 2,
rect.bottom - regionHeight,
rect.right - (rect.right - rect.left - regionWidth) / 2,
rect.bottom);
rect.set(rect.left, rect.top, rect.right, ret.top - this.options.getBarRegionPaddingTop());
return ret;
}
private void calcRowsAndCols(Rect rect, PieViewBarTable barTable, boolean rowsFirst) {
int itemHeight = this.getBarItemHeight();
int itemWidth = this.getLongestItemWidth();
int itemGapVertical = this.options.getBarItemGapVertical();
int itemGapHorizontal = this.options.getBarItemGapHorizontal();
if (rowsFirst) {
// To see how many rows can be drawn in this space.
int height = rect.bottom - rect.top;
int rows = height / (itemHeight + itemGapVertical);
barTable.Cols = this.data.size() / rows + 1;
barTable.Rows = this.data.size() / barTable.Cols;
if (this.data.size() % barTable.Cols != 0) {
barTable.Rows++;
}
} else {
// To see how many cols can be drawn in this space.
int width = rect.right - rect.left;
barTable.Cols = width / (itemWidth + itemGapHorizontal);
barTable.Rows = this.data.size() / barTable.Cols;
if (this.data.size() % barTable.Cols != 0) {
barTable.Rows++;
}
}
}
private int getLongestItemWidth() {
int textSize = (int)GlobalFuncs.getDPFromPixels((Activity)this.context, this.options.getBarItemTextSize());
this.paint.setTextSize(textSize);
int ret = 0;
for (int i = 0; i < this.data.size(); i++) {
int x = (int)this.paint.measureText(this.data.get(i).Name) + 1;
ret = x > ret ? x : ret;
}
ret += this.options.getBarItemGapBetweenColorAndText();
ret += textSize;
return ret;
}
private int getBarItemHeight() {
return (int)GlobalFuncs.getDPFromPixels(
(Activity)this.context,
this.options.getBarItemTextSize());
}
private RectF getCircleRect(Rect rect) {
RectF ret = new RectF();
float left = rect.left;
float right = rect.right;
float top = rect.top;
float bottom = rect.bottom;
float width = rect.width();
float height = rect.height();
if (width == height) {
ret.set(rect);
} else if (width > height) {
left += (width - height) / 2.0;
right -= (width - height) / 2.0;
ret.set(left, top, right, bottom);
} else {
top += (height - width) / 2.0;
bottom -= (height - width) / 2.0;
ret.set(left, top, right, bottom);
}
return ret;
}
// Classes
private class OnMyDatasetChangedListener implements PieViewAdapter.OnDatasetChangedListener {
@Override
public void OnDatasetChanged() {
PieView.this.refreshDataset();
Message msg = new Message();
msg.what = PieView.MESSAGE_DATASET_CHANGED;
msg.obj = PieView.this;
PieView.this.PieViewHandler.sendMessage(msg);
}
}
}
7. Done!