Drawing Android polygraph

Posted by Hiccup on Mon, 15 Jul 2019 02:04:43 +0200

The project needs a broken-line map, but does not want to introduce the MPAndroid Chart and HelloCharts framework. Looking at their principles and the content recommended by Wechat, I revised and sorted out the following.
Thank you to the original author.

The main forms we will achieve are as follows:

Before reading this article, I would like to start by recommending to read my last article.
Android PathEffect Custom Line Map Necessary

After reading, let's get to the main point:

Customize View in four steps.

Or the steps we took to customize the View:

  • 1. Properties of custom View
  • 2. Obtain our custom attributes in View's construction method
  • [3. Rewrite onMesure]
  • 4. Rewrite onDraw
  • 5. Rewrite onTouchEvent (if you need this control, the opponent will handle the operation in a special way)

1. Make a statement in attrs

   <! - Why is it pulled out here? Because if there are two textSize under different customized view s, there will be errors in the construction, and they will be extracted for reuse.
    <attr name="textSize" format="dimension|reference"/>
    <attr name="textColor" format="color"/>


    <declare-styleable name="ChartView">
        <attr name="max_score" format="integer"/>
        <attr name="min_score" format="integer"/>
        <attr name="broken_line_color" format="color"/>
        <attr name="textColor"/>
        <attr name="textSize"/>
        <attr name="dottedlineColor" format="color"/>
    </declare-styleable>

2. Obtain our custom attributes in View's construction method

  TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ChartView);
        maxScore = a.getInt(R.styleable.ChartView_max_score, 800);
        minScore = a.getInt(R.styleable.ChartView_min_score, 600);
        brokenLineColor = a.getColor(R.styleable.ChartView_broken_line_color, brokenLineColor);
        textNormalColor = a.getColor(R.styleable.ChartView_textColor, textNormalColor);
        textSize = a.getDimensionPixelSize(R.styleable.ChartView_textSize, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
                15, getResources().getDisplayMetrics()));
        straightLineColor = a.getColor(R.styleable.ChartView_dottedlineColor, straightLineColor);

        a.recycle();

After obtaining our custom attributes in the View's construction method, we initialize Paint and Path:

 //Initialize path and Paint
        brokenPath = new Path();

        brokenPaint = new Paint();
        brokenPaint.setAntiAlias(true);
        brokenPaint.setStyle(Paint.Style.STROKE);
        brokenPaint.setStrokeWidth(dipToPx(brokenLineWith));
        brokenPaint.setStrokeCap(Paint.Cap.ROUND);

        straightPaint = new Paint();
        straightPaint.setAntiAlias(true);
        straightPaint.setStyle(Paint.Style.STROKE);
        straightPaint.setStrokeWidth(brokenLineWith);
        straightPaint.setColor((straightLineColor));
        straightPaint.setStrokeCap(Paint.Cap.ROUND);

        dottedPaint = new Paint();
        dottedPaint.setAntiAlias(true);
        dottedPaint.setStyle(Paint.Style.STROKE);
        dottedPaint.setStrokeWidth(brokenLineWith);
        dottedPaint.setColor((straightLineColor));
        dottedPaint.setStrokeCap(Paint.Cap.ROUND);

        textPaint = new Paint();
        textPaint.setAntiAlias(true);
        textPaint.setTextAlign(Paint.Align.CENTER);
        textPaint.setStyle(Paint.Style.FILL);
        textPaint.setColor((textNormalColor));
        textPaint.setTextSize(textSize);

3. Rewrite onMesure (this View does not require us to calculate, but we can rewrite onSizeChanged for width and height determination, data acquisition, etc.)

Since the onSizeChanged method has completed the initialization of global variables and obtained the width of the control after the construction method, onMeasure and before onDraw, some values related to width and height can be determined in this method.

For example, the radius of the View, padding value, etc., facilitate the calculation of size and location when drawing:

The coordinate axis of View and its acquisition method are as follows:

