Round edges of a custom view not working as expected when using path

93 Views Asked by At

I want to draw a custom view , kind of expected result

in this image I need to draw rounded corners of the rectangle

I wrote this code

class RoundedRectUsingPath @JvmOverloads constructor(
  context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

  private val roundCorner = 10f

  private val paint = Paint().apply {
    color = Color.RED
    isAntiAlias = true
    pathEffect = CornerPathEffect(roundCorner)
    setShadowLayer(10f, -2f, 2f, android.graphics.Color.BLACK)

  }

  private var path = Path()

  override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
    super.onSizeChanged(w, h, oldw, oldh)
    val left = 10f
    val top = 0f
    val avatarRadius = 30f
    val shapeBounds = RectF(left, top+20, left + width - 40, top + height -20)

    val leftCutoutPoint = PointF(left, shapeBounds.bottom/2)
    val rightCutOutBounds = PointF(shapeBounds.right, shapeBounds.bottom/2)
    val leftAvatarBounds = fromCircle(center = leftCutoutPoint, radius = avatarRadius)
    val rightAvatarBounds = fromCircle(center = rightCutOutBounds, radius = avatarRadius)

    path = Path().apply {
      moveTo(shapeBounds.left, shapeBounds.top)
      arcTo(leftAvatarBounds, -90f, 180f, false)
      lineTo(shapeBounds.bottomLeft.x, shapeBounds.bottomLeft.y)
      lineTo(shapeBounds.bottomRight.x, shapeBounds.bottomRight.y)
      arcTo(rightAvatarBounds, 90f, 180f, false)
      lineTo(shapeBounds.topRight.x, shapeBounds.topRight.y)
      close()
    }
  }

  override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    canvas.drawPath(path, paint)
  }


  fun fromLTRB(left: Float, top: Float, right: Float, bottom: Float) =
    RectF(left, top, right, bottom)

  fun fromLTWH(left: Float, top: Float, width: Float, height: Float) =
    fromLTRB(left, top, left + width, top + height)

  fun fromCircle(center: PointF, radius: Float) =
    fromCenter(
      center = center,
      width = radius,
      height = radius
    )

  fun fromCenter(center: PointF, width: Float, height: Float) =
    fromLTRB(
      center.x - width,
      center.y - height,
      center.x + width,
      center.y + height
    )

  fun RectF.inflate(delta: Float): RectF {
    return fromLTRB(
      left - delta,
      top - delta,
      right + delta,
      bottom + delta
    )
  }
}

but when I am using pathEffect = CornerPathEffect(roundCorner). where round corner is 10fthe image is changing to this rounded corner image

  1. Here you can see the cutout shape changes, which is not expected.

  2. And the topleft corner is not perfectly round but a straight line between 2 points.

The expected is rounded corners with perfect cutouts, I tried many different ways to acheive this but none of them are working for me.

1

There are 1 best solutions below

3
Cheticamp On

It looks like you have two issues:

  1. The top left corner is not really rounded but looks to be just a line;
  2. The cutouts look to be less rounded when using CornerPathEffect.

This looking buggy to me. Maybe someone will have a more direct fix, but I recommend that you take a look at MaterialShapeDrawable to draw your shape.

Here is some more information on MaterialShapeDrawable. It looks like the "ticket" shape is what you are looking for.

From your update to the questions:

  1. Here you can see the cutout shape changes, which is not expected.

This still looks like a bug to me. However, if you add one to leftAvatarBounds.top += 1, the left cutoff will be correct. Why? I don't know. Adding rightAvatarBounds.bottom += 1 to the right cutoff works for that side.

  1. And the topleft corner is not perfectly round but a straight line between 2 points.

Do not start the path in a corner but within one side.

You can get your code working by changing the path calculation to the following:

path = Path().apply {
    moveTo(shapeBounds.right / 2, shapeBounds.top)
    lineTo(shapeBounds.left, shapeBounds.top)
    lineTo(shapeBounds.left, leftAvatarBounds.top)
    leftAvatarBounds.top += 1
    arcTo(leftAvatarBounds, -90f, 180f, false)
    lineTo(shapeBounds.left, shapeBounds.bottom)
    lineTo(shapeBounds.right, shapeBounds.bottom)
    lineTo(shapeBounds.right, rightAvatarBounds.bottom)
    rightAvatarBounds.bottom += 1
    arcTo(rightAvatarBounds, 90f, 180f, false)
    lineTo(shapeBounds.right, shapeBounds.top)
    close()
}

This is what is displayed with the above changes:

enter image description here

Although this works, you may not feel comfortable, as I would not, relying upon the magical addition of one to the avatar bounds.

I went ahead and coded up a MaterialShapeDrawable which I mentioned would be an alternate solution:

class TicketView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

    private val ticketShapePathModel = ShapeAppearanceModel
        .Builder()
        .setAllCorners(CornerFamily.ROUNDED, 10f)
        .setLeftEdge(TicketEdgeTreatment(30f))
        .setRightEdge(TicketEdgeTreatment(30f))
        .build()

    private var ticket: MaterialShapeDrawable

    init {
        ticket = MaterialShapeDrawable(ticketShapePathModel).apply {
            fillColor = ColorStateList.valueOf(Color.RED)
            shadowRadius = 10
            shadowCompatibilityMode = MaterialShapeDrawable.SHADOW_COMPAT_MODE_ALWAYS
            setShadowColor(Color.BLACK)
        }
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        ticket.bounds = Rect(0, 0, w, h)
        ticket.bounds.inset(10, 10)
    }

    override fun draw(canvas: Canvas) {
        super.draw(canvas)
        ticket.draw(canvas)
    }
}

private class TicketEdgeTreatment(
    private val size: Float
) : EdgeTreatment() {
    override fun getEdgePath(
        length: Float,
        center: Float,
        interpolation: Float,
        shapePath: ShapePath
    ) {
        val circleRadius = size * interpolation
        shapePath.lineTo(center - circleRadius, 0f)
        shapePath.addArc(
            center - circleRadius, -circleRadius,
            center + circleRadius, circleRadius,
            180f,
            -180f
        )
        shapePath.lineTo(length, 0f)
    }
}

This displays as follows:

enter image description here