Commit 2fb447c7 authored by Leonardo Aramaki's avatar Leonardo Aramaki

Lazy load emojis, keep fixed emoji dimensions so that TextView does not need...

Lazy load emojis, keep fixed emoji dimensions so that TextView does not need to relayout everytime its wrapping ViewHolder is recycled and other optimizations
parent 6e23e1c5
......@@ -29,6 +29,8 @@ dependencies {
implementation libraries.androidKtx
implementation libraries.appCompat
implementation libraries.kotlin
implementation libraries.coroutines
implementation libraries.coroutinesAndroid
implementation libraries.constraintlayout
implementation libraries.recyclerview
implementation libraries.material
......
......@@ -117,7 +117,6 @@ class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow
DrawableCompat.setTint(wrappedDrawable, getFitzpatrickColor(tone))
(changeColorView as ImageView).setImageDrawable(wrappedDrawable)
adapter.setFitzpatrick(tone)
}
@ColorInt
......@@ -155,7 +154,7 @@ class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow
callback.onEmojiAdded(emoji)
}
})
viewPager.offscreenPageLimit = 0
viewPager.offscreenPageLimit = EmojiCategory.values().size
viewPager.adapter = adapter
for (category in EmojiCategory.values()) {
......
package chat.rocket.android.emoji
import android.text.Spannable
import android.text.SpannableString
import android.text.Spanned
......@@ -11,11 +12,12 @@ class EmojiParser {
* Spannable.
*
* @param text The text to parse
* @param factory Optional. A [Spannable.Factory] instance to reuse when creating [Spannable].
* @return A rendered Spannable containing any supported emoji.
*/
fun parse(text: CharSequence): CharSequence {
fun parse(text: CharSequence, factory: Spannable.Factory? = null): CharSequence {
val unicodedText = EmojiRepository.shortnameToUnicode(text, true)
val spannable = SpannableString.valueOf(unicodedText)
val spannable = factory?.newSpannable(unicodedText) ?: SpannableString.valueOf(unicodedText)
val typeface = EmojiRepository.cachedTypeface
// Look for groups of emojis, set a EmojiTypefaceSpan with the emojione font.
val length = spannable.length
......
......@@ -3,8 +3,12 @@ package chat.rocket.android.emoji
import android.content.Context
import android.content.SharedPreferences
import android.graphics.Typeface
import android.os.SystemClock
import chat.rocket.android.emoji.internal.EmojiCategory
import chat.rocket.android.emoji.internal.PREF_EMOJI_RECENTS
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.withContext
import kotlinx.coroutines.experimental.yield
import org.json.JSONArray
import org.json.JSONObject
import java.io.BufferedReader
......@@ -12,6 +16,7 @@ import java.io.InputStream
import java.io.InputStreamReader
import java.util.*
import java.util.regex.Pattern
import kotlin.coroutines.experimental.buildSequence
object EmojiRepository {
......@@ -88,6 +93,15 @@ object EmojiRepository {
return ALL_EMOJIS.filter { it.category.toLowerCase() == category.name.toLowerCase() }
}
internal fun getEmojiSequenceByCategory(category: EmojiCategory): Sequence<Emoji> {
val list = ALL_EMOJIS.filter { it.category.toLowerCase() == category.name.toLowerCase() }
return buildSequence{
list.forEach {
yield(it)
}
}
}
/**
* Get the emoji given by a specified shortname. Returns null if can't find any.
*
......
......@@ -16,23 +16,6 @@ class EmojiTypefaceSpan(family: String, private val newType: Typeface) : Typefac
}
private fun applyCustomTypeFace(paint: Paint, tf: Typeface) {
val oldStyle: Int
val old = paint.typeface
if (old == null) {
oldStyle = 0
} else {
oldStyle = old.style
}
val fake = oldStyle and tf.style.inv()
if (fake and Typeface.BOLD != 0) {
paint.isFakeBoldText = true
}
if (fake and Typeface.ITALIC != 0) {
paint.textSkewX = -0.25f
}
paint.typeface = tf
}
}
\ No newline at end of file
package chat.rocket.android.emoji.internal
import android.text.Spannable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager.widget.PagerAdapter
......@@ -16,7 +15,12 @@ import chat.rocket.android.emoji.EmojiRepository
import chat.rocket.android.emoji.Fitzpatrick
import chat.rocket.android.emoji.R
import kotlinx.android.synthetic.main.emoji_category_layout.view.*
import java.util.*
import kotlinx.android.synthetic.main.emoji_row_item.view.*
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.withContext
internal class EmojiPagerAdapter(private val listener: EmojiKeyboardListener) : PagerAdapter() {
......@@ -32,23 +36,28 @@ internal class EmojiPagerAdapter(private val listener: EmojiKeyboardListener) :
.inflate(R.layout.emoji_category_layout, container, false)
with(view) {
val layoutManager = GridLayoutManager(context, 8)
val adapter = EmojiAdapter(layoutManager.spanCount, listener = listener)
val category = EmojiCategory.values()[position]
val emojis = if (category != EmojiCategory.RECENTS) {
EmojiRepository.getEmojisByCategory(category)
} else {
EmojiRepository.getRecents()
}
val recentEmojiSize = EmojiRepository.getRecents().size
text_no_recent_emoji.isVisible = category == EmojiCategory.RECENTS && recentEmojiSize == 0
adapter.addEmojis(emojis)
adapter.setFitzpatrick(fitzpatrick)
adapters[category] = adapter
emoji_recycler_view.layoutManager = layoutManager
emoji_recycler_view.itemAnimator = DefaultItemAnimator()
emoji_recycler_view.adapter = adapter
emoji_recycler_view.isNestedScrollingEnabled = false
emoji_recycler_view.setRecycledViewPool(RecyclerView.RecycledViewPool())
container.addView(view)
launch(UI) {
val emojis = if (category != EmojiCategory.RECENTS) {
EmojiRepository.getEmojiSequenceByCategory(category)
} else {
sequenceOf(*EmojiRepository.getRecents().toTypedArray())
}
val recentEmojiSize = EmojiRepository.getRecents().size
text_no_recent_emoji.isVisible = category == EmojiCategory.RECENTS && recentEmojiSize == 0
if (adapters[category] == null) {
val adapter = EmojiAdapter(listener = listener)
emoji_recycler_view.adapter = adapter
adapters[category] = adapter
adapter.addEmojisFromSequence(emojis)
}
adapters[category]!!.setFitzpatrick(fitzpatrick)
}
}
return view
}
......@@ -75,18 +84,29 @@ internal class EmojiPagerAdapter(private val listener: EmojiKeyboardListener) :
}
class EmojiAdapter(
private val spanCount: Int,
private var fitzpatrick: Fitzpatrick = Fitzpatrick.Default,
private val listener: EmojiKeyboardListener
) : RecyclerView.Adapter<EmojiRowViewHolder>() {
private var emojis = Collections.emptyList<Emoji>()
private val emojis = mutableListOf<Emoji>()
fun addEmojis(emojis: List<Emoji>) {
this.emojis = emojis
this.emojis.clear()
this.emojis.addAll(emojis)
notifyDataSetChanged()
}
suspend fun addEmojisFromSequence(emojiSequence: Sequence<Emoji>) {
withContext(CommonPool) {
emojiSequence.forEachIndexed { index, emoji ->
withContext(UI) {
emojis.add(emoji)
notifyItemInserted(index)
}
}
}
}
fun setFitzpatrick(fitzpatrick: Fitzpatrick) {
this.fitzpatrick = fitzpatrick
notifyDataSetChanged()
......@@ -101,7 +121,7 @@ internal class EmojiPagerAdapter(private val listener: EmojiKeyboardListener) :
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EmojiRowViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.emoji_row_item, parent, false)
return EmojiRowViewHolder(view, itemCount, spanCount, listener)
return EmojiRowViewHolder(view, listener)
}
override fun getItemCount(): Int = emojis.size
......@@ -109,25 +129,30 @@ internal class EmojiPagerAdapter(private val listener: EmojiKeyboardListener) :
class EmojiRowViewHolder(
itemView: View,
private val itemCount: Int,
private val spanCount: Int,
private val listener: EmojiKeyboardListener
) : RecyclerView.ViewHolder(itemView) {
private val emojiView: TextView = itemView.findViewById(R.id.emoji)
fun bind(emoji: Emoji) {
val context = itemView.context
emojiView.text = EmojiParser.parse(emoji.unicode)
val remainder = itemCount % spanCount
val lastLineItemCount = if (remainder == 0) spanCount else remainder
val paddingBottom = context.resources.getDimensionPixelSize(R.dimen.picker_padding_bottom)
if (adapterPosition >= itemCount - lastLineItemCount) {
itemView.setPadding(0, 0, 0, paddingBottom)
}
itemView.setOnClickListener {
listener.onEmojiAdded(emoji)
with(itemView) {
val parsedUnicode = unicodeCache[emoji.unicode]
emoji_view.setSpannableFactory(spannableFactory)
emoji_view.text = if (parsedUnicode == null) {
EmojiParser.parse(emoji.unicode, spannableFactory).let {
unicodeCache[emoji.unicode] = it
it
}
} else {
parsedUnicode
}
itemView.setOnClickListener {
listener.onEmojiAdded(emoji)
}
}
}
companion object {
private val spannableFactory = Spannable.Factory()
private val unicodeCache = mutableMapOf<CharSequence, CharSequence>()
}
}
}
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:id="@+id/emoji_view"
style="@style/TextAppearance.AppCompat.Title"
android:layout_width="48dp"
android:layout_height="48dp"
android:foreground="?selectableItemBackground"
android:gravity="center">
<TextView
android:id="@+id/emoji"
style="@style/TextAppearance.AppCompat.Title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:textColor="#000000"
android:textSize="26sp"
tools:text="😀" />
</FrameLayout>
\ No newline at end of file
android:textColor="#000000"
android:textSize="26sp"
tools:text="😀" />
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment