Commit be00b615 authored by Leonardo Aramaki's avatar Leonardo Aramaki

Add color used by the emoji keyboard; Load emojis; Add emoji related layout;...

Add color used by the emoji keyboard; Load emojis; Add emoji related layout; Add all emoji related code
parent befa16fc
This diff is collapsed.
......@@ -9,6 +9,7 @@ import chat.rocket.android.helper.CrashlyticsTree
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.MultiServerTokenRepository
import chat.rocket.android.server.domain.SettingsRepository
import chat.rocket.android.widget.emoji.EmojiLoader
import chat.rocket.common.model.Token
import chat.rocket.core.TokenRepository
import com.crashlytics.android.Crashlytics
......@@ -57,6 +58,7 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje
initCurrentServer()
AndroidThreeTen.init(this)
EmojiLoader.load(this)
setupCrashlytics()
setupFresco()
......
package chat.rocket.android.widget.emoji
import android.support.v4.view.PagerAdapter
import android.support.v7.widget.DefaultItemAnimator
import android.support.v7.widget.GridLayoutManager
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import chat.rocket.android.R
import java.util.*
class CategoryPagerAdapter(val callback: EmojiBottomPicker.OnEmojiClickCallback) : PagerAdapter() {
override fun isViewFromObject(view: View, obj: Any): Boolean {
return view == obj
}
override fun instantiateItem(container: ViewGroup, position: Int): Any {
val view = LayoutInflater.from(container.context)
.inflate(R.layout.emoji_category_layout, container, false)
val recycler = view.findViewById(R.id.emojiRecyclerView) as RecyclerView
val layoutManager = GridLayoutManager(view.context, 5)
val adapter = EmojiAdapter(callback)
val category = EmojiCategory.values().get(position)
val emojis = if (category != EmojiCategory.RECENTS)
EmojiLoader.getEmojisByCategory(category)
else
EmojiLoader.getRecents()
adapter.addEmojis(emojis)
recycler.layoutManager = layoutManager
recycler.itemAnimator = DefaultItemAnimator()
recycler.adapter = adapter
recycler.isNestedScrollingEnabled = false
container.addView(view)
return view
}
override fun destroyItem(container: ViewGroup, position: Int, view: Any) {
container.removeView(view as View)
}
override fun getCount() = EmojiCategory.values().size
override fun getPageTitle(position: Int) = EmojiCategory.values()[position].icon()
class EmojiAdapter(val callback: EmojiBottomPicker.OnEmojiClickCallback) : RecyclerView.Adapter<EmojiRowViewHolder>() {
private var emojis: List<Emoji> = Collections.emptyList()
fun addEmojis(emojis: List<Emoji>) {
this.emojis = emojis
notifyItemRangeInserted(0, emojis.size)
}
override fun onBindViewHolder(holder: EmojiRowViewHolder, position: Int) {
holder.bind(emojis[position])
}
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, callback)
}
override fun getItemCount(): Int = emojis.size
}
class EmojiRowViewHolder(itemView: View, val onEmojiClickCallback: EmojiBottomPicker.OnEmojiClickCallback) : RecyclerView.ViewHolder(itemView) {
private val emojiView: TextView = itemView.findViewById(R.id.emoji)
fun bind(emoji: Emoji) {
emojiView.text = EmojiParser.parse(emoji.unicode)
emojiView.setOnClickListener {
onEmojiClickCallback.onEmojiAdded(emoji)
}
}
}
}
\ No newline at end of file
package chat.rocket.android.widget.emoji
data class Emoji(
val shortname: String,
val shortnameAlternates: List<String>,
val unicode: String,
val keywords: List<String>,
val category: String,
val count: Int = 0
)
\ No newline at end of file
package chat.rocket.android.widget.emoji
import android.app.Dialog
import android.os.Bundle
import android.support.design.widget.BottomSheetBehavior
import android.support.design.widget.BottomSheetDialog
import android.support.design.widget.TabLayout
import android.support.v4.app.DialogFragment
import android.support.v4.view.ViewPager
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.widget.TextView
import chat.rocket.android.R
class EmojiBottomPicker : DialogFragment() {
private lateinit var viewPager: ViewPager
private lateinit var tabLayout: TabLayout
companion object {
const val PREF_EMOJI_RECENTS = "PREF_EMOJI_RECENTS"
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val view = inflater.inflate(R.layout.emoji_popup_layout, container, false)
viewPager = view.findViewById(R.id.pager_categories)
tabLayout = view.findViewById(R.id.tabs)
tabLayout.setupWithViewPager(viewPager)
view.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
view.viewTreeObserver.removeOnGlobalLayoutListener(this)
val parent = dialog.findViewById<View>(R.id.design_bottom_sheet)
parent?.let {
val bottomSheetBehavior = BottomSheetBehavior.from(parent)
if (bottomSheetBehavior != null) {
bottomSheetBehavior.setBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
override fun onSlide(bottomSheet: View, slideOffset: Float) {
}
override fun onStateChanged(bottomSheet: View, newState: Int) {
if (newState == BottomSheetBehavior.STATE_DRAGGING) {
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
}
}
})
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED)
}
}
}
})
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val callback = when (activity) {
is OnEmojiClickCallback -> activity as OnEmojiClickCallback
else -> {
val fragments = activity?.supportFragmentManager?.fragments
if (fragments == null || fragments.size == 0 || !(fragments[0] is OnEmojiClickCallback)) {
throw IllegalStateException("activity/fragment should implement OnEmojiClickCallback interface")
}
fragments[0] as OnEmojiClickCallback
}
}
viewPager.adapter = CategoryPagerAdapter(object : OnEmojiClickCallback {
override fun onEmojiAdded(emoji: Emoji) {
dismiss()
EmojiLoader.addToRecents(emoji)
callback.onEmojiAdded(emoji)
}
})
for (category in EmojiCategory.values()) {
val tab = tabLayout.getTabAt(category.ordinal)
val tabView = layoutInflater.inflate(R.layout.emoji_picker_tab, null)
tab?.setCustomView(tabView)
val textView = tabView.findViewById(R.id.text) as TextView
textView.text = category.icon()
}
viewPager.setCurrentItem(EmojiCategory.PEOPLE.ordinal)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return BottomSheetDialog(context!!, theme)
}
interface OnEmojiClickCallback {
fun onEmojiAdded(emoji: Emoji)
}
}
\ No newline at end of file
package chat.rocket.android.widget.emoji
import android.text.SpannableString
import android.text.Spanned
enum class EmojiCategory {
RECENTS {
override fun icon() = getTextIconFor("\uD83D\uDD58")
},
PEOPLE() {
override fun icon() = getTextIconFor("\uD83D\uDE00")
},
NATURE {
override fun icon() = getTextIconFor("\uD83D\uDC3B")
},
FOOD {
override fun icon() = getTextIconFor("\uD83C\uDF4E")
},
ACTIVITY {
override fun icon() = getTextIconFor("\uD83D\uDEB4")
},
TRAVEL {
override fun icon() = getTextIconFor("\uD83C\uDFD9️")
},
OBJECTS {
override fun icon() = getTextIconFor("\uD83D\uDD2A")
},
SYMBOLS {
override fun icon() = getTextIconFor("⚛")
},
FLAGS {
override fun icon() = getTextIconFor("\uD83D\uDEA9")
};
abstract fun icon(): CharSequence
protected fun getTextIconFor(text: String): CharSequence {
val span = EmojiTypefaceSpan("sans-serif", EmojiLoader.cachedTypeface)
return SpannableString.valueOf(text).apply {
setSpan(span, 0, text.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
}
}
}
\ No newline at end of file
package chat.rocket.android.widget.emoji
import android.content.Context
import android.content.SharedPreferences
import android.graphics.Typeface
import android.os.Build
import org.json.JSONArray
import org.json.JSONObject
import java.io.BufferedReader
import java.io.InputStream
import java.io.InputStreamReader
import java.util.*
import java.util.regex.Pattern
class EmojiLoader {
companion object {
private val shortNameToUnicode = HashMap<String, String>()
private val SHORTNAME_PATTERN = Pattern.compile(":([-+\\w]+):")
private val ALL_EMOJIS = mutableListOf<Emoji>()
private lateinit var preferences: SharedPreferences
internal lateinit var cachedTypeface: Typeface
fun load(context: Context, path: String = "emoji.json") {
preferences = context.getSharedPreferences("emoji", Context.MODE_PRIVATE)
ALL_EMOJIS.clear()
cachedTypeface = Typeface.createFromAsset(context.assets, "fonts/emojione-android.ttf")
val stream = context.assets.open(path)
val emojis = loadEmojis(stream)
emojis.forEach {
val unicodeIntList = mutableListOf<Int>()
it.unicode.split("-").forEach {
val value = it.toInt(16)
if (value >= 0x10000) {
val surrogatePair = calculateSurrogatePairs(value)
unicodeIntList.add(surrogatePair.first)
unicodeIntList.add(surrogatePair.second)
} else {
unicodeIntList.add(value)
}
}
val unicodeIntArray = unicodeIntList.toIntArray()
val unicode = String(unicodeIntArray, 0, unicodeIntArray.size)
ALL_EMOJIS.add(it.copy(unicode = unicode))
shortNameToUnicode.apply { put(it.shortname, unicode) }
}
}
/**
* Get all loaded emojis as list of Emoji objects.
*
* @return All emojis for all categories.
*/
fun getAll() = ALL_EMOJIS
/**
* Get all emojis for a given category.
*
* @param category Emoji category such as: PEOPLE, NATURE, ETC
*
* @return All emoji from specified category
*/
fun getEmojisByCategory(category: EmojiCategory): List<Emoji> {
return ALL_EMOJIS.filter { it.category.toLowerCase() == category.name.toLowerCase() }
}
/**
* Get the emoji given by a specified shortname. Returns null if can't find any.
*
* @param shortname The emoji shortname to search for
*
* @return Emoji given by shortname or null
*/
fun getEmojiByShortname(shortname: String) = ALL_EMOJIS.firstOrNull { it.shortname == shortname }
/**
* Add an emoji to the Recents category.
*/
fun addToRecents(emoji: Emoji) {
val emojiShortname = emoji.shortname
val recentsJson = JSONObject(preferences.getString(EmojiBottomPicker.PREF_EMOJI_RECENTS, "{}"))
if (recentsJson.has(emojiShortname)) {
val useCount = recentsJson.getInt(emojiShortname)
recentsJson.put(emojiShortname, useCount + 1)
} else {
recentsJson.put(emojiShortname, 1)
}
preferences.edit().putString(EmojiBottomPicker.PREF_EMOJI_RECENTS, recentsJson.toString()).apply()
}
/**
* Get all recently used emojis ordered by usage count.
*
* @return All recent emojis ordered by usage.
*/
fun getRecents(): List<Emoji> {
val list = mutableListOf<Emoji>()
val recentsJson = JSONObject(preferences.getString(EmojiBottomPicker.PREF_EMOJI_RECENTS, "{}"))
for (shortname in recentsJson.keys()) {
val emoji = getEmojiByShortname(shortname)
emoji?.let {
val useCount = recentsJson.getInt(it.shortname)
list.add(it.copy(count = useCount))
}
}
Collections.sort(list, { o1, o2 ->
o2.count - o1.count
})
return list
}
/**
* Replace shortnames to unicode characters.
*/
fun shortnameToUnicode(input: String, removeIfUnsupported: Boolean): String {
val matcher = SHORTNAME_PATTERN.matcher(input)
val supported = Build.VERSION.SDK_INT >= 16
var result: String = input
while (matcher.find()) {
val unicode = shortNameToUnicode.get(":${matcher.group(1)}:")
if (unicode == null) {
continue
}
if (supported) {
result = input.replace(":" + matcher.group(1) + ":", unicode)
} else if (!supported && removeIfUnsupported) {
result = input.replace(":" + matcher.group(1) + ":", "")
}
}
return result
}
private fun loadEmojis(stream: InputStream): List<Emoji> {
val emojisJSON = JSONArray(inputStreamToString(stream))
val emojis = ArrayList<Emoji>(emojisJSON.length());
for (i in 0 until emojisJSON.length()) {
val emoji = buildEmojiFromJSON(emojisJSON.getJSONObject(i))
emoji?.let {
emojis.add(it)
}
}
return emojis
}
private fun buildEmojiFromJSON(json: JSONObject): Emoji? {
if (!json.has("shortname") || !json.has("unicode")) {
return null
}
return Emoji(shortname = json.getString("shortname"),
unicode = json.getString("unicode"),
shortnameAlternates = buildStringListFromJsonArray(json.getJSONArray("shortnameAlternates")),
category = json.getString("category"),
keywords = buildStringListFromJsonArray(json.getJSONArray("keywords")))
}
private fun buildStringListFromJsonArray(array: JSONArray): List<String> {
val list = ArrayList<String>(array.length())
for (i in 0..array.length() - 1) {
list.add(array.getString(i))
}
return list
}
private fun inputStreamToString(stream: InputStream): String {
val sb = StringBuilder()
val isr = InputStreamReader(stream, Charsets.UTF_8)
val br = BufferedReader(isr)
var read: String? = br.readLine()
while (read != null) {
sb.append(read)
read = br.readLine()
}
br.close()
return sb.toString()
}
private fun calculateSurrogatePairs(scalar: Int): Pair<Int, Int> {
val temp: Int = (scalar - 0x10000) / 0x400
val s1: Int = Math.floor(temp.toDouble()).toInt() + 0xD800
val s2: Int = ((scalar - 0x10000) % 0x400) + 0xDC00
return Pair(s1, s2)
}
}
}
\ No newline at end of file
package chat.rocket.android.widget.emoji
import android.text.SpannableString
import android.text.Spanned
class EmojiParser {
companion object {
/**
* Parses a text string containing unicode characters and/or shortnames to a rendered
* Spannable.
*
* @param text The text to parse
*
* @return A rendered Spannable containing any supported emoji.
*/
fun parse(text: String): CharSequence {
val unicodedText = EmojiLoader.shortnameToUnicode(text, true)
val spannableString = SpannableString.valueOf(unicodedText)
// Look for groups of emojis, set a CustomTypefaceSpan with the emojione font
val length = spannableString.length
var inEmoji = false
var emojiStart = 0
var offset = 0
while (offset < length) {
val codepoint = unicodedText.codePointAt(offset)
val count = Character.charCount(codepoint)
if (codepoint >= 0x200) {
if (!inEmoji) {
emojiStart = offset
}
inEmoji = true
} else {
if (inEmoji) {
spannableString.setSpan(EmojiTypefaceSpan("sans-serif", EmojiLoader.cachedTypeface),
emojiStart, offset, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
}
inEmoji = false
}
offset += count
if (offset >= length && inEmoji) {
spannableString.setSpan(EmojiTypefaceSpan("sans-serif", EmojiLoader.cachedTypeface),
emojiStart, offset, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
}
}
return spannableString
}
}
}
\ No newline at end of file
package chat.rocket.android.widget.emoji
import android.graphics.Paint
import android.graphics.Typeface
import android.text.TextPaint
import android.text.style.TypefaceSpan
class EmojiTypefaceSpan(family: String, private val newType: Typeface) : TypefaceSpan(family) {
override fun updateDrawState(ds: TextPaint) {
applyCustomTypeFace(ds, newType)
}
override fun updateMeasureState(paint: TextPaint) {
applyCustomTypeFace(paint, newType)
}
private fun applyCustomTypeFace(paint: Paint, tf: Typeface) {
val oldStyle: Int
val old = paint.getTypeface()
if (old == null) {
oldStyle = 0
} else {
oldStyle = old.getStyle()
}
val fake = oldStyle and tf.style.inv()
if (fake and Typeface.BOLD != 0) {
paint.setFakeBoldText(true)
}
if (fake and Typeface.ITALIC != 0) {
paint.setTextSkewX(-0.25f)
}
paint.setTypeface(tf)
}
}
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout 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="wrap_content">
<android.support.v7.widget.RecyclerView
android:id="@+id/emojiRecyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.design.widget.CoordinatorLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/whitesmoke"
android:gravity="center"
android:textSize="14sp" />
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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="250dp">
<android.support.design.widget.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@+id/pager_categories"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tabGravity="center"
app:tabMaxWidth="48dp"
app:tabBackground="@color/whitesmoke"
app:tabMode="scrollable" />
<android.support.v4.view.ViewPager
android:id="@+id/pager_categories"
android:layout_width="match_parent"
android:layout_height="200dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tabs" />
</android.support.constraint.ConstraintLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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: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
......@@ -25,8 +25,9 @@
<color name="white">#FFFFFFFF</color>
<color name="black">#FF000000</color>
<color name="red">#FFFF0000</color>
<color name="darkGray">#a0a0a0</color>
<color name="actionMenuColor">#727272</color>
<color name="darkGray">#FFa0a0a0</color>
<color name="actionMenuColor">#FF727272</color>
<color name="whitesmoke">#FFf1f1f1</color>
<color name="linkTextColor">#FF074481</color>
<color name="linkBackgroundColor">#30074481</color>
......
/**
* Generator steps:
*
* 1. Download EmojiOne json file from: https://raw.githubusercontent.com/emojione/emojione/master/emoji.json
* 2. Install sdkman, kotlin and kscript for cli usage
* 3. Run: kscript generator.kts
*
* This file will output a json file named emoji-parsed.json
*/
@file:DependsOn("org.json:json:20090211")
import org.json.JSONArray
import org.json.JSONObject
import java.io.BufferedReader
import java.io.File
import java.io.InputStreamReader
import java.util.*
val stream = File("emoji.json").inputStream()
val sb = StringBuilder()
val isr = InputStreamReader(stream, Charsets.UTF_8)
val br = BufferedReader(isr)
var read: String? = br.readLine()
while (read != null) {
sb.append(read)
read = br.readLine()
}
br.close()
val json = JSONObject(sb.toString())
val all = JSONArray()
val jsonList = mutableListOf<JSONObject>()
json.keys().forEach {
val oldJson = json.getJSONObject(it as String) as JSONObject
val newJson = JSONObject().apply {
put("shortname", oldJson.getString("shortname"))
put("category", oldJson.getString("category"))
put("shortnameAlternates", oldJson.getJSONArray("shortname_alternates"))
put("keywords", oldJson.getJSONArray("keywords"))
put("order", oldJson.getInt("order"))
val codePoints = oldJson.get("code_points") as JSONObject
val unicode = codePoints.getString("fully_qualified")
put("unicode", unicode)
}
all.put(newJson)
jsonList.add(newJson)
}
Collections.sort(jsonList, { o1, o2 ->
val order1 = o1.getInt("order")
val order2 = o2.getInt("order")
return@sort order1 - order2
})
File("emoji-parsed.json").printWriter(Charsets.UTF_8).use { out ->
out.println("[")
for (i in 0..jsonList.size - 1) {
out.print(jsonList.get(i).toString(2))
if (i < jsonList.size - 1) {
out.println(",")
}
}
out.println("]")
}
println("Ok")
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