Prerender icon layers to bitmap
This commit is contained in:
parent
40d53720e9
commit
3f9db4751f
@ -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 {
|
||||||
|
|||||||
@ -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"))
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user