天天看點

安卓自定義折線圖、柱狀圖

自定義柱狀圖、折線圖、餅狀圖,先上效果圖。

安卓自定義折線圖、柱狀圖

上面是柱狀圖和折線圖混合圖。

需要代碼的直接上連結拿走https://github.com/yangxiaohan2014/ChartView

分析圖表

  1. X軸分析

    X軸顯示日期,并且是可左右滑動的

  2. Y軸分析

    Y軸顯示資料,比較簡單

  3. 圖表分析

    圖表分為兩部分:折線圖和柱狀圖

自定義View

1.分析ChartView需要的坐标資料

原點坐标ox、oy,

X軸步長Xstep,

X軸第一個坐标值

X軸滑動時需要的兩個變量:

X軸第一個坐标的最小值minXInit,

X軸第一個坐标的最大值maxXInit

如下圖:

安卓自定義折線圖、柱狀圖

滑動時需要計算圖表拖到最左邊的值:

安卓自定義折線圖、柱狀圖

2.初始化需要的變量和自定義屬性

public ChartView(Context context) {
        super(context);
        init(context);
    }

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

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

    private void init(Context context) {
        WindowManager wm = (WindowManager) context.getSystemService(WINDOW_SERVICE);
        screenWidth = wm.getDefaultDisplay().getWidth();
        mPaint = new Paint();
        mPaint.setColor(Color.parseColor("#FF2741"));
        mPaint.setAntiAlias(true);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setStrokeWidth(3);

    }
    private void initCustomAttrs(Context context, AttributeSet attrs) {
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.ChartView);
        xStep = typedArray.getDimension(R.styleable.ChartView_x_step, 45);
        yStep = typedArray.getDimension(R.styleable.ChartView_y_step, 35);

        xTextColor = typedArray.getColor(R.styleable.ChartView_x_text_color, Color.BLACK);
        yTextColor = typedArray.getColor(R.styleable.ChartView_y_text_color, Color.BLACK);
        linColor = typedArray.getColor(R.styleable.ChartView_x_line_color, Color.BLUE);
        columnWidth = typedArray.getDimension(R.styleable.ChartView_column_width, 20);
        columnColor = typedArray.getColor(R.styleable.ChartView_column_color, Color.BLUE);
        isColumnGradient = typedArray.getBoolean(R.styleable.ChartView_column_color_gradient, false);
        columnStartColor = typedArray.getColor(R.styleable.ChartView_column_start_color, Color.BLUE);
        columnEndColor = typedArray.getColor(R.styleable.ChartView_column_end_color, Color.BLUE);
        columnTextColor = typedArray.getColor(R.styleable.ChartView_column_text_color, Color.BLUE);
        startX = (int) typedArray.getDimension(R.styleable.ChartView_x_padding, DensityUtil.dp2px(10));
        startY = (int) typedArray.getDimension(R.styleable.ChartView_y_padding, DensityUtil.dp2px(10));

        linePointColor = typedArray.getColor(R.styleable.ChartView_line_point_color, Color.BLUE);
        lineChartColor = typedArray.getColor(R.styleable.ChartView_line_chart_color, Color.BLUE);

        lineTextColor = typedArray.getColor(R.styleable.ChartView_line_text_color, Color.BLUE);
        isChartDataVisible = typedArray.getBoolean(R.styleable.ChartView_is_chart_data_visible, false);
        typedArray.recycle();
    }
           

3.重寫onLayout、onDraw方法

onlayout方法中擷取圖表的原點O的位置分别為ox、oy,xInit為x軸第一個刻度位置,minXInit為X軸最小值,maxXInit為滑動時X軸的最大值

@Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        if (changed) {
            height = getHeight();
            width = getWidth();
            ox = startX;
            oy = startY + height - paddingBottom;

            xInit = startX + xStep;
            minXInit = width - ox - xStep * (xPoints.size() - 1);
            maxXInit = xInit;
        }

        super.onLayout(changed, left, top, right, bottom);
   
   }

  @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawColor(bgcolor);
        drawOXYPoint(canvas);
        drawYPoints(canvas);
//        drawXPoints(canvas);
        drawColumnsAndLines(canvas);

    }
           

4.開始繪制

首先繪制原點O和X軸、Y軸

