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

自定义View:画布实现自定义View(折线图的实现)

程序员文章站 2022-05-30 22:45:40
...

今天道长打算说一下用画布实现自定义View,这是道长说的自定义View的第四种实现方式了。
第一种:是放好布局后使用NineOldAndroid监听动画实现,想看一下的话点击传送门属性动画(二):如何自定义View以及自定义View:侧滑菜单动画实现
第二种:是放好布局后使用TouchEvent监听实现,传送门在此自定义View:侧滑菜单实现
第三种:是继承相关的View,拓展相关View的功能,传送门在此PopWindow:基本使用与自定义PopWindow
第三种自定义View就是拓展相关View的功能。比如自定义PopWindow要增加出现动画或者展示方式。前两种都是使用已经存在的布局,一种继承FrameLayout,另一种继承ViewGroup。放置好位置后监听事件实现。应该说前两种是自定义组合View。今天说的这种方式继承View,可以用画布绘制各种形状的图形,然后监听事件实现。这里以折线图的实现为例,折线图可以左右滑动。好了咱们开车……

一、效果图

动态图没有,先把效果图放在这里,然后绘制View。
自定义View:画布实现自定义View(折线图的实现)

二、绘制View

上面的效果图都看到折线图有网格,有横向限制区域,有标记点,有目标点,有坐标轴单位,Y轴分割为两个区域,还可以左右滑动。
把画布Canvas与生活中的纸张看成一样就可以了,要知道咱们在纸张上写东西时先写的会被后写的遮盖住。所以说绘制View时要注意分层。

  • 构造函数,初始化画布,画笔
    public CanvasView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    public CanvasView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

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

private void init(Context context) {
        mTextColorSize = sp2px(context, mTextColorSize);
        mTextColorSmall = sp2px(context, mTextColorSmall);
        mTrendLineSize = dp2px(context, mTrendLineSize);
        mInnerCircleSize = (int) dp2px(context, mInnerCircleSize);
        mOuterCircleSize = (int) dp2px(context, mOuterCircleSize);
        mOuterCircleRadius = (int) dp2px(context, mOuterCircleRadius);
        mInnerCircleRadius = (int) dp2px(context, mInnerCircleRadius);
        mYCenterSize = (int) dp2px(context, mYCenterSize);
        focusTextSize = (int) dp2px(context, focusTextSize);

        mPaint = new Paint();
        mPaint.setTextAlign(Align.CENTER);
        mPaint.setStyle(Style.STROKE);
        mPaint.setAntiAlias(true);

        mInnerCirclePaint = new Paint();
        mInnerCirclePaint.setTextAlign(Align.CENTER);
        mInnerCirclePaint.setColor(mInnerCircleColor);
        mInnerCirclePaint.setTextSize(mInnerCircleSize);
        mInnerCirclePaint.setAntiAlias(true);
        mInnerCirclePaint.setTextSize(mInnerCircleSize);

        mOuterCirclePaint = new Paint();
        mOuterCirclePaint.setTextAlign(Align.CENTER);
        mOuterCirclePaint.setTextSize(mOuterCircleSize);
        mOuterCirclePaint.setAntiAlias(true);
        mOuterCirclePaint.setTextSize(mOuterCircleSize);

        mTitlePaint = new Paint();
        mTitlePaint.setTextAlign(Align.CENTER);
        mTitlePaint.setTextSize(sp2px(context, 20));
        mTitlePaint.setTextAlign(Align.CENTER);

        mRangeTrendBackgroundPaint = new Paint();

        nRangeTrendBackgroundPaint = new Paint();
        mPulsePaint = new Paint();


        final ViewConfiguration configuration = ViewConfiguration.get(context);
        mTouchSlop = configuration.getScaledTouchSlop();

        mYTitleRect = new Rect();
        nYTitleRect = new Rect();
        pYTitleRect = new Rect();
        mPointColors = new int[]{0xFF349800, 0xFF0082b4};    // 画笔的颜色
        mYTitleWidth = (int) dp2px(context, mYTitleWidth);
        mRangeTrendColors = new int[]{0XFFDBF9CC, 0XFFDBF9CC, 0XFFDBF9CC};
        nRangeTrendColors = new int[]{0XFFE0F6FF, 0XFFE0F6FF, 0XFFE0F6FF, 0XFFE0F6FF};
        mPulseColors = new int[]{0XFFFFFBE4, 0XFFFFFBE4, 0XFFFFFBE4};
    }
  • 第一层绘制折线图限制区域
    /**
     * 设置界限的区域的
     *
     * @param canvas
     * @param paint
     * @param rangeTrendColors
     * @param up
     * @param down
     */
    private void drawBackground(Canvas canvas, Paint paint, int[] rangeTrendColors, float up, float down) {

        Map<String, Object> params = getViewParams();

        Rect rect = new Rect();
        rect.set((int) ((Float) params.get("scrollX") + 0), (int) ((Integer) params.get("xAxisHeadHeight") + (Integer) params.get("trendHeight") * up), (int) ((Float) params.get("scrollX") + getWidth()), (int) (getHeight() - (Integer) params.get("xAxisTitleHeight") - (Integer) params.get("trendHeight") * down));
        LinearGradient gradient = new LinearGradient((Float) params.get("scrollX") + getWidth(), (Integer) params.get("xAxisHeadHeight") + (Integer) params.get("trendHeight") * up, (Float) params.get("scrollX") + getWidth(), getHeight() - (Integer) params.get("xAxisTitleHeight") - (Integer) params.get("trendHeight") * down, rangeTrendColors, null, Shader.TileMode.CLAMP);

        paint.setShader(gradient);
        canvas.drawRect(rect, paint);
        canvas.save();
    }



