Programming     Travel Logs     Life Is Good     Surfing Online     About Me
Learn to sell. Learn to build. If you can do both, you will be unstoppable.
-Naval Ravikant
2018-07-17 18:04:28

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.

/Images/20171007/01.jpg

/Images/20171007/02.jpg

/Images/20171007/03.jpg

/Images/20171007/04.jpg

/Images/20171007/05.jpg

/Images/20171007/06.jpg

/Images/20171007/07.jpg

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!