Android自定义横向滚动折线图

在2020年的最后一天,来一个滚动折线图收尾吧

1609405639(1).jpg

不多说直接看view

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        if (changed) {
            mWidth = getWidth();
            mHeight = getHeight();
            // 测量Y轴数据的文本宽度
            Rect rect = getTextBounds(mYValue.get(mYValue.size() - 1).value, mYTextPaint);
            // 计算X轴的左边距
            mYLeftInterval = rect.width() + mYTextLeftInterval * 2;
            // 设置选中的位置在最后一个
            mCurrentSelectPoint = mXValue.size();
            // 第一个X轴点的位置
            mXFirstPoint = mYLeftInterval + mInterval;
            // 遍历数据最大值 如果为0那么默认为1
            for (int i = 0; i < mXValue.size(); i++) {
                max = Math.max(max, mXValue.get(i).num);
            }
            if (max == 0) {
                max = 1;
            }
            minXFirstPoint = mWidth - (mWidth - mYLeftInterval) * 0.1f - mInterval * (mXValue.size() - 1);
            maxXFirstPoint = mXFirstPoint;
        }
    }

获取宽高,测量Y轴数据文本的宽度加上边距,有注释,相信能看明白
下面是取数据最大值为计算做准备,最大和最小第一个点的距离

 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawColor(mBackgroundColor);
        drawXYLine(canvas);
        drawYText(canvas);
        drawBrokenLineAndPoint(canvas);
        if (!isScrolling && !aniLock) {
            scrollAtStart();
        }
    }
   /**
     * 绘制X、Y轴
     */
    private void drawXYLine(Canvas canvas) {
        mXYPaint.setColor(mXYColor);
        // 绘制X轴
        canvas.drawLine(mYLeftInterval, mHeight - getPaddingBottom() - mXBottomInterval,
                mWidth - getPaddingRight(), mHeight - getPaddingBottom() - mXBottomInterval, mXYPaint);
        // 绘制Y轴
        canvas.drawLine(mYLeftInterval, 0, mYLeftInterval, mHeight - getPaddingBottom() - mXBottomInterval, mXYPaint);
        //绘制y轴箭头
        mXYPaint.setStyle(Paint.Style.STROKE);
        Path path = new Path();
        path.moveTo(mYLeftInterval - dpToPx(5), mXBottomInterval);
        path.lineTo(mYLeftInterval, 0);
        path.lineTo(mYLeftInterval + dpToPx(5), mXBottomInterval);
        canvas.drawPath(path, mXYPaint);
    }
   /**
     * 绘制Y轴文本
     */
    private void drawYText(Canvas canvas) {
        for (int i = 0; i < mYValue.size(); i++) {
            Rect rect = getTextBounds(mYValue.get(i).value, mYTextPaint);
            // 绘制区域 = 总高度 - 下边距 - 上边距
            // 绘制区域 / (绘制数据的数量 - 1) (减一是计算绘制之间的间距, 如果不减一, 那么在开始第一个绘制时会多出一段间距)
            float y = (float) (mHeight - getPaddingBottom() - mXBottomInterval - getPaddingTop() - mYTopInterval) / (mYValue.size() - 1);
            // X轴 文本水平居中X轴    Y轴 从最大值到最小值,反着绘制, Y点从上到下 但要加 上边距
            canvas.drawText(mYValue.get(mYValue.size() - (i + 1)).value, rect.centerX(), y * i + mYTopInterval, mYTextPaint);
        }
//        Rect rect = getTextBounds("100", mXYPaint);
//        // X轴  文本中间开始绘制 + 距离左边的距离      Y轴   从底部开始绘制, 要减去底边距离
//        canvas.drawText("0", (float) rect.width() / 2 + mYTextLeftInterval, mHeight - getPaddingBottom() - getPaddingTop() - mXBottomInterval, mXYPaint);
//        // X轴  文本中间开始绘制两位正好中间           Y轴  (总高度 - 顶部距离 - 底部距离) / 2 是整个的中心点,要在加上距离上边的边距才是绘制部分的中心点
//        canvas.drawText("50", (float) rect.width() / 2, (float) ((mHeight - mYTopInterval - mXBottomInterval - getPaddingBottom() - getPaddingTop()) / 2) + mYTopInterval, mXYPaint);
//        // X轴  文本中间开始绘制三位要减去左边的距离    Y轴   (要使文本在中间显示) 距离上边距是底边 + 文本的高度 / 2   (正常显示是 mYTopInterval 距离上边的边距)
//        canvas.drawText("100", (float) rect.width() / 2 - mYTextLeftInterval, mYTopInterval + (float) rect.height() / 2, mXYPaint);
    }