效果图如下:
自定义View:画布实现自定义View(折线图的实现)

  • 第二层绘制表格
 /**
     * 绘制表格
     *
     * @param canvas
     * @param paint
     */
    private void drawForm(Canvas canvas, Paint paint) {

        for (int i = 0; i < mDrawCount; i++) {
            drawColumnLine(canvas, paint, 0xffd5edff, (int) mTextColorSize, i);
        }

        for (int i = 1; i <= mDrawCount + 1; i++) {
            if (i == 1 || i == 5 || i == 6 || i == 8) {
                drawRowLine(canvas, paint, 0xff7ecef9, 1, i);
            } else {
                drawRowLine(canvas, paint, 0xffe5e5e5, 1, i);
            }
        }
    }

    /**
     * 绘制表格竖线
     *
     * @param canvas
     * @param paint
     * @param lineColor
     * @param lineWith
     * @param position
     */
    private void drawColumnLine(Canvas canvas, Paint paint, int lineColor, int lineWith, int position) {

        Map<String, Object> params = getViewParams();

        paint.setColor(lineColor);
        paint.setTextSize(lineWith);
        canvas.drawLine((Float) params.get("scrollX") + mYTitleWidth + mDistance * (position), (Integer) params.get("xAxisHeadHeight"), (Float) params.get("scrollX") + mYTitleWidth + mDistance * (position), (Integer) params.get("xAxisHeadHeight") + (Integer) params.get("trendHeight") / 8 * (8 - 1), paint);
    }

    /**
     * 绘制表格横线
     *
     * @param canvas
     * @param paint
     * @param lineColor
     * @param lineWith
     * @param position
     */
    private void drawRowLine(Canvas canvas, Paint paint, int lineColor, int lineWith, int position) {

        Map<String, Object> params = getViewParams();

        paint.setColor(lineColor);
        paint.setStrokeWidth(lineWith);
        canvas.drawLine((Float) params.get("scrollX") + mYTitleWidth - 20, (Integer) params.get("xAxisHeadHeight") + (Integer) params.get("trendHeight") / 8 * (position - 1), (Float) params.get("scrollX") + getWidth(), (Integer) params.get("xAxisHeadHeight") + (Integer) params.get("trendHeight") / 8 * (position - 1), paint);
    }