private void drawOXYPoint(Canvas canvas) {
        mPaint.setColor(yTextColor);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setTextSize(DensityUtil.dp2px(14));
        String o = "0";
        Rect rect = new Rect();
        mPaint.getTextBounds("0", 0, o.length(), rect);
        int bottom = rect.bottom;
        int top = rect.top;
        int left = rect.left;
        int right = rect.right;
        int oWidth = right - left;
        int oHeight = bottom - top;
        canvas.drawText(o, ox - (oWidth + columnTextPadding), oy + oHeight / 2, mPaint);
//        mPaint.setColor(linColor);
//        //橫線
//        canvas.drawLine(startX, startY + height - paddingBottom, startX + width, startY + height - paddingBottom, mPaint);
//        //豎線
//        canvas.drawLine(startX, startY, startX, height + startY - paddingBottom, mPaint);

        canvas.drawText(o, startX - (oWidth + columnTextPadding), oy + oHeight / 2, mPaint);
        mPaint.setColor(linColor);
        //橫線
        canvas.drawLine(startX, startY + height - paddingBottom, startX + width, startY + height - paddingBottom, mPaint);
        //豎線
        canvas.drawLine(startX, startY, startX, height + startY - paddingBottom, mPaint);
    }
           

繪制Y軸坐标

/**
     * 繪制縱坐标
     *
     * @param canvas
     */
    private void drawYPoints(Canvas canvas) {
        mPaint.setStyle(Paint.Style.FILL);
        for (int i = 0; i < yPoints.length; i++) {
            mPaint.setColor(linColor);
            float yLength = (i + 1) * yStep;
            float pointY = oy - yLength;
            canvas.drawLine(startX, pointY, startX + pointWidth, pointY, mPaint);
            mPaint.setColor(yTextColor);
            String pointStr = yPoints[i];
            Rect rect = new Rect();
            mPaint.getTextBounds(pointStr, 0, pointStr.length(), rect);
            int width = rect.right - rect.left;
            int height = rect.bottom - rect.top;

            canvas.drawText(pointStr, ox - (width + yTextPadding), pointY + height / 2, mPaint);

        }

    }
           

繪制折線圖和柱狀圖

private void drawColumnsAndLines(Canvas canvas) {
        //??
        int layerId = canvas.saveLayer(0, 0, width, height, null, Canvas.ALL_SAVE_FLAG);
        drawColumns(canvas);
        drawLines(canvas);

        // 将折線超出x軸坐标的部分截取掉
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setColor(Color.BLUE);
        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
        RectF rectF = new RectF(0, 0, ox, height);
        canvas.drawRect(rectF, mPaint);
        mPaint.setXfermode(null);

        //儲存圖層
        canvas.restoreToCount(layerId);
    }
           
/**
     * 繪制柱狀圖
     *
     * @param canvas
     */
    private void drawColumns(Canvas canvas) {
        for (int i = 0; i < xPoints.size(); i++) {
            float xLength = i * xStep;
            float pointX = xInit + xLength;
            float centerX = pointX;
            float columnHeight = yStep * xPoints.get(i).getxDecimal1() / stepYNumber;

            Shader shader = new LinearGradient(centerX, oy, centerX, oy - columnHeight, columnStartColor,
                    columnEndColor, Shader.TileMode.CLAMP);

            if (isColumnGradient) {
                mPaint.setShader(shader);
            } else {
                mPaint.setColor(columnColor);
            }

            //畫柱形圖
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                canvas.drawRoundRect(centerX - columnWidth / 2, oy - columnHeight, centerX + columnWidth / 2, oy, 0, 0, mPaint);
            } else {
                canvas.drawRect(centerX - columnWidth / 2, oy - columnHeight, centerX + columnWidth / 2, oy, mPaint);
            }
            mPaint.setShader(null);

            if (xPoints.get(i).isDataVisible) {
                //繪制柱狀圖資料
                String pointNum = xPoints.get(i).getxDecimal1() + "";
                Rect rectNum = new Rect();
                mPaint.getTextBounds(pointNum, 0, pointNum.length(), rectNum);
                int numRectHeight = rectNum.bottom - rectNum.top;
                int numRectWidth = rectNum.right - rectNum.left;
                mPaint.setColor(columnTextColor);
                canvas.drawText(pointNum, centerX - numRectWidth / 2, oy - columnHeight - columnTextPadding, mPaint);
                mPaint.setColor(selectedXTitleColor);
            }

            //繪制橫坐标标題
            mPaint.setColor(xTextColor);
            String title = xPoints.get(i).getTitle();
            Rect rect = new Rect();
            mPaint.getTextBounds(title, 0, title.length(), rect);
            int rectHeight = rect.bottom - rect.top;
            int rectWidth = rect.right - rect.left;
            canvas.drawText(title, centerX - rectWidth / 2, oy + rectHeight + columnTextPadding, mPaint);
        }

    }
           

繪制折線圖:

為了使折線看起來順滑流暢,使用了二次貝塞爾曲線來繪制