注释写的很清楚, 上面最后注释的那些事我为了测试文本的距离写的,不必在意。

   /**
     * 绘制折线和折线交点处对应的点
     */
    private void drawBrokenLineAndPoint(Canvas canvas) {
        //重新开一个图层
        int layerId = canvas.saveLayer(0, 0, mWidth, mHeight, null, Canvas.ALL_SAVE_FLAG);
        drawLine(canvas);
        drawLinePoint(canvas);
        // 将折线超出x轴坐标的部分截取掉
        mXYPaint.setStyle(Paint.Style.FILL);
        mXYPaint.setColor(mBackgroundColor);
        mXYPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
        RectF rectF = new RectF(0, 0, mYLeftInterval, mHeight);
        canvas.drawRect(rectF, mXYPaint);
        mXYPaint.setXfermode(null);
        //保存图层
        canvas.restoreToCount(layerId);
    }

接下来就是主要的折线和折线点位了

   /**
     * 绘制折线
     **/
    private void drawLine(Canvas canvas) {
        if (mXValue.size() <= 0) return;
        Path path = new Path();
        // 绘制区域 = 总高度 - 下边距 - 上边距
        float totalHeight = mHeight - getPaddingBottom() - mXBottomInterval - getPaddingTop() - mYTopInterval;
        // 起点x、y轴开始绘制
        float x = mXFirstPoint;
        float y = (mHeight - getPaddingBottom() - mXBottomInterval) - mXValue.get(0).num * totalHeight / max;
        // 绘制x、y轴左下角起点
        path.moveTo(mYLeftInterval, mHeight - getPaddingBottom() - mXBottomInterval);
        // 绘制第一个点的位置
        path.lineTo(x, y);
        // 因为绘制了第一个点,所以i起始是1
        for (int i = 1; i < mXValue.size(); i++) {
            // x点绘制的位置, mInterval是两点的间距 * 点的数量 + x轴距离左边距
            x = mXFirstPoint + mInterval * i;
            // y轴上到下是数字变大 所以需要反着绘制  绘制区域 - 百分比高度(百分比高度 = 数值 * 总高度 / 数据最大值), 相反过来就是从下到上的比例高度
            y = (mHeight - getPaddingBottom() - mXBottomInterval) - mXValue.get(i).num * totalHeight / max;
            path.lineTo(x, y);
        }
        canvas.drawPath(path, mLinePaint);
    }

    /**
     * 绘制折线点和提示框
     */
    private void drawLinePoint(Canvas canvas) {
        // 绘制区域 = 总高度 - 下边距 - 上边距
        float totalHeight = mHeight - getPaddingBottom() - mXBottomInterval - getPaddingTop() - mYTopInterval;
        for (int i = 0; i < mXValue.size(); i++) {
            // x点绘制的位置, mInterval是两点的间距 * 点的数量 + x轴距离左边距
            float x = mInterval * i + mXFirstPoint;
            // y轴上到下是数字变大 所以需要反着绘制  绘制区域 - 百分比高度(百分比高度 = 数值 * 总高度 / 数据最大值), 相反过来就是从下到上的比例高度
            float y = (mHeight - getPaddingBottom() - mXBottomInterval) - mXValue.get(i).num * totalHeight / max;
            // 绘制两次点中心两个颜色, 外层透明度50
            mPointPaint.setColor(mPointColor);
            mPointPaint.setStyle(Paint.Style.FILL);
            canvas.drawCircle(x, y, dpToPx(4), mPointPaint);
            // 绘制两次点中心两个颜色, 外层透明度50
            mPointPaint.setColor(mLineColor);
            mPointPaint.setStyle(Paint.Style.FILL);
            canvas.drawCircle(x, y, dpToPx(2), mPointPaint);
            if (mCurrentSelectPoint == i + 1) {
                // 绘制选中的点
                drawCurrentSelectPoint(canvas, i + 1, x, y);
                // 绘制选中提示点
                drawCurrentTextBox(canvas, i + 1, x, y - dpToPx(10), mXValue.get(i).value);
            }
        }
    }

    /**
     * 绘制当前选中的点
     */
    private void drawCurrentSelectPoint(Canvas canvas, int i, float x, float y) {
        mPointPaint.setColor(Color.parseColor("#d0f3f2"));
        mPointPaint.setStyle(Paint.Style.FILL);
        canvas.drawCircle(x, y, dpToPx(7), mPointPaint);
        mPointPaint.setColor(mPointColor);
        mPointPaint.setStyle(Paint.Style.FILL);
        canvas.drawCircle(x, y, dpToPx(4), mPointPaint);
        mPointPaint.setColor(mLineColor);
        mPointPaint.setStyle(Paint.Style.FILL);
        canvas.drawCircle(x, y, dpToPx(2), mPointPaint);
    }

    /**
     * 绘制选中提示框
     */
    private void drawCurrentTextBox(Canvas canvas, int i, float x, float y, String text) {
        int dp6 = dpToPx(6);
        int dp20 = dpToPx(20);
        // 绘制路径三角
        Path path = new Path();
        path.moveTo(x, y);
        path.lineTo(x - dp6, y - dp6);
//        path.lineTo(x - dp20, y - dp6);
//        path.lineTo(x - dp20, y - dp6 - dp20);
//        path.lineTo(x + dp20, y - dp6 - dp20);
//        path.lineTo(x + dp20, y - dp6);
//        path.quadTo(x + dp18, y - dp4, x - dp18, y - dp4);
        path.lineTo(x + dp6, y - dp6);
        path.lineTo(x, y);
        path.close();
        mPointPaint.setStyle(Paint.Style.FILL);
        mPointPaint.setColor(mLineColor);
        canvas.drawPath(path, mPointPaint);
        RectF rectF = new RectF(x - dp20, y - dpToPx(5), x + dp20, y - dp6 - dp20);
        canvas.drawRoundRect(rectF, dpToPx(4), dpToPx(4), mPointPaint);
        mPointPaint.setColor(mPointTextColor);
        mPointPaint.setTextSize(mPointTextSize);
        Rect rect = getTextBounds(text, mPointPaint);
        // y点计算  以下两种方法均可
        // x减去文本的宽度  y - 提示框距离点的高度 - 三角的高度 - 提示框 / 2
//        canvas.drawText(text, x - (float) rect.width() / 2, y - dpToPx(10) - dp6 - dpToPx(5) - rectF.height() / 2, mPointPaint);
        // x减去文本的宽度  y - 三角的高度 - 文本高度 / 2
        canvas.drawText(text, x - (float) rect.width() / 2, y - dp6 - (float) rect.height() / 2, mPointPaint);
    }

