Skip to content

Instantly share code, notes, and snippets.

@ElianFabian
Last active September 22, 2024 20:32
Show Gist options
  • Save ElianFabian/bd6d2cb1fe87ad337ebf5c30c68830a4 to your computer and use it in GitHub Desktop.
Save ElianFabian/bd6d2cb1fe87ad337ebf5c30c68830a4 to your computer and use it in GitHub Desktop.
A CardView that allows setting the corner radius as a percentage. It also fix the issue of the corner radius exceeding the 50% limit on API 22 and below which causes the card to look weird.
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="extForceCardCornerRadiusCompatClamp" format="boolean" />
<attr name="cardCornerRadius" format="dimension" />
<attr name="extCardCornerRadiusPercentage" format="float" />
<attr name="extCardCornerRadiusTopLeft" format="dimension" />
<attr name="extCardCornerRadiusTopRight" format="dimension" />
<attr name="extCardCornerRadiusBottomRight" format="dimension" />
<attr name="extCardCornerRadiusBottomLeft" format="dimension" />
<attr name="extCardCornerRadiusTopLeftPercentage" format="float" />
<attr name="extCardCornerRadiusTopRightPercentage" format="float" />
<attr name="extCardCornerRadiusBottomRightPercentage" format="float" />
<attr name="extCardCornerRadiusBottomLeftPercentage" format="float" />
<attr name="extCardCornerFamily" format="enum">
<enum name="rounded" value="0" />
<enum name="cut" value="1" />
</attr>
<attr name="extCardTopLeftCornerFamily" format="enum">
<enum name="rounded" value="0" />
<enum name="cut" value="1" />
</attr>
<attr name="extCardTopRightCornerFamily" format="enum">
<enum name="rounded" value="0" />
<enum name="cut" value="1" />
</attr>
<attr name="extCardBottomRightCornerFamily" format="enum">
<enum name="rounded" value="0" />
<enum name="cut" value="1" />
</attr>
<attr name="extCardBottomLeftCornerFamily" format="enum">
<enum name="rounded" value="0" />
<enum name="cut" value="1" />
</attr>
<attr name="extCornerRadius" format="dimension" />
<attr name="extCornerRadiusPercentage" format="float" />
<attr name="extCornerRadiusTopLeft" format="dimension" />
<attr name="extCornerRadiusTopRight" format="dimension" />
<attr name="extCornerRadiusBottomRight" format="dimension" />
<attr name="extCornerRadiusBottomLeft" format="dimension" />
<attr name="extCornerRadiusTopLeftPercentage" format="float" />
<attr name="extCornerRadiusTopRightPercentage" format="float" />
<attr name="extCornerRadiusBottomRightPercentage" format="float" />
<attr name="extCornerRadiusBottomLeftPercentage" format="float" />
<attr name="extCornerFamily" format="enum">
<enum name="rounded" value="0" />
<enum name="cut" value="1" />
</attr>
<attr name="extTopLeftCornerFamily" format="enum">
<enum name="rounded" value="0" />
<enum name="cut" value="1" />
</attr>
<attr name="extTopRightCornerFamily" format="enum">
<enum name="rounded" value="0" />
<enum name="cut" value="1" />
</attr>
<attr name="extBottomRightCornerFamily" format="enum">
<enum name="rounded" value="0" />
<enum name="cut" value="1" />
</attr>
<attr name="extBottomLeftCornerFamily" format="enum">
<enum name="rounded" value="0" />
<enum name="cut" value="1" />
</attr>
<declare-styleable name="ExtendedCardView">
<attr name="cardCornerRadius" />
<attr name="extCardCornerRadiusPercentage" />
</declare-styleable>
<declare-styleable name="ExtendedMaterialCardView">
<attr name="extForceCardCornerRadiusCompatClamp" />
<attr name="cardCornerRadius" />
<attr name="extCardCornerRadiusPercentage" />
<attr name="extCardCornerRadiusTopLeft" />
<attr name="extCardCornerRadiusTopRight" />
<attr name="extCardCornerRadiusBottomRight" />
<attr name="extCardCornerRadiusBottomLeft" />
<attr name="extCardCornerRadiusTopLeftPercentage" />
<attr name="extCardCornerRadiusTopRightPercentage" />
<attr name="extCardCornerRadiusBottomRightPercentage" />
<attr name="extCardCornerRadiusBottomLeftPercentage" />
<attr name="extCardCornerFamily" />
<attr name="extCardTopLeftCornerFamily" />
<attr name="extCardTopRightCornerFamily" />
<attr name="extCardBottomRightCornerFamily" />
<attr name="extCardBottomLeftCornerFamily" />
</declare-styleable>
<declare-styleable name="ExtendedShapeableImageView">
<attr name="extCornerRadius" />
<attr name="extCornerRadiusPercentage" />
<attr name="extCornerRadiusTopLeft" />
<attr name="extCornerRadiusTopRight" />
<attr name="extCornerRadiusBottomRight" />
<attr name="extCornerRadiusBottomLeft" />
<attr name="extCornerRadiusTopLeftPercentage" />
<attr name="extCornerRadiusTopRightPercentage" />
<attr name="extCornerRadiusBottomRightPercentage" />
<attr name="extCornerRadiusBottomLeftPercentage" />
<attr name="extCornerFamily" />
<attr name="extTopLeftCornerFamily" />
<attr name="extTopRightCornerFamily" />
<attr name="extBottomRightCornerFamily" />
<attr name="extBottomLeftCornerFamily" />
</declare-styleable>
</resources>
package yourpackage
import android.content.Context
import android.os.Build
import android.util.AttributeSet
import android.view.View
import androidx.cardview.widget.CardView
import yourpackage.R
/**
* A CardView that allows setting the corner radius as a percentage.
* It also fix the issue of the corner radius exceeding the 50% limit on API 22 and below which
* causes the card to look weird.
*/
class ExtendedCardView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
) : CardView(context, attrs, defStyleAttr) {
var cornerSize = AbsoluteCornerSize(0F)
set(value) {
field = value
setRadiusInternal(value.toPixels(this))
}
init {
context.theme.obtainStyledAttributes(
attrs,
R.styleable.ExtendedCardView,
0,
0,
).apply {
try {
val percentage = getFloat(R.styleable.ExtendedCardView_extCardCornerRadiusPercentage, -1F)
if (percentage >= 0) {
cornerSize = RelativeCornerSize(percentage)
}
val cornerRadius = getDimension(R.styleable.ExtendedCardView_cardCornerRadius, -1F)
if (cornerRadius >= 0) {
cornerSize = AbsoluteCornerSize(cornerRadius)
}
}
finally {
recycle()
}
}
}
override fun setRadius(radius: Float) {
val minSize = minOf(width, height).toFloat()
val clampRadius = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1) {
radius.coerceAtMost(minSize / 2F)
}
else radius
cornerSize = AbsoluteCornerSize(clampRadius)
super.setRadius(clampRadius)
}
private fun setRadiusInternal(radius: Float) {
val minSize = minOf(width, height).toFloat()
val clampRadius = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1) {
radius.coerceAtMost(minSize / 2F)
}
else radius
super.setRadius(clampRadius)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
setRadiusInternal(cornerSize.toPixels(this))
}
@JvmInline
value class CornerRadius private constructor(
private val packedValue: Long,
) {
val value: Float get() = Float.fromBits((packedValue and 0xFFFFFFFF).toInt())
val type: Int get() = (packedValue shr 32).toInt()
constructor(value: Float, type: Int) : this((value.toRawBits().toLong() and 0xFFFFFFFF) or (type.toLong() shl 32))
val isRelative: Boolean get() = type == RelativeType
val isAbsolute: Boolean get() = type == AbsoluteType
override fun toString(): String {
return when (type) {
AbsoluteType -> "CornerRadius(value=$value)"
RelativeType -> "CornerRadius(value=$value%)"
else -> throw IllegalArgumentException("Unknown corner radius type: $type")
}
}
companion object {
const val AbsoluteType = 0
const val RelativeType = 1
}
}
companion object {
@Suppress("FunctionName")
fun AbsoluteCornerSize(value: Float): CornerRadius = CornerRadius(value, CornerRadius.AbsoluteType)
@Suppress("FunctionName")
fun RelativeCornerSize(value: Float): CornerRadius = CornerRadius(value, CornerRadius.RelativeType)
}
private fun CornerRadius.toPixels(view: View): Float {
return when (type) {
CornerRadius.AbsoluteType -> value
CornerRadius.RelativeType -> {
val smallerDimension = minOf(view.width, view.height)
return value * smallerDimension
}
else -> throw IllegalArgumentException("Unknown corner radius type: $type")
}
}
}
package yourpackage
import android.content.Context
import android.graphics.RectF
import android.os.Build
import android.util.AttributeSet
import yourpackage.R
import com.google.android.material.card.MaterialCardView
import com.google.android.material.shape.AbsoluteCornerSize
import com.google.android.material.shape.CornerSize
import com.google.android.material.shape.RelativeCornerSize
import com.google.android.material.shape.ShapeAppearanceModel
/**
* A MaterialCardView that allows setting the corner radius as a percentage.
* It also fix the issue of the corner radius exceeding the 50% limit on API 22 and below which
* causes the card to look weird.
*/
class ExtendedMaterialCardView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
) : MaterialCardView(context, attrs, defStyleAttr) {
/**
* Prevents the corner radius from exceeding the 50% limit on API 22 and below which causes the
* card to look weird.
*
* For custom corner sizes you might want to disable this, but, remember check how it looks on API 22
* and below.
*/
var forceCornerRadiusCompatClamp = true
init {
context.theme.obtainStyledAttributes(
attrs,
R.styleable.ExtendedMaterialCardView,
0,
0,
).apply {
try {
forceCornerRadiusCompatClamp = getBoolean(R.styleable.ExtendedMaterialCardView_extForceCardCornerRadiusCompatClamp, true)
val cardCornerFamily = getInt(R.styleable.ExtendedMaterialCardView_extCardCornerFamily, -1)
val cardTopLeftCornerFamily = getInt(R.styleable.ExtendedMaterialCardView_extCardTopLeftCornerFamily, cardCornerFamily)
val cardTopRightCornerFamily = getInt(R.styleable.ExtendedMaterialCardView_extCardTopRightCornerFamily, cardCornerFamily)
val cardBottomRightCornerFamily = getInt(R.styleable.ExtendedMaterialCardView_extCardBottomRightCornerFamily, cardCornerFamily)
val cardBottomLeftCornerFamily = getInt(R.styleable.ExtendedMaterialCardView_extCardBottomLeftCornerFamily, cardCornerFamily)
val cardCornerRadius = getDimension(R.styleable.ExtendedMaterialCardView_cardCornerRadius, -1F)
val cardCornerRadiusPercentage = getFloat(R.styleable.ExtendedMaterialCardView_extCardCornerRadiusPercentage, -1F)
val cardTopLeftCornerRadiusPercentage = getFloat(R.styleable.ExtendedMaterialCardView_extCardCornerRadiusTopLeftPercentage, -1F)
val cardTopLeftCornerRadius = getDimension(R.styleable.ExtendedMaterialCardView_extCardCornerRadiusTopLeft, -1F)
val cardTopRightCornerRadiusPercentage = getFloat(R.styleable.ExtendedMaterialCardView_extCardCornerRadiusTopRightPercentage, -1F)
val cardTopRightCornerRadius = getDimension(R.styleable.ExtendedMaterialCardView_extCardCornerRadiusTopRight, -1F)
val cardBottomRightCornerRadiusPercentage = getFloat(R.styleable.ExtendedMaterialCardView_extCardCornerRadiusBottomRightPercentage, -1F)
val cardBottomRightCornerRadius = getDimension(R.styleable.ExtendedMaterialCardView_extCardCornerRadiusBottomRight, -1F)
val cardBottomLeftCornerRadiusPercentage = getFloat(R.styleable.ExtendedMaterialCardView_extCardCornerRadiusBottomLeftPercentage, -1F)
val cardBottomLeftCornerRadius = getDimension(R.styleable.ExtendedMaterialCardView_extCardCornerRadiusBottomLeft, -1F)
val shapeAppearanceModel = ShapeAppearanceModel.builder().apply {
val topLeftCornerSize: CornerSize = when {
cardTopLeftCornerRadiusPercentage >= 0 -> RelativeCornerSize(cardTopLeftCornerRadiusPercentage)
cardTopLeftCornerRadius >= 0 -> AbsoluteCornerSize(cardTopLeftCornerRadius)
cardCornerRadiusPercentage >= 0 -> RelativeCornerSize(cardCornerRadiusPercentage)
cardCornerRadius >= 0 -> AbsoluteCornerSize(cardCornerRadius)
else -> AbsoluteCornerSize(0F)
}
setTopLeftCorner(cardTopLeftCornerFamily, topLeftCornerSize)
val topRightCornerSize: CornerSize = when {
cardTopRightCornerRadiusPercentage >= 0 -> RelativeCornerSize(cardTopRightCornerRadiusPercentage)
cardTopRightCornerRadius >= 0 -> AbsoluteCornerSize(cardTopRightCornerRadius)
cardCornerRadiusPercentage >= 0 -> RelativeCornerSize(cardCornerRadiusPercentage)
cardCornerRadius >= 0 -> AbsoluteCornerSize(cardCornerRadius)
else -> AbsoluteCornerSize(0F)
}
setTopRightCorner(cardTopRightCornerFamily, topRightCornerSize)
val bottomRightCornerSize: CornerSize = when {
cardBottomRightCornerRadiusPercentage >= 0 -> RelativeCornerSize(cardBottomRightCornerRadiusPercentage)
cardBottomRightCornerRadius >= 0 -> AbsoluteCornerSize(cardBottomRightCornerRadius)
cardCornerRadiusPercentage >= 0 -> RelativeCornerSize(cardCornerRadiusPercentage)
cardCornerRadius >= 0 -> AbsoluteCornerSize(cardCornerRadius)
else -> AbsoluteCornerSize(0F)
}
setBottomRightCorner(cardBottomRightCornerFamily, bottomRightCornerSize)
val bottomLeftCornerSize: CornerSize = when {
cardBottomLeftCornerRadiusPercentage >= 0 -> RelativeCornerSize(cardBottomLeftCornerRadiusPercentage)
cardBottomLeftCornerRadius >= 0 -> AbsoluteCornerSize(cardBottomLeftCornerRadius)
cardCornerRadiusPercentage >= 0 -> RelativeCornerSize(cardCornerRadiusPercentage)
cardCornerRadius >= 0 -> AbsoluteCornerSize(cardCornerRadius)
else -> AbsoluteCornerSize(0F)
}
setBottomLeftCorner(cardBottomLeftCornerFamily, bottomLeftCornerSize)
}.build()
setShapeAppearanceModel(shapeAppearanceModel)
}
finally {
recycle()
}
}
}
override fun setRadius(radius: Float) {
val minSize = minOf(width, height).toFloat()
val clampRadius = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1 && forceCornerRadiusCompatClamp) {
radius.coerceIn(0F, minSize / 2F)
}
else radius
super.setRadius(clampRadius)
invalidate()
}
override fun setShapeAppearanceModel(shapeAppearanceModel: ShapeAppearanceModel) {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1 || !forceCornerRadiusCompatClamp) {
super.setShapeAppearanceModel(shapeAppearanceModel)
return
}
val clampedShapeAppearanceModel = shapeAppearanceModel.run {
toBuilder()
.setTopLeftCornerSize(topLeftCornerSize.clamped())
.setTopRightCornerSize(topRightCornerSize.clamped())
.setBottomRightCornerSize(bottomRightCornerSize.clamped())
.setBottomLeftCornerSize(bottomLeftCornerSize.clamped())
.build()
}
super.setShapeAppearanceModel(clampedShapeAppearanceModel)
}
/**
* A [CornerSize] that takes a desired absolute corner size and clamps the value to be no
* larger than half the length of the shortest edge (fully rounded/cut).
*/
private class ClampedCornerSize(
private val cornerSize: CornerSize,
) : CornerSize {
override fun getCornerSize(bounds: RectF): Float {
val maxCornerSize = minOf(bounds.width() / 2F, bounds.height() / 2F)
return cornerSize.getCornerSize(bounds).coerceIn(0F, maxCornerSize)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ClampedCornerSize
return cornerSize == other.cornerSize
}
override fun hashCode(): Int = cornerSize.hashCode()
}
private fun CornerSize.clamped(): ClampedCornerSize = ClampedCornerSize(this)
}
package yourpackage
import android.content.Context
import android.util.AttributeSet
import yourpackage.R
import com.google.android.material.imageview.ShapeableImageView
import com.google.android.material.shape.AbsoluteCornerSize
import com.google.android.material.shape.CornerSize
import com.google.android.material.shape.RelativeCornerSize
import com.google.android.material.shape.ShapeAppearanceModel
/**
* A ShapeableImageView that allows setting the corner radius from XML with greater flexibility.
*
* This class is similar to [ExtendedMaterialCardView] but for [ShapeableImageView]
* but it doesn't need to handle the corner radius exceeding the 50% limit on API 22 and below.
*/
class ExtendedShapeableImageView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
) : ShapeableImageView(context, attrs, defStyleAttr) {
init {
context.theme.obtainStyledAttributes(
attrs,
R.styleable.ExtendedShapeableImageView,
0,
0,
).apply {
try {
val cornerFamily = getInt(R.styleable.ExtendedShapeableImageView_extCornerFamily, -1)
val topLeftCornerFamily = getInt(R.styleable.ExtendedShapeableImageView_extTopLeftCornerFamily, cornerFamily)
val topRightCornerFamily = getInt(R.styleable.ExtendedShapeableImageView_extTopRightCornerFamily, cornerFamily)
val bottomRightCornerFamily = getInt(R.styleable.ExtendedShapeableImageView_extBottomRightCornerFamily, cornerFamily)
val bottomLeftCornerFamily = getInt(R.styleable.ExtendedShapeableImageView_extBottomLeftCornerFamily, cornerFamily)
val cornerRadius = getDimension(R.styleable.ExtendedShapeableImageView_extCornerRadius, -1F)
val cornerRadiusPercentage = getFloat(R.styleable.ExtendedShapeableImageView_extCornerRadiusPercentage, -1F)
val topLeftCornerRadiusPercentage = getFloat(R.styleable.ExtendedShapeableImageView_extCornerRadiusTopLeftPercentage, -1F)
val topLeftCornerRadius = getDimension(R.styleable.ExtendedShapeableImageView_extCornerRadiusTopLeft, -1F)
val topRightCornerRadiusPercentage = getFloat(R.styleable.ExtendedShapeableImageView_extCornerRadiusTopRightPercentage, -1F)
val topRightCornerRadius = getDimension(R.styleable.ExtendedShapeableImageView_extCornerRadiusTopRight, -1F)
val bottomRightCornerRadiusPercentage = getFloat(R.styleable.ExtendedShapeableImageView_extCornerRadiusBottomRightPercentage, -1F)
val bottomRightCornerRadius = getDimension(R.styleable.ExtendedShapeableImageView_extCornerRadiusBottomRight, -1F)
val bottomLeftCornerRadiusPercentage = getFloat(R.styleable.ExtendedShapeableImageView_extCornerRadiusBottomLeftPercentage, -1F)
val bottomLeftCornerRadius = getDimension(R.styleable.ExtendedShapeableImageView_extCornerRadiusBottomLeft, -1F)
val shapeAppearanceModel = ShapeAppearanceModel.builder().apply {
val topLeftCornerSize: CornerSize = when {
topLeftCornerRadiusPercentage >= 0 -> RelativeCornerSize(topLeftCornerRadiusPercentage)
topLeftCornerRadius >= 0 -> AbsoluteCornerSize(topLeftCornerRadius)
cornerRadiusPercentage >= 0 -> RelativeCornerSize(cornerRadiusPercentage)
cornerRadius >= 0 -> AbsoluteCornerSize(cornerRadius)
else -> AbsoluteCornerSize(0F)
}
setTopLeftCorner(topLeftCornerFamily, topLeftCornerSize)
val topRightCornerSize: CornerSize = when {
topRightCornerRadiusPercentage >= 0 -> RelativeCornerSize(topRightCornerRadiusPercentage)
topRightCornerRadius >= 0 -> AbsoluteCornerSize(topRightCornerRadius)
cornerRadiusPercentage >= 0 -> RelativeCornerSize(cornerRadiusPercentage)
cornerRadius >= 0 -> AbsoluteCornerSize(cornerRadius)
else -> AbsoluteCornerSize(0F)
}
setTopRightCorner(topRightCornerFamily, topRightCornerSize)
val bottomRightCornerSize: CornerSize = when {
bottomRightCornerRadiusPercentage >= 0 -> RelativeCornerSize(bottomRightCornerRadiusPercentage)
bottomRightCornerRadius >= 0 -> AbsoluteCornerSize(bottomRightCornerRadius)
cornerRadiusPercentage >= 0 -> RelativeCornerSize(cornerRadiusPercentage)
cornerRadius >= 0 -> AbsoluteCornerSize(cornerRadius)
else -> AbsoluteCornerSize(0F)
}
setBottomRightCorner(bottomRightCornerFamily, bottomRightCornerSize)
val bottomLeftCornerSize: CornerSize = when {
bottomLeftCornerRadiusPercentage >= 0 -> RelativeCornerSize(bottomLeftCornerRadiusPercentage)
bottomLeftCornerRadius >= 0 -> AbsoluteCornerSize(bottomLeftCornerRadius)
cornerRadiusPercentage >= 0 -> RelativeCornerSize(cornerRadiusPercentage)
cornerRadius >= 0 -> AbsoluteCornerSize(cornerRadius)
else -> AbsoluteCornerSize(0F)
}
setBottomLeftCorner(bottomLeftCornerFamily, bottomLeftCornerSize)
}.build()
setShapeAppearanceModel(shapeAppearanceModel)
}
finally {
recycle()
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment