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
|
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user