以上都用注释, 只要肯看一下就能看懂,看不懂直接拿去拷贝。
提示框那个注释是为了让提示框好看些, 就画个矩形, 周边圆角, 不然路径画出来是直角。

   /**
     * 当宽度不足以呈现全部数据时 滚动
     */
    private void scrollAtStart() {
        // 整体数据的宽度 大于 绘制区域宽度
        if (mInterval * mXValue.size() > mWidth - mYLeftInterval) {
            float scrollLength = maxXFirstPoint - minXFirstPoint;
            ValueAnimator animator = ValueAnimator.ofFloat(0, scrollLength);
            animator.setDuration(500L);//时间最大为1000毫秒,此处使用比例进行换算
            animator.setInterpolator(new DecelerateInterpolator());
            animator.addUpdateListener(animation -> {
                float value = (float) animation.getAnimatedValue();
                mXFirstPoint = (int) Math.max(mXFirstPoint - value, minXFirstPoint);
                invalidate();
            });
            animator.addListener(new Animator.AnimatorListener() {
                @Override
                public void onAnimationStart(Animator animator) {
                    isScrolling = true;
                }

                @Override
                public void onAnimationEnd(Animator animator) {
                    isScrolling = false;
                    aniLock = true;
                }

                @Override
                public void onAnimationCancel(Animator animator) {
                    isScrolling = false;
                    aniLock = true;
                }

                @Override
                public void onAnimationRepeat(Animator animator) {

                }
            });
            animator.start();
        }
    }