效果图如下:
自定义View:画布实现自定义View(折线图的实现)

  • 第三层绘制网格分割区域
    /**
     * 分割网格
     *
     * @param canvas
     */
    private void setSplitForm(Canvas canvas, Paint paint, int color, int width) {

        Map<String, Object> params = getViewParams();

        paint.setColor(color);
        paint.setStrokeWidth(width);
        canvas.drawRect((Float) params.get("scrollX") + 0, (Integer) params.get("xAxisHeadHeight") + (Integer) params.get("trendHeight") / 8 * (5 - 1) + 1, (Float) params.get("scrollX") + getWidth(), (Integer) params.get("xAxisHeadHeight") + (Integer) params.get("trendHeight") / 8 * (6 - 1), paint);// 长方形
    }
  • 第四层绘制中心标记
    /**
     * 绘制中心标志
     *
     * @param canvas
     * @param paint
     * @param position
     */
    private void drawCenterSign(Canvas canvas, Paint paint, int position) {
        drawCenterLine(canvas, paint, position);
        drawTriangle(canvas, paint, position);
    }

    /**
     * 绘制中心线
     *
     * @param canvas
     * @param paint
     * @param position
     */
    private void drawCenterLine(Canvas canvas, Paint paint, int position) {

        Map<String, Object> params = getViewParams();

        paint.setColor(mCenterColor);  // 修改中心竖线颜色
        paint.setStrokeWidth(mYCenterSize);
        canvas.drawLine((Float) params.get("scrollX") + mYTitleWidth + mDistance * (position), (Integer) params.get("xAxisHeadHeight"), (Float) params.get("scrollX") + mYTitleWidth + mDistance * (position), (Integer) params.get("xAxisHeadHeight") + (Integer) params.get("trendHeight") / 8 * (8 - 1) - 20, paint);
    }

    /**
     * 画三角形
     *
     * @param canvas
     * @param paint
     * @param position
     */
    public void drawTriangle(Canvas canvas, Paint paint, int position) {

        Map<String, Object> params = getViewParams();

        paint.setStyle(Style.STROKE);
        paint.setStrokeWidth(2);
        paint.setColor(0xff7ecef9);
        Path path = new Path();
        path.reset();
        path.moveTo((Float) params.get("scrollX") + mYTitleWidth + mDistance * (position), (Integer) params.get("xAxisHeadHeight") + (Integer) params.get("trendHeight") / 8 * (8 - 1) - 20);// 开始坐标 也就是三角形的顶点
        path.lineTo((Float) params.get("scrollX") + mYTitleWidth + mDistance * (position) - 20, (Integer) params.get("xAxisHeadHeight") + (Integer) params.get("trendHeight") / 8 * (8 - 1));
        path.lineTo((Float) params.get("scrollX") + mYTitleWidth + mDistance * (position) + 20, (Integer) params.get("xAxisHeadHeight") + (Integer) params.get("trendHeight") / 8 * (8 - 1));
        path.close();
        canvas.drawPath(path, paint);
        // 去掉底边
        mTitlePaint.setColor(Color.WHITE);
        mTitlePaint.setStrokeWidth(3);
        canvas.drawLine((Float) params.get("scrollX") + mYTitleWidth + mDistance * (position) - 19, (Integer) params.get("xAxisHeadHeight") + (Integer) params.get("trendHeight") / 8 * (8 - 1), (Float) params.get("scrollX") + mYTitleWidth + mDistance * (position) + 19, (Integer) params.get("xAxisHeadHeight") + (Integer) params.get("trendHeight") / 8 * (8 - 1), mTitlePaint);

    }



