mirror of
https://github.com/SimpleMobileTools/Simple-Keyboard.git
synced 2025-02-26 08:37:39 +01: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:
parent
b22666eb24
commit
62b91f8436
@ -103,4 +103,8 @@ dependencies {
|
||||
|
||||
implementation(libs.bundles.room)
|
||||
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
|
||||
*/
|
||||
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.text.InputType.*
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import android.util.Size
|
||||
import android.view.KeyEvent
|
||||
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.widget.inline.InlinePresentationSpec
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.widget.AppCompatAutoCompleteTextView
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.autofill.inline.UiVersions
|
||||
import androidx.autofill.inline.common.ImageViewStyle
|
||||
import androidx.autofill.inline.common.TextViewStyle
|
||||
@ -60,9 +63,17 @@ class SimpleKeyboardIME : InputMethodService(), OnKeyboardActionListener, Shared
|
||||
private var enterKeyType = IME_ACTION_NONE
|
||||
private var switchToLetters = false
|
||||
private var breakIterator: BreakIterator? = null
|
||||
private var otherInputConnection:OtherInputConnection? = null
|
||||
|
||||
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() {
|
||||
super.onInitializeInterface()
|
||||
safeStorageContext.getSharedPrefs().registerOnSharedPreferenceChangeListener(this)
|
||||
@ -106,7 +117,7 @@ class SimpleKeyboardIME : InputMethodService(), OnKeyboardActionListener, Shared
|
||||
|
||||
val editorInfo = currentInputEditorInfo
|
||||
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)
|
||||
keyboardView?.invalidateAllKeys()
|
||||
return
|
||||
@ -149,7 +160,7 @@ class SimpleKeyboardIME : InputMethodService(), OnKeyboardActionListener, Shared
|
||||
}
|
||||
|
||||
override fun onKey(code: Int) {
|
||||
val inputConnection = currentInputConnection
|
||||
val inputConnection = getMyCurrentInputConnection()
|
||||
if (keyboard == null || inputConnection == null) {
|
||||
return
|
||||
}
|
||||
@ -216,7 +227,10 @@ class SimpleKeyboardIME : InputMethodService(), OnKeyboardActionListener, Shared
|
||||
}
|
||||
|
||||
MyKeyboard.KEYCODE_EMOJI -> {
|
||||
keyboardView?.openEmojiPalette()
|
||||
if(!searching){
|
||||
keyboardView?.openEmojiPalette()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
else -> {
|
||||
@ -324,7 +338,8 @@ class SimpleKeyboardIME : InputMethodService(), OnKeyboardActionListener, Shared
|
||||
}
|
||||
|
||||
override fun onText(text: String) {
|
||||
currentInputConnection?.commitText(text, 1)
|
||||
getMyCurrentInputConnection().commitText(text, 1)
|
||||
|
||||
}
|
||||
|
||||
override fun reloadKeyboard() {
|
||||
@ -333,6 +348,10 @@ class SimpleKeyboardIME : InputMethodService(), OnKeyboardActionListener, Shared
|
||||
keyboardView?.setKeyboard(keyboard)
|
||||
}
|
||||
|
||||
override fun searchViewFocused(searchView: AppCompatAutoCompleteTextView) {
|
||||
otherInputConnection = OtherInputConnection(searchView)
|
||||
}
|
||||
|
||||
private fun createNewKeyboard(): MyKeyboard {
|
||||
val keyboardXml = when (inputTypeClass) {
|
||||
TYPE_CLASS_NUMBER -> {
|
||||
@ -485,4 +504,20 @@ class SimpleKeyboardIME : InputMethodService(), OnKeyboardActionListener, Shared
|
||||
|
||||
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.Looper
|
||||
import android.os.Message
|
||||
import android.text.Editable
|
||||
import android.text.Html
|
||||
import android.text.TextWatcher
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import android.util.TypedValue
|
||||
import android.view.*
|
||||
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.SpanSizeLookup
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.rishabh.emojipicker.EmojiPickerView
|
||||
import com.simplemobiletools.commons.extensions.*
|
||||
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
|
||||
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.ClipsSectionLabel
|
||||
import com.simplemobiletools.keyboard.models.ListItem
|
||||
import com.simplemobiletools.keyboard.services.SimpleKeyboardIME.Companion.searching
|
||||
import java.util.*
|
||||
|
||||
@SuppressLint("UseCompatLoadingForDrawables", "ClickableViewAccessibility")
|
||||
@ -288,6 +294,7 @@ class MyKeyboardView @JvmOverloads constructor(context: Context, attrs: Attribut
|
||||
accessHelper = AccessHelper(this, mKeyboard?.mKeys.orEmpty())
|
||||
ViewCompat.setAccessibilityDelegate(this, accessHelper)
|
||||
|
||||
|
||||
// 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
|
||||
// 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 **/
|
||||
fun setKeyboardHolder(binding: KeyboardViewKeyboardBinding) {
|
||||
keyboardViewBinding = binding.apply {
|
||||
mToolbarHolder = toolbarHolder
|
||||
mToolbarHolder = mainToolbarKeyboardHolder
|
||||
mClipboardManagerHolder = clipboardManagerHolder
|
||||
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) {
|
||||
keyboardViewBinding?.apply {
|
||||
emojiPaletteTopBar.background = ColorDrawable(toolbarColor)
|
||||
emojiSearchToolbar.background = ColorDrawable(toolbarColor)
|
||||
emojiPaletteHolder.background = ColorDrawable(backgroundColor)
|
||||
emojiPaletteClose.applyColorFilter(textColor)
|
||||
emojiPaletteLabel.setTextColor(textColor)
|
||||
|
||||
emojiSearchView.setHintTextColor(mTextColor)
|
||||
emojiSearchviewCross.applyColorFilter(textColor)
|
||||
emojiSearchViewSearchIcon.applyColorFilter(textColor)
|
||||
emojiSearchResult.background = ColorDrawable(backgroundColor)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
emojiPaletteBottomBar.background = ColorDrawable(backgroundColor)
|
||||
emojiPaletteModeChange.apply {
|
||||
@ -1481,37 +1494,106 @@ class MyKeyboardView @JvmOverloads constructor(context: Context, attrs: Attribut
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setupEmojis()
|
||||
}
|
||||
|
||||
|
||||
fun openEmojiPalette() {
|
||||
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 {
|
||||
emojiPaletteHolder.beGone()
|
||||
emojisList?.scrollToPosition(0)
|
||||
emojiSearchView.setOnFocusChangeListener(object : View.OnFocusChangeListener{
|
||||
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() {
|
||||
ensureBackgroundThread {
|
||||
val fullEmojiList = parseRawEmojiSpecsFile(context, EMOJI_SPEC_FILE_PATH)
|
||||
val systemFontPaint = Paint().apply {
|
||||
typeface = Typeface.DEFAULT
|
||||
private fun closeEmojiPalette() {
|
||||
|
||||
keyboardViewBinding?.apply {
|
||||
|
||||
|
||||
if(emojiPaletteHolder.isVisible){
|
||||
emojiPaletteHolder.beGone()
|
||||
emojiSearchToolbar.beGone()
|
||||
mainToolbarKeyboardHolder.beVisible()
|
||||
}else{
|
||||
emojiSearchView.clearFocus()
|
||||
emojiSearchView.text.clear()
|
||||
emojiPaletteHolder.beVisible()
|
||||
}
|
||||
|
||||
val emojis = fullEmojiList.filter { emoji ->
|
||||
systemFontPaint.hasGlyph(emoji.emoji) || (EmojiCompat.get().loadState == EmojiCompat.LOAD_STATE_SUCCEEDED && EmojiCompat.get()
|
||||
.getEmojiMatch(emoji.emoji, emojiCompatMetadataVersion) == EMOJI_SUPPORTED)
|
||||
}
|
||||
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
setupEmojiAdapter(emojis)
|
||||
}
|
||||
searching =false
|
||||
keyboardViewBinding?.emojiSearchResult?.beGone()
|
||||
}
|
||||
}
|
||||
|
||||
@ -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() {
|
||||
if (mPreviewPopup.isShowing) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
<com.simplemobiletools.commons.views.MyRecyclerView
|
||||
android:id="@+id/emojis_list"
|
||||
android:id="@+id/emojiPickerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
|
@ -5,8 +5,90 @@
|
||||
android:layout_width="match_parent"
|
||||
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
|
||||
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_height="@dimen/toolbar_height"
|
||||
android:layout_above="@+id/keyboard_view"
|
||||
@ -121,6 +203,8 @@
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
|
||||
<!--emoji section-->
|
||||
<RelativeLayout
|
||||
android:id="@+id/emoji_palette_holder"
|
||||
android:layout_width="match_parent"
|
||||
@ -131,50 +215,18 @@
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="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
|
||||
android:id="@+id/emoji_content_holder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_above="@id/emoji_palette_bottom_bar"
|
||||
android:layout_below="@+id/emoji_palette_top_bar">
|
||||
android:layout_above="@id/emoji_palette_bottom_bar">
|
||||
|
||||
<com.simplemobiletools.commons.views.MyRecyclerView
|
||||
android:id="@+id/emojis_list"
|
||||
<com.rishabh.emojipicker.EmojiPickerView
|
||||
android:id="@+id/emojiPickerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
@ -237,7 +289,7 @@
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/toolbar_holder">
|
||||
app:layout_constraintTop_toTopOf="@+id/mainToolbarKeyboardHolder">
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/clipboard_manager_top_bar"
|
||||
|
@ -38,8 +38,9 @@
|
||||
<string name="start_sentences_capitalized">بدء الجمل بحرف كبير</string>
|
||||
<!-- Emojis -->
|
||||
<string name="emojis">الرموز التعبيرية</string>
|
||||
<string name="search_emoji">Search Emoji</string>
|
||||
<!--
|
||||
Haven't found some strings? There's more at
|
||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||
-->
|
||||
</resources>
|
||||
</resources>
|
||||
|
@ -38,6 +38,7 @@
|
||||
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
||||
<!-- Emojis -->
|
||||
<string name="emojis">Эмодзі</string>
|
||||
<string name="search_emoji">Search Emoji</string>
|
||||
<!--
|
||||
Haven't found some strings? There's more at
|
||||
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>
|
||||
<!-- Emojis -->
|
||||
<string name="emojis">Емоджита</string>
|
||||
<string name="search_emoji">Search Emoji</string>
|
||||
<!--
|
||||
Haven't found some strings? There's more at
|
||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||
|
@ -38,8 +38,9 @@
|
||||
<string name="start_sentences_capitalized">Comença les frases amb una lletra majúscula</string>
|
||||
<!-- Emojis -->
|
||||
<string name="emojis">Emojis</string>
|
||||
<string name="search_emoji">Search Emoji</string>
|
||||
<!--
|
||||
Haven't found some strings? There's more at
|
||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||
-->
|
||||
</resources>
|
||||
</resources>
|
||||
|
@ -38,4 +38,5 @@
|
||||
<string name="start_sentences_capitalized">گەورەکردنی یەکەم پیتی لاتینی</string>
|
||||
<!-- Emojis -->
|
||||
<string name="emojis">خەندەکان</string>
|
||||
</resources>
|
||||
<string name="search_emoji">Search Emoji</string>
|
||||
</resources>
|
||||
|
@ -38,6 +38,7 @@
|
||||
<string name="start_sentences_capitalized">Začínat věty velkým písmenem</string>
|
||||
<!-- Emojis -->
|
||||
<string name="emojis">Emotikony</string>
|
||||
<string name="search_emoji">Search Emoji</string>
|
||||
<!--
|
||||
Haven't found some strings? There's more at
|
||||
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>
|
||||
<!-- Emojis -->
|
||||
<string name="emojis">Emojis</string>
|
||||
<string name="search_emoji">Search Emoji</string>
|
||||
<!--
|
||||
Haven't found some strings? There's more at
|
||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||
|
@ -38,8 +38,9 @@
|
||||
<string name="start_sentences_capitalized">Sätze mit einem Großbuchstaben beginnen</string>
|
||||
<!-- Emojis -->
|
||||
<string name="emojis">Emojis</string>
|
||||
<string name="search_emoji">Search Emoji</string>
|
||||
<!--
|
||||
Haven't found some strings? There's more at
|
||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||
-->
|
||||
</resources>
|
||||
</resources>
|
||||
|
@ -38,6 +38,7 @@
|
||||
<string name="start_sentences_capitalized">Αρχίστε τις προτάσεις με κεφαλαίο γράμμα</string>
|
||||
<!-- Emojis -->
|
||||
<string name="emojis">Emojis</string>
|
||||
<string name="search_emoji">Search Emoji</string>
|
||||
<!--
|
||||
Haven't found some strings? There's more at
|
||||
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>
|
||||
<!-- Emojis -->
|
||||
<string name="emojis">Emojis</string>
|
||||
<string name="search_emoji">Search Emoji</string>
|
||||
<!--
|
||||
Haven't found some strings? There's more at
|
||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||
|
@ -38,8 +38,9 @@
|
||||
<string name="start_sentences_capitalized">Empezar las frases con mayúsculas</string>
|
||||
<!-- Emojis -->
|
||||
<string name="emojis">Emoticonos</string>
|
||||
<string name="search_emoji">Search Emoji</string>
|
||||
<!--
|
||||
Haven't found some strings? There's more at
|
||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||
-->
|
||||
</resources>
|
||||
</resources>
|
||||
|
@ -38,8 +38,9 @@
|
||||
<string name="start_sentences_capitalized">Alusta lauseid suurtähega</string>
|
||||
<!-- Emojis -->
|
||||
<string name="emojis">Emojid</string>
|
||||
<string name="search_emoji">Search Emoji</string>
|
||||
<!--
|
||||
Haven't found some strings? There's more at
|
||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||
-->
|
||||
</resources>
|
||||
</resources>
|
||||
|
@ -38,6 +38,7 @@
|
||||
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
||||
<!-- Emojis -->
|
||||
<string name="emojis">Emojit</string>
|
||||
<string name="search_emoji">Search Emoji</string>
|
||||
<!--
|
||||
Haven't found some strings? There's more at
|
||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||
|
@ -38,8 +38,9 @@
|
||||
<string name="start_sentences_capitalized">Commencer les phrases par une majuscule</string>
|
||||
<!-- Emojis -->
|
||||
<string name="emojis">Émojis</string>
|
||||
<string name="search_emoji">Search Emoji</string>
|
||||
<!--
|
||||
Haven't found some strings? There's more at
|
||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||
-->
|
||||
</resources>
|
||||
</resources>
|
||||
|
@ -38,6 +38,7 @@
|
||||
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
||||
<!-- Emojis -->
|
||||
<string name="emojis">Emoticona</string>
|
||||
<string name="search_emoji">Search Emoji</string>
|
||||
<!--
|
||||
Haven't found some strings? There's more at
|
||||
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>
|
||||
<!-- Emojis -->
|
||||
<string name="emojis">Emoji</string>
|
||||
<string name="search_emoji">Search Emoji</string>
|
||||
<!--
|
||||
Haven't found some strings? There's more at
|
||||
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>
|
||||
<!-- Emojis -->
|
||||
<string name="emojis">Emojik</string>
|
||||
<string name="search_emoji">Search Emoji</string>
|
||||
<!--
|
||||
Haven't found some strings? There's more at
|
||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||
|
@ -38,8 +38,9 @@
|
||||
<string name="start_sentences_capitalized">Mulai kalimat dengan huruf kapital</string>
|
||||
<!-- Emojis -->
|
||||
<string name="emojis">Emoji</string>
|
||||
<string name="search_emoji">Search Emoji</string>
|
||||
<!--
|
||||
Haven't found some strings? There's more at
|
||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||
-->
|
||||
</resources>
|
||||
</resources>
|
||||
|
@ -38,8 +38,9 @@
|
||||
<string name="start_sentences_capitalized">Inizia le frasi con la lettera maiuscola</string>
|
||||
<!-- Emojis -->
|
||||
<string name="emojis">Emoji</string>
|
||||
<string name="search_emoji">Search Emoji</string>
|
||||
<!--
|
||||
Haven't found some strings? There's more at
|
||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||
-->
|
||||
</resources>
|
||||
</resources>
|
||||
|
@ -38,6 +38,7 @@
|
||||
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
||||
<!-- Emojis -->
|
||||
<string name="emojis">Emojis</string>
|
||||
<string name="search_emoji">Search Emoji</string>
|
||||
<!--
|
||||
Haven't found some strings? There's more at
|
||||
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>
|
||||
<!-- Emojis -->
|
||||
<string name="emojis">絵文字</string>
|
||||
<string name="search_emoji">Search Emoji</string>
|
||||
<!--
|
||||
Haven't found some strings? There's more at
|
||||
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>
|
||||
<!-- Emojis -->
|
||||
<string name="emojis">Emojis</string>
|
||||
<string name="search_emoji">Search Emoji</string>
|
||||
<!--
|
||||
Haven't found some strings? There's more at
|
||||
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>
|
||||
<!-- Emojis -->
|
||||
<string name="emojis">Emojis</string>
|
||||
<string name="search_emoji">Search Emoji</string>
|
||||
<!--
|
||||
Haven't found some strings? There's more at
|
||||
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>
|
||||
<!-- Emojis -->
|
||||
<string name="emojis">Emocijzīmes</string>
|
||||
<string name="search_emoji">Search Emoji</string>
|
||||
<!--
|
||||
Haven't found some strings? There's more at
|
||||
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>
|
||||
<!-- Emojis -->
|
||||
<string name="emojis">ഇമോജികൾ</string>
|
||||
<string name="search_emoji">Search Emoji</string>
|
||||
</resources>
|
||||
|
@ -38,6 +38,7 @@
|
||||
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
||||
<!-- Emojis -->
|
||||
<string name="emojis">Emojis</string>
|
||||
<string name="search_emoji">Search Emoji</string>
|
||||
<!--
|
||||
Haven't found some strings? There's more at
|
||||
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,8 +38,9 @@
|
||||
<string name="start_sentences_capitalized">Zinnen met een hoofdletter beginnen</string>
|
||||
<!-- Emojis -->
|
||||
<string name="emojis">Emoji\'s</string>
|
||||
<string name="search_emoji">Search Emoji</string>
|
||||
<!--
|
||||
Haven't found some strings? There's more at
|
||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||
-->
|
||||
</resources>
|
||||
</resources>
|
||||
|
@ -38,4 +38,5 @@
|
||||
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
||||
<!-- Emojis -->
|
||||
<string name="emojis">ایموجیاں</string>
|
||||
<string name="search_emoji">Search Emoji</string>
|
||||
</resources>
|
||||
|
@ -38,4 +38,5 @@
|
||||
<string name="start_sentences_capitalized">ਵਾਕ ਵੱਡੇ ਅੱਖਰ ਨਾਲ ਸ਼ੁਰੂ ਕਰੋ</string>
|
||||
<!-- Emojis -->
|
||||
<string name="emojis">Emojis</string>
|
||||
</resources>
|
||||
<string name="search_emoji">Search Emoji</string>
|
||||
</resources>
|
||||
|
@ -38,8 +38,9 @@
|
||||
<string name="start_sentences_capitalized">Zaczynaj zdania wielką literą</string>
|
||||
<!-- Emojis -->
|
||||
<string name="emojis">Emoji</string>
|
||||
<string name="search_emoji">Search Emoji</string>
|
||||
<!--
|
||||
Haven't found some strings? There's more at
|
||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||
-->
|
||||
</resources>
|
||||
</resources>
|
||||
|
@ -38,6 +38,7 @@
|
||||
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
||||
<!-- Emojis -->
|
||||
<string name="emojis">Emojis</string>
|
||||
<string name="search_emoji">Search Emoji</string>
|
||||
<!--
|
||||
Haven't found some strings? There's more at
|
||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||
|
@ -38,8 +38,9 @@
|
||||
<string name="start_sentences_capitalized">Iniciar frases com letra maiúscula</string>
|
||||
<!-- Emojis -->
|
||||
<string name="emojis">Emojis</string>
|
||||
<string name="search_emoji">Search Emoji</string>
|
||||
<!--
|
||||
Haven't found some strings? There's more at
|
||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||
-->
|
||||
</resources>
|
||||
</resources>
|
||||
|
@ -38,6 +38,7 @@
|
||||
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
||||
<!-- Emojis -->
|
||||
<string name="emojis">Emoticoane</string>
|
||||
<string name="search_emoji">Search Emoji</string>
|
||||
<!--
|
||||
Haven't found some strings? There's more at
|
||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||
|
@ -38,6 +38,7 @@
|
||||
<string name="start_sentences_capitalized">Начинать предложения с заглавной буквы</string>
|
||||
<!-- Emojis -->
|
||||
<string name="emojis">Эмодзи</string>
|
||||
<string name="search_emoji">Search Emoji</string>
|
||||
<!--
|
||||
Haven't found some strings? There's more at
|
||||
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>
|
||||
<!-- Emojis -->
|
||||
<string name="emojis">Emoji</string>
|
||||
<string name="search_emoji">Search Emoji</string>
|
||||
<!--
|
||||
Haven't found some strings? There's more at
|
||||
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>
|
||||
<!-- Emojis -->
|
||||
<string name="emojis">Emoji-ji</string>
|
||||
<string name="search_emoji">Search Emoji</string>
|
||||
</resources>
|
||||
|
@ -38,4 +38,5 @@
|
||||
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
||||
<!-- Emojis -->
|
||||
<string name="emojis">Емоји</string>
|
||||
<string name="search_emoji">Search Emoji</string>
|
||||
</resources>
|
||||
|
@ -38,6 +38,7 @@
|
||||
<string name="start_sentences_capitalized">Börja meningar med stor bokstav</string>
|
||||
<!-- Emojis -->
|
||||
<string name="emojis">Emojier</string>
|
||||
<string name="search_emoji">Search Emoji</string>
|
||||
<!--
|
||||
Haven't found some strings? There's more at
|
||||
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>
|
||||
<!-- Emojis -->
|
||||
<string name="emojis">Emojis</string>
|
||||
<string name="search_emoji">Search Emoji</string>
|
||||
<!--
|
||||
Haven't found some strings? There's more at
|
||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||
|
@ -38,8 +38,9 @@
|
||||
<string name="start_sentences_capitalized">Cümlelere büyük harfle başla</string>
|
||||
<!-- Emojis -->
|
||||
<string name="emojis">Emojiler</string>
|
||||
<string name="search_emoji">Search Emoji</string>
|
||||
<!--
|
||||
Haven't found some strings? There's more at
|
||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||
-->
|
||||
</resources>
|
||||
</resources>
|
||||
|
@ -38,6 +38,7 @@
|
||||
<string name="start_sentences_capitalized">Починати речення з великої літери</string>
|
||||
<!-- Emojis -->
|
||||
<string name="emojis">Емодзі</string>
|
||||
<string name="search_emoji">Search Emoji</string>
|
||||
<!--
|
||||
Haven't found some strings? There's more at
|
||||
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>
|
||||
<!-- Emojis -->
|
||||
<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
|
||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||
|
@ -38,8 +38,9 @@
|
||||
<string name="start_sentences_capitalized">句子开头使用大写字母</string>
|
||||
<!-- Emojis -->
|
||||
<string name="emojis">表情符号</string>
|
||||
<string name="search_emoji">Search Emoji</string>
|
||||
<!--
|
||||
Haven't found some strings? There's more at
|
||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||
-->
|
||||
</resources>
|
||||
</resources>
|
||||
|
@ -39,6 +39,7 @@
|
||||
<string name="start_sentences_capitalized">Start sentences with a capital letter</string>
|
||||
<!-- Emojis -->
|
||||
<string name="emojis">Emojis</string>
|
||||
<string name="search_emoji">Search Emoji</string>
|
||||
<!--
|
||||
Haven't found some strings? There's more at
|
||||
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>
|
||||
<!-- Emojis -->
|
||||
<string name="emojis">Emojis</string>
|
||||
<string name="search_emoji">Search Emoji</string>
|
||||
<!--
|
||||
Haven't found some strings? There's more at
|
||||
https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res
|
||||
|
@ -1,5 +1,6 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android).apply(false)
|
||||
alias(libs.plugins.kotlinAndroid).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
Loading…
x
Reference in New Issue
Block a user