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 { ...@@ -29,6 +29,8 @@ dependencies {
implementation libraries.androidKtx implementation libraries.androidKtx
implementation libraries.appCompat implementation libraries.appCompat
implementation libraries.kotlin implementation libraries.kotlin
implementation libraries.coroutines
implementation libraries.coroutinesAndroid
implementation libraries.constraintlayout implementation libraries.constraintlayout
implementation libraries.recyclerview implementation libraries.recyclerview
implementation libraries.material implementation libraries.material
......
...@@ -117,7 +117,6 @@ class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow ...@@ -117,7 +117,6 @@ class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow
DrawableCompat.setTint(wrappedDrawable, getFitzpatrickColor(tone)) DrawableCompat.setTint(wrappedDrawable, getFitzpatrickColor(tone))
(changeColorView as ImageView).setImageDrawable(wrappedDrawable) (changeColorView as ImageView).setImageDrawable(wrappedDrawable)
adapter.setFitzpatrick(tone) adapter.setFitzpatrick(tone)
} }
@ColorInt @ColorInt
...@@ -155,7 +154,7 @@ class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow ...@@ -155,7 +154,7 @@ class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow
callback.onEmojiAdded(emoji) callback.onEmojiAdded(emoji)
} }
}) })
viewPager.offscreenPageLimit = 0 viewPager.offscreenPageLimit = EmojiCategory.values().size
viewPager.adapter = adapter viewPager.adapter = adapter
for (category in EmojiCategory.values()) { for (category in EmojiCategory.values()) {
......
package chat.rocket.android.emoji package chat.rocket.android.emoji
import android.text.Spannable
import android.text.SpannableString import android.text.SpannableString
import android.text.Spanned import android.text.Spanned
...@@ -11,11 +12,12 @@ class EmojiParser { ...@@ -11,11 +12,12 @@ class EmojiParser {
* Spannable. * Spannable.
* *
* @param text The text to parse * @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. * @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 unicodedText = EmojiRepository.shortnameToUnicode(text, true)
val spannable = SpannableString.valueOf(unicodedText) val spannable = factory?.newSpannable(unicodedText) ?: SpannableString.valueOf(unicodedText)
val typeface = EmojiRepository.cachedTypeface val typeface = EmojiRepository.cachedTypeface
// Look for groups of emojis, set a EmojiTypefaceSpan with the emojione font. // Look for groups of emojis, set a EmojiTypefaceSpan with the emojione font.
val length = spannable.length val length = spannable.length
......
...@@ -3,8 +3,12 @@ package chat.rocket.android.emoji ...@@ -3,8 +3,12 @@ package chat.rocket.android.emoji
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.graphics.Typeface import android.graphics.Typeface
import android.os.SystemClock
import chat.rocket.android.emoji.internal.EmojiCategory import chat.rocket.android.emoji.internal.EmojiCategory
import chat.rocket.android.emoji.internal.PREF_EMOJI_RECENTS 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.JSONArray
import org.json.JSONObject import org.json.JSONObject
import java.io.BufferedReader import java.io.BufferedReader
...@@ -12,6 +16,7 @@ import java.io.InputStream ...@@ -12,6 +16,7 @@ import java.io.InputStream
import java.io.InputStreamReader import java.io.InputStreamReader
import java.util.* import java.util.*
import java.util.regex.Pattern import java.util.regex.Pattern
import kotlin.coroutines.experimental.buildSequence
object EmojiRepository { object EmojiRepository {
...@@ -88,6 +93,15 @@ object EmojiRepository { ...@@ -88,6 +93,15 @@ object EmojiRepository {
return ALL_EMOJIS.filter { it.category.toLowerCase() == category.name.toLowerCase() } 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. * 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 ...@@ -16,23 +16,6 @@ class EmojiTypefaceSpan(family: String, private val newType: Typeface) : Typefac
} }
private fun applyCustomTypeFace(paint: Paint, tf: Typeface) { 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 paint.typeface = tf
} }
} }
\ No newline at end of file
package chat.rocket.android.emoji.internal package chat.rocket.android.emoji.internal
import android.text.Spannable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager.widget.PagerAdapter import androidx.viewpager.widget.PagerAdapter
...@@ -16,7 +15,12 @@ import chat.rocket.android.emoji.EmojiRepository ...@@ -16,7 +15,12 @@ import chat.rocket.android.emoji.EmojiRepository
import chat.rocket.android.emoji.Fitzpatrick import chat.rocket.android.emoji.Fitzpatrick
import chat.rocket.android.emoji.R import chat.rocket.android.emoji.R
import kotlinx.android.synthetic.main.emoji_category_layout.view.* 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() { internal class EmojiPagerAdapter(private val listener: EmojiKeyboardListener) : PagerAdapter() {
...@@ -32,23 +36,28 @@ internal class EmojiPagerAdapter(private val listener: EmojiKeyboardListener) : ...@@ -32,23 +36,28 @@ internal class EmojiPagerAdapter(private val listener: EmojiKeyboardListener) :
.inflate(R.layout.emoji_category_layout, container, false) .inflate(R.layout.emoji_category_layout, container, false)
with(view) { with(view) {
val layoutManager = GridLayoutManager(context, 8) val layoutManager = GridLayoutManager(context, 8)
val adapter = EmojiAdapter(layoutManager.spanCount, listener = listener)
val category = EmojiCategory.values()[position] 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.layoutManager = layoutManager
emoji_recycler_view.itemAnimator = DefaultItemAnimator() emoji_recycler_view.setRecycledViewPool(RecyclerView.RecycledViewPool())
emoji_recycler_view.adapter = adapter
emoji_recycler_view.isNestedScrollingEnabled = false
container.addView(view) 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 return view
} }
...@@ -75,18 +84,29 @@ internal class EmojiPagerAdapter(private val listener: EmojiKeyboardListener) : ...@@ -75,18 +84,29 @@ internal class EmojiPagerAdapter(private val listener: EmojiKeyboardListener) :
} }
class EmojiAdapter( class EmojiAdapter(
private val spanCount: Int,
private var fitzpatrick: Fitzpatrick = Fitzpatrick.Default, private var fitzpatrick: Fitzpatrick = Fitzpatrick.Default,
private val listener: EmojiKeyboardListener private val listener: EmojiKeyboardListener
) : RecyclerView.Adapter<EmojiRowViewHolder>() { ) : RecyclerView.Adapter<EmojiRowViewHolder>() {
private var emojis = Collections.emptyList<Emoji>() private val emojis = mutableListOf<Emoji>()
fun addEmojis(emojis: List<Emoji>) { fun addEmojis(emojis: List<Emoji>) {
this.emojis = emojis this.emojis.clear()
this.emojis.addAll(emojis)
notifyDataSetChanged() notifyDataSetChanged()
} }
suspend fun addEmojisFromSequence(emojiSequence: Sequence<Emoji>) {
withContext(CommonPool) {
emojiSequence.forEachIndexed { index, emoji ->
withContext(UI) {
emojis.add(emoji)
notifyItemInserted(index)
}
}
}
}
fun setFitzpatrick(fitzpatrick: Fitzpatrick) { fun setFitzpatrick(fitzpatrick: Fitzpatrick) {
this.fitzpatrick = fitzpatrick this.fitzpatrick = fitzpatrick
notifyDataSetChanged() notifyDataSetChanged()
...@@ -101,7 +121,7 @@ internal class EmojiPagerAdapter(private val listener: EmojiKeyboardListener) : ...@@ -101,7 +121,7 @@ internal class EmojiPagerAdapter(private val listener: EmojiKeyboardListener) :
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EmojiRowViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EmojiRowViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.emoji_row_item, parent, false) 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 override fun getItemCount(): Int = emojis.size
...@@ -109,25 +129,30 @@ internal class EmojiPagerAdapter(private val listener: EmojiKeyboardListener) : ...@@ -109,25 +129,30 @@ internal class EmojiPagerAdapter(private val listener: EmojiKeyboardListener) :
class EmojiRowViewHolder( class EmojiRowViewHolder(
itemView: View, itemView: View,
private val itemCount: Int,
private val spanCount: Int,
private val listener: EmojiKeyboardListener private val listener: EmojiKeyboardListener
) : RecyclerView.ViewHolder(itemView) { ) : RecyclerView.ViewHolder(itemView) {
private val emojiView: TextView = itemView.findViewById(R.id.emoji)
fun bind(emoji: Emoji) { fun bind(emoji: Emoji) {
val context = itemView.context with(itemView) {
emojiView.text = EmojiParser.parse(emoji.unicode) val parsedUnicode = unicodeCache[emoji.unicode]
val remainder = itemCount % spanCount emoji_view.setSpannableFactory(spannableFactory)
val lastLineItemCount = if (remainder == 0) spanCount else remainder emoji_view.text = if (parsedUnicode == null) {
val paddingBottom = context.resources.getDimensionPixelSize(R.dimen.picker_padding_bottom) EmojiParser.parse(emoji.unicode, spannableFactory).let {
if (adapterPosition >= itemCount - lastLineItemCount) { unicodeCache[emoji.unicode] = it
itemView.setPadding(0, 0, 0, paddingBottom) it
} }
itemView.setOnClickListener { } else {
listener.onEmojiAdded(emoji) 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"?> <?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" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:id="@+id/emoji_view"
android:layout_height="wrap_content" style="@style/TextAppearance.AppCompat.Title"
android:layout_gravity="center" android:layout_width="48dp"
android:layout_height="48dp"
android:foreground="?selectableItemBackground" android:foreground="?selectableItemBackground"
android:gravity="center"> android:textColor="#000000"
android:textSize="26sp"
<TextView tools:text="😀" />
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
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