Prerender icon layers to bitmap

This commit is contained in:
MM20 2023-05-15 16:49:10 +02:00
parent 40d53720e9
commit 3f9db4751f
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
3 changed files with 207 additions and 29 deletions

View File

@ -1,5 +1,7 @@
package de.mm20.launcher2.ui.component
import android.graphics.Bitmap
import android.graphics.BitmapShader
import android.graphics.Matrix
import android.graphics.Path
import android.graphics.PorterDuff
@ -37,11 +39,16 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.geometry.toRect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shader
import androidx.compose.ui.graphics.ShaderBrush
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.asComposePath
import androidx.compose.ui.graphics.drawOutline
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.drawscope.scale
import androidx.compose.ui.graphics.drawscope.withTransform
@ -52,6 +59,7 @@ import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@ -63,6 +71,7 @@ import de.mm20.launcher2.icons.ColorLayer
import de.mm20.launcher2.icons.DynamicLauncherIcon
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.icons.LauncherIconLayer
import de.mm20.launcher2.icons.LauncherIconRenderSettings
import de.mm20.launcher2.icons.StaticIconLayer
import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.icons.TextLayer
@ -72,7 +81,10 @@ import de.mm20.launcher2.icons.TransparentLayer
import de.mm20.launcher2.ktx.drawWithColorFilter
import de.mm20.launcher2.preferences.Settings.IconSettings.IconShape
import de.mm20.launcher2.ui.base.LocalTime
import de.mm20.launcher2.ui.ktx.toPixels
import de.mm20.launcher2.ui.locals.LocalDarkTheme
import de.mm20.launcher2.ui.locals.LocalGridSettings
import de.mm20.launcher2.ui.modifier.scale
import kotlinx.coroutines.launch
import palettes.TonalPalette
import java.time.Instant
@ -80,6 +92,7 @@ import java.time.ZoneId
import kotlin.math.abs
import kotlin.math.pow
import kotlin.math.roundToInt
import android.graphics.Shader as PlatformShader
@Composable
fun ShapedLauncherIcon(
@ -104,6 +117,24 @@ fun ShapedLauncherIcon(
)
}
val defaultIconSize = LocalGridSettings.current.iconSize.dp
val renderSettings = LauncherIconRenderSettings(
size = defaultIconSize.toPixels().toInt(),
fgThemeColor = MaterialTheme.colorScheme.onPrimaryContainer.toArgb(),
bgThemeColor = MaterialTheme.colorScheme.primaryContainer.toArgb(),
fgTone = if (LocalDarkTheme.current) 90 else 10,
bgTone = if (LocalDarkTheme.current) 30 else 90,
)
var currentBitmap by remember {
mutableStateOf(currentIcon?.getCachedBitmap(renderSettings))
}
LaunchedEffect(currentIcon, renderSettings) {
currentBitmap = currentIcon?.render(renderSettings)
}
if (_icon is DynamicLauncherIcon) {
val time = LocalTime.current
LaunchedEffect(time) {
@ -118,10 +149,6 @@ fun ShapedLauncherIcon(
Box(
modifier = Modifier
.fillMaxSize()
.graphicsLayer {
clip = currentIcon?.backgroundLayer !is TransparentLayer
this.shape = shape
}
.then(
if (onClick != null || onLongClick != null) {
Modifier.pointerInput(onClick, onLongClick) {
@ -134,19 +161,64 @@ fun ShapedLauncherIcon(
),
contentAlignment = Alignment.Center
) {
currentIcon?.let {
IconLayer(
it.backgroundLayer,
size,
colorTone = if (LocalDarkTheme.current) 30 else 90,
MaterialTheme.colorScheme.primaryContainer
)
IconLayer(
it.foregroundLayer,
size,
colorTone = if (LocalDarkTheme.current) 90 else 10,
MaterialTheme.colorScheme.onPrimaryContainer
)
val bmp = currentBitmap
val ic = currentIcon
if (bmp != null && ic != null) {
Canvas(modifier = Modifier
.size(defaultIconSize)
.scale(size / defaultIconSize, TransformOrigin.Center)
) {
val brush = BitmapShaderBrush(bmp)
if (ic.backgroundLayer is TransparentLayer) {
drawRect(brush)
} else {
val outline =
shape.createOutline(this.size, layoutDirection, Density(density, fontScale))
drawOutline(outline, brush)
}
}
// Background layer is always static layer, color layer, or transparent layer
val fg = ic.foregroundLayer
when(fg) {
is ClockLayer -> {
ClockLayer(
sublayers = fg.sublayers,
defaultMinute = fg.defaultMinute,
defaultHour = fg.defaultHour,
defaultSecond = fg.defaultSecond,
scale = fg.scale,
tintColor = null,
)
}
is TintedClockLayer -> {
ClockLayer(
sublayers = fg.sublayers,
defaultMinute = fg.defaultMinute,
defaultHour = fg.defaultHour,
defaultSecond = fg.defaultSecond,
scale = fg.scale,
tintColor = if (fg.color == 0) {
Color(renderSettings.fgThemeColor)
} else {
Color(getTone(fg.color, renderSettings.fgTone))
},
)
}
is TextLayer -> {
Text(
text = fg.text,
style = MaterialTheme.typography.headlineSmall.copy(
fontSize = 20.sp * (size / 48.dp)
),
color = if (fg.color == 0) {
Color(renderSettings.fgThemeColor)
} else {
Color(getTone(fg.color, renderSettings.fgTone))
},
)
}
else -> {}
}
}
}
val _badge = badge()
@ -306,16 +378,12 @@ private fun IconLayer(
if (layer.color == 0) defaultTintColor.toArgb()
else getTone(layer.color, colorTone)
Canvas(modifier = Modifier.fillMaxSize()) {
withTransform({
this.scale(layer.scale)
}) {
drawIntoCanvas {
layer.icon.bounds = this.size.toRect().toAndroidRect()
layer.icon.drawWithColorFilter(
it.nativeCanvas,
PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
)
}
drawIntoCanvas {
layer.icon.bounds = this.size.toRect().toAndroidRect()
layer.icon.drawWithColorFilter(
it.nativeCanvas,
PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
)
}
}
}
@ -409,6 +477,15 @@ private fun ClockLayer(
}
}
class BitmapShaderBrush(
val bitmap: Bitmap,
) : ShaderBrush() {
override fun createShader(size: Size): Shader {
return BitmapShader(bitmap, PlatformShader.TileMode.CLAMP, PlatformShader.TileMode.CLAMP)
}
}
val LocalIconShape = compositionLocalOf<Shape> { CircleShape }
fun getShape(iconShape: IconShape): Shape {

View File

@ -51,5 +51,6 @@ dependencies {
implementation(project(":core:ktx"))
implementation(project(":core:i18n"))
implementation(project(":libs:material-color-utilities"))
}

View File

@ -1,12 +1,112 @@
package de.mm20.launcher2.icons
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.graphics.PorterDuffXfermode
import android.graphics.Rect
import androidx.core.graphics.withScale
import de.mm20.launcher2.ktx.drawWithColorFilter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import kotlinx.coroutines.withContext
import palettes.TonalPalette
sealed interface LauncherIcon
data class LauncherIconRenderSettings(
val size: Int,
val fgThemeColor: Int,
val bgThemeColor: Int,
val fgTone: Int,
val bgTone: Int,
)
data class StaticLauncherIcon(
val foregroundLayer: LauncherIconLayer,
val backgroundLayer: LauncherIconLayer,
): LauncherIcon
) : LauncherIcon {
private var cachedBitmap: Bitmap? = null
private var cachedRenderSettings: LauncherIconRenderSettings? = null
private var renderSemaphore = Semaphore(1)
interface DynamicLauncherIcon: LauncherIcon {
fun getCachedBitmap(settings: LauncherIconRenderSettings): Bitmap? {
return if (cachedRenderSettings == settings) cachedBitmap else null
}
/**
* Render this icon to a bitmap.
*/
suspend fun render(settings: LauncherIconRenderSettings): Bitmap {
val cachedBmp = cachedBitmap
if (cachedRenderSettings == settings && cachedBmp != null) return cachedBmp
val bmp = withContext(Dispatchers.Default) {
renderSemaphore.withPermit {
val bmp =
if (cachedBmp == null || cachedBmp.width != settings.size || cachedBmp.height != settings.size) {
Bitmap.createBitmap(settings.size, settings.size, Bitmap.Config.ARGB_8888)!!
} else cachedBmp
val canvas = Canvas(bmp)
canvas.drawRect(
Rect(0, 0, canvas.width, canvas.height), Paint().apply {
xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
})
renderLayer(canvas, backgroundLayer, settings.bgThemeColor, settings.bgTone)
renderLayer(canvas, foregroundLayer, settings.fgThemeColor, settings.fgTone)
cachedBitmap = bmp
cachedRenderSettings = settings
bmp
}
}
return bmp
}
private fun renderLayer(canvas: Canvas, layer: LauncherIconLayer, themeColor: Int, tone: Int) {
when(layer) {
is ColorLayer -> {
val paint = Paint()
paint.color = if (layer.color == 0) themeColor else getTone(layer.color, tone)
canvas.drawRect(Rect(0, 0, canvas.width, canvas.height), paint)
}
is StaticIconLayer -> {
canvas.withScale(
layer.scale,
layer.scale,
canvas.width / 2f,
canvas.height / 2f,
) {
layer.icon.bounds = Rect(0, 0, canvas.width, canvas.height)
layer.icon.draw(canvas)
}
}
is TintedIconLayer -> {
val color = if (layer.color == 0) themeColor else getTone(layer.color, tone)
canvas.withScale(
layer.scale,
layer.scale,
canvas.width / 2f,
canvas.height / 2f,
) {
layer.icon.bounds = Rect(0, 0, canvas.width, canvas.height)
layer.icon.drawWithColorFilter(canvas,
PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
)
}
}
else -> {}
}
}
private fun getTone(argb: Int, tone: Int): Int {
return TonalPalette
.fromInt(argb)
.tone(tone)
}
}
interface DynamicLauncherIcon : LauncherIcon {
suspend fun getIcon(time: Long): StaticLauncherIcon
}