Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import android.graphics.ColorFilter
import android.graphics.Paint
import android.graphics.Path
import android.graphics.PixelFormat
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.drawable.Drawable
import androidx.annotation.RequiresApi
Expand Down Expand Up @@ -45,12 +46,8 @@ internal class InsetBoxShadowDrawable(
private val offsetY: Float,
private val blurRadius: Float,
private val spread: Float,
/*
* We assume borderRadius & borderInsets to be shared across multiple drawables
* therefore user should invalidate this drawable when changing either of them
*/
var borderInsets: BorderInsets? = null,
var borderRadius: BorderRadiusStyle? = null,
borderInsets: BorderInsets? = null,
borderRadius: BorderRadiusStyle? = null,
) : Drawable() {
private val shadowPaint =
Paint().apply {
Expand All @@ -61,6 +58,41 @@ internal class InsetBoxShadowDrawable(
}
}

private var needUpdatePath = true
private val paddingBoxRect = RectF()
private val paddingBoxRadii = FloatArray(8)
private var hasPaddingBoxRadii = false
private val paddingBoxPath = Path()
private val innerRect = RectF()
private val innerRadii = FloatArray(8)
private val outerRect = RectF()

/*
* We assume borderRadius & borderInsets to be shared across multiple drawables
* therefore user should assign them to this drawable again when changing either of them
*/
var borderInsets: BorderInsets? = borderInsets
set(value) {
field = value
needUpdatePath = true
}

var borderRadius: BorderRadiusStyle? = borderRadius
set(value) {
field = value
needUpdatePath = true
}

override fun onBoundsChange(bounds: Rect) {
super.onBoundsChange(bounds)
needUpdatePath = true
}

override fun onLayoutDirectionChanged(layoutDirection: Int): Boolean {
needUpdatePath = true
return true
}

override fun setAlpha(alpha: Int) {
shadowPaint.alpha = (((alpha / 255f) * (Color.alpha(shadowColor) / 255f)) * 255f).roundToInt()
invalidateSelf()
Expand All @@ -82,69 +114,80 @@ internal class InsetBoxShadowDrawable(
}

override fun draw(canvas: Canvas) {
updatePath()

canvas.save().let { saveCount ->
if (hasPaddingBoxRadii) {
canvas.clipPath(paddingBoxPath)
canvas.drawDoubleRoundRect(outerRect, ZERO_RADII, innerRect, innerRadii, shadowPaint)
} else {
canvas.clipRect(paddingBoxRect)
canvas.drawDoubleRoundRect(outerRect, ZERO_RADII, innerRect, ZERO_RADII, shadowPaint)
}

canvas.restoreToCount(saveCount)
}
}

private fun updatePath() {
if (!needUpdatePath) {
return
}
needUpdatePath = false

val computedBorderRadii = computeBorderRadii()
val computedBorderInsets = computeBorderInsets()

val paddingBoxRect =
RectF(
bounds.left + (computedBorderInsets?.left ?: 0f),
bounds.top + (computedBorderInsets?.top ?: 0f),
bounds.right - (computedBorderInsets?.right ?: 0f),
bounds.bottom - (computedBorderInsets?.bottom ?: 0f),
)
val paddingBoxRadii =
computedBorderRadii?.let {
floatArrayOf(
innerRadius(it.topLeft.horizontal, computedBorderInsets?.left),
innerRadius(it.topLeft.vertical, computedBorderInsets?.top),
innerRadius(it.topRight.horizontal, computedBorderInsets?.right),
innerRadius(it.topRight.vertical, computedBorderInsets?.top),
innerRadius(it.bottomRight.horizontal, computedBorderInsets?.right),
innerRadius(it.bottomRight.vertical, computedBorderInsets?.bottom),
innerRadius(it.bottomLeft.horizontal, computedBorderInsets?.left),
innerRadius(it.bottomLeft.vertical, computedBorderInsets?.bottom),
)
}
paddingBoxRect.set(
bounds.left + (computedBorderInsets?.left ?: 0f),
bounds.top + (computedBorderInsets?.top ?: 0f),
bounds.right - (computedBorderInsets?.right ?: 0f),
bounds.bottom - (computedBorderInsets?.bottom ?: 0f),
)
hasPaddingBoxRadii = computedBorderRadii != null
if (computedBorderRadii != null) {
paddingBoxRadii[0] =
innerRadius(computedBorderRadii.topLeft.horizontal, computedBorderInsets?.left)
paddingBoxRadii[1] =
innerRadius(computedBorderRadii.topLeft.vertical, computedBorderInsets?.top)
paddingBoxRadii[2] =
innerRadius(computedBorderRadii.topRight.horizontal, computedBorderInsets?.right)
paddingBoxRadii[3] =
innerRadius(computedBorderRadii.topRight.vertical, computedBorderInsets?.top)
paddingBoxRadii[4] =
innerRadius(computedBorderRadii.bottomRight.horizontal, computedBorderInsets?.right)
paddingBoxRadii[5] =
innerRadius(computedBorderRadii.bottomRight.vertical, computedBorderInsets?.bottom)
paddingBoxRadii[6] =
innerRadius(computedBorderRadii.bottomLeft.horizontal, computedBorderInsets?.left)
paddingBoxRadii[7] =
innerRadius(computedBorderRadii.bottomLeft.vertical, computedBorderInsets?.bottom)
}

val x = offsetX.dpToPx()
val y = offsetY.dpToPx()
val spreadExtent = spread.dpToPx()
val innerRect =
RectF(paddingBoxRect).apply {
if (2 * spreadExtent > paddingBoxRect.width()) {
setEmpty()
} else {
inset(spreadExtent, spreadExtent)
}
offset(x, y)
}
innerRect.set(paddingBoxRect)
if (2 * spreadExtent > paddingBoxRect.width()) {
innerRect.setEmpty()
} else {
innerRect.inset(spreadExtent, spreadExtent)
}
innerRect.offset(x, y)

// Graciously stolen from Blink
// https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/paint/box_painter_base.cc;l=338;drc=0a301506035e13015ea5c8dd39164d0d5954fa60
val blurExtent = FilterHelper.sigmaToRadius(blurRadius)
val outerRect =
RectF(innerRect).apply {
set(paddingBoxRect)
inset(-blurExtent, -blurExtent)
union(RectF(innerRect))
}

canvas.save().let { saveCount ->
if (paddingBoxRadii != null) {
canvas.clipPath(
Path().apply { addRoundRect(paddingBoxRect, paddingBoxRadii, Path.Direction.CW) },
)

val innerRadii =
paddingBoxRadii.map { adjustRadiusForSpread(it, -spreadExtent) }.toFloatArray()

canvas.drawDoubleRoundRect(outerRect, ZERO_RADII, innerRect, innerRadii, shadowPaint)
} else {
canvas.clipRect(paddingBoxRect)
canvas.drawDoubleRoundRect(outerRect, ZERO_RADII, innerRect, ZERO_RADII, shadowPaint)
outerRect.set(paddingBoxRect)
outerRect.inset(-blurExtent, -blurExtent)
outerRect.union(innerRect)

if (hasPaddingBoxRadii) {
paddingBoxPath.rewind()
paddingBoxPath.addRoundRect(paddingBoxRect, paddingBoxRadii, Path.Direction.CW)
for (index in innerRadii.indices) {
innerRadii[index] = adjustRadiusForSpread(paddingBoxRadii[index], -spreadExtent)
}

canvas.restoreToCount(saveCount)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import android.graphics.ColorFilter
import android.graphics.Paint
import android.graphics.Path
import android.graphics.PixelFormat
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.drawable.Drawable
import androidx.annotation.RequiresApi
Expand Down Expand Up @@ -42,11 +43,7 @@ internal class OutsetBoxShadowDrawable(
private val offsetY: Float,
private val blurRadius: Float,
private val spread: Float,
/*
* We assume borderRadius to be shared across multiple drawables
* therefore we should manually invalidate this drawable when changing it
*/
var borderRadius: BorderRadiusStyle? = null,
borderRadius: BorderRadiusStyle? = null,
) : Drawable() {
private val shadowPaint =
Paint().apply {
Expand All @@ -57,6 +54,34 @@ internal class OutsetBoxShadowDrawable(
}
}

private var needUpdatePath = true
private val shadowRect = RectF()
private var hasRoundedBorders = false
private val subpixelInsetBounds = RectF()
private val radii = FloatArray(8)
private val clipOutPath = Path()
private val shadowPath = Path()

/*
* We assume borderRadius to be shared across multiple drawables
* therefore we should assign it to this drawable again when changing it
*/
var borderRadius: BorderRadiusStyle? = borderRadius
set(value) {
field = value
needUpdatePath = true
}

override fun onBoundsChange(bounds: Rect) {
super.onBoundsChange(bounds)
needUpdatePath = true
}

override fun onLayoutDirectionChanged(layoutDirection: Int): Boolean {
needUpdatePath = true
return true
}

override fun setAlpha(alpha: Int) {
shadowPaint.alpha = (((alpha / 255f) * (Color.alpha(shadowColor) / 255f)) * 255f).roundToInt()
invalidateSelf()
Expand All @@ -78,6 +103,26 @@ internal class OutsetBoxShadowDrawable(
}

override fun draw(canvas: Canvas) {
updatePath()

canvas.save().let { saveCount ->
if (hasRoundedBorders) {
canvas.clipOutPath(clipOutPath)
canvas.drawPath(shadowPath, shadowPaint)
} else {
canvas.clipOutRect(bounds)
canvas.drawRect(shadowRect, shadowPaint)
}
canvas.restoreToCount(saveCount)
}
}

private fun updatePath() {
if (!needUpdatePath) {
return
}
needUpdatePath = false

val resolutionWidth = bounds.width().toFloat().pxToDp()
val resolutionHeight = bounds.height().toFloat().pxToDp()
val computedBorderRadii =
Expand All @@ -94,74 +139,35 @@ internal class OutsetBoxShadowDrawable(
}

val spreadExtent = spread.dpToPx()
val shadowRect =
RectF(bounds).apply {
inset(-spreadExtent, -spreadExtent)
offset(offsetX.dpToPx(), offsetY.dpToPx())
}
shadowRect.set(bounds)
shadowRect.inset(-spreadExtent, -spreadExtent)
shadowRect.offset(offsetX.dpToPx(), offsetY.dpToPx())

canvas.save().let { saveCount ->
if (computedBorderRadii?.hasRoundedBorders() == true) {
drawShadowRoundRect(canvas, shadowRect, spreadExtent, computedBorderRadii)
} else {
drawShadowRect(canvas, shadowRect)
}
canvas.restoreToCount(saveCount)
hasRoundedBorders = computedBorderRadii?.hasRoundedBorders() == true
if (computedBorderRadii == null || !hasRoundedBorders) {
return
}
}

private fun drawShadowRoundRect(
canvas: Canvas,
shadowRect: RectF,
spreadExtent: Float,
computedBorderRadii: ComputedBorderRadius,
) {
// We inset the clip slightly, to avoid Skia artifacts with antialiased
// clipping. This inset is only visible when no background is present.
// https://neugierig.org/software/chromium/notes/2010/07/clipping.html
val subpixelInsetBounds = RectF(bounds).apply { inset(0.4f, 0.4f) }
canvas.clipOutPath(
Path().apply {
addRoundRect(
subpixelInsetBounds,
floatArrayOf(
computedBorderRadii.topLeft.horizontal,
computedBorderRadii.topLeft.vertical,
computedBorderRadii.topRight.horizontal,
computedBorderRadii.topRight.vertical,
computedBorderRadii.bottomRight.horizontal,
computedBorderRadii.bottomRight.vertical,
computedBorderRadii.bottomLeft.horizontal,
computedBorderRadii.bottomLeft.vertical,
),
Path.Direction.CW,
)
},
)

canvas.drawPath(
Path().apply {
addRoundRect(
shadowRect,
floatArrayOf(
adjustRadiusForSpread(computedBorderRadii.topLeft.horizontal, spreadExtent),
adjustRadiusForSpread(computedBorderRadii.topLeft.vertical, spreadExtent),
adjustRadiusForSpread(computedBorderRadii.topRight.horizontal, spreadExtent),
adjustRadiusForSpread(computedBorderRadii.topRight.vertical, spreadExtent),
adjustRadiusForSpread(computedBorderRadii.bottomRight.horizontal, spreadExtent),
adjustRadiusForSpread(computedBorderRadii.bottomRight.vertical, spreadExtent),
adjustRadiusForSpread(computedBorderRadii.bottomLeft.horizontal, spreadExtent),
adjustRadiusForSpread(computedBorderRadii.bottomLeft.vertical, spreadExtent),
),
Path.Direction.CW,
)
},
shadowPaint,
)
}
subpixelInsetBounds.set(bounds)
subpixelInsetBounds.inset(0.4f, 0.4f)
radii[0] = computedBorderRadii.topLeft.horizontal
radii[1] = computedBorderRadii.topLeft.vertical
radii[2] = computedBorderRadii.topRight.horizontal
radii[3] = computedBorderRadii.topRight.vertical
radii[4] = computedBorderRadii.bottomRight.horizontal
radii[5] = computedBorderRadii.bottomRight.vertical
radii[6] = computedBorderRadii.bottomLeft.horizontal
radii[7] = computedBorderRadii.bottomLeft.vertical
clipOutPath.rewind()
clipOutPath.addRoundRect(subpixelInsetBounds, radii, Path.Direction.CW)

private fun drawShadowRect(canvas: Canvas, shadowRect: RectF) {
canvas.clipOutRect(bounds)
canvas.drawRect(shadowRect, shadowPaint)
for (index in radii.indices) {
radii[index] = adjustRadiusForSpread(radii[index], spreadExtent)
}
shadowPath.rewind()
shadowPath.addRoundRect(shadowRect, radii, Path.Direction.CW)
}
}
Loading
Loading