/**
     * 繪制折線圖
     *
     * @param canvas
     */
    private void drawLines(Canvas canvas) {
        if (xPoints.size() == 0) {
            return;
        }
        Point[] mPoints = new Point[xPoints.size()];
//        float[] pointsy = new float[xPoints.size()];
        for (int i = 0; i < xPoints.size(); i++) {

            float xLength = i * xStep;
            float pointX = xInit + xLength;
            //計算圓圈坐标
            float pointY = yStep * xPoints.get(i).getxDecimal2() / stepYNumber;
            mPaint.setColor(linePointColor);

            mPaint.setStyle(Paint.Style.FILL);

            //繪制圓圈
            canvas.drawCircle(pointX, oy - pointY, DensityUtil.dp2px(3), mPaint);
            //繪制圓圈上的數值
            if (xPoints.get(i).isDataVisible) {
                String pointNum = xPoints.get(i).getxDecimal2() + "";
                Rect rectNum = new Rect();
                mPaint.getTextBounds(pointNum, 0, pointNum.length(), rectNum);
                int numRectWidth = rectNum.right - rectNum.left;
                mPaint.setColor(lineTextColor);
                canvas.drawText(pointNum, pointX - numRectWidth / 2, oy - pointY - columnTextPadding, mPaint);
            }


            //繪制貝塞爾曲線
            Point point = new Point((int) (0.5f + pointX), (int) (0.5f + oy - pointY));
            mPoints[i] = point;
            if (i > 0 && i <= xPoints.size() - 1) {
                mPaint.setStyle(Paint.Style.STROKE);
                Point startp = mPoints[i - 1];
                Point endp = mPoints[i];
                int wt = (startp.x + endp.x) / 2;
                Point p3 = new Point();
                Point p4 = new Point();
                p3.y = startp.y;
                p3.x = wt;
                p4.y = endp.y;
                p4.x = wt;

                Path path = new Path();
                path.moveTo(startp.x, startp.y);
                //二次貝塞爾曲線
//                path.quadTo(p3.x,p3.y,endp.x,endp.y);
                //繪制三次貝塞爾曲線
                path.cubicTo(p3.x, p3.y, p4.x, p4.y, endp.x, endp.y);

                canvas.drawPath(path, mPaint);
            }
        }


    }
           

5.圖表上資料的點選事件處理

int lastX;
    int lastY;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        this.getParent().requestDisallowInterceptTouchEvent(true);
        obtainVelocityTracker(event);


        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                lastX = x;
                lastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                if (xStep*xPoints.size() + startX > getWidth()){
                    int offsetx = (int) (event.getX() - lastX);
                    lastX = (int) event.getX();
                    if (xInit + offsetx < minXInit) {
                        xInit = (int) minXInit;
                    } else if (xInit + offsetx > maxXInit) {
                        xInit = (int) maxXInit;
                    } else {
                        xInit = xInit + offsetx;
                    }
                    invalidate();
                }

                break;
            case MotionEvent.ACTION_UP:
                if (event.getX() - lastX < 5) {
                    clickAction(event);
                }
//                scrollAfterActionUp();
                this.getParent().requestDisallowInterceptTouchEvent(false);
                recycleVelocityTracker();
                break;
            case MotionEvent.ACTION_CANCEL:
                this.getParent().requestDisallowInterceptTouchEvent(false);
                recycleVelocityTracker();
                break;
            case MotionEvent.ACTION_POINTER_DOWN:
                break;
            case MotionEvent.ACTION_POINTER_UP:
                break;
        }
        return true;
    }

           
/**
     * 點選X軸坐标或者折線節點
     *
     * @param event
     */
    private void clickAction(MotionEvent event) {
        int dp8 = DensityUtil.dp2px(10);
        float eventX = event.getX();
        float eventY = event.getY();
        for (int i = 0; i < xPoints.size(); i++) {
            //節點
            float x = xInit + xStep * i;

            float linePointY = yStep * xPoints.get(i).getxDecimal2() / stepYNumber;
            float columnY = yStep * xPoints.get(i).getxDecimal1() / stepYNumber;
            float y1 = oy - linePointY;
            float y2 = oy - columnY;

            if (eventX >= x - dp8 && eventX <= x + dp8 &&
                    eventY >= y1 - dp8 && eventY <= y1 + dp8 && selectIndex != i + 1) {//每個節點周圍8dp都是可點選區域
                if (selectIndex == i) {
                    xPoints.get(i).isDataVisible = false;
                } else {
                    xPoints.get(selectIndex) .isDataVisible = false;
                    xPoints.get(i).isDataVisible = true;
                }
                selectIndex = i;

                invalidate();
                return;
            }
            if (eventX >= x - dp8 && eventX <= x + dp8 &&
                    eventY >= y2 - dp8 && eventY <= y2 + dp8 && selectIndex != i + 1) {//每個節點周圍8dp都是可點選區域
                selectIndex = i + 1;
                invalidate();
                return;
            }

        }
    }