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 package de.mm20.launcher2.ui.component
import android.graphics.Bitmap
import android.graphics.BitmapShader
import android.graphics.Matrix import android.graphics.Matrix
import android.graphics.Path import android.graphics.Path
import android.graphics.PorterDuff import android.graphics.PorterDuff
@ -37,11 +39,16 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.geometry.toRect import androidx.compose.ui.geometry.toRect
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape 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.Shape
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.asComposePath 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.drawIntoCanvas
import androidx.compose.ui.graphics.drawscope.scale import androidx.compose.ui.graphics.drawscope.scale
import androidx.compose.ui.graphics.drawscope.withTransform 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.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource 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.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp 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.DynamicLauncherIcon
import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.icons.LauncherIconLayer import de.mm20.launcher2.icons.LauncherIconLayer
import de.mm20.launcher2.icons.LauncherIconRenderSettings
import de.mm20.launcher2.icons.StaticIconLayer import de.mm20.launcher2.icons.StaticIconLayer
import de.mm20.launcher2.icons.StaticLauncherIcon import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.icons.TextLayer 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.ktx.drawWithColorFilter
import de.mm20.launcher2.preferences.Settings.IconSettings.IconShape import de.mm20.launcher2.preferences.Settings.IconSettings.IconShape
import de.mm20.launcher2.ui.base.LocalTime 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.LocalDarkTheme
import de.mm20.launcher2.ui.locals.LocalGridSettings
import de.mm20.launcher2.ui.modifier.scale
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import palettes.TonalPalette import palettes.TonalPalette
import java.time.Instant import java.time.Instant
@ -80,6 +92,7 @@ import java.time.ZoneId
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.pow import kotlin.math.pow
import kotlin.math.roundToInt import kotlin.math.roundToInt
import android.graphics.Shader as PlatformShader
@Composable @Composable
fun ShapedLauncherIcon( 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) { if (_icon is DynamicLauncherIcon) {
val time = LocalTime.current val time = LocalTime.current
LaunchedEffect(time) { LaunchedEffect(time) {
@ -118,10 +149,6 @@ fun ShapedLauncherIcon(
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.graphicsLayer {
clip = currentIcon?.backgroundLayer !is TransparentLayer
this.shape = shape
}
.then( .then(
if (onClick != null || onLongClick != null) { if (onClick != null || onLongClick != null) {
Modifier.pointerInput(onClick, onLongClick) { Modifier.pointerInput(onClick, onLongClick) {
@ -134,19 +161,64 @@ fun ShapedLauncherIcon(
), ),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
currentIcon?.let { val bmp = currentBitmap
IconLayer( val ic = currentIcon
it.backgroundLayer, if (bmp != null && ic != null) {
size, Canvas(modifier = Modifier
colorTone = if (LocalDarkTheme.current) 30 else 90, .size(defaultIconSize)
MaterialTheme.colorScheme.primaryContainer .scale(size / defaultIconSize, TransformOrigin.Center)
) ) {
IconLayer( val brush = BitmapShaderBrush(bmp)
it.foregroundLayer, if (ic.backgroundLayer is TransparentLayer) {
size, drawRect(brush)
colorTone = if (LocalDarkTheme.current) 90 else 10, } else {
MaterialTheme.colorScheme.onPrimaryContainer 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() val _badge = badge()
@ -306,16 +378,12 @@ private fun IconLayer(
if (layer.color == 0) defaultTintColor.toArgb() if (layer.color == 0) defaultTintColor.toArgb()
else getTone(layer.color, colorTone) else getTone(layer.color, colorTone)
Canvas(modifier = Modifier.fillMaxSize()) { Canvas(modifier = Modifier.fillMaxSize()) {
withTransform({ drawIntoCanvas {
this.scale(layer.scale) layer.icon.bounds = this.size.toRect().toAndroidRect()
}) { layer.icon.drawWithColorFilter(
drawIntoCanvas { it.nativeCanvas,
layer.icon.bounds = this.size.toRect().toAndroidRect() PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
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 } val LocalIconShape = compositionLocalOf<Shape> { CircleShape }
fun getShape(iconShape: IconShape): Shape { fun getShape(iconShape: IconShape): Shape {

View File

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

View File

@ -1,12 +1,112 @@
package de.mm20.launcher2.icons 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 sealed interface LauncherIcon
data class LauncherIconRenderSettings(
val size: Int,
val fgThemeColor: Int,
val bgThemeColor: Int,
val fgTone: Int,
val bgTone: Int,
)
data class StaticLauncherIcon( data class StaticLauncherIcon(
val foregroundLayer: LauncherIconLayer, val foregroundLayer: LauncherIconLayer,
val backgroundLayer: 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 suspend fun getIcon(time: Long): StaticLauncherIcon
} }