1. 程式人生 > >Android 圓圈統計圖自定義控制元件(支付寶餅圖)

Android 圓圈統計圖自定義控制元件(支付寶餅圖)

1.控制元件效果圖
這裡寫圖片描述

2.知識點

(1)圖形
這裡寫圖片描述
(2)動畫:
(1)圓餅填充動畫
(2)圓點放大動畫
(3)折線繪製動畫
(3)上部字型延折線移動動畫
(4)下部字型從下移動至上動畫
以上動畫實現都是用ValueAnimator,在ValueAnimator中利用TypeEvaluator中泛型的物件生成從開始到結束的fracation(0-1)的增長資料來生成對應的座標點或者路勁來進行繪製對應的圖形和字型。

  public ValueAnimator drawArcAnimation(final int position, final float startAngle, final
float sweepAngle) { final ValueAnimator valueAnimator = ValueAnimator.ofObject(new TypeEvaluator<Float>() { @Override public Float evaluate(float fraction, Float startValue, Float endValue) { return fraction * endValue; } }, startAngle, sweepAngle); valueAnimator.addUpdateListener(new
ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { statisticalItems.get(position).setSweepAngleAnim((Float) animation.getAnimatedValue()); invalidate(); } }); valueAnimator.setDuration(animationDuration); valueAnimator.setRepeatCount(0
); return valueAnimator; }

程式碼使用:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        CircleStatisticalView csv = findViewById(R.id.csv);
        //測試資料
        float[] percent = {0.1F, 0.2F, 0.2F, 0.4F, 0.1F};
        int[] color = {Color.parseColor("#FD695D"), Color.parseColor("#FDB57B"), Color.parseColor("#12B166"), Color.parseColor("#1DA0FB"), Color.GREEN};
        String[] markTop = {"13.20", "2.80", "5.74", "24.00", "26.00"};
        String[] markBottom = {"商超", "水房", "其他", "餐飲", "外賣"};

        List<StatisticalItem> list = new ArrayList<>();
        for (int i = 0; i < percent.length; i++) {
            StatisticalItem item = new StatisticalItem();
            item.setPercent(percent[i]);
            item.setColor(color[i]);
            item.setTopMarkText(markTop[i]);
            item.setBottomMarkText(markBottom[i]);
            list.add(item);
        }
        //設定資料方法
        csv.setStatisticalItems(list);
    }

原始碼
(1)attr.xml

    <!--圓形統計View-->
    <declare-styleable name="CircleStatisticalView">
        <!--圓圈背景顏色-->
        <attr name="circleBackgroundColor" format="color" />
        <!--圓點邊距-->
        <attr name="dotMargin" format="dimension" />
        <!--圓點半徑-->
        <attr name="dotRadius" format="dimension" />
        <!--標記線處X差值-->
        <attr name="lineGapX" format="dimension" />
        <!--標記線處Y差值-->
        <attr name="lineGapY" format="dimension" />
        <!--標記線文字和線的間距-->
        <attr name="lineNearTextMargin" format="dimension" />
        <!--標記線粗細-->
        <attr name="lineStrokeWidth" format="dimension" />
        <!--標記文字大小-->
        <attr name="markTextSize" format="dimension" />
        <!--標記文字顏色-->
        <attr name="markTextColor" format="color" />
    </declare-styleable>

(2)CircleStatisticalView

import android.animation.AnimatorSet;
import android.animation.TypeEvaluator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.RectF;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;

import java.util.ArrayList;
import java.util.List;

/**
 * Created by Relin
 * on 2018-08-14.
 * 圓形統計圖
 */
public class CircleStatisticalView extends View {

    /**
     * 寬度
     */
    private float width;
    /**
     * 高度
     */
    private float height;
    /**
     * 中心座標
     */
    private float circleX, circleY;
    /**
     * 半徑
     */
    private float circleRadius = dpToPx(50);
    /**
     * 圓圈背景顏色
     */
    private int circleBackgroundColor = Color.parseColor("#EDEDED");
    /**
     * 圓圈寬度
     */
    private float circleStrokeWidth = dpToPx(30);

    /**
     * 圓點邊距
     */
    private float dotMargin = dpToPx(5);
    /**
     * 圓點半徑
     */
    private float dotRadius = dpToPx(5);
    /**
     * 轉角線X差值
     */
    private float lineGapX = dpToPx(15);
    /**
     * 轉角線Y差值
     */
    private float lineGapY = dpToPx(15);
    /**
     * 標記線上下的檔案間距
     */
    private float lineNearTextMargin = dpToPx(5);
    /**
     * 線條粗細
     */
    private float lineStrokeWidth = dpToPx(1.2F);
    /**
     * 標記文字大小
     */
    private float markTextSize = dpToPx(14);
    /**
     * 標記文字顏色
     */
    private int markTextColor = 0;

