Android自定义饼状图

时间:2024-03-22 15:25:59

1.啥都别说,先上效果图。

Android自定义饼状图

2.这效果是平时真实项目开发可能用到的。我也是看到Android交流群里有人要这种效果,于是开始动手写一写。
3.分析:这个自定义View效果 的实现大概分为几步

3.1.分析里面第个部分的内容:大概有:中间的文本内容、所占比例、颜色等,这就需要创建JavaBean来封装这些内容

3.2.有了数据,那个就开始分析绘制的过程

3.2.1.根据比例,先绘制扇形

3.2.2.绘制中间的一个白色圆

3.2.3.绘制中间上面的文本

3.2.4.绘制中间下面的文本内容

4.下面是代码,先创建一个JavaBean,PieData。

public class PieData {
    public String name;// 名称
    public String color;// 颜色
    public float ratio;// 比例要通过计算
    public float value;// 值
}

5.创建自定义View,PieView

package cn.carhouse.viewdemo.pie;

import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.AccelerateInterpolator;

import java.util.Arrays;
import java.util.List;

/**
 * 画饼状图
 */

public class PieView extends View {
    // 上面的文字大小和颜色
    public static final int TOP_TEXT_SIZE = 16;
    public static final int TOP_TEXT_COLOR = Color.BLUE;
    public static final int BOTTOM_TEXT_COLOR = Color.BLACK;
    public static final int BOTTOM_TEXT_SIZE = 20;
    // 测试用的,实际开发改成自己的   颜色表 (注意: 此处定义颜色使用的是ARGB,带Alpha通道的)
    private int[] mColors = {0xFF889933, 0xFF6495ED, 0xFFE32636, 0xFF800000, 0xFF808000, 0xFFFF8C69, 0xFF808080, 0xFFE6B800, 0xFF7CFC00};
    private Paint mPaint, mTopTextPaint, mBottomTextPaint;
    private List<PieData> mPieDataList;
    private int mWidth, mHeight;
    private float mRadius, mInnerRadius;
    private RectF mOval;
    private float totalRadio;// 总比例
    private float mStartAngle = 0;
    private PieData mPieData;
    private float animValue;

    //动画时间
    private static final int ANIMATION_DURATION = 500;

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

