mirror of
https://github.com/SimpleMobileTools/Simple-Keyboard.git
synced 2025-06-05 21:49:26 +02:00
added emoji picker wit the feature of
1. able to get the recent used emoji 2. able to search emoji
This commit is contained in:
@@ -103,4 +103,8 @@ dependencies {
|
|||||||
|
|
||||||
implementation(libs.bundles.room)
|
implementation(libs.bundles.room)
|
||||||
ksp(libs.androidx.room.compiler)
|
ksp(libs.androidx.room.compiler)
|
||||||
|
|
||||||
|
implementation (project(":emojipicker"))
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,197 @@
|
|||||||
|
package com.simplemobiletools.keyboard.helpers;
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.Editable
|
||||||
|
import android.text.Spanned
|
||||||
|
import android.text.style.SuggestionSpan
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.inputmethod.BaseInputConnection
|
||||||
|
import android.view.inputmethod.CompletionInfo
|
||||||
|
import android.view.inputmethod.CorrectionInfo
|
||||||
|
import android.view.inputmethod.ExtractedText
|
||||||
|
import android.view.inputmethod.ExtractedTextRequest
|
||||||
|
import android.widget.SearchView
|
||||||
|
import android.widget.TextView
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Source: https://stackoverflow.com/a/39460124
|
||||||
|
*/
|
||||||
|
class OtherInputConnection(private val mTextView: androidx.appcompat.widget.AppCompatAutoCompleteTextView?) : BaseInputConnection(
|
||||||
|
mTextView!!, true
|
||||||
|
) {
|
||||||
|
// Keeps track of nested begin/end batch edit to ensure this connection always has a
|
||||||
|
// balanced impact on its associated TextView.
|
||||||
|
// A negative value means that this connection has been finished by the InputMethodManager.
|
||||||
|
private var mBatchEditNesting = 0
|
||||||
|
|
||||||
|
|
||||||
|
override fun getEditable(): Editable? {
|
||||||
|
val tv = mTextView
|
||||||
|
Log.i("heregotEditTEZT", tv!!.text.toString())
|
||||||
|
return tv?.editableText
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
override fun beginBatchEdit(): Boolean {
|
||||||
|
synchronized(this) {
|
||||||
|
if (mBatchEditNesting >= 0) {
|
||||||
|
mTextView!!.beginBatchEdit()
|
||||||
|
mBatchEditNesting++
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun endBatchEdit(): Boolean {
|
||||||
|
synchronized(this) {
|
||||||
|
if (mBatchEditNesting > 0) {
|
||||||
|
// When the connection is reset by the InputMethodManager and reportFinish
|
||||||
|
// is called, some endBatchEdit calls may still be asynchronously received from the
|
||||||
|
// IME. Do not take these into account, thus ensuring that this IC's final
|
||||||
|
// contribution to mTextView's nested batch edit count is zero.
|
||||||
|
mTextView!!.endBatchEdit()
|
||||||
|
mBatchEditNesting--
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
//clear the meta key states means shift, alt, ctrl
|
||||||
|
override fun clearMetaKeyStates(states: Int): Boolean {
|
||||||
|
val content = editable ?: return false
|
||||||
|
val kl = mTextView!!.keyListener //listen keyevents like a, enter, space
|
||||||
|
if (kl != null) {
|
||||||
|
try {
|
||||||
|
kl.clearMetaKeyState(mTextView, content, states)
|
||||||
|
} catch (e: AbstractMethodError) {
|
||||||
|
// This is an old listener that doesn't implement the
|
||||||
|
// new method.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
//When a user selects a suggestion from an autocomplete or suggestion list, the input method may call commitCompletion
|
||||||
|
override fun commitCompletion(text: CompletionInfo): Boolean {
|
||||||
|
if (DEBUG) Log.v(
|
||||||
|
TAG,
|
||||||
|
"commitCompletion $text"
|
||||||
|
)
|
||||||
|
mTextView!!.beginBatchEdit()
|
||||||
|
mTextView.onCommitCompletion(text)
|
||||||
|
mTextView.endBatchEdit()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
which is used to commit a correction to a previously entered text.
|
||||||
|
This correction could be suggested by the input method or obtained through some other means.
|
||||||
|
*/
|
||||||
|
override fun commitCorrection(correctionInfo: CorrectionInfo): Boolean {
|
||||||
|
if (DEBUG) Log.v(
|
||||||
|
TAG,
|
||||||
|
"commitCorrection$correctionInfo"
|
||||||
|
)
|
||||||
|
mTextView!!.beginBatchEdit()
|
||||||
|
mTextView.onCommitCorrection(correctionInfo)
|
||||||
|
mTextView.endBatchEdit()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/* It's used to simulate the action associated with an editor action, typically triggered by pressing the "Done" or "Enter" key on the keyboard.*/
|
||||||
|
override fun performEditorAction(actionCode: Int): Boolean {
|
||||||
|
if (DEBUG) Log.v(
|
||||||
|
TAG,
|
||||||
|
"performEditorAction $actionCode"
|
||||||
|
)
|
||||||
|
mTextView!!.onEditorAction(actionCode)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
handle actions triggered from the context menu associated with the search text.
|
||||||
|
This menu typically appears when you long-press on the search text field.
|
||||||
|
*/
|
||||||
|
override fun performContextMenuAction(id: Int): Boolean {
|
||||||
|
if (DEBUG) Log.v(
|
||||||
|
TAG,
|
||||||
|
"performContextMenuAction $id"
|
||||||
|
)
|
||||||
|
mTextView!!.beginBatchEdit()
|
||||||
|
mTextView.onTextContextMenuItem(id)
|
||||||
|
mTextView.endBatchEdit()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/*It is used to retrieve information about the currently extracted text
|
||||||
|
* eg- selected text, the start and end offsets, the total number of characters, and more.*/
|
||||||
|
override fun getExtractedText(request: ExtractedTextRequest, flags: Int): ExtractedText? {
|
||||||
|
if (mTextView != null) {
|
||||||
|
val et = ExtractedText()
|
||||||
|
if (mTextView.extractText(request, et)) {
|
||||||
|
if (flags and GET_EXTRACTED_TEXT_MONITOR != 0) {
|
||||||
|
// mTextView.setExtracting(request);
|
||||||
|
}
|
||||||
|
return et
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// API to send private commands from an input method to its connected editor. This can be used to provide domain-specific features
|
||||||
|
override fun performPrivateCommand(action: String, data: Bundle): Boolean {
|
||||||
|
mTextView!!.onPrivateIMECommand(action, data)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
//send the text to the connected editor from the keyboard pressed
|
||||||
|
override fun commitText(
|
||||||
|
text: CharSequence,
|
||||||
|
newCursorPosition: Int
|
||||||
|
): Boolean {
|
||||||
|
if (mTextView == null) {
|
||||||
|
return super.commitText(text, newCursorPosition)
|
||||||
|
}
|
||||||
|
if (text is Spanned) {
|
||||||
|
val spans = text.getSpans(
|
||||||
|
0, text.length,
|
||||||
|
SuggestionSpan::class.java
|
||||||
|
)
|
||||||
|
// mIMM.registerSuggestionSpansForNotification(spans);
|
||||||
|
}
|
||||||
|
|
||||||
|
// mTextView.resetErrorChangedFlag();
|
||||||
|
// mTextView.hideErrorIfUnchanged();
|
||||||
|
return super.commitText(text, newCursorPosition)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun requestCursorUpdates(cursorUpdateMode: Int): Boolean {
|
||||||
|
if (DEBUG) Log.v(
|
||||||
|
TAG,
|
||||||
|
"requestUpdateCursorAnchorInfo $cursorUpdateMode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// It is possible that any other bit is used as a valid flag in a future release.
|
||||||
|
// We should reject the entire request in such a case.
|
||||||
|
val KNOWN_FLAGS_MASK = CURSOR_UPDATE_IMMEDIATE or CURSOR_UPDATE_MONITOR
|
||||||
|
val unknownFlags = cursorUpdateMode and KNOWN_FLAGS_MASK.inv()
|
||||||
|
if (unknownFlags != 0) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(
|
||||||
|
TAG,
|
||||||
|
"Rejecting requestUpdateCursorAnchorInfo due to unknown flags. cursorUpdateMode=$cursorUpdateMode unknownFlags=$unknownFlags"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val DEBUG = false
|
||||||
|
private val TAG = "loool"
|
||||||
|
}
|
||||||
|
}
|
@@ -41,4 +41,9 @@ interface OnKeyboardActionListener {
|
|||||||
* Called to force the KeyboardView to reload the keyboard
|
* Called to force the KeyboardView to reload the keyboard
|
||||||
*/
|
*/
|
||||||
fun reloadKeyboard()
|
fun reloadKeyboard()
|
||||||
|
|
||||||
|
/*
|
||||||
|
* called when focus on the searchview */
|
||||||
|
fun searchViewFocused(searchView: androidx.appcompat.widget.AppCompatAutoCompleteTextView)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -13,6 +13,7 @@ import android.os.Build
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.InputType.*
|
import android.text.InputType.*
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
|
import android.util.Log
|
||||||
import android.util.Size
|
import android.util.Size
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@@ -23,6 +24,8 @@ import android.view.inputmethod.EditorInfo.IME_FLAG_NO_ENTER_ACTION
|
|||||||
import android.view.inputmethod.EditorInfo.IME_MASK_ACTION
|
import android.view.inputmethod.EditorInfo.IME_MASK_ACTION
|
||||||
import android.widget.inline.InlinePresentationSpec
|
import android.widget.inline.InlinePresentationSpec
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.appcompat.widget.AppCompatAutoCompleteTextView
|
||||||
|
import androidx.appcompat.widget.SearchView
|
||||||
import androidx.autofill.inline.UiVersions
|
import androidx.autofill.inline.UiVersions
|
||||||
import androidx.autofill.inline.common.ImageViewStyle
|
import androidx.autofill.inline.common.ImageViewStyle
|
||||||
import androidx.autofill.inline.common.TextViewStyle
|
import androidx.autofill.inline.common.TextViewStyle
|
||||||
@@ -60,9 +63,17 @@ class SimpleKeyboardIME : InputMethodService(), OnKeyboardActionListener, Shared
|
|||||||
private var enterKeyType = IME_ACTION_NONE
|
private var enterKeyType = IME_ACTION_NONE
|
||||||
private var switchToLetters = false
|
private var switchToLetters = false
|
||||||
private var breakIterator: BreakIterator? = null
|
private var breakIterator: BreakIterator? = null
|
||||||
|
private var otherInputConnection:OtherInputConnection? = null
|
||||||
|
|
||||||
private lateinit var binding: KeyboardViewKeyboardBinding
|
private lateinit var binding: KeyboardViewKeyboardBinding
|
||||||
|
|
||||||
|
companion object{
|
||||||
|
/*true and false define the inputconnection where is input is send like
|
||||||
|
* if true-> send to emoji searchview
|
||||||
|
* if false -> send to currentinputconnection*/
|
||||||
|
var searching=false
|
||||||
|
}
|
||||||
|
|
||||||
override fun onInitializeInterface() {
|
override fun onInitializeInterface() {
|
||||||
super.onInitializeInterface()
|
super.onInitializeInterface()
|
||||||
safeStorageContext.getSharedPrefs().registerOnSharedPreferenceChangeListener(this)
|
safeStorageContext.getSharedPrefs().registerOnSharedPreferenceChangeListener(this)
|
||||||
@@ -106,7 +117,7 @@ class SimpleKeyboardIME : InputMethodService(), OnKeyboardActionListener, Shared
|
|||||||
|
|
||||||
val editorInfo = currentInputEditorInfo
|
val editorInfo = currentInputEditorInfo
|
||||||
if (config.enableSentencesCapitalization && editorInfo != null && editorInfo.inputType != TYPE_NULL) {
|
if (config.enableSentencesCapitalization && editorInfo != null && editorInfo.inputType != TYPE_NULL) {
|
||||||
if (currentInputConnection.getCursorCapsMode(editorInfo.inputType) != 0) {
|
if (currentInputConnection.getCursorCapsMode(editorInfo.inputType) != 0 && !searching) {
|
||||||
keyboard?.setShifted(ShiftState.ON_ONE_CHAR)
|
keyboard?.setShifted(ShiftState.ON_ONE_CHAR)
|
||||||
keyboardView?.invalidateAllKeys()
|
keyboardView?.invalidateAllKeys()
|
||||||
return
|
return
|
||||||
@@ -149,7 +160,7 @@ class SimpleKeyboardIME : InputMethodService(), OnKeyboardActionListener, Shared
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onKey(code: Int) {
|
override fun onKey(code: Int) {
|
||||||
val inputConnection = currentInputConnection
|
val inputConnection = getMyCurrentInputConnection()
|
||||||
if (keyboard == null || inputConnection == null) {
|
if (keyboard == null || inputConnection == null) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -216,7 +227,10 @@ class SimpleKeyboardIME : InputMethodService(), OnKeyboardActionListener, Shared
|
|||||||
}
|
}
|
||||||
|
|
||||||
MyKeyboard.KEYCODE_EMOJI -> {
|
MyKeyboard.KEYCODE_EMOJI -> {
|
||||||
keyboardView?.openEmojiPalette()
|
if(!searching){
|
||||||
|
keyboardView?.openEmojiPalette()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
@@ -324,7 +338,8 @@ class SimpleKeyboardIME : InputMethodService(), OnKeyboardActionListener, Shared
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onText(text: String) {
|
override fun onText(text: String) {
|
||||||
currentInputConnection?.commitText(text, 1)
|
getMyCurrentInputConnection().commitText(text, 1)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun reloadKeyboard() {
|
override fun reloadKeyboard() {
|
||||||
@@ -333,6 +348,10 @@ class SimpleKeyboardIME : InputMethodService(), OnKeyboardActionListener, Shared
|
|||||||
keyboardView?.setKeyboard(keyboard)
|
keyboardView?.setKeyboard(keyboard)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun searchViewFocused(searchView: AppCompatAutoCompleteTextView) {
|
||||||
|
otherInputConnection = OtherInputConnection(searchView)
|
||||||
|
}
|
||||||
|
|
||||||
private fun createNewKeyboard(): MyKeyboard {
|
private fun createNewKeyboard(): MyKeyboard {
|
||||||
val keyboardXml = when (inputTypeClass) {
|
val keyboardXml = when (inputTypeClass) {
|
||||||
TYPE_CLASS_NUMBER -> {
|
TYPE_CLASS_NUMBER -> {
|
||||||
@@ -485,4 +504,20 @@ class SimpleKeyboardIME : InputMethodService(), OnKeyboardActionListener, Shared
|
|||||||
|
|
||||||
return Icon.createWithData(byteArray, 0, byteArray.size)
|
return Icon.createWithData(byteArray, 0, byteArray.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getMyCurrentInputConnection():InputConnection{
|
||||||
|
if (searching){
|
||||||
|
if(otherInputConnection==null){
|
||||||
|
Log.i("thisISrunn", "yes2")
|
||||||
|
return currentInputConnection
|
||||||
|
}else{
|
||||||
|
Log.i("thisISrunn", "yes")
|
||||||
|
return otherInputConnection!!
|
||||||
|
}
|
||||||
|
|
||||||
|
}else{
|
||||||
|
Log.i("thisISrunn", "yes3")
|
||||||
|
return currentInputConnection
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -15,7 +15,11 @@ import android.os.Build
|
|||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.os.Message
|
import android.os.Message
|
||||||
|
import android.text.Editable
|
||||||
|
import android.text.Html
|
||||||
|
import android.text.TextWatcher
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
|
import android.util.Log
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.*
|
import android.view.*
|
||||||
import android.view.animation.AccelerateInterpolator
|
import android.view.animation.AccelerateInterpolator
|
||||||
@@ -34,6 +38,7 @@ import androidx.emoji2.text.EmojiCompat.EMOJI_SUPPORTED
|
|||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup
|
import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import com.rishabh.emojipicker.EmojiPickerView
|
||||||
import com.simplemobiletools.commons.extensions.*
|
import com.simplemobiletools.commons.extensions.*
|
||||||
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
|
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
|
||||||
import com.simplemobiletools.commons.helpers.isPiePlus
|
import com.simplemobiletools.commons.helpers.isPiePlus
|
||||||
@@ -60,6 +65,7 @@ import com.simplemobiletools.keyboard.interfaces.RefreshClipsListener
|
|||||||
import com.simplemobiletools.keyboard.models.Clip
|
import com.simplemobiletools.keyboard.models.Clip
|
||||||
import com.simplemobiletools.keyboard.models.ClipsSectionLabel
|
import com.simplemobiletools.keyboard.models.ClipsSectionLabel
|
||||||
import com.simplemobiletools.keyboard.models.ListItem
|
import com.simplemobiletools.keyboard.models.ListItem
|
||||||
|
import com.simplemobiletools.keyboard.services.SimpleKeyboardIME.Companion.searching
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@SuppressLint("UseCompatLoadingForDrawables", "ClickableViewAccessibility")
|
@SuppressLint("UseCompatLoadingForDrawables", "ClickableViewAccessibility")
|
||||||
@@ -288,6 +294,7 @@ class MyKeyboardView @JvmOverloads constructor(context: Context, attrs: Attribut
|
|||||||
accessHelper = AccessHelper(this, mKeyboard?.mKeys.orEmpty())
|
accessHelper = AccessHelper(this, mKeyboard?.mKeys.orEmpty())
|
||||||
ViewCompat.setAccessibilityDelegate(this, accessHelper)
|
ViewCompat.setAccessibilityDelegate(this, accessHelper)
|
||||||
|
|
||||||
|
|
||||||
// Not really necessary to do every time, but will free up views
|
// Not really necessary to do every time, but will free up views
|
||||||
// Switching to a different keyboard should abort any pending keys so that the key up
|
// Switching to a different keyboard should abort any pending keys so that the key up
|
||||||
// doesn't get delivered to the old or new keyboard
|
// doesn't get delivered to the old or new keyboard
|
||||||
@@ -297,7 +304,7 @@ class MyKeyboardView @JvmOverloads constructor(context: Context, attrs: Attribut
|
|||||||
/** Sets the top row above the keyboard containing a couple buttons and the clipboard **/
|
/** Sets the top row above the keyboard containing a couple buttons and the clipboard **/
|
||||||
fun setKeyboardHolder(binding: KeyboardViewKeyboardBinding) {
|
fun setKeyboardHolder(binding: KeyboardViewKeyboardBinding) {
|
||||||
keyboardViewBinding = binding.apply {
|
keyboardViewBinding = binding.apply {
|
||||||
mToolbarHolder = toolbarHolder
|
mToolbarHolder = mainToolbarKeyboardHolder
|
||||||
mClipboardManagerHolder = clipboardManagerHolder
|
mClipboardManagerHolder = clipboardManagerHolder
|
||||||
mEmojiPaletteHolder = emojiPaletteHolder
|
mEmojiPaletteHolder = emojiPaletteHolder
|
||||||
|
|
||||||
@@ -354,10 +361,7 @@ class MyKeyboardView @JvmOverloads constructor(context: Context, attrs: Attribut
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
emojiPaletteClose.setOnClickListener {
|
|
||||||
vibrateIfNeeded()
|
|
||||||
closeEmojiPalette()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1439,10 +1443,19 @@ class MyKeyboardView @JvmOverloads constructor(context: Context, attrs: Attribut
|
|||||||
|
|
||||||
private fun setupEmojiPalette(toolbarColor: Int, backgroundColor: Int, textColor: Int) {
|
private fun setupEmojiPalette(toolbarColor: Int, backgroundColor: Int, textColor: Int) {
|
||||||
keyboardViewBinding?.apply {
|
keyboardViewBinding?.apply {
|
||||||
emojiPaletteTopBar.background = ColorDrawable(toolbarColor)
|
emojiSearchToolbar.background = ColorDrawable(toolbarColor)
|
||||||
emojiPaletteHolder.background = ColorDrawable(backgroundColor)
|
emojiPaletteHolder.background = ColorDrawable(backgroundColor)
|
||||||
emojiPaletteClose.applyColorFilter(textColor)
|
emojiPaletteClose.applyColorFilter(textColor)
|
||||||
emojiPaletteLabel.setTextColor(textColor)
|
|
||||||
|
emojiSearchView.setHintTextColor(mTextColor)
|
||||||
|
emojiSearchviewCross.applyColorFilter(textColor)
|
||||||
|
emojiSearchViewSearchIcon.applyColorFilter(textColor)
|
||||||
|
emojiSearchResult.background = ColorDrawable(backgroundColor)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
emojiPaletteBottomBar.background = ColorDrawable(backgroundColor)
|
emojiPaletteBottomBar.background = ColorDrawable(backgroundColor)
|
||||||
emojiPaletteModeChange.apply {
|
emojiPaletteModeChange.apply {
|
||||||
@@ -1481,37 +1494,106 @@ class MyKeyboardView @JvmOverloads constructor(context: Context, attrs: Attribut
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setupEmojis()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun openEmojiPalette() {
|
fun openEmojiPalette() {
|
||||||
keyboardViewBinding!!.emojiPaletteHolder.beVisible()
|
keyboardViewBinding!!.emojiPaletteHolder.beVisible()
|
||||||
setupEmojis()
|
keyboardViewBinding!!.emojiSearchToolbar.beVisible()
|
||||||
|
keyboardViewBinding!!.mainToolbarKeyboardHolder.beGone()
|
||||||
|
|
||||||
|
/*when any emoji is picked from the emoji Picker*/
|
||||||
|
keyboardViewBinding!!.emojiPickerView.setOnEmojiPickedListener{
|
||||||
|
mOnKeyboardActionListener?.onText(it.emoji)
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEmojiSearch()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun closeEmojiPalette() {
|
|
||||||
|
|
||||||
|
private fun setupEmojiSearch(){
|
||||||
keyboardViewBinding?.apply {
|
keyboardViewBinding?.apply {
|
||||||
emojiPaletteHolder.beGone()
|
emojiSearchView.setOnFocusChangeListener(object : View.OnFocusChangeListener{
|
||||||
emojisList?.scrollToPosition(0)
|
override fun onFocusChange(v: View?, hasFocus: Boolean) {
|
||||||
|
emojiPaletteHolder.beGone()
|
||||||
|
mainToolbarKeyboardHolder.beGone()
|
||||||
|
// emojiSearchView.text.clear()
|
||||||
|
|
||||||
|
//change the input connection
|
||||||
|
searching=true
|
||||||
|
mOnKeyboardActionListener?.searchViewFocused(emojiSearchView)
|
||||||
|
|
||||||
|
//show emoji result
|
||||||
|
emojiSearchResult.beVisible()
|
||||||
|
|
||||||
|
|
||||||
|
/*It's s interface it runn when someone picked ffrom the emoji search result suggestion*/
|
||||||
|
val emojipickedFromSuggestion= object : EmojiPickerView.EmojiPickedFromSuggestion {
|
||||||
|
override fun pickedEmoji(emoji: String) {
|
||||||
|
searching =false
|
||||||
|
mOnKeyboardActionListener?.onText(emoji)
|
||||||
|
searching =true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emojiSearchResult.emojiPickedFromSuggestion = (emojipickedFromSuggestion)
|
||||||
|
|
||||||
|
|
||||||
|
/*when nothing is typed then in the showSearchResult show the recend only*/
|
||||||
|
keyboardViewBinding?.apply {
|
||||||
|
emojiSearchResult.bodyAdapter.hideTitleAndEmptyHint = true
|
||||||
|
emojiSearchResult.emojiPickerItems = emojiSearchResult.buildEmojiPickerItems(onlyRecentEmojies = true)
|
||||||
|
emojiSearchResult.bodyAdapter.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
emojiPaletteClose.setOnClickListener {
|
||||||
|
vibrateIfNeeded()
|
||||||
|
closeEmojiPalette()
|
||||||
|
}
|
||||||
|
|
||||||
|
emojiSearchviewCross.setOnClickListener {
|
||||||
|
emojiSearchView.text.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
emojiSearchView.addTextChangedListener(object:TextWatcher{
|
||||||
|
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||||
|
|
||||||
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||||
|
|
||||||
|
override fun afterTextChanged(s: Editable?) {
|
||||||
|
emojiSearchResult.emojiPickerItems = emojiSearchResult.buildEmojiPickerItems(false,s.toString())
|
||||||
|
emojiSearchResult.bodyAdapter.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupEmojis() {
|
private fun closeEmojiPalette() {
|
||||||
ensureBackgroundThread {
|
|
||||||
val fullEmojiList = parseRawEmojiSpecsFile(context, EMOJI_SPEC_FILE_PATH)
|
keyboardViewBinding?.apply {
|
||||||
val systemFontPaint = Paint().apply {
|
|
||||||
typeface = Typeface.DEFAULT
|
|
||||||
|
if(emojiPaletteHolder.isVisible){
|
||||||
|
emojiPaletteHolder.beGone()
|
||||||
|
emojiSearchToolbar.beGone()
|
||||||
|
mainToolbarKeyboardHolder.beVisible()
|
||||||
|
}else{
|
||||||
|
emojiSearchView.clearFocus()
|
||||||
|
emojiSearchView.text.clear()
|
||||||
|
emojiPaletteHolder.beVisible()
|
||||||
}
|
}
|
||||||
|
|
||||||
val emojis = fullEmojiList.filter { emoji ->
|
searching =false
|
||||||
systemFontPaint.hasGlyph(emoji.emoji) || (EmojiCompat.get().loadState == EmojiCompat.LOAD_STATE_SUCCEEDED && EmojiCompat.get()
|
keyboardViewBinding?.emojiSearchResult?.beGone()
|
||||||
.getEmojiMatch(emoji.emoji, emojiCompatMetadataVersion) == EMOJI_SUPPORTED)
|
|
||||||
}
|
|
||||||
|
|
||||||
Handler(Looper.getMainLooper()).post {
|
|
||||||
setupEmojiAdapter(emojis)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1522,83 +1604,7 @@ class MyKeyboardView @JvmOverloads constructor(context: Context, attrs: Attribut
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupEmojiAdapter(emojis: List<EmojiData>) {
|
|
||||||
val categories = emojis.groupBy { it.category }
|
|
||||||
val allItems = mutableListOf<EmojisAdapter.Item>()
|
|
||||||
categories.entries.forEach { (category, emojis) ->
|
|
||||||
allItems.add(EmojisAdapter.Item.Category(category))
|
|
||||||
allItems.addAll(emojis.map(EmojisAdapter.Item::Emoji))
|
|
||||||
}
|
|
||||||
val checkIds = mutableMapOf<Int, String>()
|
|
||||||
keyboardViewBinding?.emojiCategoriesStrip?.apply {
|
|
||||||
weightSum = categories.count().toFloat()
|
|
||||||
val strip = this
|
|
||||||
removeAllViews()
|
|
||||||
categories.entries.forEach { (category, emojis) ->
|
|
||||||
ItemEmojiCategoryBinding.inflate(LayoutInflater.from(context), this, true).apply {
|
|
||||||
root.id = generateViewId()
|
|
||||||
checkIds[root.id] = category
|
|
||||||
root.setImageResource(emojis.first().getCategoryIcon())
|
|
||||||
root.layoutParams = LinearLayout.LayoutParams(
|
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
|
||||||
1f
|
|
||||||
)
|
|
||||||
root.setOnClickListener {
|
|
||||||
strip.children.filterIsInstance<ImageButton>().forEach {
|
|
||||||
it.imageTintList = ColorStateList.valueOf(mTextColor)
|
|
||||||
}
|
|
||||||
root.imageTintList = ColorStateList.valueOf(context.getProperPrimaryColor())
|
|
||||||
keyboardViewBinding?.emojisList?.stopScroll()
|
|
||||||
(keyboardViewBinding?.emojisList?.layoutManager as? GridLayoutManager)?.scrollToPositionWithOffset(
|
|
||||||
allItems.indexOfFirst { it is EmojisAdapter.Item.Category && it.value == category },
|
|
||||||
0
|
|
||||||
)
|
|
||||||
}
|
|
||||||
root.imageTintList = ColorStateList.valueOf(mTextColor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
keyboardViewBinding?.emojisList?.apply {
|
|
||||||
val emojiItemWidth = context.resources.getDimensionPixelSize(R.dimen.emoji_item_size)
|
|
||||||
val emojiTopBarElevation = context.resources.getDimensionPixelSize(R.dimen.emoji_top_bar_elevation).toFloat()
|
|
||||||
|
|
||||||
layoutManager = AutoGridLayoutManager(context, emojiItemWidth).apply {
|
|
||||||
spanSizeLookup = object : SpanSizeLookup() {
|
|
||||||
override fun getSpanSize(position: Int): Int =
|
|
||||||
if (allItems[position] is EmojisAdapter.Item.Category) {
|
|
||||||
spanCount
|
|
||||||
} else {
|
|
||||||
1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
adapter = EmojisAdapter(context = context, items = allItems) { emoji ->
|
|
||||||
mOnKeyboardActionListener!!.onText(emoji.emoji)
|
|
||||||
vibrateIfNeeded()
|
|
||||||
}
|
|
||||||
|
|
||||||
clearOnScrollListeners()
|
|
||||||
onScroll {
|
|
||||||
keyboardViewBinding!!.emojiPaletteTopBar.elevation = if (it > 4) emojiTopBarElevation else 0f
|
|
||||||
(keyboardViewBinding?.emojisList?.layoutManager as? LinearLayoutManager)?.findFirstCompletelyVisibleItemPosition()?.also { firstVisibleIndex ->
|
|
||||||
allItems
|
|
||||||
.withIndex()
|
|
||||||
.lastOrNull { it.value is EmojisAdapter.Item.Category && it.index <= firstVisibleIndex }
|
|
||||||
?.also { activeCategory ->
|
|
||||||
val id = checkIds.entries.first { it.value == (activeCategory.value as EmojisAdapter.Item.Category).value }.key
|
|
||||||
keyboardViewBinding?.emojiCategoriesStrip?.children?.filterIsInstance<ImageButton>()?.forEach {
|
|
||||||
if (it.id == id) {
|
|
||||||
it.imageTintList = ColorStateList.valueOf(context.getProperPrimaryColor())
|
|
||||||
} else {
|
|
||||||
it.imageTintList = ColorStateList.valueOf(mTextColor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun closing() {
|
private fun closing() {
|
||||||
if (mPreviewPopup.isShowing) {
|
if (mPreviewPopup.isShowing) {
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
<com.simplemobiletools.commons.views.MyRecyclerView
|
<com.simplemobiletools.commons.views.MyRecyclerView
|
||||||
android:id="@+id/emojis_list"
|
android:id="@+id/emojiPickerView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
|
@@ -5,8 +5,90 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<com.rishabh.emojipicker.EmojiPickerView
|
||||||
|
android:id="@+id/emojiSearchResult"
|
||||||
|
app:usedInSearchResult="true"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="170dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/emoji_search_toolbar"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!--top bar of emoji search-->
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:id="@+id/toolbar_holder"
|
android:id="@+id/emoji_search_toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="@dimen/toolbar_height"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/keyboard_view"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/emoji_palette_close"
|
||||||
|
android:layout_width="@dimen/toolbar_icon_height"
|
||||||
|
android:layout_height="@dimen/toolbar_icon_height"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:layout_marginStart="@dimen/medium_margin"
|
||||||
|
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||||
|
android:padding="@dimen/small_margin"
|
||||||
|
android:src="@drawable/ic_arrow_left_vector"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<!-- add a linearlayout with horizontal orientaion and add two button first at start and end-->
|
||||||
|
<!-- then at start is search and end is cross button -->
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/emojiSearchViewSearchIcon"
|
||||||
|
android:layout_width="28dp"
|
||||||
|
android:layout_height="28dp"
|
||||||
|
android:layout_gravity="center_vertical|left"
|
||||||
|
android:layout_marginStart="10dp"
|
||||||
|
android:src="@drawable/ic_search_vector"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/emoji_palette_close"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatAutoCompleteTextView
|
||||||
|
android:id="@+id/emoji_search_view"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:layout_toEndOf="@+id/emoji_palette_close"
|
||||||
|
android:completionThreshold="1"
|
||||||
|
android:theme="@style/Theme.AppCompat.DayNight"
|
||||||
|
android:focusable="true"
|
||||||
|
android:hint="@string/search_emoji"
|
||||||
|
android:focusableInTouchMode="true"
|
||||||
|
android:padding="10dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/emojiSearchviewCross"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/emojiSearchViewSearchIcon"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/emojiSearchviewCross"
|
||||||
|
android:layout_width="30dp"
|
||||||
|
android:layout_height="30dp"
|
||||||
|
android:layout_marginRight="16dp"
|
||||||
|
android:src="@drawable/ic_cross_vector"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/emoji_search_view"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<!--toolbar of main keyboard-->
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/mainToolbarKeyboardHolder"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="@dimen/toolbar_height"
|
android:layout_height="@dimen/toolbar_height"
|
||||||
android:layout_above="@+id/keyboard_view"
|
android:layout_above="@+id/keyboard_view"
|
||||||
@@ -121,6 +203,8 @@
|
|||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent" />
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
|
||||||
|
<!--emoji section-->
|
||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
android:id="@+id/emoji_palette_holder"
|
android:id="@+id/emoji_palette_holder"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@@ -131,50 +215,18 @@
|
|||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="@+id/toolbar_holder">
|
app:layout_constraintTop_toBottomOf="@id/emoji_search_toolbar">
|
||||||
|
|
||||||
<RelativeLayout
|
|
||||||
android:id="@+id/emoji_palette_top_bar"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="@dimen/toolbar_height"
|
|
||||||
android:layout_alignParentStart="true"
|
|
||||||
android:layout_alignParentTop="true"
|
|
||||||
android:gravity="center_vertical">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/emoji_palette_close"
|
|
||||||
android:layout_width="@dimen/toolbar_icon_height"
|
|
||||||
android:layout_height="@dimen/toolbar_icon_height"
|
|
||||||
android:layout_centerVertical="true"
|
|
||||||
android:layout_marginStart="@dimen/medium_margin"
|
|
||||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
|
||||||
android:contentDescription="@string/emojis"
|
|
||||||
android:padding="@dimen/small_margin"
|
|
||||||
android:src="@drawable/ic_arrow_left_vector" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/emoji_palette_label"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_centerVertical="true"
|
|
||||||
android:layout_marginStart="@dimen/medium_margin"
|
|
||||||
android:layout_toEndOf="@+id/emoji_palette_close"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:lines="1"
|
|
||||||
android:text="@string/emojis"
|
|
||||||
android:textSize="@dimen/big_text_size" />
|
|
||||||
|
|
||||||
</RelativeLayout>
|
|
||||||
|
|
||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
android:id="@+id/emoji_content_holder"
|
android:id="@+id/emoji_content_holder"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_above="@id/emoji_palette_bottom_bar"
|
android:layout_above="@id/emoji_palette_bottom_bar">
|
||||||
android:layout_below="@+id/emoji_palette_top_bar">
|
|
||||||
|
|
||||||
<com.simplemobiletools.commons.views.MyRecyclerView
|
<com.rishabh.emojipicker.EmojiPickerView
|
||||||
android:id="@+id/emojis_list"
|
android:id="@+id/emojiPickerView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
@@ -237,7 +289,7 @@
|
|||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="@+id/toolbar_holder">
|
app:layout_constraintTop_toTopOf="@+id/mainToolbarKeyboardHolder">
|
||||||
|
|
||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
android:id="@+id/clipboard_manager_top_bar"
|
android:id="@+id/clipboard_manager_top_bar"
|
||||||
|
@@ -38,6 +38,7 @@
|
|||||||
<string name="start_sentences_capitalized">بدء الجمل بحرف كبير</string>
|
<string name="start_sentences_capitalized">بدء الجمل بحرف كبير</string>
|
||||||
<!-- Emojis -->
|
<!-- Emojis -->
|
||||||
<string name="emojis">الرموز التعبيرية</string>
|
<string name="emojis">الرموز التعبيرية</string>
|
||||||
|
<string name="search_emoji">Search Emoji</string>
|
||||||
<!--
|
<!--
|
||||||
Haven't found some strings? There's more at
|
Haven't found some strings? There's more at
|
||||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||||
|
@@ -38,6 +38,7 @@
|
|||||||
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
||||||
<!-- Emojis -->
|
<!-- Emojis -->
|
||||||
<string name="emojis">Эмодзі</string>
|
<string name="emojis">Эмодзі</string>
|
||||||
|
<string name="search_emoji">Search Emoji</string>
|
||||||
<!--
|
<!--
|
||||||
Haven't found some strings? There's more at
|
Haven't found some strings? There's more at
|
||||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||||
|
@@ -38,6 +38,7 @@
|
|||||||
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
||||||
<!-- Emojis -->
|
<!-- Emojis -->
|
||||||
<string name="emojis">Емоджита</string>
|
<string name="emojis">Емоджита</string>
|
||||||
|
<string name="search_emoji">Search Emoji</string>
|
||||||
<!--
|
<!--
|
||||||
Haven't found some strings? There's more at
|
Haven't found some strings? There's more at
|
||||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||||
|
@@ -38,6 +38,7 @@
|
|||||||
<string name="start_sentences_capitalized">Comença les frases amb una lletra majúscula</string>
|
<string name="start_sentences_capitalized">Comença les frases amb una lletra majúscula</string>
|
||||||
<!-- Emojis -->
|
<!-- Emojis -->
|
||||||
<string name="emojis">Emojis</string>
|
<string name="emojis">Emojis</string>
|
||||||
|
<string name="search_emoji">Search Emoji</string>
|
||||||
<!--
|
<!--
|
||||||
Haven't found some strings? There's more at
|
Haven't found some strings? There's more at
|
||||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||||
|
@@ -38,4 +38,5 @@
|
|||||||
<string name="start_sentences_capitalized">گەورەکردنی یەکەم پیتی لاتینی</string>
|
<string name="start_sentences_capitalized">گەورەکردنی یەکەم پیتی لاتینی</string>
|
||||||
<!-- Emojis -->
|
<!-- Emojis -->
|
||||||
<string name="emojis">خەندەکان</string>
|
<string name="emojis">خەندەکان</string>
|
||||||
|
<string name="search_emoji">Search Emoji</string>
|
||||||
</resources>
|
</resources>
|
@@ -38,6 +38,7 @@
|
|||||||
<string name="start_sentences_capitalized">Začínat věty velkým písmenem</string>
|
<string name="start_sentences_capitalized">Začínat věty velkým písmenem</string>
|
||||||
<!-- Emojis -->
|
<!-- Emojis -->
|
||||||
<string name="emojis">Emotikony</string>
|
<string name="emojis">Emotikony</string>
|
||||||
|
<string name="search_emoji">Search Emoji</string>
|
||||||
<!--
|
<!--
|
||||||
Haven't found some strings? There's more at
|
Haven't found some strings? There's more at
|
||||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||||
|
@@ -38,6 +38,7 @@
|
|||||||
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
||||||
<!-- Emojis -->
|
<!-- Emojis -->
|
||||||
<string name="emojis">Emojis</string>
|
<string name="emojis">Emojis</string>
|
||||||
|
<string name="search_emoji">Search Emoji</string>
|
||||||
<!--
|
<!--
|
||||||
Haven't found some strings? There's more at
|
Haven't found some strings? There's more at
|
||||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||||
|
@@ -38,6 +38,7 @@
|
|||||||
<string name="start_sentences_capitalized">Sätze mit einem Großbuchstaben beginnen</string>
|
<string name="start_sentences_capitalized">Sätze mit einem Großbuchstaben beginnen</string>
|
||||||
<!-- Emojis -->
|
<!-- Emojis -->
|
||||||
<string name="emojis">Emojis</string>
|
<string name="emojis">Emojis</string>
|
||||||
|
<string name="search_emoji">Search Emoji</string>
|
||||||
<!--
|
<!--
|
||||||
Haven't found some strings? There's more at
|
Haven't found some strings? There's more at
|
||||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||||
|
@@ -38,6 +38,7 @@
|
|||||||
<string name="start_sentences_capitalized">Αρχίστε τις προτάσεις με κεφαλαίο γράμμα</string>
|
<string name="start_sentences_capitalized">Αρχίστε τις προτάσεις με κεφαλαίο γράμμα</string>
|
||||||
<!-- Emojis -->
|
<!-- Emojis -->
|
||||||
<string name="emojis">Emojis</string>
|
<string name="emojis">Emojis</string>
|
||||||
|
<string name="search_emoji">Search Emoji</string>
|
||||||
<!--
|
<!--
|
||||||
Haven't found some strings? There's more at
|
Haven't found some strings? There's more at
|
||||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||||
|
@@ -38,6 +38,7 @@
|
|||||||
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
||||||
<!-- Emojis -->
|
<!-- Emojis -->
|
||||||
<string name="emojis">Emojis</string>
|
<string name="emojis">Emojis</string>
|
||||||
|
<string name="search_emoji">Search Emoji</string>
|
||||||
<!--
|
<!--
|
||||||
Haven't found some strings? There's more at
|
Haven't found some strings? There's more at
|
||||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||||
|
@@ -38,6 +38,7 @@
|
|||||||
<string name="start_sentences_capitalized">Empezar las frases con mayúsculas</string>
|
<string name="start_sentences_capitalized">Empezar las frases con mayúsculas</string>
|
||||||
<!-- Emojis -->
|
<!-- Emojis -->
|
||||||
<string name="emojis">Emoticonos</string>
|
<string name="emojis">Emoticonos</string>
|
||||||
|
<string name="search_emoji">Search Emoji</string>
|
||||||
<!--
|
<!--
|
||||||
Haven't found some strings? There's more at
|
Haven't found some strings? There's more at
|
||||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||||
|
@@ -38,6 +38,7 @@
|
|||||||
<string name="start_sentences_capitalized">Alusta lauseid suurtähega</string>
|
<string name="start_sentences_capitalized">Alusta lauseid suurtähega</string>
|
||||||
<!-- Emojis -->
|
<!-- Emojis -->
|
||||||
<string name="emojis">Emojid</string>
|
<string name="emojis">Emojid</string>
|
||||||
|
<string name="search_emoji">Search Emoji</string>
|
||||||
<!--
|
<!--
|
||||||
Haven't found some strings? There's more at
|
Haven't found some strings? There's more at
|
||||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||||
|
@@ -38,6 +38,7 @@
|
|||||||
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
||||||
<!-- Emojis -->
|
<!-- Emojis -->
|
||||||
<string name="emojis">Emojit</string>
|
<string name="emojis">Emojit</string>
|
||||||
|
<string name="search_emoji">Search Emoji</string>
|
||||||
<!--
|
<!--
|
||||||
Haven't found some strings? There's more at
|
Haven't found some strings? There's more at
|
||||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||||
|
@@ -38,6 +38,7 @@
|
|||||||
<string name="start_sentences_capitalized">Commencer les phrases par une majuscule</string>
|
<string name="start_sentences_capitalized">Commencer les phrases par une majuscule</string>
|
||||||
<!-- Emojis -->
|
<!-- Emojis -->
|
||||||
<string name="emojis">Émojis</string>
|
<string name="emojis">Émojis</string>
|
||||||
|
<string name="search_emoji">Search Emoji</string>
|
||||||
<!--
|
<!--
|
||||||
Haven't found some strings? There's more at
|
Haven't found some strings? There's more at
|
||||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||||
|
@@ -38,6 +38,7 @@
|
|||||||
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
||||||
<!-- Emojis -->
|
<!-- Emojis -->
|
||||||
<string name="emojis">Emoticona</string>
|
<string name="emojis">Emoticona</string>
|
||||||
|
<string name="search_emoji">Search Emoji</string>
|
||||||
<!--
|
<!--
|
||||||
Haven't found some strings? There's more at
|
Haven't found some strings? There's more at
|
||||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||||
|
@@ -38,6 +38,7 @@
|
|||||||
<string name="start_sentences_capitalized">Počni rečenice s velikim slovom</string>
|
<string name="start_sentences_capitalized">Počni rečenice s velikim slovom</string>
|
||||||
<!-- Emojis -->
|
<!-- Emojis -->
|
||||||
<string name="emojis">Emoji</string>
|
<string name="emojis">Emoji</string>
|
||||||
|
<string name="search_emoji">Search Emoji</string>
|
||||||
<!--
|
<!--
|
||||||
Haven't found some strings? There's more at
|
Haven't found some strings? There's more at
|
||||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||||
|
@@ -38,6 +38,7 @@
|
|||||||
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
||||||
<!-- Emojis -->
|
<!-- Emojis -->
|
||||||
<string name="emojis">Emojik</string>
|
<string name="emojis">Emojik</string>
|
||||||
|
<string name="search_emoji">Search Emoji</string>
|
||||||
<!--
|
<!--
|
||||||
Haven't found some strings? There's more at
|
Haven't found some strings? There's more at
|
||||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||||
|
@@ -38,6 +38,7 @@
|
|||||||
<string name="start_sentences_capitalized">Mulai kalimat dengan huruf kapital</string>
|
<string name="start_sentences_capitalized">Mulai kalimat dengan huruf kapital</string>
|
||||||
<!-- Emojis -->
|
<!-- Emojis -->
|
||||||
<string name="emojis">Emoji</string>
|
<string name="emojis">Emoji</string>
|
||||||
|
<string name="search_emoji">Search Emoji</string>
|
||||||
<!--
|
<!--
|
||||||
Haven't found some strings? There's more at
|
Haven't found some strings? There's more at
|
||||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||||
|
@@ -38,6 +38,7 @@
|
|||||||
<string name="start_sentences_capitalized">Inizia le frasi con la lettera maiuscola</string>
|
<string name="start_sentences_capitalized">Inizia le frasi con la lettera maiuscola</string>
|
||||||
<!-- Emojis -->
|
<!-- Emojis -->
|
||||||
<string name="emojis">Emoji</string>
|
<string name="emojis">Emoji</string>
|
||||||
|
<string name="search_emoji">Search Emoji</string>
|
||||||
<!--
|
<!--
|
||||||
Haven't found some strings? There's more at
|
Haven't found some strings? There's more at
|
||||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||||
|
@@ -38,6 +38,7 @@
|
|||||||
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
||||||
<!-- Emojis -->
|
<!-- Emojis -->
|
||||||
<string name="emojis">Emojis</string>
|
<string name="emojis">Emojis</string>
|
||||||
|
<string name="search_emoji">Search Emoji</string>
|
||||||
<!--
|
<!--
|
||||||
Haven't found some strings? There's more at
|
Haven't found some strings? There's more at
|
||||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||||
|
@@ -38,6 +38,7 @@
|
|||||||
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
||||||
<!-- Emojis -->
|
<!-- Emojis -->
|
||||||
<string name="emojis">絵文字</string>
|
<string name="emojis">絵文字</string>
|
||||||
|
<string name="search_emoji">Search Emoji</string>
|
||||||
<!--
|
<!--
|
||||||
Haven't found some strings? There's more at
|
Haven't found some strings? There's more at
|
||||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||||
|
@@ -38,6 +38,7 @@
|
|||||||
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
||||||
<!-- Emojis -->
|
<!-- Emojis -->
|
||||||
<string name="emojis">Emojis</string>
|
<string name="emojis">Emojis</string>
|
||||||
|
<string name="search_emoji">Search Emoji</string>
|
||||||
<!--
|
<!--
|
||||||
Haven't found some strings? There's more at
|
Haven't found some strings? There's more at
|
||||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||||
|
@@ -38,6 +38,7 @@
|
|||||||
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
||||||
<!-- Emojis -->
|
<!-- Emojis -->
|
||||||
<string name="emojis">Emojis</string>
|
<string name="emojis">Emojis</string>
|
||||||
|
<string name="search_emoji">Search Emoji</string>
|
||||||
<!--
|
<!--
|
||||||
Haven't found some strings? There's more at
|
Haven't found some strings? There's more at
|
||||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||||
|
@@ -38,6 +38,7 @@
|
|||||||
<string name="start_sentences_capitalized">Sākt teikumus ar lielo burtu</string>
|
<string name="start_sentences_capitalized">Sākt teikumus ar lielo burtu</string>
|
||||||
<!-- Emojis -->
|
<!-- Emojis -->
|
||||||
<string name="emojis">Emocijzīmes</string>
|
<string name="emojis">Emocijzīmes</string>
|
||||||
|
<string name="search_emoji">Search Emoji</string>
|
||||||
<!--
|
<!--
|
||||||
Haven't found some strings? There's more at
|
Haven't found some strings? There's more at
|
||||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||||
|
@@ -38,4 +38,5 @@
|
|||||||
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
||||||
<!-- Emojis -->
|
<!-- Emojis -->
|
||||||
<string name="emojis">ഇമോജികൾ</string>
|
<string name="emojis">ഇമോജികൾ</string>
|
||||||
|
<string name="search_emoji">Search Emoji</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@@ -38,6 +38,7 @@
|
|||||||
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
||||||
<!-- Emojis -->
|
<!-- Emojis -->
|
||||||
<string name="emojis">Emojis</string>
|
<string name="emojis">Emojis</string>
|
||||||
|
<string name="search_emoji">Search Emoji</string>
|
||||||
<!--
|
<!--
|
||||||
Haven't found some strings? There's more at
|
Haven't found some strings? There's more at
|
||||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||||
|
4
app/src/main/res/values-night-v31/strings.xml
Normal file
4
app/src/main/res/values-night-v31/strings.xml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="search_emoji">Search Emoji</string>
|
||||||
|
</resources>
|
@@ -38,6 +38,7 @@
|
|||||||
<string name="start_sentences_capitalized">Zinnen met een hoofdletter beginnen</string>
|
<string name="start_sentences_capitalized">Zinnen met een hoofdletter beginnen</string>
|
||||||
<!-- Emojis -->
|
<!-- Emojis -->
|
||||||
<string name="emojis">Emoji\'s</string>
|
<string name="emojis">Emoji\'s</string>
|
||||||
|
<string name="search_emoji">Search Emoji</string>
|
||||||
<!--
|
<!--
|
||||||
Haven't found some strings? There's more at
|
Haven't found some strings? There's more at
|
||||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||||
|
@@ -38,4 +38,5 @@
|
|||||||
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
||||||
<!-- Emojis -->
|
<!-- Emojis -->
|
||||||
<string name="emojis">ایموجیاں</string>
|
<string name="emojis">ایموجیاں</string>
|
||||||
|
<string name="search_emoji">Search Emoji</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@@ -38,4 +38,5 @@
|
|||||||
<string name="start_sentences_capitalized">ਵਾਕ ਵੱਡੇ ਅੱਖਰ ਨਾਲ ਸ਼ੁਰੂ ਕਰੋ</string>
|
<string name="start_sentences_capitalized">ਵਾਕ ਵੱਡੇ ਅੱਖਰ ਨਾਲ ਸ਼ੁਰੂ ਕਰੋ</string>
|
||||||
<!-- Emojis -->
|
<!-- Emojis -->
|
||||||
<string name="emojis">Emojis</string>
|
<string name="emojis">Emojis</string>
|
||||||
|
<string name="search_emoji">Search Emoji</string>
|
||||||
</resources>
|
</resources>
|
@@ -38,6 +38,7 @@
|
|||||||
<string name="start_sentences_capitalized">Zaczynaj zdania wielką literą</string>
|
<string name="start_sentences_capitalized">Zaczynaj zdania wielką literą</string>
|
||||||
<!-- Emojis -->
|
<!-- Emojis -->
|
||||||
<string name="emojis">Emoji</string>
|
<string name="emojis">Emoji</string>
|
||||||
|
<string name="search_emoji">Search Emoji</string>
|
||||||
<!--
|
<!--
|
||||||
Haven't found some strings? There's more at
|
Haven't found some strings? There's more at
|
||||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||||
|
@@ -38,6 +38,7 @@
|
|||||||
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
||||||
<!-- Emojis -->
|
<!-- Emojis -->
|
||||||
<string name="emojis">Emojis</string>
|
<string name="emojis">Emojis</string>
|
||||||
|
<string name="search_emoji">Search Emoji</string>
|
||||||
<!--
|
<!--
|
||||||
Haven't found some strings? There's more at
|
Haven't found some strings? There's more at
|
||||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||||
|
@@ -38,6 +38,7 @@
|
|||||||
<string name="start_sentences_capitalized">Iniciar frases com letra maiúscula</string>
|
<string name="start_sentences_capitalized">Iniciar frases com letra maiúscula</string>
|
||||||
<!-- Emojis -->
|
<!-- Emojis -->
|
||||||
<string name="emojis">Emojis</string>
|
<string name="emojis">Emojis</string>
|
||||||
|
<string name="search_emoji">Search Emoji</string>
|
||||||
<!--
|
<!--
|
||||||
Haven't found some strings? There's more at
|
Haven't found some strings? There's more at
|
||||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||||
|
@@ -38,6 +38,7 @@
|
|||||||
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
||||||
<!-- Emojis -->
|
<!-- Emojis -->
|
||||||
<string name="emojis">Emoticoane</string>
|
<string name="emojis">Emoticoane</string>
|
||||||
|
<string name="search_emoji">Search Emoji</string>
|
||||||
<!--
|
<!--
|
||||||
Haven't found some strings? There's more at
|
Haven't found some strings? There's more at
|
||||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||||
|
@@ -38,6 +38,7 @@
|
|||||||
<string name="start_sentences_capitalized">Начинать предложения с заглавной буквы</string>
|
<string name="start_sentences_capitalized">Начинать предложения с заглавной буквы</string>
|
||||||
<!-- Emojis -->
|
<!-- Emojis -->
|
||||||
<string name="emojis">Эмодзи</string>
|
<string name="emojis">Эмодзи</string>
|
||||||
|
<string name="search_emoji">Search Emoji</string>
|
||||||
<!--
|
<!--
|
||||||
Haven't found some strings? There's more at
|
Haven't found some strings? There's more at
|
||||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||||
|
@@ -38,6 +38,7 @@
|
|||||||
<string name="start_sentences_capitalized">Začať vety veľkým písmenom</string>
|
<string name="start_sentences_capitalized">Začať vety veľkým písmenom</string>
|
||||||
<!-- Emojis -->
|
<!-- Emojis -->
|
||||||
<string name="emojis">Emoji</string>
|
<string name="emojis">Emoji</string>
|
||||||
|
<string name="search_emoji">Search Emoji</string>
|
||||||
<!--
|
<!--
|
||||||
Haven't found some strings? There's more at
|
Haven't found some strings? There's more at
|
||||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||||
|
@@ -38,4 +38,5 @@
|
|||||||
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
||||||
<!-- Emojis -->
|
<!-- Emojis -->
|
||||||
<string name="emojis">Emoji-ji</string>
|
<string name="emojis">Emoji-ji</string>
|
||||||
|
<string name="search_emoji">Search Emoji</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@@ -38,4 +38,5 @@
|
|||||||
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
||||||
<!-- Emojis -->
|
<!-- Emojis -->
|
||||||
<string name="emojis">Емоји</string>
|
<string name="emojis">Емоји</string>
|
||||||
|
<string name="search_emoji">Search Emoji</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@@ -38,6 +38,7 @@
|
|||||||
<string name="start_sentences_capitalized">Börja meningar med stor bokstav</string>
|
<string name="start_sentences_capitalized">Börja meningar med stor bokstav</string>
|
||||||
<!-- Emojis -->
|
<!-- Emojis -->
|
||||||
<string name="emojis">Emojier</string>
|
<string name="emojis">Emojier</string>
|
||||||
|
<string name="search_emoji">Search Emoji</string>
|
||||||
<!--
|
<!--
|
||||||
Haven't found some strings? There's more at
|
Haven't found some strings? There's more at
|
||||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||||
|
@@ -38,6 +38,7 @@
|
|||||||
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
||||||
<!-- Emojis -->
|
<!-- Emojis -->
|
||||||
<string name="emojis">Emojis</string>
|
<string name="emojis">Emojis</string>
|
||||||
|
<string name="search_emoji">Search Emoji</string>
|
||||||
<!--
|
<!--
|
||||||
Haven't found some strings? There's more at
|
Haven't found some strings? There's more at
|
||||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||||
|
@@ -38,6 +38,7 @@
|
|||||||
<string name="start_sentences_capitalized">Cümlelere büyük harfle başla</string>
|
<string name="start_sentences_capitalized">Cümlelere büyük harfle başla</string>
|
||||||
<!-- Emojis -->
|
<!-- Emojis -->
|
||||||
<string name="emojis">Emojiler</string>
|
<string name="emojis">Emojiler</string>
|
||||||
|
<string name="search_emoji">Search Emoji</string>
|
||||||
<!--
|
<!--
|
||||||
Haven't found some strings? There's more at
|
Haven't found some strings? There's more at
|
||||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||||
|
@@ -38,6 +38,7 @@
|
|||||||
<string name="start_sentences_capitalized">Починати речення з великої літери</string>
|
<string name="start_sentences_capitalized">Починати речення з великої літери</string>
|
||||||
<!-- Emojis -->
|
<!-- Emojis -->
|
||||||
<string name="emojis">Емодзі</string>
|
<string name="emojis">Емодзі</string>
|
||||||
|
<string name="search_emoji">Search Emoji</string>
|
||||||
<!--
|
<!--
|
||||||
Haven't found some strings? There's more at
|
Haven't found some strings? There's more at
|
||||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||||
|
4
app/src/main/res/values-v31/strings.xml
Normal file
4
app/src/main/res/values-v31/strings.xml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="search_emoji">Search Emoji</string>
|
||||||
|
</resources>
|
@@ -38,6 +38,7 @@
|
|||||||
<string name="start_sentences_capitalized">Bắt đầu câu bằng chữ in hoa</string>
|
<string name="start_sentences_capitalized">Bắt đầu câu bằng chữ in hoa</string>
|
||||||
<!-- Emojis -->
|
<!-- Emojis -->
|
||||||
<string name="emojis">Biểu tượng cảm xúc</string>
|
<string name="emojis">Biểu tượng cảm xúc</string>
|
||||||
|
<string name="search_emoji">Search Emoji</string>
|
||||||
<!--
|
<!--
|
||||||
Haven't found some strings? There's more at
|
Haven't found some strings? There's more at
|
||||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||||
|
@@ -38,6 +38,7 @@
|
|||||||
<string name="start_sentences_capitalized">句子开头使用大写字母</string>
|
<string name="start_sentences_capitalized">句子开头使用大写字母</string>
|
||||||
<!-- Emojis -->
|
<!-- Emojis -->
|
||||||
<string name="emojis">表情符号</string>
|
<string name="emojis">表情符号</string>
|
||||||
|
<string name="search_emoji">Search Emoji</string>
|
||||||
<!--
|
<!--
|
||||||
Haven't found some strings? There's more at
|
Haven't found some strings? There's more at
|
||||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||||
|
@@ -39,6 +39,7 @@
|
|||||||
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
||||||
<!-- Emojis -->
|
<!-- Emojis -->
|
||||||
<string name="emojis">Emojis</string>
|
<string name="emojis">Emojis</string>
|
||||||
|
<string name="search_emoji">Search Emoji</string>
|
||||||
<!--
|
<!--
|
||||||
Haven't found some strings? There's more at
|
Haven't found some strings? There's more at
|
||||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||||
|
@@ -38,6 +38,7 @@
|
|||||||
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
||||||
<!-- Emojis -->
|
<!-- Emojis -->
|
||||||
<string name="emojis">Emojis</string>
|
<string name="emojis">Emojis</string>
|
||||||
|
<string name="search_emoji">Search Emoji</string>
|
||||||
<!--
|
<!--
|
||||||
Haven't found some strings? There's more at
|
Haven't found some strings? There's more at
|
||||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android).apply(false)
|
alias(libs.plugins.android).apply(false)
|
||||||
alias(libs.plugins.kotlinAndroid).apply(false)
|
|
||||||
alias(libs.plugins.ksp).apply(false)
|
alias(libs.plugins.ksp).apply(false)
|
||||||
|
alias(libs.plugins.androidLibrary) apply false
|
||||||
|
alias(libs.plugins.kotlinAndroid) apply false
|
||||||
}
|
}
|
||||||
|
15
emojipicker/.gitignore
vendored
Normal file
15
emojipicker/.gitignore
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
*.iml
|
||||||
|
.gradle
|
||||||
|
/local.properties
|
||||||
|
/.idea/caches
|
||||||
|
/.idea/libraries
|
||||||
|
/.idea/modules.xml
|
||||||
|
/.idea/workspace.xml
|
||||||
|
/.idea/navEditor.xml
|
||||||
|
/.idea/assetWizardSettings.xml
|
||||||
|
.DS_Store
|
||||||
|
/build
|
||||||
|
/captures
|
||||||
|
.externalNativeBuild
|
||||||
|
.cxx
|
||||||
|
local.properties
|
3
emojipicker/.idea/.gitignore
generated
vendored
Normal file
3
emojipicker/.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
45
emojipicker/.idea/appInsightsSettings.xml
generated
Normal file
45
emojipicker/.idea/appInsightsSettings.xml
generated
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AppInsightsSettings">
|
||||||
|
<option name="tabSettings">
|
||||||
|
<map>
|
||||||
|
<entry key="Android Vitals">
|
||||||
|
<value>
|
||||||
|
<InsightsFilterSettings>
|
||||||
|
<option name="connection">
|
||||||
|
<ConnectionSetting>
|
||||||
|
<option name="appId" value="com.beetleInk.memest" />
|
||||||
|
</ConnectionSetting>
|
||||||
|
</option>
|
||||||
|
<option name="failureTypes">
|
||||||
|
<list>
|
||||||
|
<option value="FATAL" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
<option name="signal" value="SIGNAL_UNSPECIFIED" />
|
||||||
|
<option name="timeIntervalDays" value="SEVEN_DAYS" />
|
||||||
|
<option name="visibilityType" value="ALL" />
|
||||||
|
</InsightsFilterSettings>
|
||||||
|
</value>
|
||||||
|
</entry>
|
||||||
|
<entry key="Firebase Crashlytics">
|
||||||
|
<value>
|
||||||
|
<InsightsFilterSettings>
|
||||||
|
<option name="connection">
|
||||||
|
<ConnectionSetting>
|
||||||
|
<option name="appId" value="PLACEHOLDER" />
|
||||||
|
<option name="mobileSdkAppId" value="" />
|
||||||
|
<option name="projectId" value="" />
|
||||||
|
<option name="projectNumber" value="" />
|
||||||
|
</ConnectionSetting>
|
||||||
|
</option>
|
||||||
|
<option name="signal" value="SIGNAL_UNSPECIFIED" />
|
||||||
|
<option name="timeIntervalDays" value="THIRTY_DAYS" />
|
||||||
|
<option name="visibilityType" value="ALL" />
|
||||||
|
</InsightsFilterSettings>
|
||||||
|
</value>
|
||||||
|
</entry>
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
6
emojipicker/.idea/compiler.xml
generated
Normal file
6
emojipicker/.idea/compiler.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="CompilerConfiguration">
|
||||||
|
<bytecodeTargetLevel target="17" />
|
||||||
|
</component>
|
||||||
|
</project>
|
10
emojipicker/.idea/deploymentTargetDropDown.xml
generated
Normal file
10
emojipicker/.idea/deploymentTargetDropDown.xml
generated
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="deploymentTargetDropDown">
|
||||||
|
<value>
|
||||||
|
<entry key="app">
|
||||||
|
<State />
|
||||||
|
</entry>
|
||||||
|
</value>
|
||||||
|
</component>
|
||||||
|
</project>
|
19
emojipicker/.idea/gradle.xml
generated
Normal file
19
emojipicker/.idea/gradle.xml
generated
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||||
|
<component name="GradleSettings">
|
||||||
|
<option name="linkedExternalProjectsSettings">
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
|
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$" />
|
||||||
|
<option value="$PROJECT_DIR$/app" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
<option name="resolveExternalAnnotations" value="false" />
|
||||||
|
</GradleProjectSettings>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
6
emojipicker/.idea/kotlinc.xml
generated
Normal file
6
emojipicker/.idea/kotlinc.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="KotlinJpsPluginSettings">
|
||||||
|
<option name="version" value="1.9.22" />
|
||||||
|
</component>
|
||||||
|
</project>
|
10
emojipicker/.idea/migrations.xml
generated
Normal file
10
emojipicker/.idea/migrations.xml
generated
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectMigrations">
|
||||||
|
<option name="MigrateToGradleLocalJavaHome">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
9
emojipicker/.idea/misc.xml
generated
Normal file
9
emojipicker/.idea/misc.xml
generated
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<project version="4">
|
||||||
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
|
||||||
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectType">
|
||||||
|
<option name="id" value="Android" />
|
||||||
|
</component>
|
||||||
|
</project>
|
1
emojipicker/app/.gitignore
vendored
Normal file
1
emojipicker/app/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/build
|
60
emojipicker/app/build.gradle.kts
Normal file
60
emojipicker/app/build.gradle.kts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
plugins {
|
||||||
|
id("com.android.application")
|
||||||
|
id("org.jetbrains.kotlin.android")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.rishabh.emojipicker"
|
||||||
|
compileSdk = 34
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId = "com.rishabh.emojipicker"
|
||||||
|
minSdk = 24
|
||||||
|
targetSdk = 34
|
||||||
|
versionCode = 1
|
||||||
|
versionName = "1.0"
|
||||||
|
|
||||||
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
isMinifyEnabled = false
|
||||||
|
proguardFiles(
|
||||||
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
|
"proguard-rules.pro"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "1.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
|
||||||
|
implementation("androidx.core:core-ktx:1.13.1")
|
||||||
|
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||||
|
implementation("com.google.android.material:material:1.12.0")
|
||||||
|
implementation("androidx.emoji2:emoji2:1.4.0")
|
||||||
|
implementation("com.google.firebase:firebase-crashlytics-buildtools:3.0.0")
|
||||||
|
testImplementation("junit:junit:4.13.2")
|
||||||
|
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||||
|
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
|
||||||
|
implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.7.3")
|
||||||
|
api("androidx.core:core:1.9.0")
|
||||||
|
|
||||||
|
// Implementations
|
||||||
|
implementation("org.jetbrains.kotlin:kotlin-stdlib:1.9.22")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.7.3")
|
||||||
|
|
||||||
|
|
||||||
|
androidTestImplementation("androidx.test:core:1.5.0")
|
||||||
|
androidTestImplementation("androidx.test:runner:1.5.2")
|
||||||
|
androidTestImplementation("androidx.test:rules:1.5.0")
|
||||||
|
}
|
21
emojipicker/app/proguard-rules.pro
vendored
Normal file
21
emojipicker/app/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
9
emojipicker/app/src/androidTest/AndroidManifest.xml
Normal file
9
emojipicker/app/src/androidTest/AndroidManifest.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<application>
|
||||||
|
<uses-library android:name="android.test.runner"/>
|
||||||
|
<activity android:name="androidx.emoji2.emojipicker.EmojiPickerViewTestActivity"/>
|
||||||
|
<activity android:name="androidx.emoji2.emojipicker.EmojiViewTestActivity" />
|
||||||
|
</application>
|
||||||
|
</manifest>
|
@@ -0,0 +1,71 @@
|
|||||||
|
|
||||||
|
|
||||||
|
package com.rishabh.emojipicker
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.rishabh.emojipicker.utils.FileCache
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import androidx.test.filters.SdkSuppress
|
||||||
|
import androidx.test.filters.SmallTest
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
@SmallTest
|
||||||
|
class BundledEmojiListLoaderTest {
|
||||||
|
private val context = ApplicationProvider.getApplicationContext<Context>()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testGetCategorizedEmojiData_loaded_writeToCache() = runBlocking {
|
||||||
|
// delete cache dir first
|
||||||
|
val fileCache = FileCache.getInstance(context)
|
||||||
|
fileCache.emojiPickerCacheDir.deleteRecursively()
|
||||||
|
assertFalse(fileCache.emojiPickerCacheDir.exists())
|
||||||
|
|
||||||
|
BundledEmojiListLoader.load(context)
|
||||||
|
val result = BundledEmojiListLoader.getCategorizedEmojiData()
|
||||||
|
assertTrue(result.isNotEmpty())
|
||||||
|
|
||||||
|
// emoji_picker/osVersion|appVersion/ folder should be created
|
||||||
|
val propertyFolder = fileCache.emojiPickerCacheDir.listFiles()!![0]
|
||||||
|
assertTrue(propertyFolder!!.isDirectory)
|
||||||
|
|
||||||
|
// Number of cache files should match the size of categorizedEmojiData
|
||||||
|
val cacheFiles = propertyFolder.listFiles()
|
||||||
|
assertTrue(cacheFiles!!.size == result.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testGetCategorizedEmojiData_loaded_readFromCache() = runBlocking {
|
||||||
|
// delete cache and load again
|
||||||
|
val fileCache = FileCache.getInstance(context)
|
||||||
|
fileCache.emojiPickerCacheDir.deleteRecursively()
|
||||||
|
BundledEmojiListLoader.load(context)
|
||||||
|
|
||||||
|
val cacheFileName = fileCache.emojiPickerCacheDir.listFiles()!![0].listFiles()!![0].name
|
||||||
|
val emptyDefaultValue = listOf<EmojiViewItem>()
|
||||||
|
// Read from cache instead of using default value
|
||||||
|
var output = fileCache.getOrPut(cacheFileName) { emptyDefaultValue }
|
||||||
|
assertTrue(output.isNotEmpty())
|
||||||
|
|
||||||
|
// Remove cache, write default value to cache
|
||||||
|
fileCache.emojiPickerCacheDir.deleteRecursively()
|
||||||
|
output = fileCache.getOrPut(cacheFileName) { emptyDefaultValue }
|
||||||
|
assertTrue(output.isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SdkSuppress(minSdkVersion = 24)
|
||||||
|
fun testGetEmojiVariantsLookup_loaded() = runBlocking {
|
||||||
|
// delete cache and load again
|
||||||
|
FileCache.getInstance(context).emojiPickerCacheDir.deleteRecursively()
|
||||||
|
BundledEmojiListLoader.load(context)
|
||||||
|
val result = BundledEmojiListLoader.getEmojiVariantsLookup()
|
||||||
|
|
||||||
|
// 👃 has variants (👃,👃,👃🏻,👃🏼,👃🏽,👃🏾,👃🏿)
|
||||||
|
assertTrue(result["\uD83D\uDC43"]!!.contains("\uD83D\uDC43\uD83C\uDFFD"))
|
||||||
|
// 😀 has no variant
|
||||||
|
assertFalse(result.containsKey("\uD83D\uDE00"))
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,233 @@
|
|||||||
|
|
||||||
|
|
||||||
|
package com.rishabh.emojipicker
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageView
|
||||||
|
import androidx.core.view.children
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import com.rishabh.emojipicker.R as EmojiPickerViewR
|
||||||
|
import com.rishabh.emojipicker.test.R
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import androidx.test.espresso.Espresso.onView
|
||||||
|
import androidx.test.espresso.action.ViewActions.click
|
||||||
|
import androidx.test.espresso.action.ViewActions.longClick
|
||||||
|
import androidx.test.espresso.contrib.RecyclerViewActions
|
||||||
|
import androidx.test.espresso.matcher.BoundedMatcher
|
||||||
|
import androidx.test.espresso.matcher.RootMatchers.hasWindowLayoutParams
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||||
|
import androidx.test.ext.junit.rules.ActivityScenarioRule
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.filters.LargeTest
|
||||||
|
import androidx.test.filters.SdkSuppress
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import org.hamcrest.Description
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNotNull
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Ignore
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
class EmojiPickerViewTestActivity : Activity() {
|
||||||
|
lateinit var emojiPickerView: EmojiPickerView
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.inflation_test)
|
||||||
|
|
||||||
|
emojiPickerView = findViewById(R.id.emojiPickerTest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@LargeTest
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class EmojiPickerViewTest {
|
||||||
|
private lateinit var context: Context
|
||||||
|
|
||||||
|
@get:Rule val activityTestRule = ActivityScenarioRule(EmojiPickerViewTestActivity::class.java)
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
context = ApplicationProvider.getApplicationContext()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCustomEmojiPickerView_rendered() {
|
||||||
|
activityTestRule.scenario.onActivity {
|
||||||
|
val mEmojiPickerView = it.findViewById<EmojiPickerView>(R.id.emojiPickerTest)
|
||||||
|
assert(mEmojiPickerView.isVisible)
|
||||||
|
assertEquals(mEmojiPickerView.emojiGridColumns, 10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCustomEmojiPickerView_noVariant() {
|
||||||
|
activityTestRule.scenario.onActivity {
|
||||||
|
val targetView = findViewByEmoji(it.findViewById(R.id.emojiPickerTest), GRINNING_FACE)!!
|
||||||
|
// Not long-clickable
|
||||||
|
assertEquals(targetView.isLongClickable, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SdkSuppress(minSdkVersion = 24)
|
||||||
|
fun testCustomEmojiPickerView_hasVariant() {
|
||||||
|
lateinit var view: EmojiPickerView
|
||||||
|
activityTestRule.scenario.onActivity { view = it.findViewById(R.id.emojiPickerTest) }
|
||||||
|
findViewByEmoji(view, NOSE_EMOJI)
|
||||||
|
?: onView(withId(EmojiPickerViewR.id.emoji_picker_body))
|
||||||
|
.perform(
|
||||||
|
RecyclerViewActions.scrollToHolder(createEmojiViewHolderMatcher(NOSE_EMOJI))
|
||||||
|
)
|
||||||
|
val targetView = findViewByEmoji(view, NOSE_EMOJI)!!
|
||||||
|
// Long-clickable
|
||||||
|
assertEquals(targetView.isLongClickable, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SdkSuppress(minSdkVersion = 24)
|
||||||
|
@Ignore("b/294556440")
|
||||||
|
fun testStickyVariant_displayAndSaved() {
|
||||||
|
lateinit var view: EmojiPickerView
|
||||||
|
activityTestRule.scenario.onActivity { view = it.findViewById(R.id.emojiPickerTest) }
|
||||||
|
InstrumentationRegistry.getInstrumentation().waitForIdleSync()
|
||||||
|
|
||||||
|
// Scroll to the nose emoji, long click then select nose in dark skin tone
|
||||||
|
findViewByEmoji(view, NOSE_EMOJI)
|
||||||
|
?: onView(withId(EmojiPickerViewR.id.emoji_picker_body))
|
||||||
|
.perform(
|
||||||
|
RecyclerViewActions.scrollToHolder(createEmojiViewHolderMatcher(NOSE_EMOJI))
|
||||||
|
)
|
||||||
|
onView(createEmojiViewMatcher(NOSE_EMOJI)).perform(longClick())
|
||||||
|
onView(createEmojiViewMatcher(NOSE_EMOJI_DARK))
|
||||||
|
.inRoot(hasWindowLayoutParams())
|
||||||
|
.perform(click())
|
||||||
|
assertNotNull(findViewByEmoji(view, NOSE_EMOJI_DARK))
|
||||||
|
// Switch back to clear saved preference
|
||||||
|
onView(createEmojiViewMatcher(NOSE_EMOJI_DARK)).perform(longClick())
|
||||||
|
onView(createEmojiViewMatcher(NOSE_EMOJI)).inRoot(hasWindowLayoutParams()).perform(click())
|
||||||
|
assertNotNull(findViewByEmoji(view, NOSE_EMOJI))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore // b/260915957
|
||||||
|
@Test
|
||||||
|
fun testHeader_highlightCurrentCategory() {
|
||||||
|
disableRecent()
|
||||||
|
assertSelectedHeaderIndex(0)
|
||||||
|
scrollToEmoji(NOSE_EMOJI)
|
||||||
|
assertSelectedHeaderIndex(1)
|
||||||
|
scrollToEmoji(BAT)
|
||||||
|
assertSelectedHeaderIndex(3)
|
||||||
|
scrollToEmoji(KEY)
|
||||||
|
assertSelectedHeaderIndex(7)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testHeader_clickingIconWillScrollToCategory() {
|
||||||
|
onView(createEmojiViewMatcher(STRAWBERRY)).check { view, _ -> assertNull(view) }
|
||||||
|
|
||||||
|
onView(withId(EmojiPickerViewR.id.emoji_picker_header))
|
||||||
|
.perform(
|
||||||
|
RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(4, click())
|
||||||
|
)
|
||||||
|
|
||||||
|
onView(createEmojiViewMatcher(STRAWBERRY)).check { view, _ -> assertNotNull(view) }
|
||||||
|
assertSelectedHeaderIndex(4)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = UnsupportedOperationException::class)
|
||||||
|
fun testAddView_throwsException() {
|
||||||
|
activityTestRule.scenario.onActivity { it.emojiPickerView.addView(View(context)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findViewByEmoji(root: View, emoji: String) =
|
||||||
|
try {
|
||||||
|
mutableListOf<EmojiView>()
|
||||||
|
.apply { findEmojiViews(root, this) }
|
||||||
|
.first { it.emoji == emoji }
|
||||||
|
} catch (e: NoSuchElementException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findEmojiViews(root: View, output: MutableList<EmojiView>) {
|
||||||
|
if (root !is ViewGroup) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (i in 0 until root.childCount) {
|
||||||
|
root
|
||||||
|
.getChildAt(i)
|
||||||
|
.apply {
|
||||||
|
if (this is EmojiView) {
|
||||||
|
output.add(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.also { findEmojiViews(it, output) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createEmojiViewHolderMatcher(emoji: String) =
|
||||||
|
object :
|
||||||
|
BoundedMatcher<RecyclerView.ViewHolder, EmojiViewHolder>(EmojiViewHolder::class.java) {
|
||||||
|
override fun describeTo(description: Description) {}
|
||||||
|
|
||||||
|
override fun matchesSafely(item: EmojiViewHolder) =
|
||||||
|
(item.itemView as EmojiView).emoji == emoji
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createEmojiViewMatcher(emoji: String) =
|
||||||
|
object : BoundedMatcher<View, EmojiView>(EmojiView::class.java) {
|
||||||
|
override fun describeTo(description: Description) {}
|
||||||
|
|
||||||
|
override fun matchesSafely(item: EmojiView) = item.emoji == emoji
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assertSelectedHeaderIndex(expected: Int) =
|
||||||
|
onView(withId(EmojiPickerViewR.id.emoji_picker_header)).check { view, noViewFoundException
|
||||||
|
->
|
||||||
|
view ?: throw noViewFoundException
|
||||||
|
val selectedIndex =
|
||||||
|
(view as RecyclerView)
|
||||||
|
.children
|
||||||
|
.withIndex()
|
||||||
|
.single { (_, view) ->
|
||||||
|
view
|
||||||
|
.findViewById<ImageView>(EmojiPickerViewR.id.emoji_picker_header_icon)
|
||||||
|
.isSelected
|
||||||
|
}
|
||||||
|
.index
|
||||||
|
assertEquals(expected, selectedIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scrollToEmoji(emoji: String) =
|
||||||
|
onView(withId(EmojiPickerViewR.id.emoji_picker_body))
|
||||||
|
.perform(RecyclerViewActions.scrollToHolder(createEmojiViewHolderMatcher(emoji)))
|
||||||
|
|
||||||
|
private fun disableRecent() {
|
||||||
|
activityTestRule.scenario.onActivity {
|
||||||
|
it.emojiPickerView.setRecentEmojiProvider(
|
||||||
|
object : RecentEmojiProvider {
|
||||||
|
override fun recordSelection(emoji: String) {}
|
||||||
|
|
||||||
|
override suspend fun getRecentEmojiList(): List<String> = listOf()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val GRINNING_FACE = "\uD83D\uDE00"
|
||||||
|
const val NOSE_EMOJI = "\uD83D\uDC43"
|
||||||
|
const val NOSE_EMOJI_DARK = "\uD83D\uDC43\uD83C\uDFFF"
|
||||||
|
const val BAT = "\uD83E\uDD87"
|
||||||
|
const val KEY = "\uD83D\uDD11"
|
||||||
|
const val STRAWBERRY = "\uD83C\uDF53"
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,94 @@
|
|||||||
|
|
||||||
|
|
||||||
|
package com.rishabh.emojipicker
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.text.SpannableString
|
||||||
|
import android.text.Spanned
|
||||||
|
import android.text.style.ForegroundColorSpan
|
||||||
|
import androidx.core.graphics.applyCanvas
|
||||||
|
import androidx.core.text.toSpanned
|
||||||
|
import androidx.test.ext.junit.rules.ActivityScenarioRule
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.filters.SmallTest
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import androidx.test.screenshot.AndroidXScreenshotTestRule
|
||||||
|
import androidx.test.screenshot.assertAgainstGolden
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Ignore
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
class EmojiViewTestActivity : Activity()
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
@SmallTest
|
||||||
|
class EmojiViewTest {
|
||||||
|
companion object {
|
||||||
|
private const val GRINNING_FACE = "\uD83D\uDE00"
|
||||||
|
}
|
||||||
|
|
||||||
|
@get:Rule val screenshotRule = AndroidXScreenshotTestRule("emoji2/emoji2-emojipicker")
|
||||||
|
|
||||||
|
@get:Rule val activityRule = ActivityScenarioRule(EmojiViewTestActivity::class.java)
|
||||||
|
|
||||||
|
private lateinit var emojiView: EmojiView
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
activityRule.scenario.onActivity {
|
||||||
|
emojiView = EmojiView(it)
|
||||||
|
it.setContentView(emojiView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setAndWait(cs: CharSequence?) {
|
||||||
|
emojiView.emoji = cs
|
||||||
|
InstrumentationRegistry.getInstrumentation().waitForIdleSync()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun dumpAndAssertAgainstGolden(golden: String) {
|
||||||
|
Bitmap.createBitmap(128, 128, Bitmap.Config.ARGB_8888)
|
||||||
|
.applyCanvas { emojiView.draw(this) }
|
||||||
|
.assertAgainstGolden(screenshotRule, golden)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDrawEmoji() {
|
||||||
|
setAndWait(GRINNING_FACE)
|
||||||
|
dumpAndAssertAgainstGolden("draw_grinning_face")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDrawSpannedString() {
|
||||||
|
setAndWait(
|
||||||
|
SpannableString("0")
|
||||||
|
.apply {
|
||||||
|
setSpan(ForegroundColorSpan(Color.RED), 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
|
}
|
||||||
|
.toSpanned()
|
||||||
|
)
|
||||||
|
|
||||||
|
dumpAndAssertAgainstGolden("draw_red_zero")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testMultipleDraw() {
|
||||||
|
setAndWait(GRINNING_FACE)
|
||||||
|
setAndWait("M")
|
||||||
|
|
||||||
|
dumpAndAssertAgainstGolden("multiple_draw")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
@Test
|
||||||
|
fun testClear() {
|
||||||
|
setAndWait(GRINNING_FACE)
|
||||||
|
setAndWait(null)
|
||||||
|
|
||||||
|
dumpAndAssertAgainstGolden("draw_and_clear")
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,31 @@
|
|||||||
|
|
||||||
|
|
||||||
|
package com.rishabh.emojipicker.utils
|
||||||
|
|
||||||
|
import androidx.test.filters.SdkSuppress
|
||||||
|
import androidx.test.filters.SmallTest
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
@SmallTest
|
||||||
|
class UnicodeRenderableManagerTest {
|
||||||
|
@Test
|
||||||
|
@SdkSuppress(minSdkVersion = 21, maxSdkVersion = 23)
|
||||||
|
fun testGetClosestRenderable_lowerVersionTrimmed() {
|
||||||
|
// #️⃣
|
||||||
|
assertEquals(
|
||||||
|
UnicodeRenderableManager.getClosestRenderable("\u0023\uFE0F\u20E3"),
|
||||||
|
"\u0023\u20E3"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SdkSuppress(minSdkVersion = 24)
|
||||||
|
fun testGetClosestRenderable_higherVersionNoTrim() {
|
||||||
|
// #️⃣
|
||||||
|
assertEquals(
|
||||||
|
UnicodeRenderableManager.getClosestRenderable("\u0023\uFE0F\u20E3"),
|
||||||
|
"\u0023\uFE0F\u20E3"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<androidx.emoji2.emojipicker.EmojiPickerView
|
||||||
|
android:id="@+id/emojiPickerTest"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:emojiGridColumns="10"
|
||||||
|
app:emojiGridRows="8.5"/>
|
||||||
|
</LinearLayout>
|
5
emojipicker/app/src/main/AndroidManifest.xml
Normal file
5
emojipicker/app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
</manifest>
|
19
emojipicker/app/src/main/androidx/AndroidManifest.xml
Normal file
19
emojipicker/app/src/main/androidx/AndroidManifest.xml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
Copyright 2022 The Android Open Source Project
|
||||||
|
|
||||||
|
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.
|
||||||
|
-->
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
</manifest>
|
@@ -0,0 +1,8 @@
|
|||||||
|
# Module root
|
||||||
|
|
||||||
|
androidx.emoji2 emoji2-emojipicker
|
||||||
|
|
||||||
|
# Package androidx.emoji2.emojipicker
|
||||||
|
|
||||||
|
This library provides the latest emoji support and emoji picker UI including
|
||||||
|
skin-tone variants and emoji compat support.
|
@@ -0,0 +1,8 @@
|
|||||||
|
# Module root
|
||||||
|
|
||||||
|
androidx.emoji2 emoji2-emojipicker
|
||||||
|
|
||||||
|
# package reeshabh.emojipicker
|
||||||
|
|
||||||
|
This library provides the latest emoji support and emoji picker UI including
|
||||||
|
skin-tone variants and emoji compat support.
|
@@ -0,0 +1,134 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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 androidx.emoji2.emojipicker
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.TypedArray
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.core.content.res.use
|
||||||
|
import androidx.emoji2.emojipicker.utils.FileCache
|
||||||
|
import androidx.emoji2.emojipicker.utils.UnicodeRenderableManager
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A data loader that loads the following objects either from file based caches or from resources.
|
||||||
|
*
|
||||||
|
* categorizedEmojiData: a list that holds bundled emoji separated by category, filtered by
|
||||||
|
* renderability check. This is the data source for EmojiPickerView.
|
||||||
|
*
|
||||||
|
* emojiVariantsLookup: a map of emoji variants in bundled emoji, keyed by the base emoji. This
|
||||||
|
* allows faster variants lookup.
|
||||||
|
*
|
||||||
|
* primaryEmojiLookup: a map of base emoji to its variants in bundled emoji. This allows faster
|
||||||
|
* variants lookup.
|
||||||
|
*/
|
||||||
|
internal object BundledEmojiListLoader {
|
||||||
|
private var categorizedEmojiData: List<EmojiDataCategory>? = null
|
||||||
|
private var emojiVariantsLookup: Map<String, List<String>>? = null
|
||||||
|
|
||||||
|
internal suspend fun load(context: Context) {
|
||||||
|
val categoryNames = context.resources.getStringArray(R.array.category_names)
|
||||||
|
val categoryHeaderIconIds =
|
||||||
|
context.resources.obtainTypedArray(R.array.emoji_categories_icons).use { typedArray ->
|
||||||
|
IntArray(typedArray.length()) { typedArray.getResourceId(it, 0) }
|
||||||
|
}
|
||||||
|
val resources =
|
||||||
|
if (UnicodeRenderableManager.isEmoji12Supported())
|
||||||
|
R.array.emoji_by_category_raw_resources_gender_inclusive
|
||||||
|
else R.array.emoji_by_category_raw_resources
|
||||||
|
val emojiFileCache = FileCache.getInstance(context)
|
||||||
|
|
||||||
|
categorizedEmojiData =
|
||||||
|
context.resources.obtainTypedArray(resources).use { ta ->
|
||||||
|
loadEmoji(ta, categoryHeaderIconIds, categoryNames, emojiFileCache, context)
|
||||||
|
}
|
||||||
|
emojiVariantsLookup =
|
||||||
|
categorizedEmojiData!!
|
||||||
|
.flatMap { it.emojiDataList }
|
||||||
|
.filter { it.variants.isNotEmpty() }
|
||||||
|
.flatMap { it.variants.map { variant -> EmojiViewItem(variant, it.variants) } }
|
||||||
|
.associate { it.emoji to it.variants }
|
||||||
|
.also { emojiVariantsLookup = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun getCategorizedEmojiData() =
|
||||||
|
categorizedEmojiData
|
||||||
|
?: throw IllegalStateException("BundledEmojiListLoader.load is not called or complete")
|
||||||
|
|
||||||
|
internal fun getEmojiVariantsLookup() =
|
||||||
|
emojiVariantsLookup
|
||||||
|
?: throw IllegalStateException("BundledEmojiListLoader.load is not called or complete")
|
||||||
|
|
||||||
|
private suspend fun loadEmoji(
|
||||||
|
ta: TypedArray,
|
||||||
|
@DrawableRes categoryHeaderIconIds: IntArray,
|
||||||
|
categoryNames: Array<String>,
|
||||||
|
emojiFileCache: FileCache,
|
||||||
|
context: Context
|
||||||
|
): List<EmojiDataCategory> = coroutineScope {
|
||||||
|
(0 until ta.length())
|
||||||
|
.map {
|
||||||
|
async {
|
||||||
|
emojiFileCache
|
||||||
|
.getOrPut(getCacheFileName(it)) {
|
||||||
|
loadSingleCategory(context, ta.getResourceId(it, 0))
|
||||||
|
}
|
||||||
|
.let { data ->
|
||||||
|
EmojiDataCategory(categoryHeaderIconIds[it], categoryNames[it], data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.awaitAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadSingleCategory(
|
||||||
|
context: Context,
|
||||||
|
resId: Int,
|
||||||
|
): List<EmojiViewItem> =
|
||||||
|
context.resources
|
||||||
|
.openRawResource(resId)
|
||||||
|
.bufferedReader()
|
||||||
|
.useLines { it.toList() }
|
||||||
|
.map { filterRenderableEmojis(it.split(",")) }
|
||||||
|
.filter { it.isNotEmpty() }
|
||||||
|
.map { EmojiViewItem(it.first(), it.drop(1)) }
|
||||||
|
|
||||||
|
private fun getCacheFileName(categoryIndex: Int) =
|
||||||
|
StringBuilder()
|
||||||
|
.append("emoji.v1.")
|
||||||
|
.append(if (EmojiPickerView.emojiCompatLoaded) 1 else 0)
|
||||||
|
.append(".")
|
||||||
|
.append(categoryIndex)
|
||||||
|
.append(".")
|
||||||
|
.append(if (UnicodeRenderableManager.isEmoji12Supported()) 1 else 0)
|
||||||
|
.toString()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To eliminate 'Tofu' (the fallback glyph when an emoji is not renderable), check the
|
||||||
|
* renderability of emojis and keep only when they are renderable on the current device.
|
||||||
|
*/
|
||||||
|
private fun filterRenderableEmojis(emojiList: List<String>) =
|
||||||
|
emojiList.filter { UnicodeRenderableManager.isEmojiRenderable(it) }.toList()
|
||||||
|
|
||||||
|
internal data class EmojiDataCategory(
|
||||||
|
@DrawableRes val headerIconId: Int,
|
||||||
|
val categoryName: String,
|
||||||
|
val emojiDataList: List<EmojiViewItem>
|
||||||
|
)
|
||||||
|
}
|
@@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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 androidx.emoji2.emojipicker
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Context.MODE_PRIVATE
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides recently shared emoji. This is the default recent emoji list provider. Clients could
|
||||||
|
* specify the provider by their own.
|
||||||
|
*/
|
||||||
|
internal class DefaultRecentEmojiProvider(context: Context) : RecentEmojiProvider {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val PREF_KEY_RECENT_EMOJI = "pref_key_recent_emoji"
|
||||||
|
private const val RECENT_EMOJI_LIST_FILE_NAME = "androidx.emoji2.emojipicker.preferences"
|
||||||
|
private const val SPLIT_CHAR = ","
|
||||||
|
}
|
||||||
|
|
||||||
|
private val sharedPreferences =
|
||||||
|
context.getSharedPreferences(RECENT_EMOJI_LIST_FILE_NAME, MODE_PRIVATE)
|
||||||
|
private val recentEmojiList: MutableList<String> =
|
||||||
|
sharedPreferences.getString(PREF_KEY_RECENT_EMOJI, null)?.split(SPLIT_CHAR)?.toMutableList()
|
||||||
|
?: mutableListOf()
|
||||||
|
|
||||||
|
override suspend fun getRecentEmojiList(): List<String> {
|
||||||
|
return recentEmojiList
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun recordSelection(emoji: String) {
|
||||||
|
recentEmojiList.remove(emoji)
|
||||||
|
recentEmojiList.add(0, emoji)
|
||||||
|
saveToPreferences()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveToPreferences() {
|
||||||
|
sharedPreferences
|
||||||
|
.edit()
|
||||||
|
.putString(PREF_KEY_RECENT_EMOJI, recentEmojiList.joinToString(SPLIT_CHAR))
|
||||||
|
.commit()
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,139 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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 androidx.emoji2.emojipicker
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.ViewGroup.LayoutParams
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.annotation.LayoutRes
|
||||||
|
import androidx.annotation.UiThread
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.emoji2.emojipicker.Extensions.toItemType
|
||||||
|
import androidx.recyclerview.widget.RecyclerView.Adapter
|
||||||
|
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||||
|
|
||||||
|
/** RecyclerView adapter for emoji body. */
|
||||||
|
internal class EmojiPickerBodyAdapter(
|
||||||
|
private val context: Context,
|
||||||
|
private val emojiGridColumns: Int,
|
||||||
|
private val emojiGridRows: Float?,
|
||||||
|
private val stickyVariantProvider: StickyVariantProvider,
|
||||||
|
private val emojiPickerItemsProvider: () -> EmojiPickerItems,
|
||||||
|
private val onEmojiPickedListener: EmojiPickerBodyAdapter.(EmojiViewItem) -> Unit,
|
||||||
|
) : Adapter<ViewHolder>() {
|
||||||
|
private val layoutInflater: LayoutInflater = LayoutInflater.from(context)
|
||||||
|
private var emojiCellWidth: Int? = null
|
||||||
|
private var emojiCellHeight: Int? = null
|
||||||
|
|
||||||
|
@UiThread
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
emojiCellWidth = emojiCellWidth ?: (getParentWidth(parent) / emojiGridColumns)
|
||||||
|
emojiCellHeight =
|
||||||
|
emojiCellHeight
|
||||||
|
?: emojiGridRows?.let { getEmojiCellTotalHeight(parent) / it }?.toInt()
|
||||||
|
?: emojiCellWidth
|
||||||
|
|
||||||
|
return when (viewType.toItemType()) {
|
||||||
|
ItemType.CATEGORY_TITLE -> createSimpleHolder(R.layout.category_text_view, parent)
|
||||||
|
ItemType.PLACEHOLDER_TEXT ->
|
||||||
|
createSimpleHolder(R.layout.empty_category_text_view, parent) {
|
||||||
|
minimumHeight = emojiCellHeight!!
|
||||||
|
}
|
||||||
|
ItemType.EMOJI -> {
|
||||||
|
EmojiViewHolder(
|
||||||
|
context,
|
||||||
|
emojiCellWidth!!,
|
||||||
|
emojiCellHeight!!,
|
||||||
|
stickyVariantProvider,
|
||||||
|
onEmojiPickedListener = { emojiViewItem ->
|
||||||
|
onEmojiPickedListener(emojiViewItem)
|
||||||
|
},
|
||||||
|
onEmojiPickedFromPopupListener = { emoji ->
|
||||||
|
val baseEmoji = BundledEmojiListLoader.getEmojiVariantsLookup()[emoji]!![0]
|
||||||
|
emojiPickerItemsProvider().forEachIndexed { index, itemViewData ->
|
||||||
|
if (
|
||||||
|
itemViewData is EmojiViewData &&
|
||||||
|
BundledEmojiListLoader.getEmojiVariantsLookup()[
|
||||||
|
itemViewData.emoji]
|
||||||
|
?.get(0) == baseEmoji &&
|
||||||
|
itemViewData.updateToSticky
|
||||||
|
) {
|
||||||
|
itemViewData.emoji = emoji
|
||||||
|
notifyItemChanged(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
|
||||||
|
val item = emojiPickerItemsProvider().getBodyItem(position)
|
||||||
|
when (getItemViewType(position).toItemType()) {
|
||||||
|
ItemType.CATEGORY_TITLE ->
|
||||||
|
ViewCompat.requireViewById<TextView>(viewHolder.itemView, R.id.category_name).text =
|
||||||
|
(item as CategoryTitle).title
|
||||||
|
ItemType.PLACEHOLDER_TEXT ->
|
||||||
|
ViewCompat.requireViewById<TextView>(
|
||||||
|
viewHolder.itemView,
|
||||||
|
R.id.emoji_picker_empty_category_view
|
||||||
|
)
|
||||||
|
.text = (item as PlaceholderText).text
|
||||||
|
ItemType.EMOJI -> {
|
||||||
|
(viewHolder as EmojiViewHolder).bindEmoji((item as EmojiViewData).emoji)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemId(position: Int): Long =
|
||||||
|
emojiPickerItemsProvider().getBodyItem(position).hashCode().toLong()
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
return emojiPickerItemsProvider().size
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemViewType(position: Int): Int {
|
||||||
|
return emojiPickerItemsProvider().getBodyItem(position).viewType
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getParentWidth(parent: ViewGroup): Int {
|
||||||
|
return parent.measuredWidth - parent.paddingLeft - parent.paddingRight
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getEmojiCellTotalHeight(parent: ViewGroup) =
|
||||||
|
parent.measuredHeight -
|
||||||
|
context.resources.getDimensionPixelSize(R.dimen.emoji_picker_category_name_height) * 2 -
|
||||||
|
context.resources.getDimensionPixelSize(R.dimen.emoji_picker_category_name_padding_top)
|
||||||
|
|
||||||
|
private fun createSimpleHolder(
|
||||||
|
@LayoutRes layoutId: Int,
|
||||||
|
parent: ViewGroup,
|
||||||
|
init: (View.() -> Unit)? = null,
|
||||||
|
) =
|
||||||
|
object :
|
||||||
|
ViewHolder(
|
||||||
|
layoutInflater.inflate(layoutId, parent, /* attachToRoot= */ false).also {
|
||||||
|
it.layoutParams =
|
||||||
|
LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
|
||||||
|
init?.invoke(it)
|
||||||
|
}
|
||||||
|
) {}
|
||||||
|
}
|
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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 androidx.emoji2.emojipicker
|
||||||
|
|
||||||
|
/** A utility class to hold various constants used by the Emoji Picker library. */
|
||||||
|
internal object EmojiPickerConstants {
|
||||||
|
|
||||||
|
// The default number of body columns.
|
||||||
|
const val DEFAULT_BODY_COLUMNS = 9
|
||||||
|
|
||||||
|
// The default number of rows of recent items held.
|
||||||
|
const val DEFAULT_MAX_RECENT_ITEM_ROWS = 3
|
||||||
|
|
||||||
|
// The max pool size of the Emoji ItemType in RecyclerViewPool.
|
||||||
|
const val EMOJI_VIEW_POOL_SIZE = 100
|
||||||
|
|
||||||
|
const val ADD_VIEW_EXCEPTION_MESSAGE = "Adding views to the EmojiPickerView is unsupported"
|
||||||
|
|
||||||
|
const val REMOVE_VIEW_EXCEPTION_MESSAGE =
|
||||||
|
"Removing views from the EmojiPickerView is unsupported"
|
||||||
|
}
|
@@ -0,0 +1,88 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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 androidx.emoji2.emojipicker
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.accessibility.AccessibilityEvent
|
||||||
|
import android.widget.ImageView
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.recyclerview.widget.RecyclerView.Adapter
|
||||||
|
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||||
|
|
||||||
|
/** RecyclerView adapter for emoji header. */
|
||||||
|
internal class EmojiPickerHeaderAdapter(
|
||||||
|
context: Context,
|
||||||
|
private val emojiPickerItems: EmojiPickerItems,
|
||||||
|
private val onHeaderIconClicked: (Int) -> Unit,
|
||||||
|
) : Adapter<ViewHolder>() {
|
||||||
|
private val layoutInflater: LayoutInflater = LayoutInflater.from(context)
|
||||||
|
|
||||||
|
var selectedGroupIndex: Int = 0
|
||||||
|
set(value) {
|
||||||
|
if (value == field) return
|
||||||
|
notifyItemChanged(field)
|
||||||
|
notifyItemChanged(value)
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
return object :
|
||||||
|
ViewHolder(
|
||||||
|
layoutInflater.inflate(
|
||||||
|
R.layout.header_icon_holder,
|
||||||
|
parent,
|
||||||
|
/* attachToRoot = */ false
|
||||||
|
)
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(viewHolder: ViewHolder, i: Int) {
|
||||||
|
val isItemSelected = i == selectedGroupIndex
|
||||||
|
val headerIcon =
|
||||||
|
ViewCompat.requireViewById<ImageView>(
|
||||||
|
viewHolder.itemView,
|
||||||
|
R.id.emoji_picker_header_icon
|
||||||
|
)
|
||||||
|
.apply {
|
||||||
|
setImageDrawable(context.getDrawable(emojiPickerItems.getHeaderIconId(i)))
|
||||||
|
isSelected = isItemSelected
|
||||||
|
contentDescription = emojiPickerItems.getHeaderIconDescription(i)
|
||||||
|
}
|
||||||
|
viewHolder.itemView.setOnClickListener {
|
||||||
|
onHeaderIconClicked(i)
|
||||||
|
selectedGroupIndex = i
|
||||||
|
}
|
||||||
|
if (isItemSelected) {
|
||||||
|
headerIcon.post {
|
||||||
|
headerIcon.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ViewCompat.requireViewById<View>(viewHolder.itemView, R.id.emoji_picker_header_underline)
|
||||||
|
.apply {
|
||||||
|
visibility = if (isItemSelected) View.VISIBLE else View.GONE
|
||||||
|
isSelected = isItemSelected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
return emojiPickerItems.numGroups
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,111 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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 androidx.emoji2.emojipicker
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.annotation.IntRange
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A group of items in RecyclerView for emoji picker body. [titleItem] comes first. [contentItems]
|
||||||
|
* comes after [titleItem]. [emptyPlaceholderItem] will be served after [titleItem] only if
|
||||||
|
* [contentItems] is empty. [maxContentItemCount], if provided, will truncate [contentItems] to
|
||||||
|
* certain size.
|
||||||
|
*
|
||||||
|
* [categoryIconId] is the corresponding category icon in emoji picker header.
|
||||||
|
*/
|
||||||
|
internal class ItemGroup(
|
||||||
|
@DrawableRes internal val categoryIconId: Int,
|
||||||
|
internal val titleItem: CategoryTitle,
|
||||||
|
private val contentItems: List<EmojiViewData>,
|
||||||
|
private val maxContentItemCount: Int? = null,
|
||||||
|
private val emptyPlaceholderItem: PlaceholderText? = null
|
||||||
|
) {
|
||||||
|
|
||||||
|
val size: Int
|
||||||
|
get() =
|
||||||
|
1 /* title */ +
|
||||||
|
when {
|
||||||
|
contentItems.isEmpty() -> if (emptyPlaceholderItem != null) 1 else 0
|
||||||
|
maxContentItemCount != null && contentItems.size > maxContentItemCount ->
|
||||||
|
maxContentItemCount
|
||||||
|
else -> contentItems.size
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun get(index: Int): ItemViewData {
|
||||||
|
if (index == 0) return titleItem
|
||||||
|
val contentIndex = index - 1
|
||||||
|
if (contentIndex < contentItems.size) return contentItems[contentIndex]
|
||||||
|
if (contentIndex == 0 && emptyPlaceholderItem != null) return emptyPlaceholderItem
|
||||||
|
throw IndexOutOfBoundsException()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAll(): List<ItemViewData> = IntRange(0, size - 1).map { get(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A view of concatenated list of [ItemGroup]. */
|
||||||
|
internal class EmojiPickerItems(
|
||||||
|
private val groups: List<ItemGroup>,
|
||||||
|
) : Iterable<ItemViewData> {
|
||||||
|
val size: Int
|
||||||
|
get() = groups.sumOf { it.size }
|
||||||
|
|
||||||
|
init {
|
||||||
|
check(groups.isNotEmpty()) { "Initialized with empty categorized sources" }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getBodyItem(@IntRange(from = 0) absolutePosition: Int): ItemViewData {
|
||||||
|
var localPosition = absolutePosition
|
||||||
|
for (group in groups) {
|
||||||
|
if (localPosition < group.size) return group[localPosition]
|
||||||
|
else localPosition -= group.size
|
||||||
|
}
|
||||||
|
throw IndexOutOfBoundsException()
|
||||||
|
}
|
||||||
|
|
||||||
|
val numGroups: Int
|
||||||
|
get() = groups.size
|
||||||
|
|
||||||
|
@DrawableRes
|
||||||
|
fun getHeaderIconId(@IntRange(from = 0) index: Int): Int = groups[index].categoryIconId
|
||||||
|
|
||||||
|
fun getHeaderIconDescription(@IntRange(from = 0) index: Int): String =
|
||||||
|
groups[index].titleItem.title
|
||||||
|
|
||||||
|
fun groupIndexByItemPosition(@IntRange(from = 0) absolutePosition: Int): Int {
|
||||||
|
var localPosition = absolutePosition
|
||||||
|
var index = 0
|
||||||
|
for (group in groups) {
|
||||||
|
if (localPosition < group.size) return index
|
||||||
|
else {
|
||||||
|
localPosition -= group.size
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw IndexOutOfBoundsException()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun firstItemPositionByGroupIndex(@IntRange(from = 0) groupIndex: Int): Int =
|
||||||
|
groups.take(groupIndex).sumOf { it.size }
|
||||||
|
|
||||||
|
fun groupRange(group: ItemGroup): kotlin.ranges.IntRange {
|
||||||
|
check(groups.contains(group))
|
||||||
|
val index = groups.indexOf(group)
|
||||||
|
return firstItemPositionByGroupIndex(index).let { it until it + group.size }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun iterator(): Iterator<ItemViewData> = groups.flatMap { it.getAll() }.iterator()
|
||||||
|
}
|
@@ -0,0 +1,110 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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 androidx.emoji2.emojipicker
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.Gravity
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import androidx.appcompat.widget.AppCompatImageView
|
||||||
|
|
||||||
|
/** Emoji picker popup view with bidirectional UI design to switch emoji to face left or right. */
|
||||||
|
internal class EmojiPickerPopupBidirectionalDesign(
|
||||||
|
override val context: Context,
|
||||||
|
override val targetEmojiView: View,
|
||||||
|
override val variants: List<String>,
|
||||||
|
override val popupView: LinearLayout,
|
||||||
|
override val emojiViewOnClickListener: View.OnClickListener
|
||||||
|
) : EmojiPickerPopupDesign() {
|
||||||
|
private var emojiFacingLeft = true
|
||||||
|
|
||||||
|
init {
|
||||||
|
updateTemplate()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addLayoutHeader() {
|
||||||
|
val row =
|
||||||
|
LinearLayout(context).apply {
|
||||||
|
orientation = LinearLayout.HORIZONTAL
|
||||||
|
gravity = Gravity.CENTER
|
||||||
|
layoutParams =
|
||||||
|
LinearLayout.LayoutParams(
|
||||||
|
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
)
|
||||||
|
}
|
||||||
|
FrameLayout.inflate(context, R.layout.emoji_picker_popup_bidirectional, row)
|
||||||
|
.findViewById<AppCompatImageView>(R.id.emoji_picker_popup_bidirectional_icon)
|
||||||
|
.apply {
|
||||||
|
layoutParams =
|
||||||
|
LinearLayout.LayoutParams(targetEmojiView.width, targetEmojiView.height)
|
||||||
|
}
|
||||||
|
popupView.addView(row)
|
||||||
|
val imageView =
|
||||||
|
row.findViewById<AppCompatImageView>(R.id.emoji_picker_popup_bidirectional_icon)
|
||||||
|
imageView.setOnClickListener {
|
||||||
|
emojiFacingLeft = !emojiFacingLeft
|
||||||
|
updateTemplate()
|
||||||
|
popupView.removeViews(/* start= */ 1, getActualNumberOfRows())
|
||||||
|
addRowsToPopupView()
|
||||||
|
imageView.announceForAccessibility(
|
||||||
|
context.getString(R.string.emoji_bidirectional_switcher_clicked_desc)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getNumberOfRows(): Int {
|
||||||
|
// Adding one row for the bidirectional switcher.
|
||||||
|
return variants.size / 2 / BIDIRECTIONAL_COLUMN_COUNT + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getNumberOfColumns(): Int {
|
||||||
|
return BIDIRECTIONAL_COLUMN_COUNT
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getActualNumberOfRows(): Int {
|
||||||
|
// Removing one extra row of the bidirectional switcher.
|
||||||
|
return getNumberOfRows() - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateTemplate() {
|
||||||
|
template =
|
||||||
|
if (emojiFacingLeft)
|
||||||
|
arrayOf((variants.indices.filter { it % 12 < 6 }.map { it + 1 }).toIntArray())
|
||||||
|
else arrayOf((variants.indices.filter { it % 12 >= 6 }.map { it + 1 }).toIntArray())
|
||||||
|
|
||||||
|
val row = getActualNumberOfRows()
|
||||||
|
val column = getNumberOfColumns()
|
||||||
|
val overrideTemplate = Array(row) { IntArray(column) }
|
||||||
|
var index = 0
|
||||||
|
for (i in 0 until row) {
|
||||||
|
for (j in 0 until column) {
|
||||||
|
if (index < template[0].size) {
|
||||||
|
overrideTemplate[i][j] = template[0][index]
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
template = overrideTemplate
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val BIDIRECTIONAL_COLUMN_COUNT = 6
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,89 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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 androidx.emoji2.emojipicker
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.accessibility.AccessibilityEvent
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
|
||||||
|
/** Emoji picker popup view UI design. Each UI design needs to inherit this abstract class. */
|
||||||
|
internal abstract class EmojiPickerPopupDesign {
|
||||||
|
abstract val context: Context
|
||||||
|
abstract val targetEmojiView: View
|
||||||
|
abstract val variants: List<String>
|
||||||
|
abstract val popupView: LinearLayout
|
||||||
|
abstract val emojiViewOnClickListener: View.OnClickListener
|
||||||
|
lateinit var template: Array<IntArray>
|
||||||
|
|
||||||
|
open fun addLayoutHeader() {
|
||||||
|
// no-ops
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun addRowsToPopupView() {
|
||||||
|
for (row in template) {
|
||||||
|
val rowLayout =
|
||||||
|
LinearLayout(context).apply {
|
||||||
|
orientation = LinearLayout.HORIZONTAL
|
||||||
|
layoutParams =
|
||||||
|
LinearLayout.LayoutParams(
|
||||||
|
FrameLayout.LayoutParams.WRAP_CONTENT,
|
||||||
|
FrameLayout.LayoutParams.WRAP_CONTENT
|
||||||
|
)
|
||||||
|
}
|
||||||
|
for (item in row) {
|
||||||
|
val cell =
|
||||||
|
if (item == 0) {
|
||||||
|
EmojiView(context)
|
||||||
|
} else {
|
||||||
|
EmojiView(context).apply {
|
||||||
|
willDrawVariantIndicator = false
|
||||||
|
emoji = variants[item - 1]
|
||||||
|
setOnClickListener(emojiViewOnClickListener)
|
||||||
|
if (item == 1) {
|
||||||
|
// Hover on the first emoji in the popup
|
||||||
|
popupView.post {
|
||||||
|
sendAccessibilityEvent(
|
||||||
|
AccessibilityEvent.TYPE_VIEW_HOVER_ENTER
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.apply {
|
||||||
|
layoutParams =
|
||||||
|
ViewGroup.LayoutParams(
|
||||||
|
targetEmojiView.width,
|
||||||
|
targetEmojiView.height
|
||||||
|
)
|
||||||
|
}
|
||||||
|
rowLayout.addView(cell)
|
||||||
|
}
|
||||||
|
popupView.addView(rowLayout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun addLayoutFooter() {
|
||||||
|
// no-ops
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun getNumberOfRows(): Int
|
||||||
|
|
||||||
|
abstract fun getNumberOfColumns(): Int
|
||||||
|
}
|
@@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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 androidx.emoji2.emojipicker
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
|
||||||
|
/** Emoji picker popup view with flat design to list emojis. */
|
||||||
|
internal class EmojiPickerPopupFlatDesign(
|
||||||
|
override val context: Context,
|
||||||
|
override val targetEmojiView: View,
|
||||||
|
override val variants: List<String>,
|
||||||
|
override val popupView: LinearLayout,
|
||||||
|
override val emojiViewOnClickListener: View.OnClickListener
|
||||||
|
) : EmojiPickerPopupDesign() {
|
||||||
|
init {
|
||||||
|
template = arrayOf(variants.indices.map { it + 1 }.toIntArray())
|
||||||
|
var row = getNumberOfRows()
|
||||||
|
var column = getNumberOfColumns()
|
||||||
|
val overrideTemplate = Array(row) { IntArray(column) }
|
||||||
|
var index = 0
|
||||||
|
for (i in 0 until row) {
|
||||||
|
for (j in 0 until column) {
|
||||||
|
if (index < template[0].size) {
|
||||||
|
overrideTemplate[i][j] = template[0][index]
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
template = overrideTemplate
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getNumberOfRows(): Int {
|
||||||
|
val column = getNumberOfColumns()
|
||||||
|
return variants.size / column + if (variants.size % column == 0) 0 else 1
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getNumberOfColumns(): Int {
|
||||||
|
return minOf(FLAT_COLUMN_MAX_COUNT, template[0].size)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val FLAT_COLUMN_MAX_COUNT = 6
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,400 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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 androidx.emoji2.emojipicker
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.ColorStateList
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.ContextThemeWrapper
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.core.content.res.ResourcesCompat
|
||||||
|
import com.google.common.collect.ImmutableMap
|
||||||
|
import com.google.common.primitives.ImmutableIntArray
|
||||||
|
|
||||||
|
/** Emoji picker popup with multi-skintone selection panel. */
|
||||||
|
internal class EmojiPickerPopupMultiSkintoneDesign(
|
||||||
|
override val context: Context,
|
||||||
|
override val targetEmojiView: View,
|
||||||
|
override val variants: List<String>,
|
||||||
|
override val popupView: LinearLayout,
|
||||||
|
override val emojiViewOnClickListener: View.OnClickListener,
|
||||||
|
targetEmoji: String
|
||||||
|
) : EmojiPickerPopupDesign() {
|
||||||
|
|
||||||
|
private val inflater = LayoutInflater.from(context)
|
||||||
|
private val resultRow =
|
||||||
|
LinearLayout(context).apply {
|
||||||
|
orientation = LinearLayout.HORIZONTAL
|
||||||
|
layoutParams =
|
||||||
|
LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var selectedLeftSkintone = -1
|
||||||
|
private var selectedRightSkintone = -1
|
||||||
|
|
||||||
|
init {
|
||||||
|
val triggerVariantIndex: Int = variants.indexOf(targetEmoji)
|
||||||
|
if (triggerVariantIndex > 0) {
|
||||||
|
selectedLeftSkintone = (triggerVariantIndex - 1) / getNumberOfColumns()
|
||||||
|
selectedRightSkintone =
|
||||||
|
triggerVariantIndex - selectedLeftSkintone * getNumberOfColumns() - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addRowsToPopupView() {
|
||||||
|
for (row in 0 until getActualNumberOfRows()) {
|
||||||
|
val rowLayout =
|
||||||
|
LinearLayout(context).apply {
|
||||||
|
orientation = LinearLayout.HORIZONTAL
|
||||||
|
layoutParams =
|
||||||
|
LinearLayout.LayoutParams(
|
||||||
|
FrameLayout.LayoutParams.WRAP_CONTENT,
|
||||||
|
FrameLayout.LayoutParams.WRAP_CONTENT
|
||||||
|
)
|
||||||
|
}
|
||||||
|
for (column in 0 until getNumberOfColumns()) {
|
||||||
|
inflater.inflate(R.layout.emoji_picker_popup_image_view, rowLayout)
|
||||||
|
val imageView = rowLayout.getChildAt(column) as ImageView
|
||||||
|
imageView.apply {
|
||||||
|
layoutParams =
|
||||||
|
LinearLayout.LayoutParams(targetEmojiView.width, targetEmojiView.height)
|
||||||
|
isClickable = true
|
||||||
|
contentDescription = getImageContentDescription(context, row, column)
|
||||||
|
if (
|
||||||
|
(hasLeftSkintone() && row == 0 && selectedLeftSkintone == column) ||
|
||||||
|
(hasRightSkintone() && row == 1 && selectedRightSkintone == column)
|
||||||
|
) {
|
||||||
|
isSelected = true
|
||||||
|
isClickable = false
|
||||||
|
}
|
||||||
|
setImageDrawable(getDrawableRes(context, row, column))
|
||||||
|
setOnClickListener {
|
||||||
|
var unSelectedView: View? = null
|
||||||
|
if (row == 0) {
|
||||||
|
if (hasLeftSkintone()) {
|
||||||
|
unSelectedView = rowLayout.getChildAt(selectedLeftSkintone)
|
||||||
|
}
|
||||||
|
selectedLeftSkintone = column
|
||||||
|
} else {
|
||||||
|
if (hasRightSkintone()) {
|
||||||
|
unSelectedView = rowLayout.getChildAt(selectedRightSkintone)
|
||||||
|
}
|
||||||
|
selectedRightSkintone = column
|
||||||
|
}
|
||||||
|
if (unSelectedView != null) {
|
||||||
|
unSelectedView.isSelected = false
|
||||||
|
unSelectedView.isClickable = true
|
||||||
|
}
|
||||||
|
isClickable = false
|
||||||
|
isSelected = true
|
||||||
|
processResultView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
popupView.addView(rowLayout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processResultView() {
|
||||||
|
val childCount = resultRow.childCount
|
||||||
|
if (childCount < 1 || childCount > 2) {
|
||||||
|
Log.e(TAG, "processResultEmojiForRectangleLayout(): unexpected emoji result row size")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Remove the result emoji if it's already available. It will be available after the row is
|
||||||
|
// inflated the first time.
|
||||||
|
if (childCount == 2) {
|
||||||
|
resultRow.removeViewAt(1)
|
||||||
|
}
|
||||||
|
if (hasLeftSkintone() && hasRightSkintone()) {
|
||||||
|
inflater.inflate(R.layout.emoji_picker_popup_emoji_view, resultRow)
|
||||||
|
val layout = resultRow.getChildAt(1) as LinearLayout
|
||||||
|
layout.findViewById<EmojiView>(R.id.emoji_picker_popup_emoji_view).apply {
|
||||||
|
willDrawVariantIndicator = false
|
||||||
|
isClickable = true
|
||||||
|
emoji =
|
||||||
|
variants[
|
||||||
|
selectedLeftSkintone * getNumberOfColumns() + selectedRightSkintone + 1]
|
||||||
|
setOnClickListener(emojiViewOnClickListener)
|
||||||
|
layoutParams =
|
||||||
|
LinearLayout.LayoutParams(targetEmojiView.width, targetEmojiView.height)
|
||||||
|
}
|
||||||
|
layout.findViewById<LinearLayout>(R.id.emoji_picker_popup_emoji_view_wrapper).apply {
|
||||||
|
layoutParams =
|
||||||
|
LinearLayout.LayoutParams(
|
||||||
|
targetEmojiView.width * getNumberOfColumns() / 2,
|
||||||
|
targetEmojiView.height
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (hasLeftSkintone()) {
|
||||||
|
drawImageView(
|
||||||
|
/* row= */ 0,
|
||||||
|
/*column=*/ selectedLeftSkintone,
|
||||||
|
/* applyGrayTint= */ false
|
||||||
|
)
|
||||||
|
} else if (hasRightSkintone()) {
|
||||||
|
drawImageView(
|
||||||
|
/* row= */ 1,
|
||||||
|
/*column=*/ selectedRightSkintone,
|
||||||
|
/* applyGrayTint= */ false
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
drawImageView(/* row= */ 0, /* column= */ 0, /* applyGrayTint= */ true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawImageView(row: Int, column: Int, applyGrayTint: Boolean) {
|
||||||
|
inflater
|
||||||
|
.inflate(R.layout.emoji_picker_popup_image_view, resultRow)
|
||||||
|
.findViewById<ImageView>(R.id.emoji_picker_popup_image_view)
|
||||||
|
.apply {
|
||||||
|
layoutParams = LinearLayout.LayoutParams(0, targetEmojiView.height, 1f)
|
||||||
|
setImageDrawable(getDrawableRes(context, row, column))
|
||||||
|
if (applyGrayTint) {
|
||||||
|
imageTintList = ColorStateList.valueOf(Color.GRAY)
|
||||||
|
}
|
||||||
|
|
||||||
|
var contentDescriptionRow = selectedLeftSkintone
|
||||||
|
var contentDescriptionColumn = selectedRightSkintone
|
||||||
|
if (hasLeftSkintone()) {
|
||||||
|
contentDescriptionRow = 0
|
||||||
|
contentDescriptionColumn = selectedLeftSkintone
|
||||||
|
} else if (hasRightSkintone()) {
|
||||||
|
contentDescriptionRow = 1
|
||||||
|
contentDescriptionColumn = selectedRightSkintone
|
||||||
|
}
|
||||||
|
contentDescription =
|
||||||
|
getImageContentDescription(
|
||||||
|
context,
|
||||||
|
contentDescriptionRow,
|
||||||
|
contentDescriptionColumn
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addLayoutFooter() {
|
||||||
|
inflater.inflate(R.layout.emoji_picker_popup_emoji_view, resultRow)
|
||||||
|
val layout = resultRow.getChildAt(0) as LinearLayout
|
||||||
|
layout.findViewById<EmojiView>(R.id.emoji_picker_popup_emoji_view).apply {
|
||||||
|
willDrawVariantIndicator = false
|
||||||
|
emoji = variants[0]
|
||||||
|
layoutParams = LinearLayout.LayoutParams(targetEmojiView.width, targetEmojiView.height)
|
||||||
|
isClickable = true
|
||||||
|
setOnClickListener(emojiViewOnClickListener)
|
||||||
|
}
|
||||||
|
layout.findViewById<LinearLayout>(R.id.emoji_picker_popup_emoji_view_wrapper).apply {
|
||||||
|
layoutParams =
|
||||||
|
LinearLayout.LayoutParams(
|
||||||
|
targetEmojiView.width * getNumberOfColumns() / 2,
|
||||||
|
targetEmojiView.height
|
||||||
|
)
|
||||||
|
}
|
||||||
|
processResultView()
|
||||||
|
popupView.addView(resultRow)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getNumberOfRows(): Int {
|
||||||
|
// Add one extra row for the neutral skin tone combination
|
||||||
|
return LAYOUT_ROWS + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getNumberOfColumns(): Int {
|
||||||
|
return LAYOUT_COLUMNS
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getActualNumberOfRows(): Int {
|
||||||
|
return LAYOUT_ROWS
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hasLeftSkintone(): Boolean {
|
||||||
|
return selectedLeftSkintone != -1
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hasRightSkintone(): Boolean {
|
||||||
|
return selectedRightSkintone != -1
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getDrawableRes(context: Context, row: Int, column: Int): Drawable? {
|
||||||
|
val resArray: ImmutableIntArray? = SKIN_TONES_EMOJI_TO_RESOURCES[variants[0]]
|
||||||
|
if (resArray != null) {
|
||||||
|
val contextThemeWrapper = ContextThemeWrapper(context, VARIANT_STYLES[column])
|
||||||
|
return ResourcesCompat.getDrawable(
|
||||||
|
context.resources,
|
||||||
|
resArray[row],
|
||||||
|
contextThemeWrapper.getTheme()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getImageContentDescription(context: Context, row: Int, column: Int): String {
|
||||||
|
return context.getString(
|
||||||
|
R.string.emoji_variant_content_desc_template,
|
||||||
|
context.getString(getSkintoneStringRes(/* isLeft= */ true, row, column)),
|
||||||
|
context.getString(getSkintoneStringRes(/* isLeft= */ false, row, column))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@StringRes
|
||||||
|
private fun getSkintoneStringRes(isLeft: Boolean, row: Int, column: Int): Int {
|
||||||
|
// When there is no column, the selected position -1 will be passed in as column.
|
||||||
|
if (column == -1) {
|
||||||
|
return R.string.emoji_skin_tone_shadow_content_desc
|
||||||
|
}
|
||||||
|
return if (isLeft) {
|
||||||
|
if (row == 0) SKIN_TONE_CONTENT_DESC_RES_IDS[column]
|
||||||
|
else R.string.emoji_skin_tone_shadow_content_desc
|
||||||
|
} else {
|
||||||
|
if (row == 0) R.string.emoji_skin_tone_shadow_content_desc
|
||||||
|
else SKIN_TONE_CONTENT_DESC_RES_IDS[column]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "MultiSkintoneDesign"
|
||||||
|
private const val LAYOUT_ROWS = 2
|
||||||
|
private const val LAYOUT_COLUMNS = 5
|
||||||
|
|
||||||
|
private val SKIN_TONE_CONTENT_DESC_RES_IDS =
|
||||||
|
ImmutableIntArray.of(
|
||||||
|
R.string.emoji_skin_tone_light_content_desc,
|
||||||
|
R.string.emoji_skin_tone_medium_light_content_desc,
|
||||||
|
R.string.emoji_skin_tone_medium_content_desc,
|
||||||
|
R.string.emoji_skin_tone_medium_dark_content_desc,
|
||||||
|
R.string.emoji_skin_tone_dark_content_desc
|
||||||
|
)
|
||||||
|
|
||||||
|
private val VARIANT_STYLES =
|
||||||
|
ImmutableIntArray.of(
|
||||||
|
R.style.EmojiSkintoneSelectorLight,
|
||||||
|
R.style.EmojiSkintoneSelectorMediumLight,
|
||||||
|
R.style.EmojiSkintoneSelectorMedium,
|
||||||
|
R.style.EmojiSkintoneSelectorMediumDark,
|
||||||
|
R.style.EmojiSkintoneSelectorDark
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map from emoji that use the square layout strategy with skin tone swatches or rectangle
|
||||||
|
* strategy to their resources.
|
||||||
|
*/
|
||||||
|
private val SKIN_TONES_EMOJI_TO_RESOURCES =
|
||||||
|
ImmutableMap.Builder<String, ImmutableIntArray>()
|
||||||
|
.put(
|
||||||
|
"🤝",
|
||||||
|
ImmutableIntArray.of(
|
||||||
|
R.drawable.handshake_skintone_shadow,
|
||||||
|
R.drawable.handshake_shadow_skintone
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.put(
|
||||||
|
"👭",
|
||||||
|
ImmutableIntArray.of(
|
||||||
|
R.drawable.holding_women_skintone_shadow,
|
||||||
|
R.drawable.holding_women_shadow_skintone
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.put(
|
||||||
|
"👫",
|
||||||
|
ImmutableIntArray.of(
|
||||||
|
R.drawable.holding_woman_man_skintone_shadow,
|
||||||
|
R.drawable.holding_woman_man_shadow_skintone
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.put(
|
||||||
|
"👬",
|
||||||
|
ImmutableIntArray.of(
|
||||||
|
R.drawable.holding_men_skintone_shadow,
|
||||||
|
R.drawable.holding_men_shadow_skintone
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.put(
|
||||||
|
"🧑🤝🧑",
|
||||||
|
ImmutableIntArray.of(
|
||||||
|
R.drawable.holding_people_skintone_shadow,
|
||||||
|
R.drawable.holding_people_shadow_skintone
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.put(
|
||||||
|
"💏",
|
||||||
|
ImmutableIntArray.of(
|
||||||
|
R.drawable.kiss_people_skintone_shadow,
|
||||||
|
R.drawable.kiss_people_shadow_skintone
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.put(
|
||||||
|
"👩❤️💋👨",
|
||||||
|
ImmutableIntArray.of(
|
||||||
|
R.drawable.kiss_woman_man_skintone_shadow,
|
||||||
|
R.drawable.kiss_woman_man_shadow_skintone
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.put(
|
||||||
|
"👨❤️💋👨",
|
||||||
|
ImmutableIntArray.of(
|
||||||
|
R.drawable.kiss_men_skintone_shadow,
|
||||||
|
R.drawable.kiss_men_shadow_skintone
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.put(
|
||||||
|
"👩❤️💋👩",
|
||||||
|
ImmutableIntArray.of(
|
||||||
|
R.drawable.kiss_women_skintone_shadow,
|
||||||
|
R.drawable.kiss_women_shadow_skintone
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.put(
|
||||||
|
"💑",
|
||||||
|
ImmutableIntArray.of(
|
||||||
|
R.drawable.couple_heart_people_skintone_shadow,
|
||||||
|
R.drawable.couple_heart_people_shadow_skintone
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.put(
|
||||||
|
"👩❤️👨",
|
||||||
|
ImmutableIntArray.of(
|
||||||
|
R.drawable.couple_heart_woman_man_skintone_shadow,
|
||||||
|
R.drawable.couple_heart_woman_man_shadow_skintone
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.put(
|
||||||
|
"👨❤️👨",
|
||||||
|
ImmutableIntArray.of(
|
||||||
|
R.drawable.couple_heart_men_skintone_shadow,
|
||||||
|
R.drawable.couple_heart_men_shadow_skintone
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.put(
|
||||||
|
"👩❤️👩",
|
||||||
|
ImmutableIntArray.of(
|
||||||
|
R.drawable.couple_heart_women_skintone_shadow,
|
||||||
|
R.drawable.couple_heart_women_shadow_skintone
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.buildOrThrow()
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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 androidx.emoji2.emojipicker
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
|
||||||
|
/** Emoji picker popup view with square design. */
|
||||||
|
internal class EmojiPickerPopupSquareDesign(
|
||||||
|
override val context: Context,
|
||||||
|
override val targetEmojiView: View,
|
||||||
|
override val variants: List<String>,
|
||||||
|
override val popupView: LinearLayout,
|
||||||
|
override val emojiViewOnClickListener: View.OnClickListener
|
||||||
|
) : EmojiPickerPopupDesign() {
|
||||||
|
init {
|
||||||
|
template = SQUARE_LAYOUT_TEMPLATE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getNumberOfRows(): Int {
|
||||||
|
return SQUARE_LAYOUT_TEMPLATE.size
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getNumberOfColumns(): Int {
|
||||||
|
return SQUARE_LAYOUT_TEMPLATE[0].size
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Square variant layout template without skin tone. 0 : a place holder Positive number is
|
||||||
|
* the index + 1 in the variant array
|
||||||
|
*/
|
||||||
|
private val SQUARE_LAYOUT_TEMPLATE =
|
||||||
|
arrayOf(
|
||||||
|
intArrayOf(0, 2, 3, 4, 5, 6),
|
||||||
|
intArrayOf(0, 7, 8, 9, 10, 11),
|
||||||
|
intArrayOf(0, 12, 13, 14, 15, 16),
|
||||||
|
intArrayOf(0, 17, 18, 19, 20, 21),
|
||||||
|
intArrayOf(1, 22, 23, 24, 25, 26)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,134 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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 androidx.emoji2.emojipicker
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
|
||||||
|
/** Popup view for emoji picker to show emoji variants. */
|
||||||
|
internal class EmojiPickerPopupView
|
||||||
|
@JvmOverloads
|
||||||
|
constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet?,
|
||||||
|
defStyleAttr: Int = 0,
|
||||||
|
private val targetEmojiView: View,
|
||||||
|
private val targetEmojiItem: EmojiViewItem,
|
||||||
|
private val emojiViewOnClickListener: OnClickListener
|
||||||
|
) : FrameLayout(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
|
private val variants = targetEmojiItem.variants
|
||||||
|
private val targetEmoji = targetEmojiItem.emoji
|
||||||
|
private val popupView: LinearLayout
|
||||||
|
private val popupDesign: EmojiPickerPopupDesign
|
||||||
|
|
||||||
|
init {
|
||||||
|
popupView =
|
||||||
|
inflate(context, R.layout.variant_popup, /* root= */ null)
|
||||||
|
.findViewById<LinearLayout>(R.id.variant_popup)
|
||||||
|
val layout = getLayout()
|
||||||
|
popupDesign =
|
||||||
|
when (layout) {
|
||||||
|
Layout.FLAT ->
|
||||||
|
EmojiPickerPopupFlatDesign(
|
||||||
|
context,
|
||||||
|
targetEmojiView,
|
||||||
|
variants,
|
||||||
|
popupView,
|
||||||
|
emojiViewOnClickListener
|
||||||
|
)
|
||||||
|
Layout.SQUARE ->
|
||||||
|
EmojiPickerPopupSquareDesign(
|
||||||
|
context,
|
||||||
|
targetEmojiView,
|
||||||
|
variants,
|
||||||
|
popupView,
|
||||||
|
emojiViewOnClickListener
|
||||||
|
)
|
||||||
|
Layout.SQUARE_WITH_SKIN_TONE_CIRCLE ->
|
||||||
|
EmojiPickerPopupMultiSkintoneDesign(
|
||||||
|
context,
|
||||||
|
targetEmojiView,
|
||||||
|
variants,
|
||||||
|
popupView,
|
||||||
|
emojiViewOnClickListener,
|
||||||
|
targetEmoji
|
||||||
|
)
|
||||||
|
Layout.BIDIRECTIONAL ->
|
||||||
|
EmojiPickerPopupBidirectionalDesign(
|
||||||
|
context,
|
||||||
|
targetEmojiView,
|
||||||
|
variants,
|
||||||
|
popupView,
|
||||||
|
emojiViewOnClickListener
|
||||||
|
)
|
||||||
|
}
|
||||||
|
popupDesign.addLayoutHeader()
|
||||||
|
popupDesign.addRowsToPopupView()
|
||||||
|
popupDesign.addLayoutFooter()
|
||||||
|
addView(popupView)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPopupViewWidth(): Int {
|
||||||
|
return popupDesign.getNumberOfColumns() * targetEmojiView.width +
|
||||||
|
popupView.paddingStart +
|
||||||
|
popupView.paddingEnd
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPopupViewHeight(): Int {
|
||||||
|
return popupDesign.getNumberOfRows() * targetEmojiView.height +
|
||||||
|
popupView.paddingTop +
|
||||||
|
popupView.paddingBottom
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getLayout(): Layout {
|
||||||
|
if (variants.size == SQUARE_LAYOUT_VARIANT_COUNT)
|
||||||
|
if (SQUARE_LAYOUT_EMOJI_NO_SKIN_TONE.contains(variants[0])) return Layout.SQUARE
|
||||||
|
else return Layout.SQUARE_WITH_SKIN_TONE_CIRCLE
|
||||||
|
else if (variants.size == BIDIRECTIONAL_VARIANTS_COUNT) return Layout.BIDIRECTIONAL
|
||||||
|
else return Layout.FLAT
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private enum class Layout {
|
||||||
|
FLAT,
|
||||||
|
SQUARE,
|
||||||
|
SQUARE_WITH_SKIN_TONE_CIRCLE,
|
||||||
|
BIDIRECTIONAL
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of variants expected when using a square layout strategy. Square layouts are
|
||||||
|
* comprised of a 5x5 grid + the base variant.
|
||||||
|
*/
|
||||||
|
private const val SQUARE_LAYOUT_VARIANT_COUNT = 26
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of variants expected when using a bidirectional layout strategy. Bidirectional
|
||||||
|
* layouts are comprised of bidirectional icon and a 3x6 grid with left direction emojis as
|
||||||
|
* default. After clicking the bidirectional icon, it switches to a bidirectional icon and a
|
||||||
|
* 3x6 grid with right direction emojis.
|
||||||
|
*/
|
||||||
|
private const val BIDIRECTIONAL_VARIANTS_COUNT = 36
|
||||||
|
|
||||||
|
// Set of emojis that use the square layout without skin tone swatches.
|
||||||
|
private val SQUARE_LAYOUT_EMOJI_NO_SKIN_TONE = setOf("👪")
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,84 @@
|
|||||||
|
package androidx.emoji2.emojipicker
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright 2022 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.Gravity
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup.LayoutParams
|
||||||
|
import android.view.WindowManager
|
||||||
|
import android.widget.PopupWindow
|
||||||
|
import android.widget.Toast
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default controller class for emoji picker popup view.
|
||||||
|
*
|
||||||
|
* <p>Shows the popup view above the target Emoji. View under control is a {@code
|
||||||
|
* EmojiPickerPopupView}.
|
||||||
|
*/
|
||||||
|
internal class EmojiPickerPopupViewController(
|
||||||
|
private val context: Context,
|
||||||
|
private val emojiPickerPopupView: EmojiPickerPopupView,
|
||||||
|
private val clickedEmojiView: View
|
||||||
|
) {
|
||||||
|
private val popupWindow: PopupWindow =
|
||||||
|
PopupWindow(
|
||||||
|
emojiPickerPopupView,
|
||||||
|
LayoutParams.WRAP_CONTENT,
|
||||||
|
LayoutParams.WRAP_CONTENT,
|
||||||
|
/* focusable= */ false
|
||||||
|
)
|
||||||
|
|
||||||
|
fun show() {
|
||||||
|
popupWindow.apply {
|
||||||
|
val location = IntArray(2)
|
||||||
|
clickedEmojiView.getLocationInWindow(location)
|
||||||
|
// Make the popup view center align with the target emoji view.
|
||||||
|
val x =
|
||||||
|
location[0] + clickedEmojiView.width / 2f -
|
||||||
|
emojiPickerPopupView.getPopupViewWidth() / 2f
|
||||||
|
val y = location[1] - emojiPickerPopupView.getPopupViewHeight()
|
||||||
|
// Set background drawable so that the popup window is dismissed properly when clicking
|
||||||
|
// outside / scrolling for API < 23.
|
||||||
|
setBackgroundDrawable(context.getDrawable(R.drawable.popup_view_rounded_background))
|
||||||
|
isOutsideTouchable = true
|
||||||
|
isTouchable = true
|
||||||
|
animationStyle = R.style.VariantPopupAnimation
|
||||||
|
elevation =
|
||||||
|
clickedEmojiView.context.resources
|
||||||
|
.getDimensionPixelSize(R.dimen.emoji_picker_popup_view_elevation)
|
||||||
|
.toFloat()
|
||||||
|
try {
|
||||||
|
showAtLocation(clickedEmojiView, Gravity.NO_GRAVITY, x.roundToInt(), y)
|
||||||
|
} catch (e: WindowManager.BadTokenException) {
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
"Don't use EmojiPickerView inside a Popup",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dismiss() {
|
||||||
|
if (popupWindow.isShowing) {
|
||||||
|
popupWindow.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,460 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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 androidx.emoji2.emojipicker
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.TypedArray
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import androidx.core.util.Consumer
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.emoji2.emojipicker.EmojiPickerConstants.DEFAULT_MAX_RECENT_ITEM_ROWS
|
||||||
|
import androidx.emoji2.text.EmojiCompat
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The emoji picker view that provides up-to-date emojis in a vertical scrollable view with a
|
||||||
|
* clickable horizontal header.
|
||||||
|
*/
|
||||||
|
class EmojiPickerView
|
||||||
|
@JvmOverloads
|
||||||
|
constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
|
||||||
|
FrameLayout(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
|
internal companion object {
|
||||||
|
internal var emojiCompatLoaded: Boolean = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private var _emojiGridRows: Float? = null
|
||||||
|
/**
|
||||||
|
* The number of rows of the emoji picker.
|
||||||
|
*
|
||||||
|
* Optional field. If not set, the value will be calculated based on parent view height and
|
||||||
|
* [emojiGridColumns]. Float value indicates that the picker could display the last row
|
||||||
|
* partially, so the users get the idea that they can scroll down for more contents.
|
||||||
|
*
|
||||||
|
* @attr ref androidx.emoji2.emojipicker.R.styleable.EmojiPickerView_emojiGridRows
|
||||||
|
*/
|
||||||
|
var emojiGridRows: Float
|
||||||
|
get() = _emojiGridRows ?: -1F
|
||||||
|
set(value) {
|
||||||
|
_emojiGridRows = value.takeIf { it > 0 }
|
||||||
|
// Refresh when emojiGridRows is reset
|
||||||
|
if (isLaidOut) {
|
||||||
|
showEmojiPickerView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of columns of the emoji picker.
|
||||||
|
*
|
||||||
|
* Default value([EmojiPickerConstants.DEFAULT_BODY_COLUMNS]: 9) will be used if
|
||||||
|
* emojiGridColumns is set to non-positive value.
|
||||||
|
*
|
||||||
|
* @attr ref androidx.emoji2.emojipicker.R.styleable.EmojiPickerView_emojiGridColumns
|
||||||
|
*/
|
||||||
|
var emojiGridColumns: Int = EmojiPickerConstants.DEFAULT_BODY_COLUMNS
|
||||||
|
set(value) {
|
||||||
|
field = value.takeIf { it > 0 } ?: EmojiPickerConstants.DEFAULT_BODY_COLUMNS
|
||||||
|
// Refresh when emojiGridColumns is reset
|
||||||
|
if (isLaidOut) {
|
||||||
|
showEmojiPickerView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val stickyVariantProvider = StickyVariantProvider(context)
|
||||||
|
private val scope = CoroutineScope(EmptyCoroutineContext)
|
||||||
|
|
||||||
|
private var recentEmojiProvider: RecentEmojiProvider = DefaultRecentEmojiProvider(context)
|
||||||
|
private var recentNeedsRefreshing: Boolean = true
|
||||||
|
private val recentItems: MutableList<EmojiViewData> = mutableListOf()
|
||||||
|
private lateinit var recentItemGroup: ItemGroup
|
||||||
|
|
||||||
|
private lateinit var emojiPickerItems: EmojiPickerItems
|
||||||
|
private lateinit var bodyAdapter: EmojiPickerBodyAdapter
|
||||||
|
|
||||||
|
private var onEmojiPickedListener: Consumer<EmojiViewItem>? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
|
||||||
|
val typedArray: TypedArray =
|
||||||
|
context.obtainStyledAttributes(attrs, R.styleable.EmojiPickerView, 0, 0)
|
||||||
|
_emojiGridRows =
|
||||||
|
with(R.styleable.EmojiPickerView_emojiGridRows) {
|
||||||
|
if (typedArray.hasValue(this)) {
|
||||||
|
typedArray.getFloat(this, 0F)
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
emojiGridColumns =
|
||||||
|
typedArray.getInt(
|
||||||
|
R.styleable.EmojiPickerView_emojiGridColumns,
|
||||||
|
EmojiPickerConstants.DEFAULT_BODY_COLUMNS
|
||||||
|
)
|
||||||
|
typedArray.recycle()
|
||||||
|
|
||||||
|
if (EmojiCompat.isConfigured()) {
|
||||||
|
when (EmojiCompat.get().loadState) {
|
||||||
|
EmojiCompat.LOAD_STATE_SUCCEEDED -> emojiCompatLoaded = true
|
||||||
|
EmojiCompat.LOAD_STATE_LOADING,
|
||||||
|
EmojiCompat.LOAD_STATE_DEFAULT ->
|
||||||
|
EmojiCompat.get()
|
||||||
|
.registerInitCallback(
|
||||||
|
object : EmojiCompat.InitCallback() {
|
||||||
|
override fun onInitialized() {
|
||||||
|
emojiCompatLoaded = true
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
BundledEmojiListLoader.load(context)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
emojiPickerItems = buildEmojiPickerItems()
|
||||||
|
bodyAdapter.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailed(throwable: Throwable?) {}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
val load = launch { BundledEmojiListLoader.load(context) }
|
||||||
|
refreshRecent()
|
||||||
|
load.join()
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) { showEmojiPickerView() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createEmojiPickerBodyAdapter(): EmojiPickerBodyAdapter {
|
||||||
|
return EmojiPickerBodyAdapter(
|
||||||
|
context,
|
||||||
|
emojiGridColumns,
|
||||||
|
_emojiGridRows,
|
||||||
|
stickyVariantProvider,
|
||||||
|
emojiPickerItemsProvider = { emojiPickerItems },
|
||||||
|
onEmojiPickedListener = { emojiViewItem ->
|
||||||
|
onEmojiPickedListener?.accept(emojiViewItem)
|
||||||
|
recentEmojiProvider.recordSelection(emojiViewItem.emoji)
|
||||||
|
recentNeedsRefreshing = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun buildEmojiPickerItems() =
|
||||||
|
EmojiPickerItems(
|
||||||
|
buildList {
|
||||||
|
add(
|
||||||
|
ItemGroup(
|
||||||
|
R.drawable.quantum_gm_ic_access_time_filled_vd_theme_24,
|
||||||
|
CategoryTitle(context.getString(R.string.emoji_category_recent)),
|
||||||
|
recentItems,
|
||||||
|
maxContentItemCount = DEFAULT_MAX_RECENT_ITEM_ROWS * emojiGridColumns,
|
||||||
|
emptyPlaceholderItem =
|
||||||
|
PlaceholderText(
|
||||||
|
context.getString(R.string.emoji_empty_recent_category)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.also { recentItemGroup = it }
|
||||||
|
)
|
||||||
|
|
||||||
|
for ((i, category) in
|
||||||
|
BundledEmojiListLoader.getCategorizedEmojiData().withIndex()) {
|
||||||
|
add(
|
||||||
|
ItemGroup(
|
||||||
|
category.headerIconId,
|
||||||
|
CategoryTitle(category.categoryName),
|
||||||
|
category.emojiDataList.mapIndexed { j, emojiData ->
|
||||||
|
EmojiViewData(
|
||||||
|
stickyVariantProvider[emojiData.emoji],
|
||||||
|
dataIndex = i + j
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun showEmojiPickerView() {
|
||||||
|
emojiPickerItems = buildEmojiPickerItems()
|
||||||
|
|
||||||
|
val bodyLayoutManager =
|
||||||
|
GridLayoutManager(
|
||||||
|
context,
|
||||||
|
emojiGridColumns,
|
||||||
|
LinearLayoutManager.VERTICAL,
|
||||||
|
/* reverseLayout = */ false
|
||||||
|
)
|
||||||
|
.apply {
|
||||||
|
spanSizeLookup =
|
||||||
|
object : GridLayoutManager.SpanSizeLookup() {
|
||||||
|
override fun getSpanSize(position: Int): Int {
|
||||||
|
return when (emojiPickerItems.getBodyItem(position).itemType) {
|
||||||
|
ItemType.CATEGORY_TITLE,
|
||||||
|
ItemType.PLACEHOLDER_TEXT -> emojiGridColumns
|
||||||
|
else -> 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val headerAdapter =
|
||||||
|
EmojiPickerHeaderAdapter(
|
||||||
|
context,
|
||||||
|
emojiPickerItems,
|
||||||
|
onHeaderIconClicked = {
|
||||||
|
with(emojiPickerItems.firstItemPositionByGroupIndex(it)) {
|
||||||
|
if (this == emojiPickerItems.groupRange(recentItemGroup).first) {
|
||||||
|
scope.launch { refreshRecent() }
|
||||||
|
}
|
||||||
|
bodyLayoutManager.scrollToPositionWithOffset(this, 0)
|
||||||
|
// The scroll position change will not be reflected until the next layout
|
||||||
|
// call,
|
||||||
|
// so force a new layout call here.
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// clear view's children in case of resetting layout
|
||||||
|
super.removeAllViews()
|
||||||
|
with(inflate(context, R.layout.emoji_picker, this)) {
|
||||||
|
// set headerView
|
||||||
|
ViewCompat.requireViewById<RecyclerView>(this, R.id.emoji_picker_header).apply {
|
||||||
|
layoutManager =
|
||||||
|
object : LinearLayoutManager(context, HORIZONTAL, /* reverseLayout= */ false) {
|
||||||
|
override fun checkLayoutParams(lp: RecyclerView.LayoutParams): Boolean {
|
||||||
|
lp.width =
|
||||||
|
(width - paddingStart - paddingEnd) / emojiPickerItems.numGroups
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
adapter = headerAdapter
|
||||||
|
}
|
||||||
|
|
||||||
|
// set bodyView
|
||||||
|
ViewCompat.requireViewById<RecyclerView>(this, R.id.emoji_picker_body).apply {
|
||||||
|
layoutManager = bodyLayoutManager
|
||||||
|
adapter =
|
||||||
|
createEmojiPickerBodyAdapter()
|
||||||
|
.apply { setHasStableIds(true) }
|
||||||
|
.also { bodyAdapter = it }
|
||||||
|
addOnScrollListener(
|
||||||
|
object : RecyclerView.OnScrollListener() {
|
||||||
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||||
|
super.onScrolled(recyclerView, dx, dy)
|
||||||
|
headerAdapter.selectedGroupIndex =
|
||||||
|
emojiPickerItems.groupIndexByItemPosition(
|
||||||
|
bodyLayoutManager.findFirstCompletelyVisibleItemPosition()
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
recentNeedsRefreshing &&
|
||||||
|
bodyLayoutManager.findFirstVisibleItemPosition() !in
|
||||||
|
emojiPickerItems.groupRange(recentItemGroup)
|
||||||
|
) {
|
||||||
|
scope.launch { refreshRecent() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
// Disable item insertion/deletion animation. This keeps view holder unchanged when
|
||||||
|
// item updates.
|
||||||
|
itemAnimator = null
|
||||||
|
setRecycledViewPool(
|
||||||
|
RecyclerView.RecycledViewPool().apply {
|
||||||
|
setMaxRecycledViews(
|
||||||
|
ItemType.EMOJI.ordinal,
|
||||||
|
EmojiPickerConstants.EMOJI_VIEW_POOL_SIZE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal suspend fun refreshRecent() {
|
||||||
|
if (!recentNeedsRefreshing) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val oldGroupSize = if (::recentItemGroup.isInitialized) recentItemGroup.size else 0
|
||||||
|
val recent = recentEmojiProvider.getRecentEmojiList()
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
recentItems.clear()
|
||||||
|
recentItems.addAll(
|
||||||
|
recent.map {
|
||||||
|
EmojiViewData(
|
||||||
|
it,
|
||||||
|
updateToSticky = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if (::emojiPickerItems.isInitialized) {
|
||||||
|
val range = emojiPickerItems.groupRange(recentItemGroup)
|
||||||
|
if (recentItemGroup.size > oldGroupSize) {
|
||||||
|
bodyAdapter.notifyItemRangeInserted(
|
||||||
|
range.first + oldGroupSize,
|
||||||
|
recentItemGroup.size - oldGroupSize
|
||||||
|
)
|
||||||
|
} else if (recentItemGroup.size < oldGroupSize) {
|
||||||
|
bodyAdapter.notifyItemRangeRemoved(
|
||||||
|
range.first + recentItemGroup.size,
|
||||||
|
oldGroupSize - recentItemGroup.size
|
||||||
|
)
|
||||||
|
}
|
||||||
|
bodyAdapter.notifyItemRangeChanged(
|
||||||
|
range.first,
|
||||||
|
minOf(oldGroupSize, recentItemGroup.size)
|
||||||
|
)
|
||||||
|
recentNeedsRefreshing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function is used to set the custom behavior after clicking on an emoji icon. Clients
|
||||||
|
* could specify their own behavior inside this function.
|
||||||
|
*/
|
||||||
|
fun setOnEmojiPickedListener(onEmojiPickedListener: Consumer<EmojiViewItem>?) {
|
||||||
|
this.onEmojiPickedListener = onEmojiPickedListener
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setRecentEmojiProvider(recentEmojiProvider: RecentEmojiProvider) {
|
||||||
|
this.recentEmojiProvider = recentEmojiProvider
|
||||||
|
scope.launch {
|
||||||
|
recentNeedsRefreshing = true
|
||||||
|
refreshRecent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The following functions disallow clients to add view to the EmojiPickerView
|
||||||
|
*
|
||||||
|
* @param child the child view to be added
|
||||||
|
* @throws UnsupportedOperationException
|
||||||
|
*/
|
||||||
|
override fun addView(child: View?) {
|
||||||
|
if (childCount > 0)
|
||||||
|
throw UnsupportedOperationException(EmojiPickerConstants.ADD_VIEW_EXCEPTION_MESSAGE)
|
||||||
|
else super.addView(child)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param child
|
||||||
|
* @param params
|
||||||
|
* @throws UnsupportedOperationException
|
||||||
|
*/
|
||||||
|
override fun addView(child: View?, params: ViewGroup.LayoutParams?) {
|
||||||
|
if (childCount > 0)
|
||||||
|
throw UnsupportedOperationException(EmojiPickerConstants.ADD_VIEW_EXCEPTION_MESSAGE)
|
||||||
|
else super.addView(child, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param child
|
||||||
|
* @param index
|
||||||
|
* @throws UnsupportedOperationException
|
||||||
|
*/
|
||||||
|
override fun addView(child: View?, index: Int) {
|
||||||
|
if (childCount > 0)
|
||||||
|
throw UnsupportedOperationException(EmojiPickerConstants.ADD_VIEW_EXCEPTION_MESSAGE)
|
||||||
|
else super.addView(child, index)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param child
|
||||||
|
* @param index
|
||||||
|
* @param params
|
||||||
|
* @throws UnsupportedOperationException
|
||||||
|
*/
|
||||||
|
override fun addView(child: View?, index: Int, params: ViewGroup.LayoutParams?) {
|
||||||
|
if (childCount > 0)
|
||||||
|
throw UnsupportedOperationException(EmojiPickerConstants.ADD_VIEW_EXCEPTION_MESSAGE)
|
||||||
|
else super.addView(child, index, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param child
|
||||||
|
* @param width
|
||||||
|
* @param height
|
||||||
|
* @throws UnsupportedOperationException
|
||||||
|
*/
|
||||||
|
override fun addView(child: View?, width: Int, height: Int) {
|
||||||
|
if (childCount > 0)
|
||||||
|
throw UnsupportedOperationException(EmojiPickerConstants.ADD_VIEW_EXCEPTION_MESSAGE)
|
||||||
|
else super.addView(child, width, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The following functions disallow clients to remove view from the EmojiPickerView
|
||||||
|
*
|
||||||
|
* @throws UnsupportedOperationException
|
||||||
|
*/
|
||||||
|
override fun removeAllViews() {
|
||||||
|
throw UnsupportedOperationException(EmojiPickerConstants.REMOVE_VIEW_EXCEPTION_MESSAGE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param child
|
||||||
|
* @throws UnsupportedOperationException
|
||||||
|
*/
|
||||||
|
override fun removeView(child: View?) {
|
||||||
|
throw UnsupportedOperationException(EmojiPickerConstants.REMOVE_VIEW_EXCEPTION_MESSAGE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param index
|
||||||
|
* @throws UnsupportedOperationException
|
||||||
|
*/
|
||||||
|
override fun removeViewAt(index: Int) {
|
||||||
|
throw UnsupportedOperationException(EmojiPickerConstants.REMOVE_VIEW_EXCEPTION_MESSAGE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param child
|
||||||
|
* @throws UnsupportedOperationException
|
||||||
|
*/
|
||||||
|
override fun removeViewInLayout(child: View?) {
|
||||||
|
throw UnsupportedOperationException(EmojiPickerConstants.REMOVE_VIEW_EXCEPTION_MESSAGE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param start
|
||||||
|
* @param count
|
||||||
|
* @throws UnsupportedOperationException
|
||||||
|
*/
|
||||||
|
override fun removeViews(start: Int, count: Int) {
|
||||||
|
throw UnsupportedOperationException(EmojiPickerConstants.REMOVE_VIEW_EXCEPTION_MESSAGE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param start
|
||||||
|
* @param count
|
||||||
|
* @throws UnsupportedOperationException
|
||||||
|
*/
|
||||||
|
override fun removeViewsInLayout(start: Int, count: Int) {
|
||||||
|
throw UnsupportedOperationException(EmojiPickerConstants.REMOVE_VIEW_EXCEPTION_MESSAGE)
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,187 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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 androidx.emoji2.emojipicker
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.os.Build
|
||||||
|
import android.text.Layout
|
||||||
|
import android.text.Spanned
|
||||||
|
import android.text.StaticLayout
|
||||||
|
import android.text.TextPaint
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.View
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.core.graphics.applyCanvas
|
||||||
|
import androidx.emoji2.text.EmojiCompat
|
||||||
|
|
||||||
|
/** A customized view to support drawing emojis asynchronously. */
|
||||||
|
internal class EmojiView
|
||||||
|
@JvmOverloads
|
||||||
|
constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
) : View(context, attrs) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val EMOJI_DRAW_TEXT_SIZE_SP = 30
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
background = context.getDrawable(R.drawable.ripple_emoji_view)
|
||||||
|
importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_YES
|
||||||
|
}
|
||||||
|
|
||||||
|
internal var willDrawVariantIndicator: Boolean = true
|
||||||
|
|
||||||
|
private val textPaint =
|
||||||
|
TextPaint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG).apply {
|
||||||
|
textSize =
|
||||||
|
TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_SP,
|
||||||
|
EMOJI_DRAW_TEXT_SIZE_SP.toFloat(),
|
||||||
|
context.resources.displayMetrics
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val offscreenCanvasBitmap: Bitmap =
|
||||||
|
with(textPaint.fontMetricsInt) {
|
||||||
|
val size = bottom - top
|
||||||
|
Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||||
|
val size =
|
||||||
|
minOf(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec)) -
|
||||||
|
context.resources.getDimensionPixelSize(R.dimen.emoji_picker_emoji_view_padding)
|
||||||
|
setMeasuredDimension(size, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun draw(canvas: Canvas) {
|
||||||
|
super.draw(canvas)
|
||||||
|
canvas.run {
|
||||||
|
save()
|
||||||
|
scale(
|
||||||
|
width.toFloat() / offscreenCanvasBitmap.width,
|
||||||
|
height.toFloat() / offscreenCanvasBitmap.height
|
||||||
|
)
|
||||||
|
drawBitmap(offscreenCanvasBitmap, 0f, 0f, null)
|
||||||
|
restore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var emoji: CharSequence? = null
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
post {
|
||||||
|
if (value != null) {
|
||||||
|
if (value == this.emoji) {
|
||||||
|
drawEmoji(
|
||||||
|
if (EmojiPickerView.emojiCompatLoaded)
|
||||||
|
EmojiCompat.get().process(value) ?: value
|
||||||
|
else value,
|
||||||
|
drawVariantIndicator =
|
||||||
|
willDrawVariantIndicator &&
|
||||||
|
BundledEmojiListLoader.getEmojiVariantsLookup()
|
||||||
|
.containsKey(value)
|
||||||
|
)
|
||||||
|
contentDescription = value
|
||||||
|
}
|
||||||
|
invalidate()
|
||||||
|
} else {
|
||||||
|
offscreenCanvasBitmap.eraseColor(Color.TRANSPARENT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawEmoji(emoji: CharSequence, drawVariantIndicator: Boolean) {
|
||||||
|
offscreenCanvasBitmap.eraseColor(Color.TRANSPARENT)
|
||||||
|
offscreenCanvasBitmap.applyCanvas {
|
||||||
|
if (emoji is Spanned) {
|
||||||
|
createStaticLayout(emoji, width).draw(this)
|
||||||
|
} else {
|
||||||
|
val textWidth = textPaint.measureText(emoji, 0, emoji.length)
|
||||||
|
drawText(
|
||||||
|
emoji,
|
||||||
|
/* start = */ 0,
|
||||||
|
/* end = */ emoji.length,
|
||||||
|
/* x = */ (width - textWidth) / 2,
|
||||||
|
/* y = */ -textPaint.fontMetrics.top,
|
||||||
|
textPaint,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (drawVariantIndicator) {
|
||||||
|
context
|
||||||
|
.getDrawable(R.drawable.variant_availability_indicator)
|
||||||
|
?.apply {
|
||||||
|
val canvasWidth = this@applyCanvas.width
|
||||||
|
val canvasHeight = this@applyCanvas.height
|
||||||
|
val indicatorWidth =
|
||||||
|
context.resources.getDimensionPixelSize(
|
||||||
|
R.dimen.variant_availability_indicator_width
|
||||||
|
)
|
||||||
|
val indicatorHeight =
|
||||||
|
context.resources.getDimensionPixelSize(
|
||||||
|
R.dimen.variant_availability_indicator_height
|
||||||
|
)
|
||||||
|
bounds =
|
||||||
|
Rect(
|
||||||
|
canvasWidth - indicatorWidth,
|
||||||
|
canvasHeight - indicatorHeight,
|
||||||
|
canvasWidth,
|
||||||
|
canvasHeight
|
||||||
|
)
|
||||||
|
}!!
|
||||||
|
.draw(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(23)
|
||||||
|
internal object Api23Impl {
|
||||||
|
fun createStaticLayout(emoji: Spanned, textPaint: TextPaint, width: Int): StaticLayout =
|
||||||
|
StaticLayout.Builder.obtain(emoji, 0, emoji.length, textPaint, width)
|
||||||
|
.apply {
|
||||||
|
setAlignment(Layout.Alignment.ALIGN_CENTER)
|
||||||
|
setLineSpacing(/* spacingAdd= */ 0f, /* spacingMult= */ 1f)
|
||||||
|
setIncludePad(false)
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createStaticLayout(emoji: Spanned, width: Int): StaticLayout {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
return Api23Impl.createStaticLayout(emoji, textPaint, width)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
return StaticLayout(
|
||||||
|
emoji,
|
||||||
|
textPaint,
|
||||||
|
width,
|
||||||
|
Layout.Alignment.ALIGN_CENTER,
|
||||||
|
/* spacingmult = */ 1f,
|
||||||
|
/* spacingadd = */ 0f,
|
||||||
|
/* includepad = */ false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,95 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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 androidx.emoji2.emojipicker
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.View
|
||||||
|
import android.view.View.OnLongClickListener
|
||||||
|
import android.view.ViewGroup.LayoutParams
|
||||||
|
import android.view.accessibility.AccessibilityEvent
|
||||||
|
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||||
|
|
||||||
|
/** A [ViewHolder] containing an emoji view and emoji data. */
|
||||||
|
internal class EmojiViewHolder(
|
||||||
|
context: Context,
|
||||||
|
width: Int,
|
||||||
|
height: Int,
|
||||||
|
private val stickyVariantProvider: StickyVariantProvider,
|
||||||
|
private val onEmojiPickedListener: EmojiViewHolder.(EmojiViewItem) -> Unit,
|
||||||
|
private val onEmojiPickedFromPopupListener: EmojiViewHolder.(String) -> Unit
|
||||||
|
) : ViewHolder(EmojiView(context)) {
|
||||||
|
private val onEmojiLongClickListener: OnLongClickListener =
|
||||||
|
OnLongClickListener { targetEmojiView ->
|
||||||
|
showEmojiPopup(context, targetEmojiView)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val emojiView: EmojiView =
|
||||||
|
(itemView as EmojiView).apply {
|
||||||
|
layoutParams = LayoutParams(width, height)
|
||||||
|
isClickable = true
|
||||||
|
setOnClickListener {
|
||||||
|
it.sendAccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT)
|
||||||
|
onEmojiPickedListener(emojiViewItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private lateinit var emojiViewItem: EmojiViewItem
|
||||||
|
private lateinit var emojiPickerPopupViewController: EmojiPickerPopupViewController
|
||||||
|
|
||||||
|
fun bindEmoji(
|
||||||
|
emoji: String,
|
||||||
|
) {
|
||||||
|
emojiView.emoji = emoji
|
||||||
|
emojiViewItem = makeEmojiViewItem(emoji)
|
||||||
|
|
||||||
|
if (emojiViewItem.variants.isNotEmpty()) {
|
||||||
|
emojiView.setOnLongClickListener(onEmojiLongClickListener)
|
||||||
|
emojiView.isLongClickable = true
|
||||||
|
} else {
|
||||||
|
emojiView.setOnLongClickListener(null)
|
||||||
|
emojiView.isLongClickable = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showEmojiPopup(context: Context, clickedEmojiView: View): Boolean {
|
||||||
|
val emojiPickerPopupView =
|
||||||
|
EmojiPickerPopupView(
|
||||||
|
context,
|
||||||
|
/* attrs= */ null,
|
||||||
|
targetEmojiView = clickedEmojiView,
|
||||||
|
targetEmojiItem = emojiViewItem,
|
||||||
|
emojiViewOnClickListener = { view ->
|
||||||
|
val emojiPickedInPopup = (view as EmojiView).emoji.toString()
|
||||||
|
onEmojiPickedFromPopupListener(emojiPickedInPopup)
|
||||||
|
onEmojiPickedListener(makeEmojiViewItem(emojiPickedInPopup))
|
||||||
|
// variants[0] is always the base (i.e., primary) emoji
|
||||||
|
stickyVariantProvider.update(emojiViewItem.variants[0], emojiPickedInPopup)
|
||||||
|
emojiPickerPopupViewController.dismiss()
|
||||||
|
// Hover on the base emoji after popup dismissed
|
||||||
|
clickedEmojiView.sendAccessibilityEvent(
|
||||||
|
AccessibilityEvent.TYPE_VIEW_HOVER_ENTER
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
emojiPickerPopupViewController =
|
||||||
|
EmojiPickerPopupViewController(context, emojiPickerPopupView, clickedEmojiView)
|
||||||
|
emojiPickerPopupViewController.show()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun makeEmojiViewItem(emoji: String) =
|
||||||
|
EmojiViewItem(emoji, BundledEmojiListLoader.getEmojiVariantsLookup()[emoji] ?: listOf())
|
||||||
|
}
|
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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 androidx.emoji2.emojipicker
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [EmojiViewItem] is a class holding the displayed emoji and its emoji variants
|
||||||
|
*
|
||||||
|
* @param emoji Used to represent the displayed emoji of the [EmojiViewItem].
|
||||||
|
* @param variants Used to represent the corresponding emoji variants of this base emoji.
|
||||||
|
*/
|
||||||
|
class EmojiViewItem(val emoji: String, val variants: List<String>)
|
@@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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 androidx.emoji2.emojipicker
|
||||||
|
|
||||||
|
internal enum class ItemType {
|
||||||
|
CATEGORY_TITLE,
|
||||||
|
PLACEHOLDER_TEXT,
|
||||||
|
EMOJI,
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Represents an item within the body RecyclerView. */
|
||||||
|
internal sealed class ItemViewData(val itemType: ItemType) {
|
||||||
|
val viewType = itemType.ordinal
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Title of each category. */
|
||||||
|
internal data class CategoryTitle(val title: String) : ItemViewData(ItemType.CATEGORY_TITLE)
|
||||||
|
|
||||||
|
/** Text to display when the category contains no items. */
|
||||||
|
internal data class PlaceholderText(val text: String) : ItemViewData(ItemType.PLACEHOLDER_TEXT)
|
||||||
|
|
||||||
|
/** Represents an emoji. */
|
||||||
|
internal data class EmojiViewData(
|
||||||
|
var emoji: String,
|
||||||
|
val updateToSticky: Boolean = true,
|
||||||
|
// Needed to ensure uniqueness since we enabled stable Id.
|
||||||
|
val dataIndex: Int = 0
|
||||||
|
) : ItemViewData(ItemType.EMOJI)
|
||||||
|
|
||||||
|
internal object Extensions {
|
||||||
|
internal fun Int.toItemType() = ItemType.values()[this]
|
||||||
|
}
|
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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 androidx.emoji2.emojipicker
|
||||||
|
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture
|
||||||
|
import kotlinx.coroutines.guava.await
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A interface equivalent to [RecentEmojiProvider] that allows java clients to override the
|
||||||
|
* [ListenableFuture] based function [getRecentEmojiListAsync] in order to provide recent emojis.
|
||||||
|
*/
|
||||||
|
interface RecentEmojiAsyncProvider {
|
||||||
|
fun recordSelection(emoji: String)
|
||||||
|
|
||||||
|
fun getRecentEmojiListAsync(): ListenableFuture<List<String>>
|
||||||
|
}
|
||||||
|
|
||||||
|
/** An adapter for the [RecentEmojiAsyncProvider]. */
|
||||||
|
class RecentEmojiProviderAdapter(private val recentEmojiAsyncProvider: RecentEmojiAsyncProvider) :
|
||||||
|
RecentEmojiProvider {
|
||||||
|
override fun recordSelection(emoji: String) {
|
||||||
|
recentEmojiAsyncProvider.recordSelection(emoji)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getRecentEmojiList() =
|
||||||
|
recentEmojiAsyncProvider.getRecentEmojiListAsync().await()
|
||||||
|
}
|
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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 androidx.emoji2.emojipicker
|
||||||
|
|
||||||
|
/** An interface to provide recent emoji list. */
|
||||||
|
interface RecentEmojiProvider {
|
||||||
|
/**
|
||||||
|
* Records an emoji into recent emoji list. This fun will be called when an emoji is selected.
|
||||||
|
* Clients could specify the behavior to record recently used emojis.(e.g. click frequency).
|
||||||
|
*/
|
||||||
|
fun recordSelection(emoji: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of recent emojis. Default behavior: The most recently used emojis will be
|
||||||
|
* displayed first. Clients could also specify the behavior such as displaying the emojis from
|
||||||
|
* high click frequency to low click frequency.
|
||||||
|
*/
|
||||||
|
suspend fun getRecentEmojiList(): List<String>
|
||||||
|
}
|
@@ -0,0 +1,62 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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 androidx.emoji2.emojipicker
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Context.MODE_PRIVATE
|
||||||
|
|
||||||
|
/** A class that handles user's emoji variant selection using SharedPreferences. */
|
||||||
|
internal class StickyVariantProvider(context: Context) {
|
||||||
|
companion object {
|
||||||
|
const val PREFERENCES_FILE_NAME = "androidx.emoji2.emojipicker.preferences"
|
||||||
|
const val STICKY_VARIANT_PROVIDER_KEY = "pref_key_sticky_variant"
|
||||||
|
const val KEY_VALUE_DELIMITER = "="
|
||||||
|
const val ENTRY_DELIMITER = "|"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val sharedPreferences =
|
||||||
|
context.getSharedPreferences(PREFERENCES_FILE_NAME, MODE_PRIVATE)
|
||||||
|
|
||||||
|
private val stickyVariantMap: MutableMap<String, String> by lazy {
|
||||||
|
sharedPreferences
|
||||||
|
.getString(STICKY_VARIANT_PROVIDER_KEY, null)
|
||||||
|
?.split(ENTRY_DELIMITER)
|
||||||
|
?.associate { entry ->
|
||||||
|
entry
|
||||||
|
.split(KEY_VALUE_DELIMITER, limit = 2)
|
||||||
|
.takeIf { it.size == 2 }
|
||||||
|
?.let { it[0] to it[1] } ?: ("" to "")
|
||||||
|
}
|
||||||
|
?.toMutableMap() ?: mutableMapOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
internal operator fun get(emoji: String): String = stickyVariantMap[emoji] ?: emoji
|
||||||
|
|
||||||
|
internal fun update(baseEmoji: String, variantClicked: String) {
|
||||||
|
stickyVariantMap.apply {
|
||||||
|
if (baseEmoji == variantClicked) {
|
||||||
|
this.remove(baseEmoji)
|
||||||
|
} else {
|
||||||
|
this[baseEmoji] = variantClicked
|
||||||
|
}
|
||||||
|
sharedPreferences
|
||||||
|
.edit()
|
||||||
|
.putString(STICKY_VARIANT_PROVIDER_KEY, entries.joinToString(ENTRY_DELIMITER))
|
||||||
|
.commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,149 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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 androidx.emoji2.emojipicker.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.annotation.GuardedBy
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.emoji2.emojipicker.BundledEmojiListLoader
|
||||||
|
import androidx.emoji2.emojipicker.EmojiViewItem
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class that manages cache files for the emoji picker. All cache files are stored in DE (Device
|
||||||
|
* Encryption) storage (N+), and will be invalidated if device OS version or App version is updated.
|
||||||
|
*
|
||||||
|
* Currently this class is only used by [BundledEmojiListLoader]. All renderable emojis will be
|
||||||
|
* cached by categories under /app.package.name/cache/emoji_picker/<osVersion.appVersion>
|
||||||
|
* /emoji.<emojiPickerVersion>.<emojiCompatMetadataHashCode>.<categoryIndex>.<ifEmoji12Supported>
|
||||||
|
*/
|
||||||
|
internal class FileCache(context: Context) {
|
||||||
|
|
||||||
|
@VisibleForTesting @GuardedBy("lock") internal val emojiPickerCacheDir: File
|
||||||
|
private val currentProperty: String
|
||||||
|
private val lock = Any()
|
||||||
|
|
||||||
|
init {
|
||||||
|
val osVersion = "${Build.VERSION.SDK_INT}_${Build.TIME}"
|
||||||
|
val appVersion = getVersionCode(context)
|
||||||
|
currentProperty = "$osVersion.$appVersion"
|
||||||
|
emojiPickerCacheDir =
|
||||||
|
File(getDeviceProtectedStorageContext(context).cacheDir, EMOJI_PICKER_FOLDER)
|
||||||
|
if (!emojiPickerCacheDir.exists()) emojiPickerCacheDir.mkdir()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get cache for a given file name, or write to a new file using the [defaultValue] factory. */
|
||||||
|
internal fun getOrPut(
|
||||||
|
key: String,
|
||||||
|
defaultValue: () -> List<EmojiViewItem>
|
||||||
|
): List<EmojiViewItem> {
|
||||||
|
synchronized(lock) {
|
||||||
|
val targetDir = File(emojiPickerCacheDir, currentProperty)
|
||||||
|
// No matching cache folder for current property, clear stale cache directory if any
|
||||||
|
if (!targetDir.exists()) {
|
||||||
|
emojiPickerCacheDir.listFiles()?.forEach { it.deleteRecursively() }
|
||||||
|
targetDir.mkdirs()
|
||||||
|
}
|
||||||
|
|
||||||
|
val targetFile = File(targetDir, key)
|
||||||
|
return readFrom(targetFile) ?: writeTo(targetFile, defaultValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readFrom(targetFile: File): List<EmojiViewItem>? {
|
||||||
|
if (!targetFile.isFile) return null
|
||||||
|
return targetFile
|
||||||
|
.bufferedReader()
|
||||||
|
.useLines { it.toList() }
|
||||||
|
.map { it.split(",") }
|
||||||
|
.map { EmojiViewItem(it.first(), it.drop(1)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun writeTo(
|
||||||
|
targetFile: File,
|
||||||
|
defaultValue: () -> List<EmojiViewItem>
|
||||||
|
): List<EmojiViewItem> {
|
||||||
|
val data = defaultValue.invoke()
|
||||||
|
if (targetFile.exists()) {
|
||||||
|
if (!targetFile.delete()) {
|
||||||
|
Log.wtf(TAG, "Can't delete file: $targetFile")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!targetFile.createNewFile()) {
|
||||||
|
throw IOException("Can't create file: $targetFile")
|
||||||
|
}
|
||||||
|
targetFile.bufferedWriter().use { out ->
|
||||||
|
for (emoji in data) {
|
||||||
|
out.write(emoji.emoji)
|
||||||
|
emoji.variants.forEach { out.write(",$it") }
|
||||||
|
out.newLine()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns a new [context] for accessing device protected storage. */
|
||||||
|
private fun getDeviceProtectedStorageContext(context: Context) =
|
||||||
|
context.takeIf { ContextCompat.isDeviceProtectedStorage(it) }
|
||||||
|
?: run { ContextCompat.createDeviceProtectedStorageContext(context) }
|
||||||
|
?: context
|
||||||
|
|
||||||
|
/** Gets the version code for a package. */
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
private fun getVersionCode(context: Context): Long =
|
||||||
|
try {
|
||||||
|
if (Build.VERSION.SDK_INT >= 33) Api33Impl.getAppVersionCode(context)
|
||||||
|
else if (Build.VERSION.SDK_INT >= 28) Api28Impl.getAppVersionCode(context)
|
||||||
|
else context.packageManager.getPackageInfo(context.packageName, 0).versionCode.toLong()
|
||||||
|
} catch (e: PackageManager.NameNotFoundException) {
|
||||||
|
// Default version to 1
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@Volatile private var instance: FileCache? = null
|
||||||
|
|
||||||
|
internal fun getInstance(context: Context): FileCache =
|
||||||
|
instance ?: synchronized(this) { instance ?: FileCache(context).also { instance = it } }
|
||||||
|
|
||||||
|
private const val EMOJI_PICKER_FOLDER = "emoji_picker"
|
||||||
|
private const val TAG = "emojipicker.FileCache"
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
|
||||||
|
internal object Api33Impl {
|
||||||
|
fun getAppVersionCode(context: Context) =
|
||||||
|
context.packageManager
|
||||||
|
.getPackageInfo(context.packageName, PackageManager.PackageInfoFlags.of(0))
|
||||||
|
.longVersionCode
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
@RequiresApi(Build.VERSION_CODES.P)
|
||||||
|
internal object Api28Impl {
|
||||||
|
fun getAppVersionCode(context: Context) =
|
||||||
|
context.packageManager
|
||||||
|
.getPackageInfo(context.packageName, /* flags= */ 0)
|
||||||
|
.longVersionCode
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,83 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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 androidx.emoji2.emojipicker.utils
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import android.text.TextPaint
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import androidx.core.graphics.PaintCompat
|
||||||
|
import androidx.emoji2.emojipicker.EmojiPickerView
|
||||||
|
import androidx.emoji2.text.EmojiCompat
|
||||||
|
|
||||||
|
/** Checks renderability of unicode characters. */
|
||||||
|
internal object UnicodeRenderableManager {
|
||||||
|
|
||||||
|
private const val VARIATION_SELECTOR = "\uFE0F"
|
||||||
|
|
||||||
|
private const val YAWNING_FACE_EMOJI = "\uD83E\uDD71"
|
||||||
|
|
||||||
|
private val paint = TextPaint()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Some emojis were usual (non-emoji) characters. Old devices cannot render them with variation
|
||||||
|
* selector (U+FE0F) so it's worth trying to check renderability again without variation
|
||||||
|
* selector.
|
||||||
|
*/
|
||||||
|
private val CATEGORY_MOVED_EMOJIS =
|
||||||
|
listOf( // These three characters have been emoji since Unicode emoji version 4.
|
||||||
|
// version 3: https://unicode.org/Public/emoji/3.0/emoji-data.txt
|
||||||
|
// version 4: https://unicode.org/Public/emoji/4.0/emoji-data.txt
|
||||||
|
"\u2695\uFE0F", // STAFF OF AESCULAPIUS
|
||||||
|
"\u2640\uFE0F", // FEMALE SIGN
|
||||||
|
"\u2642\uFE0F", // MALE SIGN
|
||||||
|
// These three characters have been emoji since Unicode emoji version 11.
|
||||||
|
// version 5: https://unicode.org/Public/emoji/5.0/emoji-data.txt
|
||||||
|
// version 11: https://unicode.org/Public/emoji/11.0/emoji-data.txt
|
||||||
|
"\u265F\uFE0F", // BLACK_CHESS_PAWN
|
||||||
|
"\u267E\uFE0F" // PERMANENT_PAPER_SIGN
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For a given emoji, check it's renderability with EmojiCompat if enabled. Otherwise, use
|
||||||
|
* [PaintCompat#hasGlyph].
|
||||||
|
*
|
||||||
|
* Note: For older API version, codepoints {@code U+0xFE0F} are removed.
|
||||||
|
*/
|
||||||
|
internal fun isEmojiRenderable(emoji: String) =
|
||||||
|
if (EmojiPickerView.emojiCompatLoaded)
|
||||||
|
EmojiCompat.get().getEmojiMatch(emoji, Int.MAX_VALUE) == EmojiCompat.EMOJI_SUPPORTED
|
||||||
|
else getClosestRenderable(emoji) != null
|
||||||
|
|
||||||
|
// Yawning face is added in emoji 12 which is the first version starts to support gender
|
||||||
|
// inclusive emojis.
|
||||||
|
internal fun isEmoji12Supported() = isEmojiRenderable(YAWNING_FACE_EMOJI)
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
fun getClosestRenderable(emoji: String): String? {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||||
|
return emoji.replace(VARIATION_SELECTOR, "").takeIfHasGlyph()
|
||||||
|
}
|
||||||
|
return emoji.takeIfHasGlyph()
|
||||||
|
?: run {
|
||||||
|
if (CATEGORY_MOVED_EMOJIS.contains(emoji))
|
||||||
|
emoji.replace(VARIATION_SELECTOR, "").takeIfHasGlyph()
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.takeIfHasGlyph() = takeIf { PaintCompat.hasGlyph(paint, this) }
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user