Draw emojis in emoji menu asynchronously

This commit is contained in:
Aleksandras Kostarevas 2023-12-05 18:07:19 +00:00
parent 314cf8c84c
commit c3018cdd86

View File

@ -1,6 +1,13 @@
package org.futo.inputmethod.latin.uix.actions package org.futo.inputmethod.latin.uix.actions
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.Paint
import android.text.TextPaint
import android.util.TypedValue
import androidx.compose.foundation.Canvas import androidx.compose.foundation.Canvas
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -21,16 +28,28 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.drawscope.translate import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.core.graphics.applyCanvas
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
@ -38,6 +57,9 @@ import kotlinx.serialization.json.jsonPrimitive
import org.futo.inputmethod.latin.R import org.futo.inputmethod.latin.R
import org.futo.inputmethod.latin.uix.Action import org.futo.inputmethod.latin.uix.Action
import org.futo.inputmethod.latin.uix.ActionWindow import org.futo.inputmethod.latin.uix.ActionWindow
import org.futo.inputmethod.latin.uix.PersistentActionState
import kotlin.math.ceil
import kotlin.math.roundToInt
data class EmojiItem( data class EmojiItem(
val emoji: String, val emoji: String,
@ -45,23 +67,102 @@ data class EmojiItem(
val category: String val category: String
) )
@Composable const val EMOJI_HEIGHT = 30.0f //sp
fun EmojiGrid(onClick: (EmojiItem) -> Unit, onExit: () -> Unit, onBackspace: () -> Unit, onSpace: () -> Unit) {
val context = LocalContext.current
val emojis = remember { data class BitmapRecycler(
val stream = context.resources.openRawResource(R.raw.gemoji) private val freeBitmaps: MutableList<Bitmap> = mutableListOf()
val text = stream.bufferedReader().readText() ) {
val emojidata = Json.parseToJsonElement(text) var textPaint: TextPaint? = null
emojidata.jsonArray.map { var total = 0
EmojiItem( fun getTextPaint(context: Context): TextPaint {
emoji = it.jsonObject["emoji"]!!.jsonPrimitive.content, return textPaint ?: run {
description = it.jsonObject["description"]!!.jsonPrimitive.content, this.textPaint = TextPaint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG).apply {
category = it.jsonObject["category"]!!.jsonPrimitive.content, textSize = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP,
EMOJI_HEIGHT,
context.resources.displayMetrics
) )
} }
this.textPaint!!
}
} }
fun getBitmap(): Bitmap {
return freeBitmaps.removeFirstOrNull()?.apply {
eraseColor(Color.TRANSPARENT)
} ?: with(textPaint!!.fontMetricsInt) {
println("creating new bitmap, total $total")
total += 1
val size = bottom - top
Bitmap.createBitmap(size, size, android.graphics.Bitmap.Config.ARGB_8888)
}
}
fun freeBitmap(bitmap: Bitmap) {
if(freeBitmaps.size > 60) {
println("Recycling bitmap, new total $total")
total -= 1
bitmap.recycle()
} else {
freeBitmaps.add(bitmap)
}
}
fun freeAllBitmaps() {
freeBitmaps.forEach {
println("Recycling bitmap due to freeAllBitmaps, new total $total")
total -= 1
it.recycle()
}
freeBitmaps.clear()
}
}
@Composable fun EmojiIcon(emoji: String, bitmaps: BitmapRecycler) {
var rendering by remember { mutableStateOf(true) }
val offscreenCanvasBitmap: Bitmap = remember { bitmaps.getBitmap() }
val imageBitmap = remember { offscreenCanvasBitmap.asImageBitmap() }
DisposableEffect(offscreenCanvasBitmap) {
onDispose {
bitmaps.freeBitmap(bitmap = offscreenCanvasBitmap)
}
}
LaunchedEffect(emoji) {
withContext(Dispatchers.Unconfined) {
val textPaint = bitmaps.textPaint!!
yield()
offscreenCanvasBitmap.applyCanvas {
yield()
val textWidth = textPaint.measureText(emoji, 0, emoji.length)
yield()
drawText(
emoji,
/* start = */ 0,
/* end = */ emoji.length,
/* x = */ (width - textWidth) / 2,
/* y = */ -textPaint.fontMetrics.top,
textPaint,
)
yield()
}
yield()
rendering = false
}
}
Image(
bitmap = imageBitmap,
contentDescription = emoji,
modifier = Modifier.fillMaxSize()
)
}
@Composable
fun EmojiGrid(onClick: (EmojiItem) -> Unit, onExit: () -> Unit, onBackspace: () -> Unit, onSpace: () -> Unit, bitmaps: BitmapRecycler, emojis: List<EmojiItem>) {
val context = LocalContext.current
val spToDp = context.resources.displayMetrics.scaledDensity / context.resources.displayMetrics.density val spToDp = context.resources.displayMetrics.scaledDensity / context.resources.displayMetrics.density
Column { Column {
@ -70,25 +171,27 @@ fun EmojiGrid(onClick: (EmojiItem) -> Unit, onExit: () -> Unit, onBackspace: ()
contentPadding = PaddingValues(10.dp), contentPadding = PaddingValues(10.dp),
modifier = Modifier.weight(1.0f) modifier = Modifier.weight(1.0f)
) { ) {
items(emojis) { emoji -> items(emojis, key = { it.emoji }) { emoji ->
Box(modifier = Modifier.fillMaxSize().clickable { Box(modifier = Modifier
.fillMaxSize()
.clickable {
onClick(emoji) onClick(emoji)
}) { }) {
Text( EmojiIcon(emoji.emoji, bitmaps)
text = emoji.emoji,
fontSize = 24.sp,
modifier = Modifier.align(Alignment.Center)
)
} }
} }
} }
Surface(color = MaterialTheme.colorScheme.background, modifier = Modifier.fillMaxWidth().height(48.dp)) { Surface(color = MaterialTheme.colorScheme.background, modifier = Modifier
.fillMaxWidth()
.height(48.dp)) {
Row(modifier = Modifier.padding(2.dp, 8.dp, 2.dp, 0.dp)) { Row(modifier = Modifier.padding(2.dp, 8.dp, 2.dp, 0.dp)) {
IconButton(onClick = { onExit() }) { IconButton(onClick = { onExit() }) {
Text("ABC", fontSize = 14.sp) Text("ABC", fontSize = 14.sp)
} }
Button(onClick = { onSpace() }, modifier = Modifier.weight(1.0f).padding(8.dp, 2.dp), colors = ButtonDefaults.buttonColors( Button(onClick = { onSpace() }, modifier = Modifier
.weight(1.0f)
.padding(8.dp, 2.dp), colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.33f), containerColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.33f),
contentColor = MaterialTheme.colorScheme.onBackground, contentColor = MaterialTheme.colorScheme.onBackground,
disabledContainerColor = MaterialTheme.colorScheme.outline, disabledContainerColor = MaterialTheme.colorScheme.outline,
@ -122,6 +225,7 @@ fun EmojiGrid(onClick: (EmojiItem) -> Unit, onExit: () -> Unit, onBackspace: ()
} }
} }
/*
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
fun EmojiGridPreview() { fun EmojiGridPreview() {
@ -132,15 +236,49 @@ fun EmojiGridPreview() {
onSpace = {} onSpace = {}
) )
} }
*/
class PersistentEmojiState: PersistentActionState {
val bitmaps: BitmapRecycler = BitmapRecycler()
var emojis: MutableState<List<EmojiItem>?> = mutableStateOf(null)
suspend fun loadEmojis(context: Context) = withContext(Dispatchers.IO) {
val stream = context.resources.openRawResource(R.raw.gemoji)
val text = stream.bufferedReader().readText()
withContext(Dispatchers.Default) {
val emojiData = Json.parseToJsonElement(text)
emojis.value = emojiData.jsonArray.map {
EmojiItem(
emoji = it.jsonObject["emoji"]!!.jsonPrimitive.content,
description = it.jsonObject["description"]!!.jsonPrimitive.content,
category = it.jsonObject["category"]!!.jsonPrimitive.content,
)
}
}
}
override suspend fun cleanUp() {
bitmaps.freeAllBitmaps()
}
}
val EmojiAction = Action( val EmojiAction = Action(
icon = R.drawable.smile, icon = R.drawable.smile,
name = R.string.title_emojis, name = R.string.title_emojis,
simplePressImpl = null, simplePressImpl = null,
windowImpl = { manager, _ -> persistentState = { manager ->
val state = PersistentEmojiState()
state.bitmaps.getTextPaint(manager.getContext())
manager.getLifecycleScope().launch {
state.loadEmojis(manager.getContext())
}
state
},
windowImpl = { manager, persistentState ->
val state = persistentState as PersistentEmojiState
object : ActionWindow { object : ActionWindow {
@Composable @Composable
override fun windowName(): String { override fun windowName(): String {
@ -149,6 +287,7 @@ val EmojiAction = Action(
@Composable @Composable
override fun WindowContents() { override fun WindowContents() {
state.emojis.value?.let { emojis ->
EmojiGrid(onClick = { EmojiGrid(onClick = {
manager.typeText(it.emoji) manager.typeText(it.emoji)
}, onExit = { }, onExit = {
@ -157,11 +296,12 @@ val EmojiAction = Action(
manager.typeText(" ") manager.typeText(" ")
}, onBackspace = { }, onBackspace = {
manager.backspace(1) manager.backspace(1)
}) }, bitmaps = state.bitmaps, emojis = emojis)
}
} }
override fun close() { override fun close() {
state.bitmaps.freeAllBitmaps()
} }
} }
} }