Here is the onSizeChanged method:

  @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        viewWith = w;
        viewHeight = h;
        initData();
    }


    //Initialize the data, where the data is converted into a set of point points, which are drawn when ondraw, connected.
    private void initData() {
        scorePoints = new ArrayList<Point>();
        float maxScoreYCoordinate = viewHeight * 0.1f;
        float minScoreYCoordinate = viewHeight * 0.6f;

        Log.v(TAG, "initData: " + maxScoreYCoordinate);

        float newWith = viewWith - (viewWith * 0.15f) * 2;//The distance between the separator and the leftmost and rightmost is 0.15 times that of viewWith.
        int coordinateX;

        for (int i = 0; i < score.length; i++) {
            Log.v(TAG, "initData: " + score[i]);
            Point point = new Point();
            coordinateX = (int) (newWith * ((float) (i) / (monthCount - 1)) + (viewWith * 0.15f));//Determining the X-coordinates of a point
            point.x = coordinateX;
            if (score[i] > maxScore) {
                score[i] = maxScore;
            } else if (score[i] < minScore) {
                score[i] = minScore;
            }
            point.y = (int) (((float) (maxScore - score[i]) / (maxScore - minScore)) * (minScoreYCoordinate - maxScoreYCoordinate) + maxScoreYCoordinate);//// Determining Y coordinates of point
            scorePoints.add(point);
        }
    }


4. Rewrite onDraw (generally showing the most complex aspect of view)

The onDraw method is as follows

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawDottedLine(canvas, viewWith * 0.15f, viewHeight * 0.1f, viewWith, viewHeight * 0.1f);//The drawing of the dotted line above does not understand the picture of the coordinate system.
        drawDottedLine(canvas, viewWith * 0.15f, viewHeight * 0.6f, viewWith, viewHeight * 0.6f);//Drawing the following dashed line
        drawText(canvas);//Draw text, minScore, maxScore
        drawMonthLine(canvas);//Lines and coordinates of months
        drawBrokenLine(canvas);//Drawing a broken line is drawing dots, moveto connection
        drawPoint(canvas);//Drawing points across a broken line
    }

Next, let's decompose it step by step:

  • 1. Draw two dotted lines
    /**
     * @param canvas canvas
     * @param startX Starting point X coordinates
     * @param startY Starting point Y coordinate
     * @param stopX  Endpoint X coordinates
     * @param stopY  Endpoint Y coordinates
     */


    private void drawDottedLine(Canvas canvas, float startX, float startY, float stopX,
                                float stopY) {

        dottedPaint.setPathEffect(new DashPathEffect(new float[]{20, 10}, 4));//If Dash Path Effect doesn't understand, read my last article.
        dottedPaint.setStrokeWidth(1);
        // Instance path
        Path mPath = new Path();
        mPath.reset();
        // Define the starting point of a path
        mPath.moveTo(startX, startY);
        mPath.lineTo(stopX, stopY);
        canvas.drawPath(mPath, dottedPaint);

    }
  • 2. Drawing text, minScore, maxScore, etc.
 /**
     * @param canvas
     * */
    private void drawText(Canvas canvas) {

        textPaint.setTextSize(textSize);//Default font 15
        textPaint.setColor(textNormalColor);

        canvas.drawText(String.valueOf(maxScore), viewWith * 0.1f - dipToPx(10), viewHeight * 0.1f + textSize * 0.25f, textPaint);
        canvas.drawText(String.valueOf(minScore), viewWith * 0.1f - dipToPx(10), viewHeight * 0.6f + textSize * 0.25f, textPaint);

        textPaint.setColor(0xff7c7c7c);

        float newWith = viewWith - (viewWith * 0.15f) * 2;//The distance between the separator and the leftmost and rightmost is 0.15 times that of viewWith.
        float coordinateX;//Separator X coordinates
        textPaint.setTextSize(textSize);
        textPaint.setStyle(Paint.Style.FILL);
        textPaint.setColor(textNormalColor);
        textSize = (int) textPaint.getTextSize();
        for (int i = 0; i < monthText.length; i++) {//Here's the month of drawing. Take it out of the array and write it one by one.
            coordinateX = newWith * ((float) (i) / (monthCount - 1)) + (viewWith * 0.15f);

            if (i == selectMonth - 1)//Selected months should be drawn in several circles separately
            {

                textPaint.setStyle(Paint.Style.STROKE);
                textPaint.setColor(brokenLineColor);
                RectF r2 = new RectF();
                r2.left = coordinateX - textSize - dipToPx(4);
                r2.top = viewHeight * 0.7f + dipToPx(4) + textSize / 2;
                r2.right = coordinateX + textSize + dipToPx(4);
                r2.bottom = viewHeight * 0.7f + dipToPx(4) + textSize + dipToPx(8);
                canvas.drawRoundRect(r2, 10, 10, textPaint);

            }
            //Drawing months
            canvas.drawText(monthText[i], coordinateX, viewHeight * 0.7f + dipToPx(4) + textSize + dipToPx(5), textPaint);//It's not just normal drawing.

            textPaint.setColor(textNormalColor);

        }


    }
  • Drawing of coordinate axes and points in March and Month
    //Draw month's lines (including scales)
    private void drawMonthLine(Canvas canvas) {

        straightPaint.setStrokeWidth(dipToPx(1));
        canvas.drawLine(0, viewHeight * 0.7f, viewWith, viewHeight * 0.7f, straightPaint);

        float newWith = viewWith - (viewWith * 0.15f) * 2;//The distance between the separator and the leftmost and rightmost is 0.15 times that of viewWith.
        float coordinateX;//Separator X coordinates
        for (int i = 0; i < monthCount; i++) {
            coordinateX = newWith * ((float) (i) / (monthCount - 1)) + (viewWith * 0.15f);
            canvas.drawLine(coordinateX, viewHeight * 0.7f, coordinateX, viewHeight * 0.7f + dipToPx(4), straightPaint);
        //viewHeight * 0.7f + dipToPx(4) This method is the vertical bar on the coordinate axis. You can modify the length of the vertical bar here.
        }

    }
  • 4. Draw a broken line, that is, draw a dot. lineTo connects drawPath to draw it.
