Prerender icon layers to bitmap
This commit is contained in:
parent
40d53720e9
commit
3f9db4751f
@ -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 {
|
||||
|
||||
@ -51,5 +51,6 @@ dependencies {
|
||||
|
||||
implementation(project(":core:ktx"))
|
||||
implementation(project(":core:i18n"))
|
||||
implementation(project(":libs:material-color-utilities"))
|
||||
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user