自定義View:畫布實現自定義View(折線圖的實現)

雨幕青山發表於2017-05-11

今天道長打算說一下用畫布實現自定義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


相關文章