I am trying to draw a custom shape on a canvas that at present looks like this:
and
here is what I want to achieve:
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:
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()
)
}
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 Path
s 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.
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()
}