//Draw a broken line
    private void drawBrokenLine(Canvas canvas) {
        brokenPath.reset();
        brokenPaint.setColor(brokenLineColor);
        brokenPaint.setStyle(Paint.Style.STROKE);
        if (score.length == 0) {
            return;
        }
        Log.v(TAG, "drawBrokenLine: " + scorePoints.get(0));
        brokenPath.moveTo(scorePoints.get(0).x, scorePoints.get(0).y);
        for (int i = 0; i < scorePoints.size(); i++) {
            brokenPath.lineTo(scorePoints.get(i).x, scorePoints.get(i).y);
        }
        canvas.drawPath(brokenPath, brokenPaint);

    }
  • 5. Draw the point through which the broken line passes.
  protected void drawPoint(Canvas canvas) {

        if (scorePoints == null) {
            return;
        }
        brokenPaint.setStrokeWidth(dipToPx(1));
        for (int i = 0; i < scorePoints.size(); i++) {
            brokenPaint.setColor(brokenLineColor);
            brokenPaint.setStyle(Paint.Style.STROKE);
            canvas.drawCircle(scorePoints.get(i).x, scorePoints.get(i).y, dipToPx(3), brokenPaint);
            brokenPaint.setColor(Color.WHITE);
            brokenPaint.setStyle(Paint.Style.FILL);
            if (i == selectMonth - 1) {//The default selection will draw different points, as shown in the figure
                brokenPaint.setColor(0xffd0f3f2);
                canvas.drawCircle(scorePoints.get(i).x, scorePoints.get(i).y, dipToPx(8f), brokenPaint);
                brokenPaint.setColor(0xff81dddb);
                canvas.drawCircle(scorePoints.get(i).x, scorePoints.get(i).y, dipToPx(5f), brokenPaint);

                //Draw a floating text background box
                drawFloatTextBackground(canvas, scorePoints.get(i).x, scorePoints.get(i).y - dipToPx(8f));

                textPaint.setColor(0xffffffff);
                //Draw floating text
                canvas.drawText(String.valueOf(score[i]), scorePoints.get(i).x, scorePoints.get(i).y - dipToPx(5f) - textSize, textPaint);
            }
            brokenPaint.setColor(0xffffffff);
            canvas.drawCircle(scorePoints.get(i).x, scorePoints.get(i).y, dipToPx(1.5f), brokenPaint);
            brokenPaint.setStyle(Paint.Style.STROKE);
            brokenPaint.setColor(brokenLineColor);
            canvas.drawCircle(scorePoints.get(i).x, scorePoints.get(i).y, dipToPx(2.5f), brokenPaint);
        }
    }

        //This method uses path and point to draw the graph and set the background color.
    private void drawFloatTextBackground(Canvas canvas, int x, int y) {
        brokenPath.reset();
        brokenPaint.setColor(brokenLineColor);
        brokenPaint.setStyle(Paint.Style.FILL);

        //P1
        Point point = new Point(x, y);
        brokenPath.moveTo(point.x, point.y);

        //P2
        point.x = point.x + dipToPx(5);
        point.y = point.y - dipToPx(5);
        brokenPath.lineTo(point.x, point.y);

        //P3
        point.x = point.x + dipToPx(12);
        brokenPath.lineTo(point.x, point.y);

        //P4
        point.y = point.y - dipToPx(17);
        brokenPath.lineTo(point.x, point.y);

        //P5
        point.x = point.x - dipToPx(34);
        brokenPath.lineTo(point.x, point.y);

        //P6
        point.y = point.y + dipToPx(17);
        brokenPath.lineTo(point.x, point.y);

        //P7
        point.x = point.x + dipToPx(12);
        brokenPath.lineTo(point.x, point.y);

        //The last point connects to the first point
        brokenPath.lineTo(x, y);

        canvas.drawPath(brokenPath, brokenPaint);

    }

