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

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