自定义折线股票显示图

先看UI出的设计图


19C91932-CC43-4396-9DCA-171456A00E8B.png

这是修改之后的图了,在之前的途中 选中的点位上面是有一个显示的价格的,在下面的点位显示的是日期;
再看自定义view完成的图

1618543609(1).jpg

这就基本搞定了 只要把绘制点位上面的显示文本还有下面的显示文本去掉就ok了,

在之前的画的时候 当时以为是滑动的是视图,都已经撸完了, 然后项目主管才告诉我说滑动的不是视图, 是垂直的线, 当时满脸问号,不是看图么?
没办法 人家既然说了不对,那得改呀,哎!!!

撸都撸完了, 改起来就方便多了,不多说,看代码!
先把动态需要改的列出来,比如颜色、线的宽度,背景色,文本颜色等等...

<declare-styleable name="StockPriceView">
        <!-- 背景颜色 -->
        <attr name="background_color_dbz" format="color" />
        <!-- x、y轴线颜色 -->
        <attr name="x_y_color_dbz" format="color" />
        <!-- x轴上的文本颜色 -->
        <attr name="x_text_color_dbz" format="color" />
        <!-- x轴上的文本颜色 -->
        <attr name="y_text_color_dbz" format="color" />
        <!-- x、y轴上的文本大小 -->
        <attr name="x_text_size_dbz" format="dimension" />
        <!-- y轴上的文本大小 -->
        <attr name="y_text_size_dbz" format="dimension" />
        <!-- 背景虚线颜色 -->
        <attr name="dotted_line_dbz" format="color" />
        <!-- 折线颜色 -->
        <attr name="line_color_dbz" format="color" />
        <!-- 折线内阴影颜色 -->
        <attr name="line_shadow_color_dbz" format="color" />
        <!-- 折线点位颜色颜色 -->
        <attr name="point_color_dbz" format="color" />
        <!-- 折线点位直线颜色 -->
        <attr name="line_point_color_dbz" format="color" />
        <!-- 折线宽度 -->
        <attr name="line_width_dbz" format="dimension" />
        <!-- 折线点位文字颜色 -->
        <attr name="point_text_color_dbz" format="color" />
        <!-- 折线点位文字大小 -->
        <attr name="point_text_size_dbz" format="dimension" />
    </declare-styleable>

