統計圖2--餅圖

餅圖本身沒有什麼難度,難點就在產品要求點擊圖例,餅圖要有對應區域的動畫,這個可把我折磨了很久,MPAndroidChart沒有改造出來,無奈去自定義的一個餅圖,也是站在前人的肩膀上修改爲自己需要的餅圖效果

好害怕這樣侵權了公司UI妹妹的設計,爲保護公司業務隱私,把公司數據給塗抹了

代碼在下面;先介紹圖例和餅圖關聯起來的代碼

button.setOnClickListener(new View.OnClickListener() {

@Override

public void onClick(View view) {

// pieChart.mCurrentPressedPosition = 0;

pieChart.mCurrentPressedPosition = 1;//可以做到點擊圖例找到對應的餅圖並有動畫效果,只需要指定position即可

pieChart.startTouchDownAnim();

}

});

 

 

import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.RectF;
import android.text.Layout;
import android.text.SpannableString;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.text.TextUtils;
import android.text.style.RelativeSizeSpan;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.view.Display;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

/**
 * Created by inf on 2016/11/29.
 */
// TODO: 2017/12/28 添加點擊事件的回調
public class PieChart extends View implements GestureDetector.OnGestureListener {


    public static final int CLICK_ANIM_LENGTH = 50;
    public static final int DURATION = 200;
    /**
     * view的寬高
     */
    private int mWidth, mHeight;
    /**
     * 餅狀圖的半徑、內部空白圓的半徑
     */
    private float mRadius, mInnerRadius;
    /**
     * 餅狀圖的外切
     */
    private RectF mPieRect;
    /**
     * 各種畫筆
     */
    private Paint mPiePaint, mBlankPaint, mLinePaint, mTextPaint, mLegendPaint;
    private TextPaint mCenterTextPaint;
    /**
     * 實體類集合
     */
    private List<IPieElement> mElements;
    /**
     * 各個元素的角度
     */
    private List<Float> mAngles = new ArrayList<>();
    /**
     * 元素的顏色
     */
    private List<String> mColors = new ArrayList<>();
    /**
     * 元素的描述
     */
    private List<String> mDescription = new ArrayList<>();
    /**
     * 元素的佔比
     */
    private List<String> mPercents = new ArrayList<>();
    /**
     * 中心文字
     */
    private CharSequence mText;

    private SparseArray<double[]> angles = new SparseArray<>();

    private GestureDetector mDetector;

    private boolean mIsAnimEnable;
    public int y;
    public int x;
    private RectF[] mRectBuffer = {new RectF(), new RectF(), new RectF()};

    public PieChart(Context context) {
        this(context, null);
    }

