From 2cc6f1a99fd794c81bdac3e0dc537b6fb32f68d8 Mon Sep 17 00:00:00 2001 From: Kevin Arutyunyan Date: Fri, 10 Feb 2023 22:30:23 +0100 Subject: [PATCH 1/5] Group custom emoji by category --- .../tusky/adapter/EmojiAdapter.kt | 45 +++++++++++++++++-- .../components/compose/ComposeActivity.kt | 2 +- .../com/keylesspalace/tusky/entity/Emoji.kt | 3 +- app/src/main/res/layout/activity_compose.xml | 14 ++++-- 4 files changed, 56 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt index 51aa43f731..a0f58097fd 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt @@ -18,6 +18,7 @@ package com.keylesspalace.tusky.adapter import android.view.LayoutInflater import android.view.ViewGroup import androidx.appcompat.widget.TooltipCompat +import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide import com.keylesspalace.tusky.databinding.ItemEmojiButtonBinding @@ -31,8 +32,37 @@ class EmojiAdapter( private val animate: Boolean ) : RecyclerView.Adapter>() { - private val emojiList: List = emojiList.filter { emoji -> emoji.visibleInPicker == null || emoji.visibleInPicker } - .sortedBy { it.shortcode.lowercase(Locale.ROOT) } + private val trueEmojiList: List = emojiList.filter { emoji -> emoji.visibleInPicker == null || emoji.visibleInPicker } + .sortedWith(compareBy(nullsLast()) { it.category?.lowercase(Locale.ROOT) }.thenBy { it.shortcode.lowercase(Locale.ROOT) }) + private val emojiList = mutableListOf() + private lateinit var emojiCategories: Map + + data class EmojiGridItem(val emoji: Emoji?, val trueIndex: Int?) + + override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { + super.onAttachedToRecyclerView(recyclerView) + + val spanCount = (recyclerView.layoutManager as GridLayoutManager).spanCount + + val catMap = HashMap() + var currentCategory: String? = null + for (index in trueEmojiList.indices) { + val emoji = trueEmojiList[index] + if (emoji.category != currentCategory) { + currentCategory = emoji.category + catMap[currentCategory] = index + val emojiListIndex = emojiList.size - 1 + if (index > 0) { + repeat(2 * spanCount - (emojiListIndex % spanCount) - 1) { + emojiList.add(EmojiGridItem(null, null)) + } + } + } + emojiList.add(EmojiGridItem(emoji, index)) + } + + emojiCategories = catMap + } override fun getItemCount() = emojiList.size @@ -42,8 +72,17 @@ class EmojiAdapter( } override fun onBindViewHolder(holder: BindingHolder, position: Int) { - val emoji = emojiList[position] + val emojiGridItem = emojiList[position] + val emoji = emojiGridItem.emoji val emojiImageView = holder.binding.root + if (emoji == null) { + emojiImageView.background = null + emojiImageView.setImageResource(android.R.color.transparent) + emojiImageView.contentDescription = null + emojiImageView.setOnClickListener(null) + TooltipCompat.setTooltipText(emojiImageView, null) + return + } if (animate) { Glide.with(emojiImageView) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt index 79a043c094..5b6c3bb389 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt @@ -483,7 +483,7 @@ class ComposeActivity : composeOptionsBehavior = BottomSheetBehavior.from(binding.composeOptionsBottomSheet) addMediaBehavior = BottomSheetBehavior.from(binding.addMediaBottomSheet) scheduleBehavior = BottomSheetBehavior.from(binding.composeScheduleView) - emojiBehavior = BottomSheetBehavior.from(binding.emojiView) + emojiBehavior = BottomSheetBehavior.from(binding.emojiBottomSheet) enableButton(binding.composeEmojiButton, clickable = false, colorActive = false) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Emoji.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Emoji.kt index 130831a2d6..ba406947e8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Emoji.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Emoji.kt @@ -24,5 +24,6 @@ data class Emoji( val shortcode: String, val url: String, @SerializedName("static_url") val staticUrl: String, - @SerializedName("visible_in_picker") val visibleInPicker: Boolean? + @SerializedName("visible_in_picker") val visibleInPicker: Boolean?, + val category: String? ) : Parcelable diff --git a/app/src/main/res/layout/activity_compose.xml b/app/src/main/res/layout/activity_compose.xml index 367d88baa5..1bae973cd3 100644 --- a/app/src/main/res/layout/activity_compose.xml +++ b/app/src/main/res/layout/activity_compose.xml @@ -236,19 +236,27 @@ android:textSize="?attr/status_text_medium" /> - + app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"> + + + + Date: Sat, 11 Feb 2023 01:26:40 +0100 Subject: [PATCH 2/5] Scroll to category in emoji picker --- .../tusky/adapter/EmojiAdapter.kt | 21 ++++++++---- .../components/compose/ComposeActivity.kt | 32 ++++++++++++++++++- app/src/main/res/layout/activity_compose.xml | 14 ++++++++ app/src/main/res/values/strings.xml | 3 +- 4 files changed, 62 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt index a0f58097fd..f48480117e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt @@ -33,9 +33,11 @@ class EmojiAdapter( ) : RecyclerView.Adapter>() { private val trueEmojiList: List = emojiList.filter { emoji -> emoji.visibleInPicker == null || emoji.visibleInPicker } - .sortedWith(compareBy(nullsLast()) { it.category?.lowercase(Locale.ROOT) }.thenBy { it.shortcode.lowercase(Locale.ROOT) }) + .sortedWith(compareBy(nullsFirst()) { it.category?.lowercase(Locale.ROOT) }.thenBy { it.shortcode.lowercase(Locale.ROOT) }) private val emojiList = mutableListOf() - private lateinit var emojiCategories: Map + lateinit var emojiCategories: LinkedHashMap + private set + private lateinit var emojiCategoryPositions: HashMap data class EmojiGridItem(val emoji: Emoji?, val trueIndex: Int?) @@ -44,24 +46,31 @@ class EmojiAdapter( val spanCount = (recyclerView.layoutManager as GridLayoutManager).spanCount - val catMap = HashMap() + val catMap = LinkedHashMap() + val catPosMap = HashMap() var currentCategory: String? = null for (index in trueEmojiList.indices) { val emoji = trueEmojiList[index] - if (emoji.category != currentCategory) { + if (emoji.category != currentCategory || index == 0) { currentCategory = emoji.category catMap[currentCategory] = index - val emojiListIndex = emojiList.size - 1 if (index > 0) { + val emojiListIndex = emojiList.size - 1 repeat(2 * spanCount - (emojiListIndex % spanCount) - 1) { emojiList.add(EmojiGridItem(null, null)) } } + catPosMap[currentCategory] = emojiList.size } emojiList.add(EmojiGridItem(emoji, index)) } emojiCategories = catMap + emojiCategoryPositions = catPosMap + } + + fun getCategoryStartPosition(category: String?): Int { + return emojiCategoryPositions.getValue(category) } override fun getItemCount() = emojiList.size @@ -77,7 +86,7 @@ class EmojiAdapter( val emojiImageView = holder.binding.root if (emoji == null) { emojiImageView.background = null - emojiImageView.setImageResource(android.R.color.transparent) + Glide.with(emojiImageView).clear(emojiImageView) emojiImageView.contentDescription = null emojiImageView.setOnClickListener(null) TooltipCompat.setTooltipText(emojiImageView, null) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt index 5b6c3bb389..163bbf66b1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt @@ -31,6 +31,7 @@ import android.os.Build import android.os.Bundle import android.os.Parcelable import android.provider.MediaStore +import android.util.DisplayMetrics import android.util.Log import android.view.KeyEvent import android.view.MenuItem @@ -41,6 +42,7 @@ import android.widget.ImageButton import android.widget.LinearLayout import android.widget.PopupMenu import android.widget.Toast +import android.widget.TextView import androidx.activity.OnBackPressedCallback import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels @@ -58,6 +60,8 @@ import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope import androidx.preference.PreferenceManager import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.LinearSmoothScroller +import androidx.recyclerview.widget.RecyclerView import androidx.transition.TransitionManager import com.canhub.cropper.CropImage import com.canhub.cropper.CropImageContract @@ -1220,7 +1224,33 @@ class ComposeActivity : private fun setEmojiList(emojiList: List?) { if (emojiList != null) { val animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) - binding.emojiView.adapter = EmojiAdapter(emojiList, this@ComposeActivity, animateEmojis) + val emojiAdapter = EmojiAdapter(emojiList, this@ComposeActivity, animateEmojis) + binding.emojiView.adapter = emojiAdapter + + emojiAdapter.emojiCategories.keys.forEachIndexed() { i: Int, s: String? -> + val categoryTextView = TextView(this) + categoryTextView.text = s ?: getString(R.string.custom_emoji_default_category_title) + val padding = (8 * resources.displayMetrics.density + 0.5f).toInt() + categoryTextView.setPadding(padding, padding, padding, padding) + // categoryTextView.textSize = ... R.attr.status_text_medium ... + // if (i == 0) categoryTextView.setTypeface(null, Typeface.BOLD) + categoryTextView.setOnClickListener() { + val p = emojiAdapter.getCategoryStartPosition(s) + val smoothScroller: RecyclerView.SmoothScroller = object : LinearSmoothScroller(binding.emojiView.context) { + override fun getHorizontalSnapPreference(): Int { + return SNAP_TO_START + } + override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics?): Float { + return 2000f / binding.emojiView.computeHorizontalScrollRange(); + } + } + smoothScroller.targetPosition = p; + binding.emojiView.layoutManager?.startSmoothScroll(smoothScroller) + } + binding.emojiCategoryLinearLayout.addView(categoryTextView) + } + if (emojiAdapter.emojiCategories.isNotEmpty()) binding.emojiCategoryScrollView.visibility = View.VISIBLE + enableButton(binding.composeEmojiButton, true, emojiList.isNotEmpty()) } } diff --git a/app/src/main/res/layout/activity_compose.xml b/app/src/main/res/layout/activity_compose.xml index 1bae973cd3..585ae1dd44 100644 --- a/app/src/main/res/layout/activity_compose.xml +++ b/app/src/main/res/layout/activity_compose.xml @@ -251,6 +251,20 @@ app:behavior_peekHeight="0dp" app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"> + + + + + Compose Your instance %s does not have any custom emojis + Custom Emoji style System default You\'ll need to download these emoji sets first @@ -727,7 +728,7 @@ Unfollow #%s? Mute notifications - + Reading order Oldest first From 9198175cbbc90b615f96c9c4d8b2d30197ce8122 Mon Sep 17 00:00:00 2001 From: Kevin Arutyunyan Date: Sat, 11 Feb 2023 02:14:40 +0100 Subject: [PATCH 3/5] Fix emoji category font, set bold typeface and scroll to left edge --- .../components/compose/ComposeActivity.kt | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt index 163bbf66b1..e158568636 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt @@ -26,6 +26,7 @@ import android.content.pm.PackageManager import android.graphics.Bitmap import android.graphics.PorterDuff import android.graphics.PorterDuffColorFilter +import android.graphics.Typeface import android.net.Uri import android.os.Build import android.os.Bundle @@ -33,6 +34,7 @@ import android.os.Parcelable import android.provider.MediaStore import android.util.DisplayMetrics import android.util.Log +import android.util.TypedValue import android.view.KeyEvent import android.view.MenuItem import android.view.View @@ -55,6 +57,7 @@ import androidx.core.content.ContextCompat import androidx.core.content.FileProvider import androidx.core.view.ContentInfoCompat import androidx.core.view.OnReceiveContentListener +import androidx.core.view.forEach import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope @@ -1227,14 +1230,16 @@ class ComposeActivity : val emojiAdapter = EmojiAdapter(emojiList, this@ComposeActivity, animateEmojis) binding.emojiView.adapter = emojiAdapter - emojiAdapter.emojiCategories.keys.forEachIndexed() { i: Int, s: String? -> + emojiAdapter.emojiCategories.keys.forEachIndexed { i: Int, s: String? -> val categoryTextView = TextView(this) categoryTextView.text = s ?: getString(R.string.custom_emoji_default_category_title) val padding = (8 * resources.displayMetrics.density + 0.5f).toInt() categoryTextView.setPadding(padding, padding, padding, padding) - // categoryTextView.textSize = ... R.attr.status_text_medium ... - // if (i == 0) categoryTextView.setTypeface(null, Typeface.BOLD) - categoryTextView.setOnClickListener() { + val typedValue = TypedValue() + this.theme.resolveAttribute(R.attr.status_text_medium, typedValue, true) + categoryTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, typedValue.getDimension(resources.displayMetrics)) + if (i == 0) categoryTextView.setTypeface(null, Typeface.BOLD) + categoryTextView.setOnClickListener { val p = emojiAdapter.getCategoryStartPosition(s) val smoothScroller: RecyclerView.SmoothScroller = object : LinearSmoothScroller(binding.emojiView.context) { override fun getHorizontalSnapPreference(): Int { @@ -1246,6 +1251,11 @@ class ComposeActivity : } smoothScroller.targetPosition = p; binding.emojiView.layoutManager?.startSmoothScroll(smoothScroller) + + binding.emojiCategoryLinearLayout.forEach { view -> + (view as TextView).setTypeface(null, if (view == it) Typeface.BOLD else Typeface.NORMAL) + } + binding.emojiCategoryScrollView.smoothScrollTo(it.left, 0) } binding.emojiCategoryLinearLayout.addView(categoryTextView) } From 9bc257d65aa2e6f4bf915a35541e9b18bc6d3e7e Mon Sep 17 00:00:00 2001 From: Kevin Arutyunyan Date: Sat, 11 Feb 2023 02:49:47 +0100 Subject: [PATCH 4/5] Highlight active emoji category --- .../tusky/adapter/EmojiAdapter.kt | 15 ++++++-- .../components/compose/ComposeActivity.kt | 34 +++++++++++++++---- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt index f48480117e..50484fbd87 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt @@ -37,7 +37,7 @@ class EmojiAdapter( private val emojiList = mutableListOf() lateinit var emojiCategories: LinkedHashMap private set - private lateinit var emojiCategoryPositions: HashMap + private lateinit var emojiCategoryPositions: LinkedHashMap data class EmojiGridItem(val emoji: Emoji?, val trueIndex: Int?) @@ -47,7 +47,7 @@ class EmojiAdapter( val spanCount = (recyclerView.layoutManager as GridLayoutManager).spanCount val catMap = LinkedHashMap() - val catPosMap = HashMap() + val catPosMap = LinkedHashMap() var currentCategory: String? = null for (index in trueEmojiList.indices) { val emoji = trueEmojiList[index] @@ -73,6 +73,17 @@ class EmojiAdapter( return emojiCategoryPositions.getValue(category) } + fun getCategoryForPosition(position: Int): String? { + var prevKey: String? = null + for (entry in emojiCategoryPositions) { + if (position < entry.value) { + return prevKey + } + prevKey = entry.key + } + return prevKey + } + override fun getItemCount() = emojiList.size override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt index e158568636..efca86a6bf 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt @@ -62,6 +62,7 @@ import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope import androidx.preference.PreferenceManager +import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearSmoothScroller import androidx.recyclerview.widget.RecyclerView @@ -1224,12 +1225,21 @@ class ComposeActivity : replaceTextAtCaret(":$shortcode: ") } + private fun setActiveEmojiCategory(categoryTextView: TextView) { + binding.emojiCategoryLinearLayout.forEach { view -> + (view as TextView).setTypeface(null, if (view == categoryTextView) Typeface.BOLD else Typeface.NORMAL) + } + binding.emojiCategoryScrollView.smoothScrollTo(categoryTextView.left, 0) + } + private fun setEmojiList(emojiList: List?) { if (emojiList != null) { val animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) val emojiAdapter = EmojiAdapter(emojiList, this@ComposeActivity, animateEmojis) binding.emojiView.adapter = emojiAdapter + val categoryTextViews = hashMapOf() + emojiAdapter.emojiCategories.keys.forEachIndexed { i: Int, s: String? -> val categoryTextView = TextView(this) categoryTextView.text = s ?: getString(R.string.custom_emoji_default_category_title) @@ -1246,20 +1256,30 @@ class ComposeActivity : return SNAP_TO_START } override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics?): Float { - return 2000f / binding.emojiView.computeHorizontalScrollRange(); + return 2000f / binding.emojiView.computeHorizontalScrollRange() } } - smoothScroller.targetPosition = p; + smoothScroller.targetPosition = p binding.emojiView.layoutManager?.startSmoothScroll(smoothScroller) - binding.emojiCategoryLinearLayout.forEach { view -> - (view as TextView).setTypeface(null, if (view == it) Typeface.BOLD else Typeface.NORMAL) - } - binding.emojiCategoryScrollView.smoothScrollTo(it.left, 0) + setActiveEmojiCategory(it as TextView) } binding.emojiCategoryLinearLayout.addView(categoryTextView) + categoryTextViews[s] = categoryTextView + } + if (emojiAdapter.emojiCategories.isNotEmpty()) { + binding.emojiCategoryScrollView.visibility = View.VISIBLE + + binding.emojiView.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + if (binding.emojiView.layoutManager?.isSmoothScrolling == true) return // TODO needs to be improved + val firstVisibleItemPosition = (binding.emojiView.layoutManager as GridLayoutManager).findFirstVisibleItemPosition() + val activeCategory = emojiAdapter.getCategoryForPosition(firstVisibleItemPosition) + setActiveEmojiCategory(categoryTextViews.getValue(activeCategory)) + } + }) } - if (emojiAdapter.emojiCategories.isNotEmpty()) binding.emojiCategoryScrollView.visibility = View.VISIBLE enableButton(binding.composeEmojiButton, true, emojiList.isNotEmpty()) } From 88d17b3bca3cb54aac86b22ecd36dd7e8eedb587 Mon Sep 17 00:00:00 2001 From: Kevin Arutyunyan Date: Sat, 11 Feb 2023 03:35:08 +0100 Subject: [PATCH 5/5] Fix import order --- .../keylesspalace/tusky/components/compose/ComposeActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt index efca86a6bf..2376045099 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt @@ -43,8 +43,8 @@ import android.widget.AdapterView import android.widget.ImageButton import android.widget.LinearLayout import android.widget.PopupMenu -import android.widget.Toast import android.widget.TextView +import android.widget.Toast import androidx.activity.OnBackPressedCallback import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels