Android自定义View——一个通用的折线趋势图组件

为啥说是通用的呢?因为你可以随便放几条折线都行,随便几个说明背景都可以。。。颜色神马都可设置。。。

为啥这么随便?因为公司业务需要,有的折线图是2条折线、2个说明背景色块,有的需要1条折线、3个说明背景,还有个奇葩的是1条折线、4个说明背景,总不能每个都要写一个自定义View吧~

先看效果图:这是两条折线,两个背景图的(拍照不是截图,所以看起来没那么工整,数据我都对比过了,准的一塌糊涂!!!)

先想想思路,一个趋势图需要有以下6个元素:

1,横轴集合 ; 2,纵轴集合; 3,横轴的单位;4,纵轴的单位;5,数据点集合,多少条折线就多少集合;6,说明背景集合。

然后慢慢实现:

写一个表示折线的类:

public class CurveModel {
    //座标点列表
    private List<Value> curveLineDataList;
    //折线颜色
    private int curveColor;
    //折线描述语句
    private String curveDesc;

    public CurveModel(List<Value> curveLineDataList, int curveColor, String curveDesc) {
        this.curveLineDataList = curveLineDataList;
        this.curveColor = curveColor;
        this.curveDesc = curveDesc;
    }
    ...这儿是get方法...
}

再写一个背景的类:

public class BkgModel {
    //背景的最小值和最大值
    private Value value;
    //背景颜色
    private int color;
    //背景说明文字
    private String desc;

    public BkgModel(Value value, int color, String desc) {
        this.value = value;
        this.color = color;
        this.desc = desc;
    }
    ...这儿是get方法...
}

Value 类在CurveModel 类中表示x、y的数据,在BkgModel 类表示最小值和最大值

public class Value {
    private int x;
    private int y;

    public Value(int x, int y) {
        this.x = x;
        this.y = y;
    }
    ...这儿是get方法...
}

好了,所有Model都写好了,接下来,就是重头戏:绘制VIew

这儿用到了建造者模式:

先看下使用吧,看,是不是非常简单,这样构造一个View真是太方便了:

curveTrendChartView.Builder(getApplicationContext())
        //设置Y轴的数据
        .setY("mmHg", xList, 100.00)
        //设置X轴的数据
        .setX("日期", yList, 10.0)
        //添加背景说明色
        .addBKG(new BkgModel(new Value(8000, 9000), Color.parseColor("#FFF5EE"), "低压正常范围"))
        //再添加背景说明色,如果你还想添加,那就继续add
        .addBKG(new BkgModel(new Value(12000, 14000), Color.parseColor("#E0FFFF"), "高压正常范围"))
        //添加折线
        .addLine(new CurveModel(lowList, Color.parseColor("#006400"), "低压"))
        //我还想添加
        .addLine(new CurveModel(highList, Color.RED,"高压"))
        .build();

接下来是CurveTrendChartView了:

1,主要重写了onDraw方法:

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    //绘制背景和说明文字的方法,在该方法初始化了高度的一些变量,所以首先执行该方法
    drawDesc(canvas);
    //绘制Y座标
    drawYLineText(canvas);
    //绘制X座标
    drawXLineText(canvas);
    //绘制背景虚线
    drawDottedLine(canvas);
    //绘制每条折线
    drawCurceLine(canvas);
}

2,接下来就是绘制各个模块了,里面对于座标的绘制真是让我头疼,调了好多次,各种加减乘除,注意,高度是和趋势图相反的:咱们的趋势图是左图,原点在左下。找XY座标时,要把思想转换为右图,原点在左上。

举例说明:

虚线的高度是这样的:因为要把虚线绘制在中间,所以得加上mYItemHeight / 2,再加上y轴单位和描述框的高度

 i * mYItemHeight + mYItemHeight / 2 + mYUnitHeight + descHeight;

点的高度是这样的:因为虚线是在中间绘制的,所以总高度得减去最上虚线的上半部分和最下虚线的下半部分,然后根据比例求座标点的位置,最后加上那一大串高度

(mYHeight  - mYItemHeight)* ((curveLineDataList.get(i).getY() - maxData) / (minData - maxData)) + 
mYItemHeight / 2 + mYUnitHeight + descHeight

但这样还是不能使座标点完全正确,这个问题困扰了我好几个小时,各种debug啊,问题在于mYItemHeight:

//注意,就是这个float,我之前没加,结果精度变了,小数点后的都为0了,怪自己太粗心啊
mYItemHeight = mYHeight / (float) mYDataList.size();

其实也没啥了,上完整代码:

public class CurveTrendChartView  extends View {
    /**
     * 先想想思路~
     * 一个曲线图有以下6个要素
     * 1,需要有横轴集合 List<T> xDataList
     * 2,需要有Y轴集合
     * 3,需要有横轴单位
     * 4,需要有Y轴单位
     * 5,需要有数据点集合,可能是多条折线,以及折现颜色,折线含义
     * 6,需要有正常范围、异常范围,以及范围背景色,范围含义
     *
     * 需要的画笔有
     * 1,虚线画笔
     * 2,文字(横座标文字,纵座标文字,描述含义的文字)
     * 3,折线的画笔
     *
     *
     * 需要的类:
     * 1,表示X数据和Y数据的
     * 2,背景
     * 3,每个折线
     */
    private Context mContext;
    //总宽高
    private int mWidth;
    private int mHeight;
    //Y轴宽高
    private int mYWidth = 100, mYHeight;
    //X轴宽高
    private int mXWidth, mXHeight = 50;

    //虚线的画笔
    private Paint mDottedLinePaint;
    private Path mDottedLinePath;
    private int mDottedLineColor = Color.BLUE;
    //曲线的画笔
    private Paint mCurvePaint;
    //曲线圆点的画笔
    private Paint mPointPaint;
    private int mPointColor = Color.BLACK;
    private int pointSize = 8;

    //文字的画笔
    private Paint mTextPaint;
    private int mTextColor = Color.BLACK;
    private int mTextSize = 30;
    //X轴文字大小,颜色
    private int mXTextSize, mXTextColor;
    //Y轴文字大小,颜色
    private int mYTextSize, mYTextColor;

    // Y轴的数据源
    private List<Integer> mYDataList;
    private double minData, maxData;
    // Y轴数据的单位
    private String mYDataUnit = "mmHg";
    private int mYUnitHeight = 30;
    //y轴每个item的高度
    private float mYItemHeight;

    //X轴的数据源
    private List<Integer> mXDataList;
    //X轴数据的单位
    private String mXDataUnit = "日期";
    private float mXUnitHeight = 70;
    //X轴每个item的宽度
    private float mXItemWidth;

    //曲线的数据源
    private List<CurveModel> mLineList;
    private List<BkgModel> mBkgList;

    //正常范围/异常范围背景
    private Paint bkgPaint;

    //顶部描述文字
    //描述框的高度
    private int descHeight = 100;
    //X轴和Y轴保留的小数点
    private double mXNum,mYNum;

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

