0
0
mirror of https://github.com/florisboard/florisboard.git synced 2024-09-19 19:42:20 +02:00

Merge pull request #1855 from florisboard/emoji-fixes-and-small-improvements

Emoji minor bug fixes / improvements
This commit is contained in:
Patrick Goldinger 2022-05-17 15:39:11 +02:00 committed by GitHub
commit d63792cb15
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 177 additions and 13 deletions

View File

@ -145,6 +145,17 @@
android:resource="@xml/file_paths"/>
</provider>
<!-- Disable default EmojiCompat initializer -->
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="androidx.emoji2.text.EmojiCompatInitializer"
tools:node="remove"/>
</provider>
</application>
</manifest>

View File

@ -28,6 +28,7 @@ import dev.patrickgold.florisboard.ime.core.SubtypeManager
import dev.patrickgold.florisboard.ime.dictionary.DictionaryManager
import dev.patrickgold.florisboard.ime.editor.EditorInstance
import dev.patrickgold.florisboard.ime.keyboard.KeyboardManager
import dev.patrickgold.florisboard.ime.media.emoji.FlorisEmojiCompat
import dev.patrickgold.florisboard.ime.nlp.NlpManager
import dev.patrickgold.florisboard.ime.spelling.SpellingManager
import dev.patrickgold.florisboard.ime.spelling.SpellingService
@ -89,6 +90,7 @@ class FlorisApplication : Application() {
flogOutputs = Flog.OUTPUT_CONSOLE,
)
CrashUtility.install(this)
FlorisEmojiCompat.init(this)
if (!UserManagerCompat.isUserUnlocked(this)) {
val context = createDeviceProtectedStorageContext()

View File

@ -16,6 +16,7 @@
package dev.patrickgold.florisboard.ime.media
import android.annotation.SuppressLint
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.forEachGesture
import androidx.compose.foundation.gestures.waitForUpOrCancellation
@ -31,6 +32,7 @@ import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@ -47,9 +49,11 @@ import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.input.InputEventDispatcher
import dev.patrickgold.florisboard.ime.input.LocalInputFeedbackController
import dev.patrickgold.florisboard.ime.keyboard.FlorisImeSizing
import dev.patrickgold.florisboard.ime.keyboard.KeyData
import dev.patrickgold.florisboard.ime.media.emoji.EmojiPaletteView
import dev.patrickgold.florisboard.ime.media.emoji.PlaceholderLayoutDataMap
import dev.patrickgold.florisboard.ime.media.emoji.parseRawEmojiSpecsFile
import dev.patrickgold.florisboard.ime.text.keyboard.TextKeyData
import dev.patrickgold.florisboard.ime.theme.FlorisImeTheme
@ -58,6 +62,7 @@ import dev.patrickgold.florisboard.keyboardManager
import dev.patrickgold.florisboard.lib.snygg.ui.SnyggSurface
import kotlinx.coroutines.coroutineScope
@SuppressLint("MutableCollectionMutableState")
@Composable
fun MediaInputLayout(
modifier: Modifier = Modifier,
@ -65,6 +70,11 @@ fun MediaInputLayout(
val context = LocalContext.current
val keyboardManager by context.keyboardManager()
var emojiLayoutDataMap by remember { mutableStateOf(PlaceholderLayoutDataMap) }
LaunchedEffect(Unit) {
emojiLayoutDataMap = parseRawEmojiSpecsFile(context, "ime/media/emoji/root.txt")
}
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
Column(
modifier = modifier
@ -73,7 +83,7 @@ fun MediaInputLayout(
) {
EmojiPaletteView(
modifier = Modifier.weight(1f),
fullEmojiMappings = parseRawEmojiSpecsFile(context, "ime/media/emoji/root.txt"),
fullEmojiMappings = emojiLayoutDataMap,
)
Row(
modifier = Modifier
@ -108,6 +118,7 @@ internal fun KeyboardLikeButton(
keyData: KeyData,
content: @Composable RowScope.() -> Unit,
) {
val inputFeedbackController = LocalInputFeedbackController.current
var isPressed by remember { mutableStateOf(false) }
val keyStyle = FlorisImeTheme.style.get(
element = FlorisImeUi.EmojiKey,
@ -122,6 +133,7 @@ internal fun KeyboardLikeButton(
awaitFirstDown(requireUnconsumed = false).also { it.consumeDownChange() }
isPressed = true
inputEventDispatcher.sendDown(keyData)
inputFeedbackController.keyPress(keyData)
val up = waitForUpOrCancellation()
isPressed = false
if (up != null) {

View File

@ -26,6 +26,12 @@ import java.util.*
*/
typealias EmojiLayoutDataMap = EnumMap<EmojiCategory, MutableList<EmojiSet>>
val PlaceholderLayoutDataMap = EmojiLayoutDataMap(EmojiCategory::class.java).also { map ->
for (category in EmojiCategory.values()) {
map[category] = mutableListOf()
}
}
private var cachedEmojiLayoutMap: EmojiLayoutDataMap? = null
/**

View File

@ -25,6 +25,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@ -79,7 +80,9 @@ import com.google.accompanist.flowlayout.FlowRow
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.florisPreferenceModel
import dev.patrickgold.florisboard.editorInstance
import dev.patrickgold.florisboard.ime.input.LocalInputFeedbackController
import dev.patrickgold.florisboard.ime.keyboard.FlorisImeSizing
import dev.patrickgold.florisboard.ime.text.keyboard.TextKeyData
import dev.patrickgold.florisboard.ime.theme.FlorisImeTheme
import dev.patrickgold.florisboard.ime.theme.FlorisImeUi
import dev.patrickgold.florisboard.keyboardManager
@ -87,7 +90,6 @@ import dev.patrickgold.florisboard.lib.android.showShortToast
import dev.patrickgold.florisboard.lib.compose.florisScrollbar
import dev.patrickgold.florisboard.lib.compose.safeTimes
import dev.patrickgold.florisboard.lib.compose.stringRes
import dev.patrickgold.florisboard.lib.kotlin.tryOrNull
import dev.patrickgold.florisboard.lib.snygg.ui.snyggBackground
import dev.patrickgold.florisboard.lib.snygg.ui.snyggBorder
import dev.patrickgold.florisboard.lib.snygg.ui.snyggShadow
@ -133,8 +135,9 @@ fun EmojiPaletteView(
}
}
val metadataVersion = activeEditorInfo.emojiCompatMetadataVersion
val emojiCompatInstance = tryOrNull { EmojiCompat.get().takeIf { it.loadState == EmojiCompat.LOAD_STATE_SUCCEEDED } }
val emojiMappings = remember(emojiCompatInstance, metadataVersion, systemFontPaint) {
val replaceAll = activeEditorInfo.emojiCompatReplaceAll
val emojiCompatInstance by FlorisEmojiCompat.getAsFlow(replaceAll).collectAsState()
val emojiMappings = remember(emojiCompatInstance, fullEmojiMappings, metadataVersion, systemFontPaint) {
fullEmojiMappings.mapValues { (_, emojiSetList) ->
emojiSetList.mapNotNull { emojiSet ->
emojiSet.emojis.filter { emoji ->
@ -248,6 +251,7 @@ private fun EmojiCategoriesTabRow(
activeCategory: EmojiCategory,
onCategoryChange: (EmojiCategory) -> Unit,
) {
val inputFeedbackController = LocalInputFeedbackController.current
val tabStyle = FlorisImeTheme.style.get(element = FlorisImeUi.EmojiTab)
val tabStyleFocused = FlorisImeTheme.style.get(element = FlorisImeUi.EmojiTab, isFocus = true)
val unselectedContentColor = tabStyle.foreground.solidColor(default = FlorisImeTheme.fallbackContentColor())
@ -273,7 +277,10 @@ private fun EmojiCategoriesTabRow(
) {
for (category in EmojiCategoryValues) {
Tab(
onClick = { onCategoryChange(category) },
onClick = {
inputFeedbackController.keyPress(TextKeyData.UNSPECIFIED)
onCategoryChange(category)
},
selected = activeCategory == category,
icon = { Icon(
modifier = Modifier.size(ButtonDefaults.IconSize),
@ -298,19 +305,24 @@ private fun EmojiKey(
onEmojiInput: (Emoji) -> Unit,
onLongPress: (Emoji) -> Unit,
) {
val inputFeedbackController = LocalInputFeedbackController.current
val base = emojiSet.base(withSkinTone = preferredSkinTone)
val variations = emojiSet.variations(withoutSkinTone = preferredSkinTone)
var showVariantsBox by remember { mutableStateOf(false) }
Box(
modifier = Modifier
.height(FlorisImeSizing.smartbarHeight)
.aspectRatio(1f)
.pointerInput(Unit) {
detectTapGestures(
onPress = {
inputFeedbackController.keyPress(TextKeyData.UNSPECIFIED)
},
onTap = {
onEmojiInput(base)
},
onLongPress = {
inputFeedbackController.keyLongPress(TextKeyData.UNSPECIFIED)
onLongPress(base)
if (variations.isNotEmpty()) {
showVariantsBox = true

View File

@ -0,0 +1,127 @@
/*
* Copyright (C) 2022 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.media.emoji
import android.annotation.SuppressLint
import android.content.Context
import androidx.emoji2.text.DefaultEmojiCompatConfig
import androidx.emoji2.text.EmojiCompat
import dev.patrickgold.florisboard.lib.devtools.flogError
import dev.patrickgold.florisboard.lib.devtools.flogInfo
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
/**
* Helper object which manages two separate EmojiCompat instances, something EmojiCompat by default does not want us
* to do for unknown reasons. Additionally we implement a proper loaded callback and a state flow, so the UI can always
* receive the EmojiCompat instance as soon as it is loaded. This helper still uses the default config and thus relies
* either on a system font with emoji or Google GMS services with their downloadable font provider.
*
* TODO: investigate how AOSP-like ROMs without any GMS services installed handle backwards emoji compatibility. Same
* goes for newer Huawei devices, which are subjected to no Google services. (Probably these devices rely on the good
* old method of just querying the system painter, which we already use as a fallback in the palette logic).
*
* TODO: investigate if having two instances of EmojiCompat has significant memory impact. Based on the docs one
* instance has ~300kB, so two should have ~600kB, which should not cause issues.
*
* TODO: investigate if having two instances of EmojiCompat causes other logic issues or if there's a better way of
* achieving the same result than the current implementation does.
*/
object FlorisEmojiCompat {
private lateinit var instanceNoReplace: InstanceHandler
private lateinit var instanceReplaceAll: InstanceHandler
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
/**
* Initialize this helper and its EmojiCompat instances with given [context]. Immediately begins loading the emoji
* metadata in a background thread. After this method has been called, it is safe to call [getAsFlow].
*/
fun init(context: Context) {
instanceNoReplace = InstanceHandler(context, replaceAll = false)
instanceReplaceAll = InstanceHandler(context, replaceAll = true)
scope.launch {
instanceNoReplace.load()
}
scope.launch {
instanceReplaceAll.load()
}
}
/**
* Gets the current EmojiCompat instance based on [replaceAll] and sets it as the default instance if
* [setAsDefaultInstance] is true. Calling this method before [init] will cause an exception to be thrown.
*
* @return A state flow providing the latest EmojiCompat instance for given args. The flow may provide null if
* EmojiCompat is still loading or if it has failed.
*/
@SuppressLint("RestrictedApi")
fun getAsFlow(replaceAll: Boolean, setAsDefaultInstance: Boolean = true): StateFlow<EmojiCompat?> {
val instanceFlow = if (replaceAll) {
instanceReplaceAll.publishedInstanceFlow
} else {
instanceNoReplace.publishedInstanceFlow
}
val instance = instanceFlow.value
if (setAsDefaultInstance && instance != null) {
flogInfo { "Set default EmojiCompat instance to $instance(replaceAll=$replaceAll)" }
// This API is not really supposed to be used by third-party apps, but it is really handy and does
// exactly what we need, so we suppress the restriction here
EmojiCompat.reset(instance)
}
return instanceFlow
}
private class InstanceHandler(context: Context, replaceAll: Boolean = false) {
private val initCallback: EmojiCompat.InitCallback = object : EmojiCompat.InitCallback() {
override fun onInitialized() {
super.onInitialized()
flogInfo { "EmojiCompat(replaceAll=$replaceAll) successfully loaded!" }
publishedInstanceFlow.value = instance
}
override fun onFailed(throwable: Throwable?) {
super.onFailed(throwable)
flogError { "EmojiCompat(replaceAll=$replaceAll) failed to load: $throwable" }
}
}
private val config: EmojiCompat.Config? = DefaultEmojiCompatConfig.create(context)?.apply {
setReplaceAll(replaceAll)
setMetadataLoadStrategy(EmojiCompat.LOAD_STRATEGY_MANUAL)
registerInitCallback(initCallback)
}
// Despite its name, `EmojiCompat.reset()` actually creates a new instance, exactly what we need
private val instance: EmojiCompat? = if (config != null) EmojiCompat.reset(config) else null
val publishedInstanceFlow = MutableStateFlow<EmojiCompat?>(null)
/**
* Manually loads the EmojiCompat instance. Call this method on a background thread to avoid blocking main.
*
* @see EmojiCompat.load
*/
fun load() {
instance?.load()
}
}
}

View File

@ -1,11 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#000000" android:pathData="M3,2h8v2h-8z"/>
<path android:fillColor="#000000" android:pathData="M6,11l2,0l0,-4l3,0l0,-2l-8,0l0,2l3,0z"/>
<path android:fillColor="#000000" android:pathData="M12.4036,20.1819l7.7781,-7.7781l1.4142,1.4142l-7.7781,7.7781z"/>
<path android:fillColor="#000000" android:pathData="M14.5,14.5m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0"/>
<path android:fillColor="#000000" android:pathData="M19.5,19.5m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0"/>
<path android:fillColor="#000000" android:pathData="M15.5,11c1.38,0 2.5,-1.12 2.5,-2.5V4h3V2h-4v4.51C16.58,6.19 16.07,6 15.5,6C14.12,6 13,7.12 13,8.5C13,9.88 14.12,11 15.5,11z"/>
<path android:fillColor="#000000" android:pathData="M9.74,15.96l-1.41,1.41l-0.71,-0.71l0.35,-0.35c0.98,-0.98 0.98,-2.56 0,-3.54c-0.49,-0.49 -1.13,-0.73 -1.77,-0.73c-0.64,0 -1.28,0.24 -1.77,0.73c-0.98,0.98 -0.98,2.56 0,3.54l0.35,0.35l-1.06,1.06c-0.98,0.98 -0.98,2.56 0,3.54C4.22,21.76 4.86,22 5.5,22s1.28,-0.24 1.77,-0.73l1.06,-1.06l1.41,1.41l1.41,-1.41l-1.41,-1.41l1.41,-1.41L9.74,15.96zM5.85,14.2c0.12,-0.12 0.26,-0.15 0.35,-0.15s0.23,0.03 0.35,0.15c0.19,0.2 0.19,0.51 0,0.71l-0.35,0.35L5.85,14.9C5.66,14.71 5.66,14.39 5.85,14.2zM5.85,19.85C5.73,19.97 5.59,20 5.5,20s-0.23,-0.03 -0.35,-0.15c-0.19,-0.19 -0.19,-0.51 0,-0.71l1.06,-1.06l0.71,0.71L5.85,19.85z"/>
<path android:fillColor="#000000" android:pathData="M20,4H4C2.9,4 2,4.9 2,6v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V6C22,4.9 21.1,4 20,4zM7.64,15H6.49v-4.5l-0.9,0.66l-0.58,-0.89L6.77,9h0.87V15zM13.5,15H9.61v-1.02c1.07,-1.07 1.77,-1.77 2.13,-2.15c0.4,-0.42 0.54,-0.69 0.54,-1.06c0,-0.4 -0.31,-0.72 -0.81,-0.72c-0.52,0 -0.8,0.39 -0.9,0.72l-1.01,-0.42c0.01,-0.02 0.18,-0.76 1,-1.15c0.69,-0.33 1.48,-0.2 1.95,0.03c0.86,0.44 0.91,1.24 0.91,1.48c0,0.64 -0.31,1.26 -0.92,1.86c-0.25,0.25 -0.72,0.71 -1.4,1.39l0.03,0.05h2.37V15zM18.75,14.15C18.67,14.28 18.19,15 16.99,15c-0.04,0 -1.6,0.08 -2.05,-1.51l1.03,-0.41c0.03,0.1 0.19,0.86 1.02,0.86c0.41,0 0.89,-0.28 0.89,-0.77c0,-0.55 -0.48,-0.79 -1.04,-0.79h-0.5v-1h0.46c0.33,0 0.88,-0.14 0.88,-0.72c0,-0.39 -0.31,-0.65 -0.75,-0.65c-0.5,0 -0.74,0.32 -0.85,0.64l-0.99,-0.41C15.2,9.9 15.68,9 16.94,9c1.09,0 1.54,0.64 1.62,0.75c0.33,0.5 0.28,1.16 0.02,1.57c-0.15,0.22 -0.32,0.38 -0.52,0.48v0.07c0.28,0.11 0.51,0.28 0.68,0.52C19.11,12.91 19.07,13.66 18.75,14.15z"/>
</vector>