Skip to content

Instantly share code, notes, and snippets.

@vadiole
Last active March 13, 2023 22:22
Show Gist options
  • Save vadiole/f37416e700eb4799af8ca997463c0273 to your computer and use it in GitHub Desktop.
Save vadiole/f37416e700eb4799af8ca997463c0273 to your computer and use it in GitHub Desktop.
SquircleDrawable with iOS-like rounded corners
import android.graphics.Canvas
import android.graphics.ColorFilter
import android.graphics.Matrix
import android.graphics.Paint
import android.graphics.Path
import android.graphics.PixelFormat
import android.graphics.Rect
import android.graphics.drawable.Drawable
import kotlin.math.pow
/**
* SquircleDrawable with iOS-like rounded corners
*
* [Read more about Squircle](https://www.figma.com/blog/desperately-seeking-squircles)
*/
class SquircleDrawable(private val cornerRadiusPx: Int, private val paint: Paint) : Drawable() {
var skipTopLeft = false
var skipTopRight = false
var skipBottomLeft = false
var skipBottomRight = false
private val squirclePath = Path()
private val cornerPath = Path()
private val mirrorMatrix = Matrix()
override fun isStateful(): Boolean = true
override fun setAlpha(alpha: Int) = Unit
override fun setColorFilter(colorFilter: ColorFilter?) = Unit
override fun getOpacity(): Int = PixelFormat.OPAQUE
override fun onBoundsChange(bounds: Rect) {
val left = bounds.left.toFloat()
val top = bounds.top.toFloat()
val right = bounds.right.toFloat()
val bottom = bounds.bottom.toFloat()
val centerX = (right + left) / 2f
val centerY = (bottom + top) / 2f
// (0..1), ios = 0.6f
val smooth = 0.6f
val r = cornerRadiusPx
// a, b, c -> https://telegra.ph/file/a65d7a87521e9c75e7579.png
val c = 0.2929f * r
val b = (1.5f * (2f * c * c).pow(x = 1.5f) / (c * r))
val a = r * (1 + smooth) - c - c - b
val ab = a + b
val cb = c + b
val abc = a + b + c
val abcc = a + b + c + c
cornerPath.apply {
rewind()
moveTo(left, top)
lineTo(left, top + abcc)
rCubicTo(0f, -a, 0f, -ab, c, -abc)
rCubicTo(c, -c, cb, -c, abc, -c)
lineTo(left, top)
}
squirclePath.apply {
rewind()
addRect(left, top, right, bottom, Path.Direction.CW)
mirrorParams.forEachIndexed { index, (scaleX, scaleY) ->
if (isCornerSkipped(index)) return@forEachIndexed
mirrorMatrix.setScale(scaleX, scaleY, centerX, centerY)
cornerPath.transform(mirrorMatrix)
op(cornerPath, Path.Op.DIFFERENCE)
}
}
}
override fun draw(canvas: Canvas) {
canvas.drawPath(squirclePath, paint)
}
private fun isCornerSkipped(cornerIndex: Int): Boolean = when (cornerIndex) {
0 -> skipTopLeft
1 -> skipTopRight
2 -> skipBottomRight
3 -> skipBottomLeft
else -> error("Invalid corner index")
}
companion object {
private val mirrorParams = arrayOf(1f to 1f, -1f to 1f, 1f to -1f, -1f to 1f)
}
}
@vadiole
Copy link
Author

vadiole commented Sep 10, 2021

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment