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

来源:互联网 发布:linux只读文件怎么修改 编辑:程序博客网 时间:2024/05/17 01:49

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

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

然后触摸柱子需要出现一条标识线和一个小卡片,如下图:
这里写图片描述

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

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

属性动态设置

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

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轴计算,这里需要计算文字的高度,关于文字的高度问题参考下面这张图:
这里写图片描述

(注: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

阅读全文
0 0
原创粉丝点击