效果图如下:
自定义View:画布实现自定义View(折线图的实现)

  • 第五层绘制折线
    /**
     * 绘制转折线
     *
     * @param canvas
     * @param paint
     * @param canvasLine
     * @param color
     */
    private void drawCanvasLine(Canvas canvas, Paint paint, List<Integer> canvasLine, int color) {

        Path mPath = new Path();  // 绘制趋势图对于的Path对象
        ArrayList<Integer> LinePosition = dealCanvasData(canvasLine);
        Map<String, Object> params = getViewParams();

        int startPosition = LinePosition.get(0);
        int endPosition = LinePosition.get(1);
        paint.setColor(color);
        // draw trend
        if (endPosition > startPosition && endPosition > 0) {

            for (int i = startPosition; i < endPosition; i++) {
                int currentY = (int) ((Integer) params.get("xAxisHeadHeight") + (mMaxHeight - canvasLine.get(i)) / mScaleValue * ((Integer) params.get("trendHeight") / 8));
                // 处理为负的数据,不需要可以屏蔽
                if (canvasLine.get(i) < 0) {
                    double Y = canvasLine.get(i) * 32.0 / 40.0;
                    currentY = (int) ((Integer) params.get("xAxisHeadHeight") + (mMaxHeight - Y) / scaleValue * ((Integer) params.get("trendHeight") / 8));
                }

                if (i == startPosition) {
                    mPath.moveTo(i * mDistance, currentY);
                } else {
                    mPath.lineTo(i * mDistance, currentY);
                }
            }
        }
        canvas.drawPath(mPath, paint);
        canvas.save();
        mPath.reset();
    }



效果图如下:
自定义View:画布实现自定义View(折线图的实现)

  • 第六层绘制圆点
    /**
     * 绘制圆
     *
     * @param canvas
     * @param paint
     * @param canvasLine
     * @param color
     * @param condition
     */
    private void drawCircles(Canvas canvas, Paint paint, List<Integer> canvasLine, int color, ArrayList<Integer> condition) {

        ArrayList<Integer> LinePosition = dealCanvasData(canvasLine);
        int startPosition = LinePosition.get(0);
        int endPosition = LinePosition.get(1);

        Map<String, Object> params = getViewParams();

        mOuterCirclePaint.setStrokeWidth(mTrendLineSize);
        mInnerCirclePaint.setStrokeWidth(mTrendLineSize);
        mOuterCirclePaint.setColor(color);
        if (endPosition > startPosition && endPosition > 0) {
            for (int i = startPosition; i < endPosition; i++) {
                int currentY = (int) ((Integer) params.get("xAxisHeadHeight") + (mMaxHeight - canvasLine.get(i)) / mScaleValue * ((Integer) params.get("trendHeight") / 8));
                // 处理为负的数据,不需要可以屏蔽
                if (canvasLine.get(i) < 0) {
                    double Y = canvasLine.get(i) * 32.0 / 40.0;
                    currentY = (int) ((Integer) params.get("xAxisHeadHeight") + (mMaxHeight - Y) / scaleValue * ((Integer) params.get("trendHeight") / 8));
                }

                if (canvasLine.get(i) > condition.get(1) || canvasLine.get(i) < condition.get(0)) {
                    drawCircle(canvas, i * mDistance, currentY);  // 实心
                } else {
                    // 下面需要对 90~140的数据处理
                    drawCirque(canvas, i * mDistance, currentY);  // 空心
                }
            }
        }
    }

    /**
     * 绘制实心圆
     *
     * @param canvas
     * @param positionX
     * @param positionY
     */
    private void drawCircle(Canvas canvas, int positionX, int positionY) {
        canvas.drawCircle(positionX, positionY, mOuterCircleRadius, mOuterCirclePaint);
    }

    /**
     * 绘制空心圆
     *
     * @param canvas
     * @param positionX
     * @param positionY
     */
    private void drawCirque(Canvas canvas, int positionX, int positionY) {
        canvas.drawCircle(positionX, positionY, mOuterCircleRadius, mOuterCirclePaint);
        canvas.drawCircle(positionX, positionY, mInnerCircleRadius, mInnerCirclePaint);
    }