初始化

    public StockPriceView(Context context) {
        this(context, null);
    }

    public StockPriceView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public StockPriceView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView(context, attrs, defStyleAttr);
        initPaint();
    }

    private void initView(Context context, AttributeSet attrs, int defStyleAttr) {
        final TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.StockPriceView, defStyleAttr, 0);
        mBackgroundColor = typedArray.getColor(R.styleable.StockPriceView_background_color_dbz, Color.WHITE);
        mLinePointColor = typedArray.getColor(R.styleable.StockPriceView_line_point_color_dbz, mLinePointColor);
        mLineColor = typedArray.getColor(R.styleable.StockPriceView_line_color_dbz, mLineColor);
        mPointColor = typedArray.getColor(R.styleable.StockPriceView_point_color_dbz, mPointColor);
        mPointTextColor = typedArray.getColor(R.styleable.StockPriceView_point_text_color_dbz, mPointTextColor);
        mXYColor = typedArray.getColor(R.styleable.StockPriceView_x_y_color_dbz, mXYColor);
        mXTextColor = typedArray.getColor(R.styleable.StockPriceView_x_text_color_dbz, mXTextColor);
        mYTextColor = typedArray.getColor(R.styleable.StockPriceView_y_text_color_dbz, mYTextColor);
        mLineShadowColor = typedArray.getColor(R.styleable.StockPriceView_line_shadow_color_dbz, mLineShadowColor);
        mPointTextSize = (int) typedArray.getDimension(R.styleable.StockPriceView_point_text_size_dbz, mPointTextSize);
        mLineWidth = (int) typedArray.getDimension(R.styleable.StockPriceView_line_width_dbz, mLineWidth);
        mXTextSize = (int) typedArray.getDimension(R.styleable.StockPriceView_x_text_size_dbz, mXTextSize);
        mYTextSize = (int) typedArray.getDimension(R.styleable.StockPriceView_y_text_size_dbz, mYTextSize);
        typedArray.recycle();
    }

    private void initPaint() {
        mXPaint = new Paint();
        mXPaint.setAntiAlias(true);
        mXPaint.setColor(mXYColor);
        mXPaint.setStrokeWidth(dpToPx(1));

        mXTextPaint = new Paint();
        mXTextPaint.setAntiAlias(true);
        mXTextPaint.setColor(mXTextColor);
        mXTextPaint.setTextSize(mXTextSize);

        mYTextPaint = new Paint();
        mYTextPaint.setAntiAlias(true);
        mYTextPaint.setColor(mYTextColor);
        mYTextPaint.setTextSize(mYTextSize);

        mDottedPaint = new Paint();
        mDottedPaint.setAntiAlias(true);
        mDottedPaint.setColor(mXYColor);
        mDottedPaint.setStyle(Paint.Style.STROKE);
        mDottedPaint.setStrokeWidth(dpToPx(1));
        // DashPathEffect () 数组 第一个是线的宽度 第二个数据是虚线间隔,
        mDottedPaint.setPathEffect(new DashPathEffect(new float[]{dpToPx(4), dpToPx(2)}, 0));

        mLinePaint = new Paint();
        mLinePaint.setAntiAlias(true);
        mLinePaint.setColor(mLineShadowColor);
        mLinePaint.setStrokeWidth(dpToPx(1));
        mLinePaint.setStyle(Paint.Style.FILL);

        mPointPaint = new Paint();
        mPointPaint.setAntiAlias(true);
        mPointPaint.setColor(mLineColor);
        mPointPaint.setStrokeWidth(mLineWidth);
        mPointPaint.setStyle(Paint.Style.STROKE);

        mLinePointPaint = new Paint();
        mLinePointPaint.setAntiAlias(true);
        mLinePointPaint.setColor(mLinePointColor);
        mLinePointPaint.setStrokeWidth(dpToPx(1));
    }
   /**
     * 计算文本间距,和第一个点的位置  取数据最大值
     */
    private void calculation(){
        String text = mYValue.size() == 0 ? "00-00" : mYValue.get(mYValue.size() - 1).value;
        // 测量Y轴数据的文本宽度
        yRect = getTextBounds(text, mYTextPaint);
        // 计算X轴的左边距
        mYLeftInterval = yRect.width() + mXTextLeftInterval * 2;
        // 每个点位的间距 mXValue.size() - 2 减去2 是因为两边的点各占一个  不用间距
        mInterval = (mWidth - mYLeftInterval - mXRightInterval) / (mXValue.size() - 2);
        // 第一个X轴点的位置
        mXFirstPoint = mYLeftInterval;
        // 遍历数据最大值 如果为0那么默认为1
        for (int i = 0; i < mXValue.size(); i++) {
            max = Math.max(max, Float.parseFloat(mXValue.get(i).value));
        }
        if (max == 0) {
            max = 1;
        }
    }