    /**
     * 資料
     */
    private List<StatisticalItem> statisticalItems;

    /**
     * 是否使用動畫
     */
    private boolean isUseAnimation = true;


    public CircleStatisticalView(Context context) {
        super(context);
        init(context, null);
    }

    public CircleStatisticalView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }

    public CircleStatisticalView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int w = widthSpecSize;
        int h = heightSpecSize;
        int needHeight = (int) (circleRadius * 2 + dotMargin + dotRadius * 2 + circleStrokeWidth * 2 + lineGapY * 4);
        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            w = (int) (1.7F * needHeight);
            h = needHeight;
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            w = 4 * needHeight / 5;
            h = heightSpecSize;
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            w = widthSpecSize;
            h = needHeight;
        }
        setMeasuredDimension(w, h);
        //獲取最終的寬高
        width = getMeasuredWidth();
        height = getMeasuredHeight();
        circleX = width / 2;
        circleY = height / 2;
        circleRadius -= getPaddingLeft() - getPaddingRight();
    }

    /**
     * 初始化
     *
     * @param context
     * @param attrs
     */
    private void init(Context context, AttributeSet attrs) {
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CircleStatisticalView);
        circleBackgroundColor = array.getColor(R.styleable.CircleStatisticalView_circleBackgroundColor, circleBackgroundColor);
        dotMargin = array.getDimension(R.styleable.CircleStatisticalView_dotMargin, dotMargin);
        dotRadius = array.getDimension(R.styleable.CircleStatisticalView_dotRadius, dotRadius);
        lineGapX = array.getDimension(R.styleable.CircleStatisticalView_lineGapX, lineGapX);
        lineGapY = array.getDimension(R.styleable.CircleStatisticalView_lineGapY, lineGapY);
        lineNearTextMargin = array.getDimension(R.styleable.CircleStatisticalView_lineNearTextMargin, lineNearTextMargin);
        lineStrokeWidth = array.getDimension(R.styleable.CircleStatisticalView_lineStrokeWidth, lineStrokeWidth);
        markTextSize = array.getDimension(R.styleable.CircleStatisticalView_markTextSize, markTextSize);
        markTextColor = array.getColor(R.styleable.CircleStatisticalView_markTextColor, markTextColor);
        statisticalItems = new ArrayList<>();
        array.recycle();
    }

    private int drawCount = 0;

    @Override
    protected void onDraw(final Canvas canvas) {
        super.onDraw(canvas);
        drawCircle(canvas);
        int size = statisticalItems == null ? 0 : statisticalItems.size();
        float startAngle = 0F;
        float markAngle;
        for (int i = 0; i < size; i++) {
            StatisticalItem item = statisticalItems.get(i);
            float sweepAngle = item.getPercent() * 360F;
            statisticalItems.get(i).setStartAngle(startAngle);
            statisticalItems.get(i).setSweepAngle(sweepAngle);
            if (!isUseAnimation) {
                drawArc(canvas, startAngle - 1F, sweepAngle + 1F, item.getColor());
            }
            if (i == 0) {
                markAngle = sweepAngle;
            } else {
                markAngle = 2 * (startAngle + sweepAngle / 2);
            }
            drawMark(canvas, i, item, markAngle);
            startAngle += sweepAngle;
        }
        userAnimationDraw(canvas, size, drawCount);
        drawCount++;
    }

    /**
     * 利用動畫繪製
     *
     * @param canvas    畫布
     * @param size      資料大小
     * @param drawCount 繪製次數
     */
    private void userAnimationDraw(Canvas canvas, int size, int drawCount) {
        //動畫繪製
        for (int i = 0; i < size; i++) {
            StatisticalItem item = statisticalItems.get(i);
            Paint paint = new Paint();
            paint.setAntiAlias(true);
            paint.setColor(item.getColor());
            if (item.getSweepAngleAnim() != 0) {
                drawArc(canvas, item.getStartAngle() - 1F, item.getSweepAngleAnim() + 1F, item.getColor());
            }
            //標記線和標記
            paint.setStyle(Paint.Style.STROKE);
            paint.setStrokeWidth(dpToPx(1));
            if (item.getGapAnimationPoint() != null) {
                canvas.drawLine(item.getDotPoint().x, item.getDotPoint().y, item.getGapAnimationPoint().x, item.getGapAnimationPoint().y, paint);
            }
            if (item.getLineEndAnimPoint() != null) {
                canvas.drawLine(item.getGapPoint().x, item.getGapPoint().y, item.getLineEndAnimPoint().x, item.getLineEndAnimPoint().y, paint);
            }
            //文字
            paint.setStyle(Paint.Style.FILL);
            paint.setTextSize(dpToPx(16));
            paint.setColor(item.getColor());
            if (item.getTopMarkAnimPoint() != null) {
                canvas.drawText(item.getTopMarkText(), item.getTopMarkAnimPoint().x, item.getTopMarkAnimPoint().y, paint);
            }
            if (item.getBottomMarkAnimPoint() != null) {
                canvas.drawText(item.getBottomMarkText(), item.getBottomMarkAnimPoint().x, item.getBottomMarkAnimPoint().y, paint);
            }
        }
        //開啟動畫
        if (drawCount == 0) {
            for (int i = 0; i < size; i++) {
                final StatisticalItem item = statisticalItems.get(i);
                ValueAnimator angleAnim = drawArcAnimation(i, item.getStartAngle(), item.getSweepAngle());
                ValueAnimator lineStartAnim = drawMarkLineAnimation(i, item.getDotPoint(), item.getGapPoint(), 1);
                ValueAnimator lineEndAnim = drawMarkLineAnimation(i, item.getGapPoint(), item.getLineEndPoint(), 2);
                ValueAnimator topMarkAnim = drawMarkLineAnimation(i, item.getGapPoint(), item.getTopMarkPoint(), 3);
                Point point = new Point();
                point.x = item.getBottomMarkPoint().x;
                point.y = (int) (item.getBottomMarkPoint().y + dpToPx(30));
                ValueAnimator bottomMarkAnim = drawMarkLineAnimation(i, point, item.getBottomMarkPoint(), 4);
                //頂部文字延遲500ms
                topMarkAnim.setStartDelay(500);
                //動畫集
                AnimatorSet animatorSet = new AnimatorSet();
                animatorSet.play(lineStartAnim).with(angleAnim);
                animatorSet.play(lineEndAnim).after(lineStartAnim);
                animatorSet.play(topMarkAnim).with(lineStartAnim);
                animatorSet.play(bottomMarkAnim).with(topMarkAnim);
                animatorSet.start();
            }
        }
    }


    /**
     * 畫餅動畫
     *
     * @param position   位置
     * @param startAngle 開始弧度
     * @param sweepAngle 結束弧度
     * @return
     */
    public ValueAnimator drawArcAnimation(final int position, final float startAngle, final float sweepAngle) {
        final ValueAnimator valueAnimator = ValueAnimator.ofObject(new TypeEvaluator<Float>() {
            @Override
            public Float evaluate(float fraction, Float startValue, Float endValue) {
                return fraction * endValue;
            }
        }, startAngle, sweepAngle);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                statisticalItems.get(position).setSweepAngleAnim((Float) animation.getAnimatedValue());
                invalidate();
            }
        });
        valueAnimator.setDuration(500);
        valueAnimator.setRepeatCount(0);
        return valueAnimator;
    }

    /**
     * 繪製標記和線的動畫
     *
     * @param position   位置
     * @param startPoint 開始點
     * @param endPoint   結束點
     * @param step       步驟
     * @return
     */
    public ValueAnimator drawMarkLineAnimation(final int position, final Point startPoint, final Point endPoint, final int step) {
        final ValueAnimator valueAnimator = ValueAnimator.ofObject(new TypeEvaluator<Point>() {
            @Override
            public Point evaluate(float fraction, Point startValue, Point endValue) {
                Point point = new Point();
                point.x = (int) (fraction * (endPoint.x - startPoint.x) + startPoint.x);
                point.y = (int) (fraction * (endValue.y - startValue.y) + startPoint.y);
                return point;
            }
        }, startPoint, endPoint);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                switch (step) {
                    case 1:
                        valueAnimator.setDuration(250);
                        statisticalItems.get(position).setGapAnimationPoint((Point) animation.getAnimatedValue());
                        break;
                    case 2:
                        valueAnimator.setDuration(500);
                        statisticalItems.get(position).setLineEndAnimPoint((Point) animation.getAnimatedValue());
                        break;
                    case 3:
                        valueAnimator.setDuration(500);
                        statisticalItems.get(position).setTopMarkAnimPoint((Point) animation.getAnimatedValue());
                        break;
                    case 4:
                        valueAnimator.setDuration(500);
                        statisticalItems.get(position).setBottomMarkAnimPoint((Point) animation.getAnimatedValue());
                        break;
                }
                invalidate();
            }
        });

        valueAnimator.setRepeatCount(0);
        return valueAnimator;
    }


    /**
     * 建立畫筆物件
     *
     * @return
     */
    private Paint buildPaint() {
        Paint paint = new Paint();
        paint.setAntiAlias(true);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(circleStrokeWidth);
        return paint;
    }

    /**
     * 畫出背景圓圈
     *
     * @param canvas 畫布
     */
    private void drawCircle(Canvas canvas) {
        Paint paint = buildPaint();
        paint.setColor(circleBackgroundColor);
        canvas.drawCircle(circleX, circleY, circleRadius, paint);
    }


    /**
     * 畫圓弧
     *
     * @param canvas     畫布
     * @param startAngle 開始弧度
     * @param sweepAngle 進過的弧度
     * @param color      弧面顏色
     */
    private void drawArc(Canvas canvas, float startAngle, float sweepAngle, int color) {
        Paint paint = buildPaint();
        paint.setColor(color);
        RectF rectF = new RectF(circleX - circleRadius, circleY - circleRadius, circleX + circleRadius, circleY + circleRadius);
        canvas.drawArc(rectF, -90 + startAngle, sweepAngle, false, paint);
    }

    /**
     * 繪製標記
     *
     * @param canvas    畫布
     * @param item      標記資料物件
     * @param markAngle 標記弧度
     */
    private void drawMark(Canvas canvas, int position, StatisticalItem item, float markAngle) {
        final Paint paint = new Paint();
        paint.setAntiAlias(true);
        paint.setStrokeWidth(lineStrokeWidth);
        paint.setColor(item.getColor());
        //圓點
        float dotToCenterLine = circleRadius + dpToPx(20) + dotMargin;
        float dotX;
        float dotY;
        float dotAngle = (float) ((markAngle / 2 * Math.PI / 180));//角度轉弧度
        if (markAngle / 2 < 180 * 2) {
            dotX = (float) (circleX + Math.sin(dotAngle) * dotToCenterLine);
            dotY = (float) (circleY - Math.cos(dotAngle) * dotToCenterLine);
        } else {
            dotX = (float) (circleX - Math.sin(dotAngle) * dotToCenterLine);
            dotY = (float) (circleY + Math.cos(dotAngle) * dotToCenterLine);
        }
        canvas.drawCircle(dotX, dotY, dotRadius, paint);
        //線
        float lineMiddleX, lineMiddleY, lineEdnX, lineEndY;
        if (circleX < dotX) {
            lineMiddleX = markAngle / 2 > 45 && markAngle / 2 < 135 ? dotX : dotX + lineGapX;
            lineMiddleY = markAngle / 2 > 45 && markAngle / 2 < 135 ? dotY : dotY + (circleY < dotY ? +lineGapY : -lineGapY);
            lineEdnX = markAngle / 2 > 45 && markAngle / 2 < 135 ? lineMiddleX + (width - lineMiddleX - getPaddingRight()) : lineMiddleX + (width - lineMiddleX - getPaddingRight());
            lineEndY = lineMiddleY;
        } else {
            lineMiddleX = markAngle / 2 > 225 && markAngle / 2 < 315 ? dotX : dotX - lineGapX;
            lineMiddleY = markAngle / 2 > 225 && markAngle / 2 < 315 ? dotY : dotY + (circleY < dotY ? +lineGapY : -lineGapY);
            lineEdnX = 0 + getPaddingLeft();
            lineEndY = lineMiddleY;
        }
        if (!isUseAnimation) {
            canvas.drawLine(dotX, dotY, lineMiddleX, lineMiddleY, paint);
            canvas.drawLine(lineMiddleX, lineMiddleY, lineEdnX, lineEndY, paint);
        }
        statisticalItems.get(position).setDotPoint(new Point((int) dotX, (int) dotY));
        statisticalItems.get(position).setGapPoint(new Point((int) lineMiddleX, (int) lineMiddleY));
        statisticalItems.get(position).setLineEndPoint(new Point((int) lineEdnX, (int) lineEndY));

        //文字
        paint.setColor(item.getColor());
        if (markTextColor != 0) {
            paint.setColor(markTextColor);
        }
        if (item.getMarkTextColor() != 0) {
            paint.setColor(item.getMarkTextColor());
        }
        paint.setTextSize(markTextSize);
        String topMark = item.getTopMarkText();
        String bottomMark = item.getBottomMarkText();
        float topX, topY, bottomX, bottomY;
        if (circleX < dotX) {
            //上部分文字
            topX = lineEdnX - paint.measureText(topMark);
            topY = lineEndY - lineNearTextMargin;
            //下部分文字
            bottomX = lineEdnX - paint.measureText(bottomMark);
            bottomY = lineEndY + (paint.measureText(bottomMark) / bottomMark.length()) / 2 + lineNearTextMargin * 1.5F + lineStrokeWidth;
        } else {
            //上部分文字
            topX = lineEdnX;
            topY = lineEndY - lineNearTextMargin;
            //下部分文字
            bottomX = lineEdnX;
            bottomY = lineEndY + (paint.measureText(bottomMark) / bottomMark.length()) / 2 + lineNearTextMargin * 1.5F + lineStrokeWidth;
        }
        if (!isUseAnimation) {
            canvas.drawText(topMark, topX, topY, paint);
            canvas.drawText(bottomMark, bottomX, bottomY, paint);
        }
        statisticalItems.get(position).setTopMarkPoint(new Point((int) topX, (int) topY));
        statisticalItems.get(position).setBottomMarkPoint(new Point((int) bottomX, (int) bottomY));
    }


    private static float getScreenDensity() {
        return Resources.getSystem().getDisplayMetrics().density;
    }


    private static float dpToPx(float dp) {
        return dp * getScreenDensity();
    }


    public float getCircleRadius() {
        return circleRadius;
    }

    public void setCircleRadius(float circleRadius) {
        this.circleRadius = circleRadius;
    }

    public int getCircleBackgroundColor() {
        return circleBackgroundColor;
    }

    public void setCircleBackgroundColor(int circleBackgroundColor) {
        this.circleBackgroundColor = circleBackgroundColor;
    }

    public float getCircleStrokeWidth() {
        return circleStrokeWidth;
    }

    public void setCircleStrokeWidth(float circleStrokeWidth) {
        this.circleStrokeWidth = circleStrokeWidth;
    }

    public float getDotMargin() {
        return dotMargin;
    }

    public void setDotMargin(float dotMargin) {
        this.dotMargin = dotMargin;
    }

    public float getDotRadius() {
        return dotRadius;
    }

    public void setDotRadius(float dotRadius) {
        this.dotRadius = dotRadius;
    }

    public float getLineGapX() {
        return lineGapX;
    }

    public void setLineGapX(float lineGapX) {
        this.lineGapX = lineGapX;
    }

    public float getLineGapY() {
        return lineGapY;
    }

    public void setLineGapY(float lineGapY) {
        this.lineGapY = lineGapY;
    }

    public float getLineNearTextMargin() {
        return lineNearTextMargin;
    }

    public void setLineNearTextMargin(float lineNearTextMargin) {
        this.lineNearTextMargin = lineNearTextMargin;
    }

    public float getLineStrokeWidth() {
        return lineStrokeWidth;
    }

    public void setLineStrokeWidth(float lineStrokeWidth) {
        this.lineStrokeWidth = lineStrokeWidth;
    }

    public float getMarkTextSize() {
        return markTextSize;
    }

    public void setMarkTextSize(float markTextSize) {
        this.markTextSize = markTextSize;
    }

    public int getMarkTextColor() {
        return markTextColor;
    }

    public void setMarkTextColor(int markTextColor) {
        this.markTextColor = markTextColor;
    }

    public List<StatisticalItem> getStatisticalItems() {
        return statisticalItems;
    }

    public void setStatisticalItems(List<StatisticalItem> statisticalItems) {
        int size = statisticalItems == null ? 0 : statisticalItems.size();
        float totalPercent = 0;

        List<StatisticalItem> newItems = new ArrayList<>();
        for (int i = 0; i < size; i++) {
            if (statisticalItems.get(i).getPercent() != 0) {
                newItems.add(statisticalItems.get(i));
            }
        }
        size = newItems == null ? 0 : newItems.size();
        for (int i = 0; i < size; i++) {
            StatisticalItem item = newItems.get(i);
            totalPercent += item.getPercent();
        }
        if (totalPercent != 1) {
            for (int i = 0; i < size; i++) {
                StatisticalItem item = newItems.get(i);
                newItems.get(i).setPercent(item.getPercent() / totalPercent);
            }
        }
        this.statisticalItems = newItems;

    }

    public boolean isUseAnimation() {
        return isUseAnimation;
    }

    public void setUseAnimation(boolean useAnimation) {
        isUseAnimation = useAnimation;
    }
}