mirror of
https://gitlab.futo.org/keyboard/latinime.git
synced 2024-09-28 14:54:30 +01:00
Draw emojis in emoji menu asynchronously
This commit is contained in:
parent
314cf8c84c
commit
c3018cdd86
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user