开始绘制 在绘制折线图的时候有一点我也不明白,就是明明已经分好间距了,为什么在右边还会出来一点,如果有知道的麻烦告诉我一下,(多出来的一点我已经在绘制X轴和虚线的时候强制多绘制了4dp)

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        calculation();
        canvas.drawColor(mBackgroundColor);
        // 绘制X轴时间文本
        drawBottomText(canvas);
        // 绘制X轴
        drawXLine(canvas);
        // 绘制Y轴文本
        drawYText(canvas);
        // 绘制背景虚线
        drawBackDottedLine(canvas);
        // 绘制阴影折线
        drawLine(canvas);
        // 绘制折线点和提示文本
        drawLinePoint(canvas);
    }

    /**
     * 绘制X轴时间文本
     */
    private void drawBottomText(Canvas canvas) {
        Rect endText = getTextBounds(mXEndText, mXTextPaint);
        canvas.drawText(mXStartText, mXTextLeftInterval, mHeight - mXBottomInterval, mXTextPaint);
        canvas.drawText(mXEndText, mWidth - mXTextLeftInterval - endText.width(), mHeight - mXBottomInterval, mXTextPaint);
    }

    /**
     * 绘制X轴
     */
    private void drawXLine(Canvas canvas) {
        Rect startText = getTextBounds(mXStartText, mXTextPaint);
        // 距离底边的距离 = 底边距离 * 2 + X轴文本高度
        int xBottom = mHeight - getPaddingTop() - getPaddingBottom() - mXBottomInterval * 2 - startText.height();
        // mXRightInterval + dpToPx(4) (正常是不用加上的, 但不知道为什么就是少那么一丢丢)
        canvas.drawLine(mYLeftInterval, xBottom, mWidth - mXRightInterval + dpToPx(4), xBottom, mXPaint);
    }

    /**
     * 绘制Y轴文本
     */
    private void drawYText(Canvas canvas) {
        Rect startText = getTextBounds(mXStartText, mXTextPaint);
        for (int i = 0; i < mYValue.size(); i++) {
            // 绘制区域 = 总高度 - 下边距 - 上边距
            // 下边距 = 文本高度 + 文本距离上下的间距
            // 绘制区域 / (绘制数据的数量 - 1) (减一是计算绘制之间的间距, 如果不减一, 那么在开始第一个绘制时会多出一段间距)
            float y = (float) (mHeight - getPaddingBottom() - getPaddingTop() - mXBottomInterval * 2 - mYTopInterval - startText.height()) / (mYValue.size() - 1);
            // X轴 文本水平居中X轴    Y轴 从最大值到最小值,反着绘制, Y点从上到下 但要加 上边距
            // Y轴文本  距离左边边距, Y轴文本的宽度 / 2  居中绘制
            canvas.drawText(mYValue.get(mYValue.size() - (i + 1)).value, mXTextLeftInterval, y * i + mYTopInterval, mYTextPaint);
        }
    }

    /**
     * 绘制背景虚线
     */
    private void drawBackDottedLine(Canvas canvas) {
        Rect startText = getTextBounds(mXStartText, mXTextPaint);
        for (int i = 0; i < mYValue.size(); i++) {
            float y = (float) (mHeight - getPaddingBottom() - getPaddingTop() - mXBottomInterval * 2 - mYTopInterval - startText.height()) / (mYValue.size() - 1);
            // x轴距离左边 = Y轴文本宽度 + Y轴文本左右边距
            // mXRightInterval + dpToPx(4) (正常是不用加上的, 但不知道为什么就是少那么一丢丢)
            canvas.drawLine(mXTextLeftInterval * 2 + yRect.width(), y * i + mYTopInterval, mWidth - mXRightInterval + dpToPx(4), y * i + mYTopInterval, mDottedPaint);
        }
    }

    /**
     * 绘制阴影折线
     **/
    private void drawLine(Canvas canvas) {
        if (mXValue.size() <= 0) return;
        Rect startText = getTextBounds(mXStartText, mXTextPaint);
        Path path = new Path();
        // 绘制区域 = 总高度 - 下边距 - 上边距
        float totalHeight = mHeight - getPaddingBottom() - getPaddingTop() - mXBottomInterval * 2 - startText.height() - mYTopInterval;
        // 起点x、y轴开始绘制
        float x = mXFirstPoint;
        float y = (mHeight - getPaddingBottom() - mXBottomInterval * 2 - startText.height()) - Float.parseFloat(mXValue.get(0).value) * totalHeight / max;
        // 绘制x、y轴左下角起点
        path.moveTo(mYLeftInterval, mHeight - getPaddingBottom() - getPaddingTop() - mXBottomInterval * 2 - startText.height());
        // 绘制第一个点的位置
        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 * 2 - startText.height()) - Float.parseFloat(mXValue.get(i).value) * totalHeight / max;
            path.lineTo(x, y);
        }
        // 如果不绘制点位到下方的路径, 那么 不会实现全部阴影效果, x 就是 绘制最后一个点的位置, y 就是底边的位置
        path.lineTo(x, mHeight - mXBottomInterval * 2 - startText.height());
        canvas.drawPath(path, mLinePaint);

        // 绘制折线
        Path linePath = new Path();
        // 起点x、y轴开始绘制
        float x1;
        float y1 = (mHeight - getPaddingBottom() - mXBottomInterval * 2 - startText.height()) - Float.parseFloat(mXValue.get(0).value) * totalHeight / max;
        // 绘制第一个点的位置
        linePath.moveTo(mXFirstPoint, y1);
        // 因为绘制了第一个点,所以i起始是1
        for (int i = 1; i < mXValue.size(); i++) {
            // x点绘制的位置, mInterval是两点的间距 * 点的数量 + x轴距离左边距
            x1 = mXFirstPoint + mInterval * i;
            // y轴上到下是数字变大 所以需要反着绘制  绘制区域 - 百分比高度(百分比高度 = 数值 * 总高度 / 数据最大值), 相反过来就是从下到上的比例高度
            y1 = (mHeight - getPaddingBottom() - mXBottomInterval * 2 - startText.height()) - Float.parseFloat(mXValue.get(i).value) * totalHeight / max;
            linePath.lineTo(x1, y1);
        }
        // 绘制折线
        mPointPaint.setColor(mLineColor);
        mPointPaint.setStyle(Paint.Style.STROKE);
        canvas.drawPath(linePath, mPointPaint);
    }

    /**
     * 绘制折线和提示文本
     */
    private void drawLinePoint(Canvas canvas) {
        Rect startText = getTextBounds(mXStartText, mXTextPaint);
        // 绘制区域 = 总高度 - 下边距 - 上边距
        float totalHeight = mHeight - getPaddingBottom() - getPaddingTop() - mXBottomInterval * 2 - startText.height() - mYTopInterval;
        for (int i = 0; i < mXValue.size(); i++) {
            // x点绘制的位置, mInterval是两点的间距 * 点的数量 + x轴距离左边距
            float x = mInterval * i + mXFirstPoint;
            // y轴上到下是数字变大 所以需要反着绘制  绘制区域 - 百分比高度(百分比高度 = 数值 * 总高度 / 数据最大值), 相反过来就是从下到上的比例高度
            float y = (mHeight - getPaddingBottom() - getPaddingTop() - mXBottomInterval * 2 - startText.height()) - Float.parseFloat(mXValue.get(i).value) * totalHeight / max;
            if (mCurrentSelectPoint == i + 1) {
                // 绘制垂直直线、点、文本
                drawVerticalLinePointText(canvas, x, startText, mXValue.get(i).num);
                // 绘制选中的点
                drawCurrentSelectPoint(canvas, x, y);
                // 绘制选中提示点
                drawCurrentTextBox(canvas, x, y - dpToPx(10), mXValue.get(i).value);
            }
        }
    }

    /**
     * 绘制垂直直线、点、文本
     */
    private void drawVerticalLinePointText(Canvas canvas, float x, Rect startText, String text) {
        int y = mHeight - getPaddingBottom() - mXBottomInterval * 2 - startText.height();
        mLinePointPaint.setStyle(Paint.Style.STROKE);
        mLinePointPaint.setColor(mLinePointColor);
        canvas.drawLine(x, y, x, mYTopInterval, mLinePointPaint);
        mLinePointPaint.setStyle(Paint.Style.FILL);
        canvas.drawCircle(x, y, dpToPx(4), mLinePointPaint);
        mLinePointPaint.setColor(mPointColor);
        canvas.drawCircle(x, y, dpToPx(2), mLinePointPaint);
        Rect rect = getTextBounds(text, mXTextPaint);
        canvas.drawText(text, x - (float) rect.width() / 2, y - dpToPx(6) - (float) rect.height() / 2, mXTextPaint);
    }

    /**
     * 绘制选中的点
     */
    private void drawCurrentSelectPoint(Canvas canvas, float x, float y) {
        mPointPaint.setStyle(Paint.Style.FILL);
        canvas.drawCircle(x, y, dpToPx(4), mPointPaint);
        mPointPaint.setColor(mPointColor);
        mPointPaint.setStyle(Paint.Style.FILL);
        canvas.drawCircle(x, y, dpToPx(2), mPointPaint);
    }

    /**
     * 绘制选中提示框
     */
    private void drawCurrentTextBox(Canvas canvas, float x, float y, String text) {
        Rect rect = getTextBounds(text, mXTextPaint);
        // y点计算  以下两种方法均可
        // x减去文本的宽度  y - 三角的高度 - 文本高度 / 2
        canvas.drawText(text, x - (float) rect.width() / 2, y - dpToPx(6) - (float) rect.height() / 2, mXTextPaint);
    }

    /**
     * dp转化成为px
     *
     * @param dp 单位
     */
    private int dpToPx(int dp) {
        float density = getContext().getResources().getDisplayMetrics().density;
        return (int) (dp * density + 0.5f * (dp >= 0 ? 1 : -1));
    }

    /**
     * 获取丈量文本的矩形
     *
     * @param text  文本
     * @param paint 画笔
     */
    private Rect getTextBounds(String text, Paint paint) {
        Rect rect = new Rect();
        paint.getTextBounds(text, 0, text.length(), rect);
        return rect;
    }

看滑动部分,因为是在绘制区域滑动手势,所以按下的时候要判断是否在X轴垂直线上, Y轴要判断是否在绘制区域,绘制区域以外不能滑动

    @SuppressLint("ClickableViewAccessibility")
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        this.getParent().requestDisallowInterceptTouchEvent(true);//当该view获得点击事件,就请求父控件不拦截事件
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                break;
            case MotionEvent.ACTION_MOVE:
                clickAction(event);
                break;
            case MotionEvent.ACTION_UP:
                clickAction(event);
                this.getParent().requestDisallowInterceptTouchEvent(false);
                break;
            case MotionEvent.ACTION_CANCEL:
                this.getParent().requestDisallowInterceptTouchEvent(false);
                break;
        }
        return true;
    }

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

基本都有注释,只要细看相信都能看懂
git:https://github.com/xiaobinAndroid421726260/Android_CustomAllCollection.git
里面有多种折线统计图找到需要的,直接拿去用,不用客气, 有bug 麻烦告诉一下
发邮件:421726260@qq.com

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

推荐阅读更多精彩内容