5. Rewrite onTouchEvent

Requirement: Click on a point and it will appear the same effect as the default selection, showing the background circle and text. The bottom text is also selected

 //Rewrite ontouchevent,


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        this.getParent().requestDisallowInterceptTouchEvent(true);
        //Once the underlying View receives the touch action and calls this method, the parent View will no longer call onInterceptTouchEvent, nor can it intercept the subsequent action, which is consumed.

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                break;
            case MotionEvent.ACTION_MOVE:
                break;
            case MotionEvent.ACTION_UP://Touch (ACTION_DOWN operation), slide (ACTION_MOVE operation) and lift (ACTION_UP)
                onActionUpEvent(event);
                this.getParent().requestDisallowInterceptTouchEvent(false);
                break;
            case MotionEvent.ACTION_CANCEL:
                this.getParent().requestDisallowInterceptTouchEvent(false);
                break;
        }
        return true;
    }

    private void onActionUpEvent(MotionEvent event) {


        boolean isValidTouch = validateTouch(event.getX(), event.getY());//Determine whether it is the specified touching area

        if (isValidTouch) {
            invalidate();
        }

    }


    //Is it an effective touch range?
    private boolean validateTouch(float x, float y) {

        //Curve Touch Area
        for (int i = 0; i < scorePoints.size(); i++) {
            // DidipToPx (8) multiplied by 2 to appropriately increase the touch area
            if (x > (scorePoints.get(i).x - dipToPx(8) * 2) && x < (scorePoints.get(i).x + dipToPx(8) * 2)) {
                if (y > (scorePoints.get(i).y - dipToPx(8) * 2) && y < (scorePoints.get(i).y + dipToPx(8) * 2)) {
                    selectMonth = i + 1;
                    return true;
                }
            }
        }
        //Monthly Touch Area
        //Calculate the central point of the X coordinates for each month
        float monthTouchY = viewHeight * 0.7f - dipToPx(3);//Subtract dipToPx(3) to increase touch area

        float newWith = viewWith - (viewWith * 0.15f) * 2;//The distance between the separator and the leftmost and rightmost is 0.15 times that of viewWith.
        float validTouchX[] = new float[monthText.length];
        for (int i = 0; i < monthText.length; i++) {
            validTouchX[i] = newWith * ((float) (i) / (monthCount - 1)) + (viewWith * 0.15f);
        }

        if (y > monthTouchY) {
            for (int i = 0; i < validTouchX.length; i++) {
                Log.v(TAG, "validateTouch: validTouchX:" + validTouchX[i]);
                if (x < validTouchX[i] + dipToPx(8) && x > validTouchX[i] - dipToPx(8)) {
                    Log.v(TAG, "validateTouch: " + (i + 1));
                    selectMonth = i + 1;
                    return true;
                }
            }
        }

        return false;
    }

The whole has been completed. Summarize the general steps:

  • Initialize View properties
  • Initialization Brush
  • Draw two dotted lines representing the highest score and the lowest score
  • Drawing Text
  • Draw attributes representing months
  • Drawing Sesame Breakdown Line
  • Draw a dot representing sesame
  • Drawing suspended text with selected scores and background
  • Handling Click Events
  • If you want to draw multiple line maps, just look at the way I blogged last time. Android PathEffect Custom Line Map Necessary

You can follow this logic when you are looking, and you can understand the process of customizing View very well.

Attached below is GitHub address:

Topics: Android P4 github