    public PieChart(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public PieChart(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    public void setData(List<IPieElement> elements) {
        mElements = elements;
        setValuesAndColors();
        invalidate();
    }

    public void setAnimEnable(boolean enable) {
        mIsAnimEnable = enable;
    }

    private void init() {
        mDetector = new GestureDetector(getContext(), this);
        mDetector.setIsLongpressEnabled(false);
        mPieRect = new RectF();
        mPiePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPiePaint.setColor(Color.RED);

        mBlankPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mBlankPaint.setStrokeWidth((float) 0.1);
        mBlankPaint.setColor(Color.WHITE);

        mLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mLinePaint.setStrokeWidth(4);
        mLinePaint.setStyle(Paint.Style.STROKE);

        mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mTextPaint.setTextSize(30);
        mTextPaint.setColor(Color.WHITE);

        mCenterTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
        mCenterTextPaint.setTextSize(30);
        mCenterTextPaint.setColor(Color.BLACK);

        mLegendPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mLegendPaint.setTextSize(30);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return mDetector.onTouchEvent(event);
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int width = 0, height = 0;
        if (widthMode == MeasureSpec.AT_MOST) {
            width = (int) getSize();
        } else {
            width = widthSize;
        }
        if (heightMode == MeasureSpec.AT_MOST) {
            height = (int) getSize();
        } else {
            height = heightSize;
        }
        int size = Math.min(width, height);
        setMeasuredDimension(size, size);

    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = w - getPaddingLeft() - getPaddingRight();
        mHeight = h - getPaddingTop() - getPaddingBottom();
        mRadius = (float) (Math.min(mWidth, mHeight) / 2 * 0.6);
        resetRect();

        mInnerRadius = (float) (mRadius * 0.6);

    }

    private Path mPath = new Path();

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.save();
        canvas.translate(mWidth / 2, mHeight / 2);
        //從12點方向開始畫
        float sweepedAngle = -90;
        mTextPaint.setTextSize(30);
        for (int i = 0; mElements != null && mElements.size() > i; i++) {
            //設置扇形的顏色
            mPiePaint.setColor(Color.parseColor(mColors.get(i)));
            mLinePaint.setColor(Color.parseColor(mColors.get(i)));
            //畫扇形
            if (mIsAnimEnable && i == mCurrentPressedPosition) {
                setRect(sweepedAngle, i, mCurrentLength);
            }
            canvas.drawArc(mPieRect, sweepedAngle, mAngles.get(i), true, mPiePaint);
            resetRect();

            //掃過的角度++
            double[] ang = new double[2];
            ang[0] = sweepedAngle + 90;
            ang[1] = ang[0] + mAngles.get(i);
            angles.put(i, ang);
            sweepedAngle += mAngles.get(i);

            String percentText = mPercents.get(i) + "%";
            //畫分割線
            canvas.drawArc(mPieRect, sweepedAngle, 1, true, mBlankPaint);
            sweepedAngle += 1;
            float x = getXCoordinate(mAngles.get(i), sweepedAngle);
            float y = getYCoordinate(mAngles.get(i), sweepedAngle);
            mPath.reset();
            mPath.moveTo(x, y);
            mPath.lineTo((float) (x * 1.2), (float) (y * 1.2));
//            canvas.drawPath(mPath, mLinePaint);
            mPath.reset();
            mPath.moveTo((float) (x * 1.2), (float) (y * 1.2));

            //水平線的長度設置爲文字長度的1.5倍
            float horizontalLineLength = (float) (getTextWidth(mTextPaint, percentText) * 1.5);
            //當線的起點在第三、四象限時,先把path移動到終點位置,然後向起點畫線,使後面畫文字時,文字方向是正確的
            if (x < 0) {
                horizontalLineLength = -horizontalLineLength;
                mPath.moveTo((float) (x * 1.2) + horizontalLineLength, (float) (y * 1.2));
                mPath.lineTo((float) (x * 1.2), (float) (y * 1.2));
            } else {
                mPath.lineTo((float) (x * 1.2) + horizontalLineLength, (float) (y * 1.2));

            }
//            canvas.drawPath(mPath, mLinePaint);
            //垂直方向的偏移量,畫文字時,文字顯示在path的下方,爲了讓文字顯示在上方,設置一個文字高度的垂直偏移量
            float offsetV = -getTextHeight(mTextPaint, percentText);
            canvas.drawTextOnPath(percentText, mPath, 0, offsetV, mTextPaint);
        }
        mPath.close();

        //這裏開始畫中心空白部分以及文字,空白部分半徑設置爲整個圓半徑的0.6倍
        RectF holeRect = mRectBuffer[0];
        holeRect.left = x - mInnerRadius;
        holeRect.top = y - mInnerRadius;
        holeRect.right = x + mInnerRadius;
        holeRect.bottom = y + mInnerRadius;
        RectF boundingRect = mRectBuffer[1];
        boundingRect.set(holeRect);
        canvas.drawCircle(0, 0, mInnerRadius, mBlankPaint);
        mCenterTextPaint.setTextAlign(Paint.Align.CENTER);
        if (!TextUtils.isEmpty(mText)) {
//            String[] texts = String.valueOf(mText).split(System.getProperty("line.separator"));//支持中間文本換行
//            for (String text : texts) {
//                calculateTextPaint(text);
//                canvas.drawText(text, 0, y, mCenterTextPaint);
//                y = y + 40;
//            }
            StaticLayout mCenterTextLayout = new StaticLayout(mText, mCenterTextPaint, canvas.getWidth(),
                    Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false);

            float layoutHeight = mCenterTextLayout.getHeight();
            canvas.translate(0, boundingRect.top + (boundingRect.height() - layoutHeight) / 2.f);
            mCenterTextLayout.draw(canvas);
        }
        canvas.restore();
        if (mShowLegend) {
            drawLegend(canvas);
        }
    }

    private void resetRect() {
        mPieRect.left = -mRadius;
        mPieRect.top = -mRadius;
        mPieRect.right = mRadius;
        mPieRect.bottom = mRadius;
    }

    public void setRect(float sweepedAngle, int i, int animatedLength) {
        float currentCenterAngle = sweepedAngle + mAngles.get(i) / 2;
        if (currentCenterAngle >= -90 && currentCenterAngle <= 0) {
            double actualAng = currentCenterAngle + 90;
            mPieRect.right += Math.sin(getRadian(actualAng)) * animatedLength;
            mPieRect.top -= Math.cos(getRadian(actualAng)) * animatedLength;

        } else if (currentCenterAngle > 0 && currentCenterAngle <= 90) {
            mPieRect.right += Math.cos(getRadian(currentCenterAngle)) * animatedLength;
            mPieRect.bottom += Math.sin(getRadian(currentCenterAngle)) * animatedLength;
        } else if (currentCenterAngle > 90 && currentCenterAngle <= 180) {
            double actualAng = currentCenterAngle - 90;
            mPieRect.left -= Math.sin(getRadian(actualAng)) * animatedLength;
            mPieRect.bottom += Math.cos(getRadian(actualAng)) * animatedLength;
        } else {
            double actualAng = currentCenterAngle - 180;
            mPieRect.left -= Math.cos(getRadian(actualAng)) * animatedLength;
            mPieRect.top -= Math.sin(getRadian(actualAng)) * animatedLength;
        }
    }

    private double getRadian(double actualAng) {
        return actualAng * 2 * Math.PI / 360;
    }

    private Rect rect = new Rect();


    /**
     * 設置中心文字
     *
     * @param
     */
    public void setCenterText(CharSequence text) {
        if (text == null)
            mText = "";
        else {
            mText = text;
        }

    }

    /**
     * 計算角度值和各個值的佔比
     */
    private void setValuesAndColors() {
        float sum = 0;
        if (mElements != null && mElements.size() > 0) {
            for (IPieElement ele : mElements) {
                sum += ele.getValue();
                mColors.add(ele.getColor());
                mDescription.add(ele.getDescription());
            }
            BigDecimal totleAngel = BigDecimal.valueOf(360 - mElements.size());
            for (int i = 0; i < mElements.size(); i++) {
                IPieElement ele = mElements.get(i);
                BigDecimal bigDecimal = new BigDecimal(String.valueOf(ele.getValue()));
                BigDecimal sumBigDecimal = BigDecimal.valueOf(sum);
                BigDecimal res = bigDecimal.divide(sumBigDecimal, 5, BigDecimal.ROUND_HALF_UP);
                //計算角度
                BigDecimal angle = res.multiply(totleAngel);
                mAngles.add(angle.floatValue());
                //計算百分比保留兩位小數並保存
                mPercents.add(bigDecimal.multiply(new BigDecimal(100)).divide(sumBigDecimal, 2, BigDecimal.ROUND_HALF_UP).toPlainString());
            }
        }

    }

    @Override
    public boolean onDown(MotionEvent motionEvent) {
        mCurrentPressedPosition = getPosition(motionEvent);
        startTouchDownAnim();
        return true;
    }

    @Override
    public void onShowPress(MotionEvent motionEvent) {

    }

    @Override
    public boolean onSingleTapUp(MotionEvent motionEvent) {

        mCurrentPressedPosition = getPosition(motionEvent);
//        startTouchUpAnim();
        if (mCurrentPressedPosition >= 0 && mListener != null) {
            mListener.onItemClick(mCurrentPressedPosition);
        }

        return false;
    }

    @Override
    public boolean onScroll(MotionEvent motionEvent, MotionEvent motionEvent1, float v, float v1) {
        mCurrentPressedPosition = getPosition(motionEvent);
        startTouchUpAnim();
        return true;
    }

    @Override
    public void onLongPress(MotionEvent motionEvent) {
    }

    @Override
    public boolean onFling(MotionEvent motionEvent, MotionEvent motionEvent1, float v, float v1) {
        return false;
    }

    private int mCurrentLength;
    public int mCurrentPressedPosition;

    public void startTouchDownAnim() {
//        ValueAnimatorCompat va= new ValueAnimatorCompat();
        ValueAnimator va = ValueAnimator.ofInt(0, CLICK_ANIM_LENGTH);
        va.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mCurrentLength = (int) animation.getAnimatedValue();
                invalidate();
            }
        });
        va.setDuration(DURATION);
        va.start();
    }

    public void startTouchUpAnim() {
//        ValueAnimatorCompat va= new ValueAnimatorCompat();
        ValueAnimator va = ValueAnimator.ofInt(CLICK_ANIM_LENGTH, 0);
        va.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mCurrentLength = (int) animation.getAnimatedValue();
                invalidate();
            }
        });
        va.setDuration(DURATION);
        va.start();
    }

    /**
     * 獲取點擊位置座標對應的餅狀圖的區域
     *
     * @param motionEvent
     * @return 數據的position
     */
    public int getPosition(MotionEvent motionEvent) {
        float x = motionEvent.getX();
        float y = motionEvent.getY();
        float centerX = getWidth() / 2;
        float centerY = getHeight() / 2;

        //判斷點擊位置是否在innerRadius內
        if ((Math.pow(x - centerX, 2) + Math.pow(y - centerY, 2)) < mInnerRadius * mInnerRadius) {
            return -1;
        }

        //判斷點擊位置是否在餅狀圖以外
        if ((Math.pow(x - centerX, 2) + Math.pow(y - centerY, 2)) > mRadius * mRadius) {
            return -1;
        }

        // 判斷象限
        // 第一象限
        double angle = 0;

        if (x > centerX && y < centerY) {
            angle = Math.toDegrees(Math.atan((Math.abs(x - centerX)) / (Math.abs(centerY - y))));

        } else if (x > centerX && y > centerY) {//第二象限
            angle = Math.toDegrees(Math.atan(((y - centerY) / (x - centerX))));
            angle += 90;
        } else if (x < centerX && y > centerY) {//第三象限
            angle = Math.toDegrees(Math.atan(((centerX - x) / (y - centerY))));
            angle += 180;
        } else if (x < centerX && y < centerY) {//第四象限
            angle = Math.toDegrees(Math.atan(((centerY - y) / (centerX - x))));
            angle += 270;
        }

        for (int i = 0; i < angles.size(); i++) {
            double[] angs = angles.get(i);
            if (angle >= angs[0] && angle <= angs[1]) {
                return i;
            }
        }
        return -1;
    }

    private OnItemClickListener mListener;

    public interface OnItemClickListener {
        void onItemClick(int position);
    }

    public void setOnItemClickListener(OnItemClickListener listener) {
        mListener = listener;
    }

    private boolean mShowLegend = true;

    /**
     * 圖例開關
     *
     * @param enable
     */
    public void enableLegend(boolean enable) {
        mShowLegend = enable;
    }

    /**
     * 畫圖例
     *
     * @param canvas
     */
    private void drawLegend(Canvas canvas) {
        float verticalOffset = 0;
        for (int i = 0; i < mElements.size(); i++) {
            IPieElement ele = mElements.get(i);
            mLegendPaint.setColor(Color.parseColor(ele.getColor()));
            mLegendPaint.getTextBounds(ele.getDescription(), 0, ele.getDescription().length(), rect);
            verticalOffset = rect.height() + 20;
            canvas.translate(0, verticalOffset);
            mLegendPaint.setStrokeWidth(8);
            canvas.drawLine(10, 0, 80, 0, mLegendPaint);
            canvas.drawText(ele.getDescription(), 90, rect.height() / 2, mLegendPaint);
        }
    }

    /**
     * 把文字分兩行,並畫在圓內接正方形內,依此計算畫筆的textSize
     *
     * @param text
     */
    private void calculateTextPaint(String text) {
        if (!TextUtils.isEmpty(text)) {
            measureText(text, 200);
        }
    }

    /**
     * 遞歸調用,計算testSize
     *
     * @param text
     * @param textSize
     */
    private void measureText(String text, int textSize) {
        mTextPaint.setTextSize(textSize);
        float width = getTextWidth(mTextPaint, text);
        float height = getTextHeight(mTextPaint, text);
        if (width > mInnerRadius * 1.41421) {
            textSize--;
            measureText(text, textSize);
            return;
        }
        if (height * 2.5 > mInnerRadius * 1.41421) {
            textSize--;
            measureText(text, textSize);
        }
    }

    private float getTextHeight(Paint paint, String text) {
        Rect rect = new Rect();
        paint.getTextBounds(text, 0, text.length(), rect);
        return rect.height();
    }

    /**
     * @param paint
     * @param text
     * @return
     */
    private float getTextWidth(Paint paint, String text) {
        Rect rect = new Rect();
        paint.getTextBounds(text, 0, text.length(), rect);
        return rect.width();
    }

    /**
     * 獲取圓弧中點的x軸座標
     *
     * @param angle        圓弧對應的角度
     * @param sweepedAngle 掃過的角度
     * @return 圓弧中點的x軸座標
     */
    private float getXCoordinate(float angle, float sweepedAngle) {
        float x = (float) (mRadius * Math.cos(Math.toRadians(sweepedAngle - angle / 2)));
        return x;

    }

    /**
     * 獲取圓弧中點的y軸座標
     *
     * @param angle        圓弧對應的角度
     * @param sweepedAngle 掃過的角度
     * @return 圓弧中點的y軸座標
     */
    private float getYCoordinate(float angle, float sweepedAngle) {
        float y = (float) (mRadius * Math.sin(Math.toRadians(sweepedAngle - angle / 2)));
        return y;

    }

    private float getSize() {
        Display display = ((WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
        float widht = display.getWidth();
        float height = display.getHeight();
        return Math.min(widht, height);
    }

    public static SpannableString centerSpannableText(String start, String middle, String end) {
        String stringConetnt = start + middle + end;
        SpannableString str = new SpannableString(stringConetnt);
        if (middle.length() <= 6) {
            str.setSpan(new RelativeSizeSpan(1.6f), start.length(), start.length() + middle.length(), 0);
        } else if (middle.length() < 9) {
            str.setSpan(new RelativeSizeSpan(1.5f), start.length(), start.length() + middle.length(), 0);
        } else if (middle.length() < 12) {
            str.setSpan(new RelativeSizeSpan(1.3f), start.length(), start.length() + middle.length(), 0);
        } else if (middle.length() < 18) {
            str.setSpan(new RelativeSizeSpan(0.9f), start.length(), start.length() + middle.length(), 0);
        }
        return str;
    }

    public static SpannableString centerSpannableText(String start) {
//        String stringConetnt = start + middle + end;
        String stringConetnt = start;
        SpannableString str = new SpannableString(stringConetnt);
        if (start.length() <= 6) {
            str.setSpan(new RelativeSizeSpan(1.6f), start.length(), 0 + start.length(), 0);
        } else if (start.length() < 9) {
            str.setSpan(new RelativeSizeSpan(1.0f), start.length(), start.length() + start.length(), 0);
        }
//        else if (middle.length() < 9) {
//            str.setSpan(new RelativeSizeSpan(1.5f), start.length(), start.length() + middle.length(), 0);
//        } else if (middle.length() < 12) {
//            str.setSpan(new RelativeSizeSpan(1.3f), start.length(), start.length() + middle.length(), 0);
//        } else if (middle.length() < 18) {
//            str.setSpan(new RelativeSizeSpan(0.9f), start.length(), start.length() + middle.length(), 0);
//        }
        return str;
    }


}
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章