    public CurveTrendChartView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, -1);
    }

    public CurveTrendChartView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.mContext = context;
        //获取属性文件
        TypedArray array = mContext.obtainStyledAttributes(attrs, R.styleable.CurveTrendChartView);
        mXTextSize = array.getDimensionPixelSize(R.styleable.CurveTrendChartView_xTextSize,mTextSize);
        mYTextSize = array.getDimensionPixelSize(R.styleable.CurveTrendChartView_yTextSize,mTextSize);
        mXTextColor = array.getColor(R.styleable.CurveTrendChartView_xTextColor,mTextColor);
        mYTextColor = array.getColor(R.styleable.CurveTrendChartView_yTextColor,mTextColor);
        //TypedArray使用完一定要回收,否则会造成内存泄漏
        array.recycle();
    }

    /**
     * 初始化画笔和路径
     */
    private void initPaint() {
        mDottedLinePaint = new Paint();
        mDottedLinePaint.setAntiAlias(true);//抗锯齿效果
        mDottedLinePaint.setStyle(Paint.Style.STROKE);
        mDottedLinePaint.setColor(mDottedLineColor);
        mDottedLinePaint.setStrokeWidth(2);
        mDottedLinePath = new Path();

        mCurvePaint = new Paint();
        mCurvePaint.setAntiAlias(true);
        mCurvePaint.setStyle(Paint.Style.STROKE);

        mPointPaint = new Paint();
        mPointPaint.setAntiAlias(true);
        mPointPaint.setStyle(Paint.Style.FILL);
        mPointPaint.setColor(mPointColor);
        mPointPaint.setStrokeWidth(pointSize);
        mPointPaint.setTextAlign(Paint.Align.CENTER);

        mTextPaint = new Paint();
        mTextPaint.setAntiAlias(true);
        mTextPaint.setColor(mTextColor);
        mTextPaint.setStyle(Paint.Style.FILL);
        mTextPaint.setTextSize(mTextSize);
        mTextPaint.setTextAlign(Paint.Align.CENTER);

        bkgPaint = new Paint();
        bkgPaint.setAntiAlias(true);
        bkgPaint.setStyle(Paint.Style.FILL);

    }
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = w;
        mHeight = h;
        mXWidth = mWidth - mYWidth;
        mXItemWidth = (mWidth - mYWidth - mXUnitHeight) / (float) mXDataList.size();
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //绘制背景和说明文字的方法,在该方法初始化了高度的一些变量,所以首先执行该方法
        drawDesc(canvas);
        //绘制Y座标
        drawYLineText(canvas);
        //绘制X座标
        drawXLineText(canvas);
        //绘制背景虚线
        drawDottedLine(canvas);
        //绘制每条折线
        drawCurceLine(canvas);
    }
    /**
     * 绘制描述语句部分
     * @param canvas
     * 初始化了全局变量@{mYHeight}
     */
    private void drawDesc(Canvas canvas) {
        if(null != mBkgList && mBkgList.size()>0){
            //设置对齐方式为左对齐
            mTextPaint.setTextAlign(Paint.Align.LEFT);
            int bkgHeight = 40;
            mTextPaint.setTextSize(mTextSize);
            mTextPaint.setColor(mTextColor);

            //绘制背景说明颜色和说明文字
            //定义框的左边距,定义每个说明背景颜色块宽度
            int left = mWidth / 2 + 20, width = 80;
            //说明框的高度
            descHeight =  10+bkgHeight/2;
            for(BkgModel item : mBkgList){
                canvas.save();
                bkgPaint.setStrokeWidth(bkgHeight);
                bkgPaint.setColor(item.getColor());
                //画背景框
                canvas.drawLine(left, descHeight+10 , left + width,descHeight+10 , bkgPaint);
                //画描述语句
                canvas.drawText(item.getDesc(), left + width + 20, descHeight+10+mTextSize/2, mTextPaint);
                canvas.restore();
                descHeight = descHeight+10+bkgHeight;
            }

            //绘制折线说明颜色和说明文字
            left = left + width + 230;
            int height =10+ bkgHeight/2;
            for(CurveModel item:mLineList){
                canvas.save();
                mCurvePaint.setColor(item.getCurveColor());
                //画折线颜色
                canvas.drawLine(left, height+10, left + width, height+10, mCurvePaint);
                //画描述语句
                canvas.drawText(item.getCurveDesc(), left + width + 20, height+10+mTextSize/2, mTextPaint);
                canvas.restore();
                height = height+10+bkgHeight;

            }

            //画描述语句的外框
            canvas.drawRect(new Rect(mWidth / 2, 10, mWidth - 10, descHeight + 10 - bkgHeight/2), mDottedLinePaint);

            //初始化y轴高度和每行的高度
//            descHeight -= 10;
            mYHeight = mHeight - mXHeight -mYUnitHeight- descHeight;
            //注意,这儿最好转为float,不然会导致高度计算不太准,我找位置不准的原因找了很久很久,终于找到了
            mYItemHeight = mYHeight / (float) mYDataList.size();

            //在趋势表中绘制背景色
            for(BkgModel item : mBkgList){
                canvas.save();
                //获取最低值和最高值的下标
                int bottom = mYDataList.indexOf(item.getValue().getX()),high = mYDataList.indexOf(item.getValue().getY());
                //计算背景的高度
                bkgPaint.setStrokeWidth((bottom - high) * mYItemHeight);
                bkgPaint.setColor(item.getColor());
                //计算背景的起始位置
                float diastolicStart = (float) ((high +(bottom - high)/2.0 +0.5 ) * mYItemHeight + mYUnitHeight + descHeight);
                canvas.drawLine(mYWidth, diastolicStart, mWidth, diastolicStart, bkgPaint);
                canvas.restore();

            }

        }
    }

    /**
     * 对应Y轴的虚线
     * @param canvas
     */
    private void drawDottedLine(Canvas canvas) {
        int count = mYDataList.size();
        if (count > 0) {
            canvas.save();
            //虚线效果:先画5的实线,再画5的空白,开始绘制的偏移值为0
            mDottedLinePaint.setPathEffect(new DashPathEffect(new float[]{5, 5}, 0));
            mDottedLinePaint.setStrokeWidth(1f);
            float startY;
            for (int i = 0; i < count; i++) {
                //因为要绘制在中间,所以得加上mYItemHeight / 2,再加上mYUnitHeight + descHeight
                startY = i * mYItemHeight + mYItemHeight / 2 + mYUnitHeight + descHeight;
                mDottedLinePath.reset();
                mDottedLinePath.moveTo(mYWidth, startY);
                mDottedLinePath.lineTo(mWidth, startY);
                canvas.drawPath(mDottedLinePath, mDottedLinePaint);
            }
            canvas.restore();
        }
    }

    /**
     * Y轴的文字
     * @param canvas
     */
    private void drawYLineText(Canvas canvas) {
        int count = mYDataList.size();
        if (count > 0) {
            canvas.save();
            mTextPaint.setTextSize(mYTextSize);
            mTextPaint.setColor(mYTextColor);
            float startY;
            float baseline;
            Paint.FontMetricsInt metrics = mTextPaint.getFontMetricsInt();
            mTextPaint.setTextAlign(Paint.Align.CENTER);
            for (int i = 0; i < count; i++) {
                startY = (i + 1) * mYItemHeight;
                baseline = (startY * 2 - mYItemHeight - metrics.bottom - metrics.top) / 2 + mYUnitHeight + descHeight;
                canvas.drawText(String.valueOf(mYDataList.get(i)/mYNum), mYWidth / 2, baseline, mTextPaint);
            }
            canvas.drawText(mYDataUnit, mYWidth / 2, descHeight, mTextPaint);
            canvas.restore();
        }
    }


    /**
     * X轴的文字
     * @param canvas
     */
    private void drawXLineText(Canvas canvas) {
        int count = mXDataList.size();
        if (count > 0) {
            canvas.save();
            mTextPaint.setTextAlign(Paint.Align.CENTER);
            mTextPaint.setTextSize(mXTextSize);
            mTextPaint.setColor(mXTextColor);
            float startX;
            for (int i = 0; i < count; i++) {
                startX = mYWidth + i * mXItemWidth + mXItemWidth / 2;
                canvas.drawText(String.valueOf((mXDataList.get(i)/mXNum)), startX, mHeight-mXHeight/2, mTextPaint);
            }
            canvas.restore();
            mTextPaint.setTextSize(mTextSize);
            mTextPaint.setColor(mTextColor);
            canvas.drawText(mXDataUnit, mWidth - mXItemWidth / 2, mHeight-mXHeight/2, mTextPaint);
        }
    }

    /**
     * 绘制曲线
     * @param canvas
     */
    private void drawCurceLine(Canvas canvas) {
        for(CurveModel item:mLineList) {
            canvas.save();
            mCurvePaint.setColor(item.getCurveColor());
            List<Value> curveLineDataList = item.getCurveLineDataList();
            int count = curveLineDataList.size();
            if (count > 0) {
                mDottedLinePath.reset();
                float stopX, stopY;
                float baseHeight = mYItemHeight / 2 + mYUnitHeight + descHeight;
                //因为虚线是在中间绘制的,所以得减去最上虚线的上半部分和最下虚线的下半部分
                float totalHeight = mYHeight  - mYItemHeight;
                float totalWidth = mWidth - mYWidth - mXUnitHeight - mXItemWidth;
                mDottedLinePath.moveTo(mXItemWidth / 2 + mYWidth, (float) (totalHeight * ((curveLineDataList.get(0).getY() - maxData) / (minData - maxData))) + baseHeight);
                canvas.drawPoint(mXItemWidth / 2 + mYWidth, (float) (totalHeight * ((curveLineDataList.get(0).getY() - maxData) / (minData - maxData))) + baseHeight, mPointPaint);
                for (int i = 1; i < count; i++) {
                    stopX = (float) (totalWidth * (curveLineDataList.get(i).getX() - mXDataList.get(0)) / (mXDataList.get(mXDataList.size() - 1) - mXDataList.get(0)) + mYWidth + mXItemWidth / 2);
                    //根据比例求得点的座标
                    stopY = (float) (totalHeight * ((curveLineDataList.get(i).getY() - maxData) / (minData - maxData)) + baseHeight);
                    mDottedLinePath.lineTo(stopX, stopY);
                    canvas.drawPoint(stopX, stopY, mPointPaint);

                }
                canvas.drawPath(mDottedLinePath, mCurvePaint);
            }
            canvas.restore();
        }
    }

    /**
     * 初始化全局变量
     * @param context 上下文
     * @param xUnit x轴的单位
     * @param xDataList X轴数据源
     * @param xNum x轴保留的小数点位数
     * @param yUnit y轴的单位
     * @param yDataList y轴的数据源
     * @param yNum y轴保留的小数点位数
     * @param lineList 曲线列表
     * @param bkgItemList 说明背景列表
     * @return
     */
    private CurveTrendChartView init(Context context, String xUnit, List<Integer> xDataList, double xNum, String yUnit, List<Integer> yDataList, double yNum, List<CurveModel> lineList, List<BkgModel> bkgItemList){
        this.mContext = context;
        this.mXDataUnit = xUnit;
        this.mXDataList = xDataList;
        this.mXNum = xNum;
        this.mYDataUnit = yUnit;
        this.mYDataList = yDataList;
        this.mYNum = yNum;
        this.mLineList = lineList;
        this.mBkgList = bkgItemList;
        this.minData = mYDataList.get(mYDataList.size() - 1);
        this.maxData = mYDataList.get(0);
        initPaint();
        invalidate();
        return this;
    }

    public Builder Builder(Context context){
        return new Builder(context);
    }

    //利用建造者模式构造一个数据表
    public class Builder{
        private Context mContext;
        //Y轴数据
        private List<Integer> mYDataList;
        //Y轴单位
        private String mYUnit;
        //X轴数据
        private List<Integer> mXDataList;
        //X轴单位
        private String mXUnit;
        private double mXNum,mYNum;
        //每条数据源集合
        private List<CurveModel> mLineList = new ArrayList<>();
        //每个背景集合
        private List<BkgModel> mBkgItemList = new ArrayList<>();

        private Builder(Context context){
            this.mContext = context;
        }
        public Builder setX(String xUnit,List<Integer> xDataList,double xNum){
            this.mXUnit = xUnit;
            this.mXDataList = xDataList;
            this.mXNum = xNum;
            return this;
        }
        public Builder setY(String yUnit,List<Integer> yDataList,double yNum){
            this.mYUnit = yUnit;
            this.mYDataList = yDataList;
            this.mYNum = yNum;
            return this;
        }
        public Builder addLine(CurveModel item){
            this.mLineList.add(item);
            return this;
        }
        public Builder addBKG(BkgModel item){
            mBkgItemList.add(item);
            return this;
        }
        public CurveTrendChartView build(){
            return init(mContext,mXUnit,mXDataList,mXNum,mYUnit,mYDataList,mYNum,mLineList,mBkgItemList);
        }
    }

    public void destory(){
        mBkgList = null;
        mLineList = null;
        mXDataList = null;
        mYDataList = null;
        mContext = null;
    }
}

这样就结束了,很简单,最后附上地址吧

https://github.com/androidGL/CurveTrendChartView

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