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
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.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@ -21,16 +28,28 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
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.ui.Alignment
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.dp
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.jsonArray
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.uix.Action
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(
val emoji: String,
@ -45,23 +67,102 @@ data class EmojiItem(
val category: String
)
@Composable
fun EmojiGrid(onClick: (EmojiItem) -> Unit, onExit: () -> Unit, onBackspace: () -> Unit, onSpace: () -> Unit) {
val context = LocalContext.current
const val EMOJI_HEIGHT = 30.0f //sp
val emojis = remember {
val stream = context.resources.openRawResource(R.raw.gemoji)
val text = stream.bufferedReader().readText()
val emojidata = Json.parseToJsonElement(text)
emojidata.jsonArray.map {
EmojiItem(
emoji = it.jsonObject["emoji"]!!.jsonPrimitive.content,
description = it.jsonObject["description"]!!.jsonPrimitive.content,
category = it.jsonObject["category"]!!.jsonPrimitive.content,
)
data class BitmapRecycler(
private val freeBitmaps: MutableList<Bitmap> = mutableListOf()
) {
var textPaint: TextPaint? = null
var total = 0
fun getTextPaint(context: Context): TextPaint {
return textPaint ?: run {
this.textPaint = TextPaint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG).apply {
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
Column {
@ -70,25 +171,27 @@ fun EmojiGrid(onClick: (EmojiItem) -> Unit, onExit: () -> Unit, onBackspace: ()
contentPadding = PaddingValues(10.dp),
modifier = Modifier.weight(1.0f)
) {
items(emojis) { emoji ->
Box(modifier = Modifier.fillMaxSize().clickable {
onClick(emoji)
}) {
Text(
text = emoji.emoji,
fontSize = 24.sp,
modifier = Modifier.align(Alignment.Center)
)
items(emojis, key = { it.emoji }) { emoji ->
Box(modifier = Modifier
.fillMaxSize()
.clickable {
onClick(emoji)
}) {
EmojiIcon(emoji.emoji, bitmaps)
}
}
}
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)) {
IconButton(onClick = { onExit() }) {
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),
contentColor = MaterialTheme.colorScheme.onBackground,
disabledContainerColor = MaterialTheme.colorScheme.outline,
@ -122,6 +225,7 @@ fun EmojiGrid(onClick: (EmojiItem) -> Unit, onExit: () -> Unit, onBackspace: ()
}
}
/*
@Preview(showBackground = true)
@Composable
fun EmojiGridPreview() {
@ -132,15 +236,49 @@ fun EmojiGridPreview() {
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(
icon = R.drawable.smile,
name = R.string.title_emojis,
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 {
@Composable
override fun windowName(): String {
@ -149,19 +287,21 @@ val EmojiAction = Action(
@Composable
override fun WindowContents() {
EmojiGrid(onClick = {
manager.typeText(it.emoji)
}, onExit = {
manager.closeActionWindow()
}, onSpace = {
manager.typeText(" ")
}, onBackspace = {
manager.backspace(1)
})
state.emojis.value?.let { emojis ->
EmojiGrid(onClick = {
manager.typeText(it.emoji)
}, onExit = {
manager.closeActionWindow()
}, onSpace = {
manager.typeText(" ")
}, onBackspace = {
manager.backspace(1)
}, bitmaps = state.bitmaps, emojis = emojis)
}
}
override fun close() {
state.bitmaps.freeAllBitmaps()
}
}
}