欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

自定义View-柱状图和折线图的合体

程序员文章站 2022-07-14 16:22:42
...

前端时间,工作中有个需求,柱状图和折线图合体,左右两个坐标分别表示柱状图和折线图,是不是看的有点抽象?没关系,先上个效果图来看一下
自定义View-柱状图和折线图的合体

左边坐标用来表示柱状图的高度,右边坐标用来表示折线图的点的位置。

然后触摸柱子需要出现一条标识线和一个小卡片,如下图:
自定义View-柱状图和折线图的合体

图中柱子中间有一根白色的标识线,手势滑动标识线和小卡片需要跟着改变。

虽然网上很多强大的图表库,不过由于时间关系,需要去改别人代码,不如自己动手来的快,记录下来,也给需要的朋友作个小参考。

属性动态设置

各种属性还是建议动态赋值,以后修改起来也方便很多

class ChartParam {
        private int verNumberSpace;
        private int horNumberSpace;
        private int numberSize;
        private int rectangleWidth;
        private int circleRadius; // 小圆半径
        private int baseLineWidth;
        private int screenWidth;
        private int sideSpace; // 距两边边距
        private int leftNumSpace;// 左边数字终点距y轴的距离
        private int leftToBaseLine; // 左边数字终点距基线的距离
        private int baseLineToYear; // 基线到年份的距离
        private int baseLineToLeft;
        private int firstRectCenterX; // 第一个矩形中点距边界距离
        private int firstRectLeft; // 第一个矩形left属性
        private int firstRextRight;
        private int rectSpace;
        private int overRectHeight;  // 高出相对柱状图的高度


        private int cardToBaseLine; // 提示相对基线的高度
        private int cardToRect; // 提示相对柱状图的边距
        private int roundRectAngle; // card圆角大小
        private int cardWidth;// card宽
        private int cardHeight;// card高

        private int card_textMarginTop;
        private int card_textMarginLeft;
        private int card_text_1to2;// 第一行文字距第二行文字距离
        private int card_text_2to3; // 第二行文字距第三行文字距离
        private int card_text_1toRect;// 第一行文字的x距圆角矩形的距离
        private int card_text_2toRect;//
        private int card_text_3toRect;//
    }

然后在构造函数中进行初始化赋值

private void init() {

        mContext = getContext();
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mChartParam = new ChartParam();
        mChartParam.baseLineWidth = getWidth(mContext) - dpToPx(mContext, 38 * 2);
        mChartParam.screenWidth = dpToPx(mContext, 375);
        mChartParam.horNumberSpace = dpToPx(mContext, 28);
        mChartParam.verNumberSpace = dpToPx(mContext, 23);
        mChartParam.rectangleWidth = dpToPx(mContext, 12);
        mChartParam.circleRadius = dpToPx(mContext, 2);
        mChartParam.numberSize = dpToPx(mContext, 11);
        mChartParam.sideSpace = dpToPx(mContext, 15);
        mChartParam.leftNumSpace = dpToPx(mContext, 30);
        mChartParam.leftToBaseLine = dpToPx(mContext, 8);
        mChartParam.baseLineToYear = dpToPx(mContext, 5);
        mChartParam.baseLineToLeft = dpToPx(mContext, 38);
        mChartParam.firstRectCenterX = dpToPx(mContext, 79);
        mChartParam.firstRectLeft = dpToPx(mContext, 73);
        mChartParam.firstRextRight = dpToPx(mContext, 85);
        mChartParam.rectSpace = dpToPx(mContext, 40);
        mChartParam.overRectHeight = dpToPx(mContext, 20);
        mChartParam.cardToBaseLine = dpToPx(mContext, 58);
        mChartParam.cardToRect = dpToPx(mContext, 8);
        mChartParam.roundRectAngle = dpToPx(mContext, 2);
        mChartParam.cardWidth = dpToPx(mContext, 127);
        mChartParam.cardHeight = dpToPx(mContext, 63);
        mChartParam.card_textMarginTop = dpToPx(mContext, 8);
        mChartParam.card_textMarginLeft = dpToPx(mContext, 8);
        mChartParam.card_text_1to2 = dpToPx(mContext, 8);
        mChartParam.card_text_2to3 = dpToPx(mContext, 6);
        mChartParam.card_text_1toRect = dpToPx(mContext,20);
        mChartParam.card_text_2toRect = dpToPx(mContext,38);
        mChartParam.card_text_3toRect = dpToPx(mContext,55);

        rectStartX = new float[5];
        rectStopX = new float[5];
        rectStopY = new float[5];

        touchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop();
    }