效果图如下:
自定义View:画布实现自定义View(折线图的实现)

  • 第七层绘制X轴Title文字
    /**
     * 绘制x轴Title文字
     *
     * @param canvas
     * @param paint
     * @param data
     */
    private void drawXTitle(Canvas canvas, Paint paint, List<String[]> data) {

        paint.setColor(0xff888888);
        paint.setTextSize(mTextColorSize);
        paint.setStyle(Style.FILL);

        List<Integer> maxItem = getMaxItem();
        Map<String, Object> params = getViewParams();
        int startPosition = ((Integer) params.get("firstPosition") - (Integer) params.get("offsetCount")) >= 0 ? ((Integer) params.get("firstPosition") - (Integer) params.get("offsetCount")) : 0;
        int endPosition = maxItem.size();

        if (endPosition > startPosition && endPosition > 0) {
            float textBaseY_x_up = (Integer) params.get("xAxisHeadHeight") + (Integer) params.get("trendHeight") / 8 * (8 - 1) + 40;
            for (int i = startPosition; i < endPosition; i++) {

                drawCenterTextColor(i);
                // draw x axis up
                mYTitleRect.set(mDistance * (i - 1), (getHeight() - (Integer) params.get("xAxisHeight_up") - (Integer) params.get("xAxisHeight_blow") - 10), mDistance * (i + 1), getHeight() - (Integer) params.get("xAxisHeight_blow") - 10);
                canvas.drawText(data.get(i)[0].toString(), mYTitleRect.centerX(), textBaseY_x_up, paint);
            }
        }
    }
  • 第八层绘制Y轴Title文字
    /**
     * 绘制Y轴Title文字
     *
     * @param canvas
     * @param paint
     * @param backgroundColor
     * @param textColor
     */
    private void drawYTitle(Canvas canvas, Paint paint, int backgroundColor, int textColor) {

        Map<String, Object> params = getViewParams();
        FontMetrics fontMetrics = mPaint.getFontMetrics();
        float fontHeight = fontMetrics.bottom - fontMetrics.top;

        // 由于折线图是左右贯通的,Y轴Title在画布上会造成显示混乱,所以添加底部遮挡
        paint.setColor(backgroundColor);
        paint.setStyle(Style.FILL);
        mYTitleRect.set((int) ((Float) params.get("scrollX") + 0), 0, (int) ((Float) params.get("scrollX") + mYTitleWidth) - 30, getHeight() - 80);
        canvas.drawRect(mYTitleRect, paint);

        // and y-axis values
        paint.setColor(textColor);
        paint.setTextSize(mTextColorSize);
        //绘制Y轴值
        for (int i = 0; i <= 8; i++) {
            String showTitle;
            if (i >= 6 && i <= 8) {
                showTitle = 40 * (6 - i) + 120 + "";
            } else {
                showTitle = 40 + (5 - i) * 35 + "";
            }

            float textBaseY = ((Integer) params.get("xAxisHeadHeight") + (Integer) params.get("trendHeight") / 8 * (i - 1)) * 2 - (((Integer) params.get("xAxisHeadHeight") + (Integer) params.get("trendHeight") / 8 * (i - 1)) * 2 - fontHeight) / 2 - fontMetrics.bottom;
            canvas.drawText(showTitle, ((Float) params.get("scrollX") + mYTitleWidth / 2) - 20, textBaseY, paint);
        }
    }
  • 在onDraw中添加绘制View代码
    @Override
    protected void onDraw(Canvas canvas) {

        mDistance = (getWidth() - mYTitleWidth) / mDrawCount;
        currentCenter = (getWidth() - mDistance);
        if (mCenterPosition == -1) {
            int positionLocal = mCenterRecorded * mDistance;
            scrollTo(positionLocal - currentCenter, 0); // 根据可显示的区域 动态计算中点
            mCenterPosition = 0;
        }

        // 设置字体、笔画宽度
//        mTitlePaint.setTypeface(Typeface.DEFAULT_BOLD);
//        mTitlePaint.setStrokeWidth(4);

        // draw trend background
        // 设置dataOne界限的区域
        drawBackground(canvas, mRangeTrendBackgroundPaint, mRangeTrendColors, RatioUp, RatioDown);
        // 设置dataTwo界限的区域
        drawBackground(canvas, nRangeTrendBackgroundPaint, nRangeTrendColors, colorUp, colorDown);
        // 设置dataThree界限的区域
        drawBackground(canvas, mPulsePaint, mPulseColors, mPulseUp, mPulseDown);

        // draw form
        drawForm(canvas, mTitlePaint);
        // split form
        setSplitForm(canvas, mTitlePaint, Color.WHITE, 2);
        // draw sign
        drawCenterSign(canvas, mTitlePaint, 6);


        if (mPoints != null) {
            // draw canvas line
            drawCanvasLine(canvas, mPaint, mPoints[0], mPointColors[0]);
            drawCanvasLine(canvas, mPaint, mPoints[1], mPointColors[1]);

            // draw circles
            ArrayList<Integer> conditionsOne = new ArrayList<>();
            conditionsOne.add(90);
            conditionsOne.add(140);
            ArrayList<Integer> conditionsTwo = new ArrayList<>();
            conditionsTwo.add(60);
            conditionsTwo.add(90);

            drawCircles(canvas, mPaint, mPoints[0], mPointColors[0], conditionsOne);
            drawCircles(canvas, mPaint, mPoints[1], mPointColors[1], conditionsTwo);

        }

        if (mData != null) {

            // draw canvas line
            drawCanvasLine(canvas, mPaint, mData[0], mPulseColor);

            // draw circles
            ArrayList<Integer> conditionsThree = new ArrayList<>();
            conditionsThree.add(-70);
            conditionsThree.add(-10);

            drawCircles(canvas, mPaint, mData[0], mPulseColor, conditionsThree);

        }

        //and x-axis values
        if (mXAxisValues != null && mXAxisValues.size() > 0) {
            drawXTitle(canvas, mTitlePaint, mXAxisValues);
        }

        // draw y r xis rect
        drawYTitle(canvas, mTitlePaint, Color.WHITE, 0xff888888);

    }



效果图如下:
自定义View:画布实现自定义View(折线图的实现)

现在咱们的界面绘制完成,要记住顺序,不然会遮挡。然后咱们实现监听。

二、监听事件,实现逻辑

  • 实现onTouchEvent监听事件逻辑
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:

                oldX = (int) event.getX();
                if ((mIsBeingDragged)) {
                    final ViewParent parent = getParent();
                    if (parent != null) {
                        parent.requestDisallowInterceptTouchEvent(true);
                    }
                }
                invalidate();
                mActivePointerId = event.getPointerId(0);
                return true;
            case MotionEvent.ACTION_MOVE:

                final int activePointerIndex = event.findPointerIndex(mActivePointerId);
                if (activePointerIndex == -1) {
                    break;
                }

                final int x = (int) event.getX(activePointerIndex);
                int deltaX = oldX - x;
                if (!mIsBeingDragged && Math.abs(deltaX) > mTouchSlop) {
                    final ViewParent parent = getParent();
                    if (parent != null) {
                        parent.requestDisallowInterceptTouchEvent(true);
                    }
                    mIsBeingDragged = true;
                    if (deltaX > 0) {
                        deltaX -= mTouchSlop;
                    } else {
                        deltaX += mTouchSlop;
                    }
                }

                // HorizontalScrollView
                if (mIsBeingDragged && Math.abs(deltaX) > mTouchSlop) {
                    oldX = x;
                    mTowards = deltaX;
                    scrollBy(deltaX, 0);
                    invalidate();
                    if (mPCenterListener != null) {
                        int nextCenter = getToNextCenter();
                        mPCenterListener.passCenter(nextCenter);
                    }
                }
                invalidate();
                return true;
            default:

                if (mIsBeingDragged) {
                    mIsBeingDragged = false;
                    int nextCenter = getToNextCenter();

                    mTowards = 0;
                    int halfWidth = currentCenter;
                    int positionLocal = nextCenter * mDistance;
                    scrollTo(positionLocal - halfWidth, 0);
                    if (mAEndListener != null) {
                        mAEndListener.actionEnd(nextCenter);
                        invalidate();
                        centerPosition = nextCenter;
                    }
                } else {

                }
                invalidate();
                return true;
        }
        return super.onTouchEvent(event);
    }

在监听事件中不只有左右滑动监听,还添加了滑动过中心的监听。这里道长不多说了,这篇博客还是这么干巴巴的,以后道长可能只贴代码了[手动滑稽]。有不明白的地方看Demo。现在自定义View,自定义组合View道长都说了,怎么会不说一下自定义属性,这个在自定义View中也是很常用的。咱们在开一篇博客,画布实现自定义View暂时到这里,希望这篇博客能为你提供一些帮助。

源码下载

CanvasDemo