Skip to content

Commit 8b24fee

Browse files
committed
feat: add emoji helper classes
1 parent a71b8e5 commit 8b24fee

3 files changed

Lines changed: 279 additions & 1 deletion

File tree

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,37 @@
1+
// SPDX-License-Identifier: GPL-3.0-or-later
12
package be.scri.helpers
23

3-
class AutoGridLayoutManager {
4+
import android.content.Context
5+
import androidx.recyclerview.widget.GridLayoutManager
6+
import androidx.recyclerview.widget.RecyclerView
7+
8+
/**
9+
* A GridLayoutManager that automatically calculates the number of columns
10+
* based on the available width and desired item width.
11+
* Based on Fossify's AutoGridLayoutManager approach.
12+
*
13+
* @param context The application context.
14+
* @param itemWidth The desired width of each item in pixels.
15+
*/
16+
class AutoGridLayoutManager(
17+
context: Context,
18+
private val itemWidth: Int,
19+
) : GridLayoutManager(context, 1) {
20+
21+
/**
22+
* Recalculates the span count based on available width before laying out children.
23+
*/
24+
override fun onLayoutChildren(
25+
recycler: RecyclerView.Recycler?,
26+
state: RecyclerView.State?,
27+
) {
28+
val width = width
29+
val height = height
30+
if (itemWidth > 0 && width > 0 && height > 0) {
31+
val totalSpace = width - paddingRight - paddingLeft
32+
val spanCount = maxOf(1, totalSpace / itemWidth)
33+
setSpanCount(spanCount)
34+
}
35+
super.onLayoutChildren(recycler, state)
36+
}
437
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// SPDX-License-Identifier: GPL-3.0-or-later
2+
package be.scri.helpers
3+
4+
import android.annotation.SuppressLint
5+
import android.content.Context
6+
import android.view.LayoutInflater
7+
import android.view.ViewGroup
8+
import android.widget.TextView
9+
import androidx.recyclerview.widget.RecyclerView
10+
import be.scri.R
11+
12+
/**
13+
* RecyclerView adapter for displaying emojis and category headers in the emoji palette.
14+
* Directly based on Fossify's EmojisAdapter implementation.
15+
*
16+
* @param context The application context.
17+
* @param items The list of items to display, either categories or emojis.
18+
* @param itemClick Callback invoked when the user taps an emoji.
19+
*/
20+
class EmojiAdapter(
21+
val context: Context,
22+
var items: List<Item>,
23+
val itemClick: (emoji: EmojiData) -> Unit,
24+
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
25+
26+
private val layoutInflater = LayoutInflater.from(context)
27+
28+
override fun onCreateViewHolder(
29+
parent: ViewGroup,
30+
viewType: Int,
31+
): RecyclerView.ViewHolder =
32+
when (viewType) {
33+
ITEM_TYPE_EMOJI -> EmojiViewHolder(
34+
layoutInflater.inflate(R.layout.item_emoji, parent, false),
35+
)
36+
ITEM_TYPE_CATEGORY -> EmojiCategoryViewHolder(
37+
layoutInflater.inflate(R.layout.item_emoji_category_title, parent, false),
38+
)
39+
else -> throw IllegalArgumentException("Unsupported view type: $viewType")
40+
}
41+
42+
override fun onBindViewHolder(
43+
holder: RecyclerView.ViewHolder,
44+
position: Int,
45+
) {
46+
when (holder) {
47+
is EmojiViewHolder -> holder.bindView(items[position] as Item.Emoji)
48+
is EmojiCategoryViewHolder -> holder.bindView(items[position] as Item.Category)
49+
}
50+
}
51+
52+
override fun getItemViewType(position: Int): Int =
53+
when (items[position]) {
54+
is Item.Emoji -> ITEM_TYPE_EMOJI
55+
is Item.Category -> ITEM_TYPE_CATEGORY
56+
}
57+
58+
override fun getItemCount() = items.size
59+
60+
/**
61+
* Updates the adapter's item list and refreshes the RecyclerView.
62+
*
63+
* @param emojiItems The new list of items to display.
64+
*/
65+
@SuppressLint("NotifyDataSetChanged")
66+
fun updateItems(emojiItems: List<Item>) {
67+
items = emojiItems
68+
notifyDataSetChanged()
69+
}
70+
71+
/**
72+
* ViewHolder for a single emoji item.
73+
*
74+
* @param view The inflated item_emoji view.
75+
*/
76+
inner class EmojiViewHolder(
77+
view: android.view.View,
78+
) : RecyclerView.ViewHolder(view) {
79+
private val emojiValue: TextView = view.findViewById(R.id.emoji_value)
80+
81+
fun bindView(emoji: Item.Emoji) {
82+
emojiValue.text = emoji.emojiData.emoji
83+
itemView.setOnClickListener {
84+
itemClick.invoke(emoji.emojiData)
85+
}
86+
}
87+
}
88+
89+
/**
90+
* ViewHolder for a category header.
91+
*
92+
* @param view The inflated item_emoji_category_title view.
93+
*/
94+
inner class EmojiCategoryViewHolder(
95+
view: android.view.View,
96+
) : RecyclerView.ViewHolder(view) {
97+
private val emojiCategoryTitle: TextView = view.findViewById(R.id.emoji_category_title)
98+
99+
fun bindView(category: Item.Category) {
100+
emojiCategoryTitle.text = context.getString(getCategoryTitleRes(category.value))
101+
}
102+
}
103+
104+
/**
105+
* Sealed interface representing items in the emoji list.
106+
* Either a category header or an individual emoji.
107+
*/
108+
sealed interface Item {
109+
/** A single tappable emoji. */
110+
data class Emoji(val emojiData: EmojiData) : Item
111+
/** A category header row. */
112+
data class Category(val value: String) : Item
113+
}
114+
115+
companion object {
116+
private const val ITEM_TYPE_EMOJI = 0
117+
private const val ITEM_TYPE_CATEGORY = 1
118+
}
119+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
// SPDX-License-Identifier: GPL-3.0-or-later
2+
package be.scri.helpers
3+
4+
import android.content.Context
5+
import be.scri.R
6+
7+
private var cachedEmojiData: MutableList<EmojiData>? = null
8+
9+
const val EMOJI_SPEC_FILE_PATH = "emoji_spec.txt"
10+
11+
/**
12+
* Reads the emoji spec file and returns a parsed list of EmojiData.
13+
* Directly based on Fossify's parseRawEmojiSpecsFile() implementation.
14+
*
15+
* @param context The application context used to access assets.
16+
* @param path The path to the emoji spec file within assets.
17+
* @return A mutable list of [EmojiData] objects parsed from the file.
18+
*/
19+
fun parseRawEmojiSpecsFile(
20+
context: Context,
21+
path: String,
22+
): MutableList<EmojiData> {
23+
// if (cachedEmojiData != null) {
24+
// return cachedEmojiData!!
25+
// }
26+
27+
val emojis = mutableListOf<EmojiData>()
28+
var emojiEditorList: MutableList<String>? = null
29+
var category: String? = null
30+
31+
fun commitEmojiEditorList() {
32+
emojiEditorList?.let {
33+
val base = it.first()
34+
val variants = it.drop(1)
35+
emojis.add(EmojiData(category ?: "none", base, variants))
36+
}
37+
emojiEditorList = null
38+
}
39+
40+
context.assets.open(path).bufferedReader().useLines { lines ->
41+
for (line in lines) {
42+
when {
43+
line.startsWith("#") -> { }
44+
line.startsWith("[") -> {
45+
commitEmojiEditorList()
46+
category = line.replace("[", "").replace("]", "")
47+
}
48+
line.trim().isEmpty() -> continue
49+
else -> {
50+
if (!line.startsWith("\t")) {
51+
commitEmojiEditorList()
52+
}
53+
val data = line.split(";")
54+
if (data.size == 3) {
55+
val emoji = data[0].trim()
56+
if (emojiEditorList != null) {
57+
emojiEditorList!!.add(emoji)
58+
} else {
59+
emojiEditorList = mutableListOf(emoji)
60+
}
61+
}
62+
}
63+
}
64+
}
65+
commitEmojiEditorList()
66+
}
67+
68+
cachedEmojiData = emojis
69+
return emojis
70+
}
71+
72+
/**
73+
* Data class representing a single emoji with its category and skin tone variants.
74+
* Directly based on Fossify's EmojiData model.
75+
*
76+
* @param category The category this emoji belongs to.
77+
* @param emoji The base emoji character string.
78+
* @param variants The list of skin tone or other variants.
79+
*/
80+
data class EmojiData(
81+
val category: String,
82+
val emoji: String,
83+
val variants: List<String>,
84+
)
85+
86+
/**
87+
* Returns the drawable resource ID for a given emoji category icon.
88+
* Based on Fossify's getCategoryIconRes() function.
89+
*
90+
* @param category The category name from the emoji spec file.
91+
* @return The drawable resource ID for the category icon.
92+
*/
93+
fun getCategoryIconRes(category: String): Int =
94+
when (category) {
95+
"smileys_emotion" -> R.drawable.ic_emoji_smileys
96+
"people_body" -> R.drawable.ic_emoji_people
97+
"animals_nature" -> R.drawable.ic_emoji_animals
98+
"food_drink" -> R.drawable.ic_emoji_food
99+
"travel_places" -> R.drawable.ic_emoji_travel
100+
"activities" -> R.drawable.ic_emoji_activities
101+
"objects" -> R.drawable.ic_emoji_objects
102+
"symbols" -> R.drawable.ic_emoji_symbols
103+
"flags" -> R.drawable.ic_emoji_flags
104+
else -> R.drawable.ic_emoji_vector
105+
}
106+
107+
/**
108+
* Returns the string resource ID for a given emoji category title.
109+
* Based on Fossify's getCategoryTitleRes() function.
110+
*
111+
* @param category The category name from the emoji spec file.
112+
* @return The string resource ID for the category title.
113+
*/
114+
fun getCategoryTitleRes(category: String): Int =
115+
when (category) {
116+
"smileys_emotion" -> R.string.smileys_and_emotions
117+
"people_body" -> R.string.people_and_body
118+
"animals_nature" -> R.string.animals_and_nature
119+
"food_drink" -> R.string.food_and_drink
120+
"travel_places" -> R.string.travel_and_places
121+
"activities" -> R.string.activities
122+
"objects" -> R.string.objects
123+
"symbols" -> R.string.symbols
124+
"flags" -> R.string.flags
125+
else -> R.string.recently_used
126+
}

0 commit comments

Comments
 (0)