具体的值看大家的需求自己定义啦。


下面开始画具体的图了。

左侧坐标栏

 private void drawLeftNumber(Canvas canvas) {

        for (int i = 0; i < 6; i++) {
            setPaint(NUMBER_TEXT);
            String text = ((5.0 - i) + "").equals("0.0") ? "0" : 5.0 - i + "";
            float number = Float.parseFloat(text);
            int textWidth = (int) mPaint.measureText(text);
            Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
            canvas.drawText(text, mChartParam.leftNumSpace - textWidth,
                    mChartParam.verNumberSpace * i + (Math.abs(fontMetrics.ascent) + fontMetrics.descent) * (i + 1),
                    mPaint);
            if (number == 0 || Math.abs(number) <= 0.2) {
                baseNumber = number + "";
                // 该值是基线相关数字最上面的值
                baseLineY = mChartParam.verNumberSpace * i + (Math.abs(fontMetrics.ascent) + fontMetrics.descent) * i;
            }
            if (i == 5) {
                yearY = mChartParam.verNumberSpace * i + (Math.abs(fontMetrics.ascent) + fontMetrics.descent) * (i + 1);
            }
        }
    }

这里需要注意一点的是drawText()绘制文字的起点是差不多文字左下角的位置(这里只是差不多,其实不完全准确,这里我忽略了那小差别)。由于需求是数字靠右对齐,所以在计算文字的起点坐标的时候,需要用文字靠右的距离减去文字的宽度,也就是x轴坐标:

mChartParam.leftNumSpace - textWidth,

文字宽度的计算如下:

int textWidth = (int) mPaint.measureText(text);

然后是文字y轴计算,这里需要计算文字的高度,关于文字的高度问题参考下面这张图:
自定义View-柱状图和折线图的合体