    public PieView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public PieView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);

        mTopTextPaint = new Paint();
        mTopTextPaint.setAntiAlias(true);
        mTopTextPaint.setDither(true);
        mTopTextPaint.setColor(Color.BLUE);
        mTopTextPaint.setTextAlign(Paint.Align.CENTER);
        mTopTextPaint.setTextSize(dip2px(TOP_TEXT_SIZE));
        mTopTextPaint.setColor(TOP_TEXT_COLOR);

        mBottomTextPaint = new Paint();
        mBottomTextPaint.setAntiAlias(true);
        mBottomTextPaint.setDither(true);
        mBottomTextPaint.setColor(Color.BLUE);
        mBottomTextPaint.setTextAlign(Paint.Align.CENTER);
        mBottomTextPaint.setTextSize(dip2px(BOTTOM_TEXT_SIZE));
        mBottomTextPaint.setColor(BOTTOM_TEXT_COLOR);


    }

    private float dip2px(int dip) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dip, getResources().getDisplayMetrics());
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        this.mWidth = w;
        this.mHeight = h;
        // 半径
        mRadius = Math.min(mWidth, mHeight) * 0.70f / 2;
        mInnerRadius = mRadius * 0.50f;
        // 扇形绘制的矩形区域
        mOval = new RectF(-mRadius, -mRadius, mRadius, mRadius);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if (mPieDataList == null || mPieDataList.size() <= 0) {
            return;
        }
        canvas.save();
        // 将坐标点移动到View的中心
        canvas.translate(mWidth / 2, mHeight / 2);
        // 1. 画扇形
        float startAngle = 0;
        for (int i = 0; i < mPieDataList.size(); i++) {
            startAngles[i] = startAngle;
            PieData pieData = mPieDataList.get(i);
            float sweepAngle = (pieData.ratio * animValue) * 360 - 0.5f;
            mPaint.setColor(mColors[i % mColors.length]);
            canvas.drawArc(mOval, startAngle, sweepAngle, true, mPaint);
            startAngle += sweepAngle + 0.5f;
        }
        // 2.画内圆
        mPaint.setColor(Color.WHITE);
        canvas.drawCircle(0, 0, mInnerRadius, mPaint);
        // 3.画上面文本
        String text = mPieData.name;
        Rect rect = new Rect();
        mTopTextPaint.getTextBounds(text, 0, text.length(), rect);
        canvas.drawText(text, 0, -rect.height() / 2, mTopTextPaint);
        // 4.画下面的面文本
        text = String.format("%.2f", mPieData.ratio * 100) + "%";
        rect = new Rect();
        mBottomTextPaint.getTextBounds(text, 0, text.length(), rect);
        canvas.drawText(text, 0, rect.height() + rect.height() / 2, mBottomTextPaint);
        canvas.restore();
    }

    //当用户与手机屏幕进行交互的时候(触摸)
    //触摸事件处理
    //1,按下去
    //2,移动
    //3,抬起
    //参数:触摸事件,这个事件是由用户与屏幕交互产生的,这个事件包含上述三种情况
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //获取用户对屏幕的行为
        int action = event.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                //做点击范围的认定
                //获取用户点击的位置距当前视图的左边缘的距离
                float x = event.getX();
                float y = event.getY();
                //将点击的x和y坐标转换为以饼状图为圆心的坐标
                x = x - mWidth / 2;
                y = y - mHeight / 2;
                float touchAngle = getTouchAngle(x, y);
                float touchRadius = (float) Math.sqrt(x * x + y * y);
                //判断触摸的点距离饼状图圆心的距离<饼状图对应圆的圆心
                if (touchRadius > mInnerRadius && touchRadius < mRadius) {
                    //说明是一个有效点击区域
                    //查找触摸的角度是否位于起始角度集合中
                    //binarySearch:参数2在参数1对应的集合中的索引
                    //未找到,则返回 -(和搜索的值附近的大于搜索值的正确值对应的索引值+1)
                    //{1,2,3}
                    //搜索1:返回值1在集合中对应的索引0
                    //1.2:返回值为 -(1+1) -2
                    //1.8:返回值 -(1+1) -2
                    int searchResult = Arrays.binarySearch(startAngles, touchAngle);
                    if (searchResult < 0) {
                        position = -searchResult - 1 - 1;
                    } else {
                        position = searchResult;
                    }
                    if (mListener != null) {
                        mListener.onSpecialTypeClick(mPieDataList.get(position));
                    }
                    mPieData = mPieDataList.get(position);
                    invalidate();
                }
                break;
        }
        return super.onTouchEvent(event);
    }

    //被点击的扇形的位置
    private int position;
    private float[] startAngles;

    public float getTouchAngle(float x, float y) {
        float touchAngle = 0;
        if (x < 0 && y < 0) {  //2 象限
            touchAngle += 180;
        } else if (y < 0 && x > 0) {  //1象限
            touchAngle += 360;
        } else if (y > 0 && x < 0) {  //3象限
            touchAngle += 180;
        }
        //Math.atan(y/x) 返回正数值表示相对于 x 轴的逆时针转角,返回负数值则表示顺时针转角。
        //返回值乘以 180/π,将弧度转换为角度。
        touchAngle += Math.toDegrees(Math.atan(y / x));
        if (touchAngle < 0) {
            touchAngle = touchAngle + 360;
        }
        return touchAngle;
    }

    private OnPieClickListener mListener;

    public void setOnPieListener(OnPieClickListener mListener) {
        this.mListener = mListener;
    }

    public interface OnPieClickListener {
        void onSpecialTypeClick(PieData data);
    }


    /**
     * 设置饼状图的数据
     */
    public void setData(List<PieData> pieList) {
        this.mPieDataList = pieList;
        if (mPieDataList != null && mPieDataList.size() > 0) {
            totalRadio = 0;
            for (PieData pieData : mPieDataList) {
                totalRadio += pieData.value;
            }
            for (PieData pieData : mPieDataList) {
                pieData.ratio = pieData.value / totalRadio;
            }
            startAngles = new float[mPieDataList.size()];
            mPieData = mPieDataList.get(0);
            // 0-1
            ValueAnimator valueAnimator = ObjectAnimator.ofFloat(0, 1);
            valueAnimator.setDuration(ANIMATION_DURATION);
            valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    animValue = (float) animation.getAnimatedValue();
                    invalidate();
                }
            });
            valueAnimator.setInterpolator(new AccelerateInterpolator());
            valueAnimator.start();
        }
    }


}
6.处理点击事件:就是重写OnTouchEvent,然后根据点击的x y坐标计算出用户点击的位置是在哪块扇形上。

点击的有效范围: 内圆的半径< (x*x+y*y)开根号<扇形的半径。

7.加上属性动画,让绘制动起来。