Use GPU compositing for highlight and shadow.
This is a compromise. The overall performance is improved. But non-lazy scrollable layouts can be surprisingly laggy on device with poor GPU performance.
This commit is contained in:
1 parent
455f81b221
commit
86ab3fdc0e
10 files changed
+198
-244
No files matched your search
@@ -1,34 +1,26 @@
|
||||
package com.kyant.backdrop.highlight
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapShader
|
||||
import android.graphics.BlendMode
|
||||
import android.graphics.BlurMaskFilter
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Path
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.RectF
|
||||
import android.graphics.RuntimeShader
|
||||
import android.graphics.Shader
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Outline
|
||||
import androidx.compose.ui.graphics.asAndroidPath
|
||||
import androidx.compose.ui.graphics.asComposePaint
|
||||
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.isUnspecified
|
||||
import androidx.compose.ui.graphics.layer.CompositingStrategy
|
||||
import androidx.compose.ui.graphics.layer.GraphicsLayer
|
||||
import androidx.compose.ui.graphics.layer.drawLayer
|
||||
import androidx.compose.ui.graphics.nativeCanvas
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.node.DrawModifierNode
|
||||
import androidx.compose.ui.node.ModifierNodeElement
|
||||
import androidx.compose.ui.node.ObserverModifierNode
|
||||
import androidx.compose.ui.node.invalidateDraw
|
||||
import androidx.compose.ui.node.observeReads
|
||||
import androidx.compose.ui.node.requireGraphicsContext
|
||||
import androidx.compose.ui.platform.InspectorInfo
|
||||
import androidx.compose.ui.util.fastCoerceAtMost
|
||||
import com.kyant.backdrop.RuntimeShaderCacheScope
|
||||
@@ -47,7 +39,7 @@ internal class HighlightElement(
|
||||
override fun update(node: HighlightNode) {
|
||||
node.shapeProvider = shapeProvider
|
||||
node.highlight = highlight
|
||||
node.invalidateDrawCache()
|
||||
node.invalidateDraw()
|
||||
}
|
||||
|
||||
override fun InspectorInfo.inspectableProperties() {
|
||||
@@ -76,19 +68,17 @@ internal class HighlightElement(
|
||||
internal class HighlightNode(
|
||||
var shapeProvider: ShapeProvider,
|
||||
var highlight: () -> Highlight?
|
||||
) : DrawModifierNode, ObserverModifierNode, Modifier.Node() {
|
||||
) : DrawModifierNode, Modifier.Node() {
|
||||
|
||||
override val shouldAutoInvalidate: Boolean = false
|
||||
|
||||
private var bitmap: Bitmap? = null
|
||||
private var canvas: Canvas? = null
|
||||
private var bitmapShader: BitmapShader? = null
|
||||
private var highlightLayer: GraphicsLayer? = null
|
||||
private var maskLayer: GraphicsLayer? = null
|
||||
|
||||
private val paint =
|
||||
Paint().apply {
|
||||
style = Paint.Style.STROKE
|
||||
}
|
||||
private val shaderPaint = Paint()
|
||||
private var prevSize = Size.Unspecified
|
||||
private var clipPath: Path? = null
|
||||
|
||||
private val runtimeShaderCacheScope = object : RuntimeShaderCacheScope {
|
||||
@@ -101,66 +91,81 @@ internal class HighlightNode(
|
||||
}
|
||||
}
|
||||
|
||||
private var isCacheValid = false
|
||||
private var _highlight: Highlight? = null
|
||||
private val cacheDrawBlock: DrawScope.() -> Unit = cacheDrawBlock@{
|
||||
val highlight = highlight().also { _highlight = it }
|
||||
val size = size
|
||||
if (highlight == null ||
|
||||
highlight.width.value <= 0f || highlight.color.isUnspecified || highlight.alpha == 0f ||
|
||||
size.width < 0.5f || size.height < 0.5f
|
||||
) {
|
||||
_highlight = null
|
||||
return@cacheDrawBlock
|
||||
private var prevStyle: HighlightStyle? = null
|
||||
|
||||
override fun ContentDrawScope.draw() {
|
||||
val highlight = highlight()
|
||||
if (highlight == null || highlight.width.value <= 0f || highlight.color.isUnspecified) {
|
||||
return drawContent()
|
||||
}
|
||||
|
||||
val color =
|
||||
if (highlight.alpha == 1f) {
|
||||
highlight.color
|
||||
} else {
|
||||
highlight.color.copy(alpha = highlight.color.alpha * highlight.alpha)
|
||||
drawContent()
|
||||
|
||||
val highlightLayer = highlightLayer
|
||||
val maskLayer = maskLayer
|
||||
if (highlightLayer != null && maskLayer != null) {
|
||||
val size = size
|
||||
val outline = shapeProvider.shape.createOutline(size, layoutDirection, this)
|
||||
|
||||
configurePaint(highlight)
|
||||
|
||||
val style = highlight.style()
|
||||
if (prevStyle != style) {
|
||||
highlightLayer.renderEffect =
|
||||
with(highlight.style()) { runtimeShaderCacheScope.createRenderEffect(size) }
|
||||
prevStyle = style
|
||||
}
|
||||
paint.color = color.toArgb()
|
||||
val strokeWidth = ceil(highlight.width.toPx().fastCoerceAtMost(size.minDimension / 2f))
|
||||
paint.strokeWidth = strokeWidth * 2f
|
||||
highlightLayer.record { drawHighlight(outline) }
|
||||
|
||||
maskLayer.alpha = highlight.alpha
|
||||
maskLayer.blendMode = highlight.blendMode
|
||||
maskLayer.record { drawLayer(highlightLayer) }
|
||||
drawLayer(maskLayer)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttach() {
|
||||
val graphicsContext = requireGraphicsContext()
|
||||
highlightLayer = graphicsContext.createGraphicsLayer()
|
||||
maskLayer =
|
||||
graphicsContext.createGraphicsLayer().apply {
|
||||
compositingStrategy = CompositingStrategy.Offscreen
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
val graphicsContext = requireGraphicsContext()
|
||||
highlightLayer?.let { layer ->
|
||||
graphicsContext.releaseGraphicsLayer(layer)
|
||||
highlightLayer = null
|
||||
}
|
||||
maskLayer?.let { layer ->
|
||||
graphicsContext.releaseGraphicsLayer(layer)
|
||||
maskLayer = null
|
||||
}
|
||||
clipPath = null
|
||||
prevStyle = null
|
||||
}
|
||||
|
||||
private fun DrawScope.configurePaint(highlight: Highlight) {
|
||||
paint.color = highlight.color.toArgb()
|
||||
paint.strokeWidth =
|
||||
ceil(highlight.width.toPx().fastCoerceAtMost(size.minDimension / 2f)) * 2f
|
||||
val blurRadius = highlight.blurRadius.toPx()
|
||||
if (blurRadius > 0f) {
|
||||
paint.maskFilter = BlurMaskFilter(blurRadius, BlurMaskFilter.Blur.NORMAL)
|
||||
}
|
||||
shaderPaint.asComposePaint().blendMode = highlight.blendMode
|
||||
}
|
||||
|
||||
if (prevSize != size) {
|
||||
bitmap = null
|
||||
canvas = null
|
||||
prevSize = size
|
||||
}
|
||||
|
||||
@Suppress("UseKtx")
|
||||
val bitmap =
|
||||
bitmap ?: Bitmap.createBitmap(
|
||||
ceil(size.width).toInt(),
|
||||
ceil(size.height).toInt(),
|
||||
Bitmap.Config.ARGB_8888
|
||||
).also {
|
||||
bitmap = it
|
||||
bitmapShader = BitmapShader(it, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
|
||||
}
|
||||
val canvas =
|
||||
canvas?.apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
drawColor(Color.TRANSPARENT, BlendMode.CLEAR)
|
||||
} else {
|
||||
drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
|
||||
}
|
||||
} ?: Canvas(bitmap).also { canvas = it }
|
||||
private fun DrawScope.drawHighlight(outline: Outline) {
|
||||
val canvas = drawContext.canvas.nativeCanvas
|
||||
|
||||
canvas.save()
|
||||
|
||||
when (val outline = shapeProvider.shape.createOutline(size, layoutDirection, this)) {
|
||||
when (outline) {
|
||||
is Outline.Rectangle -> {
|
||||
val rect = RectF(0f, 0f, size.width, size.height)
|
||||
canvas.clipRect(rect)
|
||||
canvas.drawRect(rect, paint)
|
||||
canvas.clipRect(0f, 0f, size.width, size.height)
|
||||
canvas.drawRect(0f, 0f, size.width, size.height, paint)
|
||||
}
|
||||
|
||||
is Outline.Rounded -> {
|
||||
@@ -170,12 +175,12 @@ internal class HighlightNode(
|
||||
canvas.clipPath(path)
|
||||
canvas.drawPath(path, paint)
|
||||
} else {
|
||||
val rect = with(outline.roundRect) { RectF(left, top, right, bottom) }
|
||||
val rr = outline.roundRect
|
||||
val radius = outline.roundRect.topLeftCornerRadius.x
|
||||
val clipPath = clipPath?.apply { rewind() } ?: Path().also { clipPath = it }
|
||||
clipPath.addRoundRect(rect, radius, radius, Path.Direction.CW)
|
||||
clipPath.addRoundRect(rr.left, rr.top, rr.right, rr.bottom, radius, radius, Path.Direction.CW)
|
||||
canvas.clipPath(clipPath)
|
||||
canvas.drawRoundRect(rect, radius, radius, paint)
|
||||
canvas.drawRoundRect(rr.left, rr.top, rr.right, rr.bottom, radius, radius, paint)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,38 +193,4 @@ internal class HighlightNode(
|
||||
|
||||
canvas.restore()
|
||||
}
|
||||
|
||||
override fun ContentDrawScope.draw() {
|
||||
if (!isCacheValid) {
|
||||
observeReads { cacheDrawBlock() }
|
||||
isCacheValid = true
|
||||
}
|
||||
|
||||
drawContent()
|
||||
|
||||
val highlight = _highlight
|
||||
val bitmap = bitmap
|
||||
val bitmapShader = bitmapShader
|
||||
if (highlight != null && bitmap != null && bitmapShader != null) {
|
||||
val maskShader =
|
||||
with(highlight.style()) { runtimeShaderCacheScope.createShader(size, bitmapShader) }
|
||||
shaderPaint.shader = maskShader
|
||||
|
||||
val canvas = drawContext.canvas.nativeCanvas
|
||||
if (maskShader != null) {
|
||||
canvas.drawRect(0f, 0f, size.width, size.height, shaderPaint)
|
||||
} else {
|
||||
canvas.drawBitmap(bitmap, 0f, 0f, shaderPaint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onObservedReadsChanged() {
|
||||
invalidateDrawCache()
|
||||
}
|
||||
|
||||
fun invalidateDrawCache() {
|
||||
isCacheValid = false
|
||||
invalidateDraw()
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
package com.kyant.backdrop.highlight
|
||||
|
||||
import android.graphics.BitmapShader
|
||||
import android.graphics.Shader
|
||||
import android.os.Build
|
||||
import androidx.annotation.FloatRange
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.RenderEffect
|
||||
import androidx.compose.ui.graphics.asComposeRenderEffect
|
||||
import androidx.compose.ui.util.lerp
|
||||
import com.kyant.backdrop.AmbientHighlightShaderString
|
||||
import com.kyant.backdrop.DynamicHighlightShaderString
|
||||
@@ -16,18 +17,14 @@ import kotlin.math.PI
|
||||
@Immutable
|
||||
sealed interface HighlightStyle {
|
||||
|
||||
fun RuntimeShaderCacheScope.createShader(
|
||||
size: Size,
|
||||
bitmapShader: BitmapShader
|
||||
): Shader?
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
fun RuntimeShaderCacheScope.createRenderEffect(size: Size): RenderEffect?
|
||||
|
||||
@Immutable
|
||||
data object Solid : HighlightStyle {
|
||||
|
||||
override fun RuntimeShaderCacheScope.createShader(
|
||||
size: Size,
|
||||
bitmapShader: BitmapShader
|
||||
): Shader? = null
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
override fun RuntimeShaderCacheScope.createRenderEffect(size: Size): RenderEffect? = null
|
||||
}
|
||||
|
||||
@Immutable
|
||||
@@ -37,10 +34,8 @@ sealed interface HighlightStyle {
|
||||
val isAmbient: Boolean = false
|
||||
) : HighlightStyle {
|
||||
|
||||
override fun RuntimeShaderCacheScope.createShader(
|
||||
size: Size,
|
||||
bitmapShader: BitmapShader
|
||||
): Shader? {
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
override fun RuntimeShaderCacheScope.createRenderEffect(size: Size): RenderEffect? {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
val shader =
|
||||
if (isAmbient) {
|
||||
@@ -49,11 +44,12 @@ sealed interface HighlightStyle {
|
||||
obtainRuntimeShader("Dynamic", DynamicHighlightShaderString)
|
||||
}
|
||||
shader.apply {
|
||||
setInputBuffer("image", bitmapShader)
|
||||
setFloatUniform("size", size.width, size.height)
|
||||
setFloatUniform("angle", angle * (PI / 180f).toFloat())
|
||||
setFloatUniform("falloff", falloff)
|
||||
}
|
||||
android.graphics.RenderEffect.createRuntimeShaderEffect(shader, "image")
|
||||
.asComposeRenderEffect()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import androidx.compose.ui.util.lerp
|
||||
|
||||
@Immutable
|
||||
data class Shadow(
|
||||
val elevation: Dp = 24f.dp,
|
||||
val radius: Dp = 24f.dp,
|
||||
val offset: DpOffset = DpOffset(0f.dp, 4f.dp),
|
||||
val color: Color = Color.Black.copy(alpha = 0.15f),
|
||||
val alpha: Float = 1f,
|
||||
@@ -31,7 +31,7 @@ data class Shadow(
|
||||
@Stable
|
||||
fun lerp(start: Shadow, stop: Shadow, fraction: Float): Shadow {
|
||||
return Shadow(
|
||||
elevation = lerp(start.elevation, stop.elevation, fraction),
|
||||
radius = lerp(start.radius, stop.radius, fraction),
|
||||
offset = lerp(start.offset, stop.offset, fraction),
|
||||
color = lerp(start.color, stop.color, fraction),
|
||||
alpha = lerp(start.alpha, stop.alpha, fraction),
|
||||
|
||||
@@ -1,30 +1,27 @@
|
||||
package com.kyant.backdrop.shadow
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BlendMode
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffXfermode
|
||||
import android.graphics.RectF
|
||||
import android.os.Build
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.BlurEffect
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Outline
|
||||
import androidx.compose.ui.graphics.TileMode
|
||||
import androidx.compose.ui.graphics.asAndroidPath
|
||||
import androidx.compose.ui.graphics.asComposePaint
|
||||
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.isUnspecified
|
||||
import androidx.compose.ui.graphics.drawscope.translate
|
||||
import androidx.compose.ui.graphics.layer.CompositingStrategy
|
||||
import androidx.compose.ui.graphics.layer.GraphicsLayer
|
||||
import androidx.compose.ui.graphics.layer.drawLayer
|
||||
import androidx.compose.ui.graphics.nativeCanvas
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.node.DrawModifierNode
|
||||
import androidx.compose.ui.node.ModifierNodeElement
|
||||
import androidx.compose.ui.node.ObserverModifierNode
|
||||
import androidx.compose.ui.node.invalidateDraw
|
||||
import androidx.compose.ui.node.observeReads
|
||||
import androidx.compose.ui.node.requireGraphicsContext
|
||||
import androidx.compose.ui.platform.InspectorInfo
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import com.kyant.backdrop.ShapeProvider
|
||||
@@ -42,7 +39,7 @@ internal class ShadowElement(
|
||||
override fun update(node: ShadowNode) {
|
||||
node.shapeProvider = shapeProvider
|
||||
node.shadow = shadow
|
||||
node.invalidateDrawCache()
|
||||
node.invalidateDraw()
|
||||
}
|
||||
|
||||
override fun InspectorInfo.inspectableProperties() {
|
||||
@@ -71,92 +68,96 @@ internal class ShadowElement(
|
||||
internal class ShadowNode(
|
||||
var shapeProvider: ShapeProvider,
|
||||
var shadow: () -> Shadow?
|
||||
) : DrawModifierNode, ObserverModifierNode, Modifier.Node() {
|
||||
) : DrawModifierNode, Modifier.Node() {
|
||||
|
||||
override val shouldAutoInvalidate: Boolean = false
|
||||
|
||||
private var bitmap: Bitmap? = null
|
||||
private var canvas: Canvas? = null
|
||||
private val paint =
|
||||
Paint().apply {
|
||||
color = Color.TRANSPARENT
|
||||
}
|
||||
private val maskPaint =
|
||||
Paint().apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
blendMode = BlendMode.CLEAR
|
||||
} else {
|
||||
xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
|
||||
private var shadowLayer: GraphicsLayer? = null
|
||||
private var maskLayer: GraphicsLayer? = null
|
||||
|
||||
private val shadowPaint = Paint()
|
||||
|
||||
private var prevRadius = Float.NaN
|
||||
|
||||
override fun ContentDrawScope.draw() {
|
||||
val shadow = shadow() ?: return drawContent()
|
||||
|
||||
val shadowLayer = shadowLayer
|
||||
val maskLayer = maskLayer
|
||||
if (shadowLayer != null && maskLayer != null) {
|
||||
val size = size
|
||||
val radius = shadow.radius.toPx()
|
||||
val offsetX = shadow.offset.x.toPx()
|
||||
val offsetY = shadow.offset.y.toPx()
|
||||
val shadowSize = IntSize(
|
||||
ceil(size.width + radius * 2 + offsetX).toInt(),
|
||||
ceil(size.height + radius * 2 + offsetY).toInt()
|
||||
)
|
||||
val outline = shapeProvider.shape.createOutline(size, layoutDirection, this)
|
||||
|
||||
configurePaint(shadow)
|
||||
|
||||
if (prevRadius != radius) {
|
||||
shadowLayer.renderEffect =
|
||||
if (radius > 0f) {
|
||||
BlurEffect(radius, radius, TileMode.Decal)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
prevRadius = radius
|
||||
}
|
||||
}
|
||||
private val drawPaint = Paint()
|
||||
private var prevSize = Size.Unspecified
|
||||
private var dx = 0f
|
||||
private var dy = 0f
|
||||
shadowLayer.record { drawOutline(outline, shadowPaint) }
|
||||
|
||||
private var _shadow: Shadow? = null
|
||||
private var isCacheValid = false
|
||||
private val cacheDrawBlock: DrawScope.() -> Unit = cacheDrawBlock@{
|
||||
val shadow = shadow().also { _shadow = it }
|
||||
val size = size
|
||||
if (shadow == null ||
|
||||
shadow.elevation.value <= 0f || shadow.color.isUnspecified || shadow.alpha == 0f
|
||||
) {
|
||||
_shadow = null
|
||||
return@cacheDrawBlock
|
||||
}
|
||||
|
||||
val elevation = shadow.elevation.toPx()
|
||||
val offset = Offset(shadow.offset.x.toPx(), shadow.offset.y.toPx())
|
||||
val shadowSize = IntSize(
|
||||
ceil(size.width + elevation * 2).toInt(),
|
||||
ceil(size.height + elevation * 2).toInt()
|
||||
)
|
||||
if (shadowSize.width < 0.5f || shadowSize.height < 0.5f) {
|
||||
_shadow = null
|
||||
return@cacheDrawBlock
|
||||
}
|
||||
|
||||
val color =
|
||||
if (shadow.alpha == 1f) {
|
||||
shadow.color
|
||||
} else {
|
||||
shadow.color.copy(alpha = shadow.color.alpha * shadow.alpha)
|
||||
}
|
||||
paint.setShadowLayer(elevation, offset.x, offset.y, color.toArgb())
|
||||
drawPaint.asComposePaint().blendMode = shadow.blendMode
|
||||
|
||||
if (prevSize != size) {
|
||||
bitmap = null
|
||||
canvas = null
|
||||
prevSize = size
|
||||
}
|
||||
|
||||
@Suppress("UseKtx")
|
||||
val bitmap =
|
||||
bitmap ?: Bitmap.createBitmap(
|
||||
shadowSize.width,
|
||||
shadowSize.height,
|
||||
Bitmap.Config.ARGB_8888
|
||||
).also { bitmap = it }
|
||||
val canvas =
|
||||
canvas?.apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
drawColor(Color.TRANSPARENT, BlendMode.CLEAR)
|
||||
} else {
|
||||
drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
|
||||
maskLayer.blendMode = shadow.blendMode
|
||||
maskLayer.record(size = shadowSize) {
|
||||
translate(radius + offsetX, radius + offsetY) {
|
||||
drawLayer(shadowLayer)
|
||||
translate(-offsetX, -offsetY) {
|
||||
drawOutline(outline, ShadowMaskPaint)
|
||||
}
|
||||
}
|
||||
} ?: Canvas(bitmap).also { canvas = it }
|
||||
}
|
||||
|
||||
dx = elevation - offset.x
|
||||
dy = elevation - offset.y
|
||||
canvas.translate(dx, dy)
|
||||
translate(-radius, -radius) {
|
||||
drawLayer(maskLayer)
|
||||
}
|
||||
}
|
||||
|
||||
when (val outline = shapeProvider.shape.createOutline(size, layoutDirection, this)) {
|
||||
drawContent()
|
||||
}
|
||||
|
||||
override fun onAttach() {
|
||||
val graphicsContext = requireGraphicsContext()
|
||||
shadowLayer = graphicsContext.createGraphicsLayer()
|
||||
maskLayer =
|
||||
graphicsContext.createGraphicsLayer().apply {
|
||||
compositingStrategy = CompositingStrategy.Offscreen
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
val graphicsContext = requireGraphicsContext()
|
||||
shadowLayer?.let { layer ->
|
||||
graphicsContext.releaseGraphicsLayer(layer)
|
||||
shadowLayer = null
|
||||
}
|
||||
maskLayer?.let { layer ->
|
||||
graphicsContext.releaseGraphicsLayer(layer)
|
||||
maskLayer = null
|
||||
}
|
||||
prevRadius = Float.NaN
|
||||
}
|
||||
|
||||
private fun DrawScope.configurePaint(shadow: Shadow) {
|
||||
shadowPaint.color = shadow.color.modulate(shadow.alpha).toArgb()
|
||||
}
|
||||
|
||||
private fun DrawScope.drawOutline(outline: Outline, paint: Paint) {
|
||||
val canvas = drawContext.canvas.nativeCanvas
|
||||
|
||||
when (outline) {
|
||||
is Outline.Rectangle -> {
|
||||
val rect = RectF(0f, 0f, size.width, size.height)
|
||||
canvas.drawRect(rect, paint)
|
||||
canvas.drawRect(rect, maskPaint)
|
||||
canvas.drawRect(0f, 0f, size.width, size.height, paint)
|
||||
}
|
||||
|
||||
is Outline.Rounded -> {
|
||||
@@ -164,46 +165,32 @@ internal class ShadowNode(
|
||||
val path = outline.roundRectPath?.asAndroidPath()
|
||||
if (path != null) {
|
||||
canvas.drawPath(path, paint)
|
||||
canvas.drawPath(path, maskPaint)
|
||||
} else {
|
||||
val rect = with(outline.roundRect) { RectF(left, top, right, bottom) }
|
||||
val rr = outline.roundRect
|
||||
val radius = outline.roundRect.topLeftCornerRadius.x
|
||||
canvas.drawRoundRect(rect, radius, radius, paint)
|
||||
canvas.drawRoundRect(rect, radius, radius, maskPaint)
|
||||
canvas.drawRoundRect(rr.left, rr.top, rr.right, rr.bottom, radius, radius, paint)
|
||||
}
|
||||
}
|
||||
|
||||
is Outline.Generic -> {
|
||||
val path = outline.path.asAndroidPath()
|
||||
canvas.drawPath(path, paint)
|
||||
canvas.drawPath(path, maskPaint)
|
||||
}
|
||||
}
|
||||
|
||||
canvas.translate(-dx, -dy)
|
||||
}
|
||||
|
||||
override fun ContentDrawScope.draw() {
|
||||
if (!isCacheValid) {
|
||||
observeReads { cacheDrawBlock() }
|
||||
isCacheValid = true
|
||||
}
|
||||
|
||||
val bitmap = bitmap
|
||||
if (bitmap != null) {
|
||||
val canvas = drawContext.canvas.nativeCanvas
|
||||
canvas.drawBitmap(bitmap, -dx, -dy, drawPaint)
|
||||
}
|
||||
|
||||
drawContent()
|
||||
}
|
||||
|
||||
override fun onObservedReadsChanged() {
|
||||
invalidateDrawCache()
|
||||
}
|
||||
|
||||
fun invalidateDrawCache() {
|
||||
isCacheValid = false
|
||||
invalidateDraw()
|
||||
}
|
||||
}
|
||||
|
||||
private val ShadowMaskPaint = Paint().apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
blendMode = android.graphics.BlendMode.CLEAR
|
||||
} else {
|
||||
xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Color.modulate(alpha: Float): Color =
|
||||
if (alpha != 1.0f) {
|
||||
copy(alpha = this.alpha * alpha)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
Reference in new issue
Block a user