(注:copy from http://hencoder.com/ui-1-3/


基线

基线也就是底部的那根线了 ,这个就比较简单了

 /**
     * 基线
     *
     * @param canvas
     */
    private void drawBaseLine(Canvas canvas) {

        setPaint(BASE_LINE);

        canvas.drawLine(mChartParam.baseLineToLeft, baseLineY - mChartParam.baseLineToYear, mChartParam.baseLineToLeft + mChartParam.baseLineWidth, baseLineY - mChartParam.baseLineToYear, mPaint);
    }

右侧坐标栏

右侧坐标栏的绘制和左侧是一样的,值得注意的点也只有是文字的高度

/**
     * 右边数字栏
     *
     * @param canvas
     */
    private void drawRightNumber(Canvas canvas) {

        for (int i = 0; i < 6; i++) {
            setPaint(NUMBER_TEXT);
            String text = ((100 - 20 * i) + "").equals("0") ? "0" : (100 - 20 * i + "%");
            int textWidth = (int) mPaint.measureText(text);
            Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
            canvas.drawText(text, getWidth(mContext) - mChartParam.sideSpace - textWidth,
                    mChartParam.verNumberSpace * i + (Math.abs(fontMetrics.ascent) + fontMetrics.descent) * (i + 1),
                    mPaint);
        }
    }

年份

年份的绘制也很简单,也就是文字绘制,由于标注图有给出各种距离,所以绘制起来也很顺利

/**
     * 年份
     *
     * @param canvas
     */
    private void drawYear(Canvas canvas) {

        setPaint(NUMBER_TEXT);
        Calendar calendar = Calendar.getInstance();
        int year = calendar.get(Calendar.YEAR);
        for (int i = 0; i < 5; i++) {
            String text = year - (4 - i) + "";
            int textWidth = (int) mPaint.measureText(text);
            canvas.drawText(text, mChartParam.firstRectCenterX - textWidth / 2 + (textWidth + mChartParam.horNumberSpace) * i, yearY, mPaint);
            rectLeft = mChartParam.firstRectCenterX - textWidth / 2 + (textWidth + mChartParam.horNumberSpace) * i + textWidth / 2 - dpToPx(mContext, 6);
        }
    }

柱状图

柱状图的话,只要你知道第一个柱形图的x轴的位置,柱形的宽度,和个柱形之间的距离,也和很顺利

/**
     * 柱状图
     *
     * @param canvas
     */
    private void drawRectangle(Canvas canvas) {
        setPaint(RECTANGLE);
        for (int i = 0; i < 5; i++) {
            canvas.drawRect(mChartParam.firstRectLeft + (mChartParam.rectangleWidth + mChartParam.rectSpace) * i,
                    dpToPx(mContext, 60) - i * 2,
                    mChartParam.firstRextRight + (mChartParam.rectangleWidth + mChartParam.rectSpace) * i,
                    baseLineY - mChartParam.baseLineToYear, mPaint);
            rectStartX[i] = mChartParam.firstRectLeft + (mChartParam.rectangleWidth + mChartParam.rectSpace) * i;
            rectStopX[i] = mChartParam.firstRextRight + (mChartParam.rectangleWidth + mChartParam.rectSpace) * i;
            rectStopY[i] = dpToPx(mContext, 60) - i * 2;
        }
    }

这里的:

 rectStartX[i] = mChartParam.firstRectLeft + (mChartParam.rectangleWidth + mChartParam.rectSpace) * i;
            rectStopX[i] = mChartParam.firstRextRight + (mChartParam.rectangleWidth + mChartParam.rectSpace) * i;
            rectStopY[i] = dpToPx(mContext, 60) - i * 2;

是为了记录柱形的坐标,因为后面需要在触摸柱形图范围内显示标识线。


点和折线

接下里是先把点绘制出来,然后再连接起来,形成折线

/**
     * 点和折线
     *
     * @param canvas
     */
    private void drawPointAndLine(Canvas canvas) {
        setPaint(BROKE_LINE);
        for (int i = 0; i < 5; i++) {
            canvas.drawCircle(mChartParam.firstRectLeft + (mChartParam.rectangleWidth + mChartParam.rectSpace) * i + mChartParam.rectangleWidth / 2,
                    50 * i + 20, mChartParam.circleRadius, mPaint);
            if (i != 4) {
                float startX = mChartParam.firstRectLeft + (mChartParam.rectangleWidth + mChartParam.rectSpace) * i + mChartParam.rectangleWidth / 2;
                float stopX = mChartParam.firstRectLeft + (mChartParam.rectangleWidth + mChartParam.rectSpace) * (i + 1) + mChartParam.rectangleWidth / 2;
                float startY = 50 * i + 20;
                float stopY = 50 * (i + 1) + 20;
                canvas.drawLine(startX, startY, stopX, stopY, mPaint);
            }
        }
    }

两个点作为一条线的起点和终点。


标志线

前面在绘制矩形图的时候已经记录了各个矩形的坐标,所以这里标志线在每个柱形图的中间位置

/**
     * 触摸柱状图时中间的竖线
     * @param canvas
     */
    private void drawVerLine(Canvas canvas) {
        if (touchRectPoi != -1) {
            setPaint(CARD_LINE);
            float lineX = rectStartX[touchRectPoi] + (rectStopX[touchRectPoi] - rectStartX[touchRectPoi]) / 2;
            canvas.drawLine(lineX, baseLineY - mChartParam.baseLineToYear, lineX, rectStopY[touchRectPoi] - mChartParam.overRectHeight, mPaint);
        }
    }

小卡片

接下来是小卡片的展示,我这里的小卡片的位置是根据触摸 的柱状图的位置来确定的,前面三个是显示在柱状图的右边,后面两个是显示在柱状图的左边,但是卡片相对柱状图的距离和高度是确定的,小卡片的背景是用的一张图片。

/**
     * 卡片
     * @param canvas
     */
    private void drawTipsCard(Canvas canvas) {
        // 圆角矩形与其中的文字
        float roundRectLeft = 0, roundRectTop = 0;
        if (touchRectPoi != -1) {
            setPaint(ROUND_RECT);
            float lineX = rectStartX[touchRectPoi] + (rectStopX[touchRectPoi] - rectStartX[touchRectPoi]) / 2;
            if (touchRectPoi >= 0 && touchRectPoi < 3) {
                // 显示在柱状图右边
                canvas.drawBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.roundrect), lineX + mChartParam.cardToRect, baseLineY - mChartParam.cardToBaseLine - mChartParam.cardHeight,mPaint);
                roundRectLeft = lineX + mChartParam.cardToRect;
                roundRectTop = baseLineY - mChartParam.cardToBaseLine - mChartParam.cardHeight;
            } else {
                // 显示在柱状图左边
                canvas.drawBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.roundrect), lineX - mChartParam.cardToRect - mChartParam.cardWidth, baseLineY - mChartParam.cardToBaseLine - mChartParam.cardHeight,mPaint);
                roundRectLeft = lineX - mChartParam.cardToRect - mChartParam.cardWidth;
                roundRectTop = baseLineY - mChartParam.cardToBaseLine - mChartParam.cardHeight;
            }

            // 文字
            setPaint(CARD_TEXT);
            String text1 = "2017年年报";
            String text2 = "每股收益:1.83元";
            String text3 = "同比增长率:-12.00%";
            Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
            canvas.drawText(text1, roundRectLeft + mChartParam.card_textMarginLeft,
                    roundRectTop + mChartParam.card_text_1toRect, mPaint);
            canvas.drawText(text2, roundRectLeft + mChartParam.card_textMarginLeft,
                    roundRectTop + mChartParam.card_text_2toRect, mPaint);
            canvas.drawText(text3, roundRectLeft + mChartParam.card_textMarginLeft,
                    roundRectTop + mChartParam.card_text_3toRect, mPaint);
        }
    }

