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

今天道长打算说一下用画布实现自定义View,这是道长说的自定义View的第四种实现方式了。
第一种:是放好布局后使用NineOldAndroid监听动画实现,想看一下的话点击传送门属性动画(二):如何自定义View以及自定义View:侧滑菜单动画实现
第二种:是放好布局后使用TouchEvent监听实现,传送门在此自定义View:侧滑菜单实现
第三种:是继承相关的View,拓展相关View的功能,传送门在此PopWindow:基本使用与自定义PopWindow
第三种自定义View就是拓展相关View的功能。比如自定义PopWindow要增加出现动画或者展示方式。前两种都是使用已经存在的布局,一种继承FrameLayout,另一种继承ViewGroup。放置好位置后监听事件实现。应该说前两种是自定义组合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();
    }



效果图如下:
这里写图片描述

  • 第二层绘制表格
 /**
     * 绘制表格
     *
     * @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);
    }



效果图如下:
这里写图片描述

  • 第三层绘制网格分割区域
    /**
     * 分割网格
     *
     * @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);

    }



效果图如下:
这里写图片描述

  • 第五层绘制折线
    /**
     * 绘制转折线
     *
     * @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();
    }



效果图如下:
这里写图片描述

  • 第六层绘制圆点
    /**
     * 绘制圆
     *
     * @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);
    }



效果图如下:
这里写图片描述

  • 第七层绘制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);

    }



效果图如下:
这里写图片描述

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

二、监听事件,实现逻辑

  • 实现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


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值