private float startX;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (isScrolling) return super.onTouchEvent(event);
        this.getParent().requestDisallowInterceptTouchEvent(true);//当该view获得点击事件,就请求父控件不拦截事件
        obtainVelocityTracker(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                startX = event.getX();
                break;
            case MotionEvent.ACTION_MOVE:
                if (mInterval * mXValue.size() > mWidth - mYLeftInterval) {//当期的宽度不足以呈现全部数据
                    float scrollX = event.getX() - startX;
                    startX = event.getX();
                    if (mXFirstPoint + scrollX < minXFirstPoint) {
                        mXFirstPoint = (int) minXFirstPoint;
                    } else {
                        mXFirstPoint = Math.min(mXFirstPoint + scrollX, maxXFirstPoint);
                    }
                    invalidate();
                }
                break;
            case MotionEvent.ACTION_UP:
                clickAction(event);
//                scrollAfterActionUp();
                this.getParent().requestDisallowInterceptTouchEvent(false);
                recycleVelocityTracker();
                break;
            case MotionEvent.ACTION_CANCEL:
                this.getParent().requestDisallowInterceptTouchEvent(false);
                recycleVelocityTracker();
                break;
        }
        return true;
    }

   /**
     * 点击X轴坐标或者折线节点
     *
     * @param event 事件
     */
    private void clickAction(MotionEvent event) {
        int dp8 = dpToPx(8);
        float eventX = event.getX();
        float eventY = event.getY();
        // 绘制区域 = 总高度 - 下边距 - 上边距
        float totalHeight = mHeight - getPaddingBottom() - mXBottomInterval - getPaddingTop() - mYTopInterval;
        for (int i = 0; i < mXValue.size(); i++) {
            // x点绘制的位置, mInterval是两点的间距 * 点的数量 + x轴距离左边距
            float x = mInterval * i + mXFirstPoint;
            // y轴上到下是数字变大 所以需要反着绘制  绘制区域 - 百分比高度(百分比高度 = 数值 * 总高度 / 数据最大值), 相反过来就是从下到上的比例高度
            float y = (mHeight - getPaddingBottom() - mXBottomInterval) - mXValue.get(i).num * totalHeight / max;
            // 判断点击的位置在点的旁边
            if (eventX >= x - dp8 && eventX <= x + dp8 && eventY >= y - dp8 && eventY <= y + dp8 && mCurrentSelectPoint != i + 1) {
                mCurrentSelectPoint = i + 1;
                invalidate();
                if (onSelectedActionClick != null) {
                    onSelectedActionClick.onActionClick(i, mXValue.get(i).num, mXValue.get(i).value);
                }
                return;
            }
        }
    }

以上是点击事件, 点击点位返回点位的数据
基本上没有什么难点,就是滑动的时候计算那地方需要注意一下, 之前就没注意导致刚滑动一点就会到最边上,后来才发现滑动的时候没计算X轴。

地址:https://github.com/xiaobinAndroid421726260/Android_CustomAllCollection.git

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,924评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,902评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,716评论 0 239
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,783评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,166评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,510评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,784评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,476评论 0 196
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,196评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,459评论 2 243
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,978评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,321评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,964评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,046评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,803评论 0 193
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,530评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,420评论 2 265

推荐阅读更多精彩内容