手势识别

标识线和小卡片是需要根据手势移动来改变展示的,所以这里需要监听onTouchEvent事件:

@Override
    public boolean onTouchEvent(MotionEvent event) {
        float touchX = event.getX();
        float touchY = event.getY();
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                touchToRect(touchX);
                lastX = touchX;
                return true;
            case MotionEvent.ACTION_MOVE:
                float moveX = event.getX();
                if (Math.abs(moveX - lastX) > touchSlop) {
                    lastX = moveX;
                    touchToRect(moveX);
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return super.onTouchEvent(event);
    }

这里记录下手指触摸和手指滑动时的x值,然后判断是否在对应的柱状图的范围之内,来展示标识线和小卡片:

 /**
     * 在柱状图范围
     * @param x
     */
    private void touchToRect(float x) {
        if (x >= rectStartX[0] && x <= rectStopX[0]) {
            touchRectPoi = 0;
            Log.d("wyk", "第一个柱状图");
        } else if (x >= rectStartX[1] && x <= rectStopX[1]) {
            touchRectPoi = 1;
            Log.d("wyk", "第二个柱状图");
        } else if (x >= rectStartX[2] && x <= rectStopX[2]) {
            touchRectPoi = 2;
            Log.d("wyk", "第三个柱状图");
        } else if (x >= rectStartX[3] && x <= rectStopX[3]) {
            touchRectPoi = 3;
            Log.d("wyk", "第四个柱状图");
        } else if (x >= rectStartX[4] && x <= rectStopX[4]) {
            touchRectPoi = 4;
            Log.d("wyk", "第五个柱状图");
        }
        invalidate();
    }

最后记得要调用invalidate()方法进行重绘


写的比较匆忙,有问题可以在下面评论或者私我。

最后附上完整的项目地址:
https://github.com/upperLucky/CombineChartDemo