mirror of
https://github.com/florisboard/florisboard.git
synced 2024-09-20 03:52:18 +02:00
Merge pull request #1855 from florisboard/emoji-fixes-and-small-improvements
Emoji minor bug fixes / improvements
This commit is contained in:
commit
d63792cb15
@ -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>
|
||||
|
@ -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()
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user