androidandroid-canvasandroid-graphics

Custom Shape drawing using Path in Android


I am trying to draw a custom shape on a canvas that at present looks like this: enter image description here

and here is what I want to achieve: enter image description here

Along with that, I want the attributes for the RGB borders to be configurable, for ex. should be able to change the stroke width as required. However, I am facing several issues:

  1. Not able to remove the base of the inverted triangle (The RGB border lines should not be straight at the bottom)
  2. If I try to change the width of the RGB lines (using paint.setStrokeWidth()), it introduces undesired gaps b/w them, whereas I want them to be continuous. I am sure I am making some calculation mistake, but can't figure it out.
  3. I have experienced that drawing a line along the view edge using lineTo on Path is drawn with half of the stroke width set on the Paint. However, I am unable to find out any reading material on the same. Can someone please enlighten me?

The onDraw method of the custom view is as below:

override fun onDraw(canvas: Canvas?) {
    outerBorderPath.reset()
    mainBorderPath.reset()
    innerBorderPath.reset()


    ///let's draw our content first
    canvas?.let { drawingCanvas ->
        outerBorderPath.addRect(outerBorderWidth.toFloat(),outerBorderWidth.toFloat(),width.toFloat() - outerBorderWidth, (height - arrowHeight - outerBorderWidth).toFloat(), Path.Direction.CW)
        outerBorderPath.addPath(mArrowPath, width.toFloat() - arrowWidth - 100,
            (height - arrowHeight - outerBorderWidth).toFloat()
        )
        drawingCanvas.drawPath(outerBorderPath, outerBorderPaint)
        mainBorderPath.addRect(outerBorderWidth + mainBorderWidth.toFloat(),
        outerBorderWidth + mainBorderWidth.toFloat(),
            width.toFloat() - outerBorderWidth - mainBorderWidth,
            (height - arrowHeight - outerBorderWidth - mainBorderWidth).toFloat(),
            Path.Direction.CW
            )

        mainBorderPath.addPath(mainArrowPath, width.toFloat() - arrowWidth + (outerBorderWidth/2) - 100,
            (height - arrowHeight - outerBorderWidth - mainBorderWidth).toFloat()
            )
        drawingCanvas.drawPath(mainBorderPath, mainBorderPaint)

        innerBorderPath.addRect(outerBorderWidth + mainBorderWidth + innerBorderWidth.toFloat(),
            outerBorderWidth + mainBorderWidth*1f + innerBorderWidth,
            width.toFloat() - outerBorderWidth - mainBorderWidth*1f - innerBorderWidth,
            (height - arrowHeight - outerBorderWidth - mainBorderWidth*1f - innerBorderWidth).toFloat(),
            Path.Direction.CW
            )
        innerBorderPath.addPath(innerArrowPath, width.toFloat() - arrowWidth + (outerBorderWidth + mainBorderWidth)/2 - 100,
            (height - arrowHeight - outerBorderWidth - mainBorderWidth - innerBorderWidth).toFloat()
            )
        drawingCanvas.drawPath(innerBorderPath, innerBorderPaint)
    }

    ///translate canvas to the child can be drawn now
    canvas?.save()
    super.onDraw(canvas)
    canvas?.restore()
}

Also, the onMeasure of the view class is as follows:

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    setMeasuredDimension(
        (measuredWidth + (2 * outerBorderWidth) + (2 * innerBorderWidth) + (2 * innerPadding)).toInt(),
        (measuredHeight + (2 * outerBorderWidth) + (2 * innerBorderWidth) + (2 * innerPadding) + arrowHeight).toInt()
    )
}

Solution

  • I have experienced that drawing a line along the view edge using lineTo on Path is drawn with half of the stroke width set on the Paint. However, I am unable to find out any reading material on the same. Can someone please enlighten me?

    I never found any official documentation on this, but I can confirm this happens not only with Path but also with Canvas.drawRect() or Canvas.drawArc(), possibly because Path is used under the hood (see for example Drawing an arc inside a circle). The behavior makes sense: why should a View be allowed to draw outside of its bounds? But I also wish we would not all have to learn by trial and error...

    If I try to change the width of the RGB lines (using paint.setStrokeWidth()), it introduces undesired gaps b/w them, whereas I want them to be continuous. I am sure I am making some calculation mistake, but can't figure it out.

    The following screenshot shows two emulators with different specs side by side. One can see that the View is not always rendered the same way. The differences may be due to rounding - in the end you have to map a Float to an Int value, since the number of pixels on the device is an Int value.

    Since the Paths are drawn one on top of the other, one approach could be to make the outer and main border wider, so that there is no gap.

    view rendered differenty depending on device

    Not able to remove the base of the inverted triangle (The RGB border lines should not be straight at the bottom)

    The base of the inverted triangle is part of the Rect which you use for configuring the Path. You can fix that by using multiple lineTo() instead of addRect()

    fun Path.createShapeWithPadding(padding: Int, arrowStartX: Int, arrowStartY:Int, width: Int, height: Int, arrowWidth: Int, arrowHeight: Int ) {
    
        val paddingF = padding.toFloat()
        moveTo(paddingF, paddingF)
        lineTo(width - paddingF, paddingF)
        lineTo(width - paddingF, arrowStartY.toFloat())
        lineTo(arrowStartX.toFloat(), arrowStartY.toFloat())
        rLineTo(-arrowWidth / 2f, arrowHeight.toFloat())
        rLineTo(-arrowWidth / 2f, -arrowHeight.toFloat())
        lineTo(paddingF, arrowStartY.toFloat())
        close()
    }
    

    Using the extension function in onDraw():

    override fun onDraw(canvas: Canvas?) {
        outerBorderPath.reset()
        mainBorderPath.reset()
        innerBorderPath.reset()
        
        canvas?.let { drawingCanvas ->
    
            val arrowX = width - 100
            val arrowY = height - arrowHeight - outerBorderWidth
            outerBorderPath.apply {
                createShapeWithPadding(outerBorderWidth, arrowX, arrowY, width, height, arrowWidth, arrowHeight)
            }
    
            drawingCanvas.drawPath(outerBorderPath, outerBorderPaint)
    
            mainBorderPath.apply {
                createShapeWithPadding(
                    mainBorderWidth + outerBorderWidth,
                    arrowX - (outerBorderWidth / 2) ,
                    arrowY - mainBorderWidth,
                    width,
                    height,
                    arrowWidth - outerBorderWidth,
                    arrowHeight - outerBorderWidth
                )
            }
            drawingCanvas.drawPath(mainBorderPath, mainBorderPaint)
    
            innerBorderPath.apply {
                createShapeWithPadding(
                    outerBorderWidth + mainBorderWidth + innerBorderWidth,
                    arrowX - (outerBorderWidth + mainBorderWidth) / 2,
                    arrowY - (mainBorderWidth + innerBorderWidth),
                    width,
                    height,
                    arrowWidth - (outerBorderWidth + mainBorderWidth),
                    arrowHeight - (outerBorderWidth + mainBorderWidth)
                )
            }
            drawingCanvas.drawPath(innerBorderPath, innerBorderPaint)
    
        }
    
        canvas?.save()
        super.onDraw(canvas)
        canvas?.restore()
    }