Commit 1e4ac51e authored by Leonardo Aramaki's avatar Leonardo Aramaki

Set emoji keyboard height to the same size as the softkeyboard. Store the height for later use

parent 80cacca2
...@@ -45,6 +45,7 @@ ...@@ -45,6 +45,7 @@
<activity <activity
android:name=".chatroom.ui.ChatRoomActivity" android:name=".chatroom.ui.ChatRoomActivity"
android:windowSoftInputMode="adjustPan"
android:theme="@style/AppTheme" /> android:theme="@style/AppTheme" />
<activity <activity
......
...@@ -9,7 +9,7 @@ import chat.rocket.android.helper.CrashlyticsTree ...@@ -9,7 +9,7 @@ import chat.rocket.android.helper.CrashlyticsTree
import chat.rocket.android.server.domain.GetCurrentServerInteractor import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.MultiServerTokenRepository import chat.rocket.android.server.domain.MultiServerTokenRepository
import chat.rocket.android.server.domain.SettingsRepository import chat.rocket.android.server.domain.SettingsRepository
import chat.rocket.android.widget.emoji.EmojiLoader import chat.rocket.android.widget.emoji.EmojiRepository
import chat.rocket.common.model.Token import chat.rocket.common.model.Token
import chat.rocket.core.TokenRepository import chat.rocket.core.TokenRepository
import com.crashlytics.android.Crashlytics import com.crashlytics.android.Crashlytics
...@@ -58,7 +58,7 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje ...@@ -58,7 +58,7 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje
initCurrentServer() initCurrentServer()
AndroidThreeTen.init(this) AndroidThreeTen.init(this)
EmojiLoader.load(this) EmojiRepository.load(this)
setupCrashlytics() setupCrashlytics()
setupFresco() setupFresco()
......
...@@ -8,6 +8,7 @@ import android.support.v7.app.AppCompatActivity ...@@ -8,6 +8,7 @@ import android.support.v7.app.AppCompatActivity
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.util.extensions.addFragment import chat.rocket.android.util.extensions.addFragment
import chat.rocket.android.util.extensions.textContent import chat.rocket.android.util.extensions.textContent
import chat.rocket.android.widget.emoji.EmojiFragment
import dagger.android.AndroidInjection import dagger.android.AndroidInjection
import dagger.android.AndroidInjector import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector import dagger.android.DispatchingAndroidInjector
...@@ -61,7 +62,14 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector { ...@@ -61,7 +62,14 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
} }
} }
override fun onBackPressed() = finishActivity() override fun onBackPressed() {
val frag = supportFragmentManager.findFragmentByTag(EmojiFragment.TAG) as EmojiFragment?
if (frag != null && frag.isShown()) {
frag.hide()
} else {
finishActivity()
}
}
override fun supportFragmentInjector(): AndroidInjector<Fragment> { override fun supportFragmentInjector(): AndroidInjector<Fragment> {
return fragmentDispatchingAndroidInjector return fragmentDispatchingAndroidInjector
......
...@@ -12,6 +12,7 @@ import android.support.v4.app.Fragment ...@@ -12,6 +12,7 @@ import android.support.v4.app.Fragment
import android.support.v7.widget.DefaultItemAnimator import android.support.v7.widget.DefaultItemAnimator
import android.support.v7.widget.LinearLayoutManager import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView import android.support.v7.widget.RecyclerView
import android.text.method.ScrollingMovementMethod
import android.view.* import android.view.*
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatroom.presentation.ChatRoomPresenter import chat.rocket.android.chatroom.presentation.ChatRoomPresenter
...@@ -22,7 +23,7 @@ import chat.rocket.android.helper.KeyboardHelper ...@@ -22,7 +23,7 @@ import chat.rocket.android.helper.KeyboardHelper
import chat.rocket.android.helper.MessageParser import chat.rocket.android.helper.MessageParser
import chat.rocket.android.util.extensions.* import chat.rocket.android.util.extensions.*
import chat.rocket.android.widget.emoji.Emoji import chat.rocket.android.widget.emoji.Emoji
import chat.rocket.android.widget.emoji.EmojiBottomPicker import chat.rocket.android.widget.emoji.EmojiFragment
import chat.rocket.android.widget.emoji.EmojiParser import chat.rocket.android.widget.emoji.EmojiParser
import dagger.android.support.AndroidSupportInjection import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_chat_room.* import kotlinx.android.synthetic.main.fragment_chat_room.*
...@@ -47,7 +48,7 @@ private const val BUNDLE_CHAT_ROOM_TYPE = "chat_room_type" ...@@ -47,7 +48,7 @@ private const val BUNDLE_CHAT_ROOM_TYPE = "chat_room_type"
private const val BUNDLE_IS_CHAT_ROOM_READ_ONLY = "is_chat_room_read_only" private const val BUNDLE_IS_CHAT_ROOM_READ_ONLY = "is_chat_room_read_only"
private const val REQUEST_CODE_FOR_PERFORM_SAF = 42 private const val REQUEST_CODE_FOR_PERFORM_SAF = 42
class ChatRoomFragment : Fragment(), ChatRoomView, EmojiBottomPicker.OnEmojiClickCallback { class ChatRoomFragment : Fragment(), ChatRoomView, EmojiFragment.OnEmojiClickCallback {
@Inject lateinit var presenter: ChatRoomPresenter @Inject lateinit var presenter: ChatRoomPresenter
@Inject lateinit var parser: MessageParser @Inject lateinit var parser: MessageParser
private lateinit var adapter: ChatRoomAdapter private lateinit var adapter: ChatRoomAdapter
...@@ -225,7 +226,6 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiBottomPicker.OnEmojiClic ...@@ -225,7 +226,6 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiBottomPicker.OnEmojiClic
val cursorPosition = text_message.selectionStart val cursorPosition = text_message.selectionStart
text_message.text.insert(cursorPosition, EmojiParser.parse(emoji.shortname)) text_message.text.insert(cursorPosition, EmojiParser.parse(emoji.shortname))
text_message.setSelection(cursorPosition + emoji.unicode.length) text_message.setSelection(cursorPosition + emoji.unicode.length)
KeyboardHelper.showSoftKeyboard(text_message)
} }
private fun setupComposer() { private fun setupComposer() {
...@@ -234,6 +234,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiBottomPicker.OnEmojiClic ...@@ -234,6 +234,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiBottomPicker.OnEmojiClic
input_container.setVisible(false) input_container.setVisible(false)
} else { } else {
var playAnimation = true var playAnimation = true
text_message.movementMethod = ScrollingMovementMethod()
text_message.asObservable(0) text_message.asObservable(0)
.subscribe({ t -> .subscribe({ t ->
if (t.isNotEmpty() && playAnimation) { if (t.isNotEmpty() && playAnimation) {
...@@ -279,14 +280,24 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiBottomPicker.OnEmojiClic ...@@ -279,14 +280,24 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiBottomPicker.OnEmojiClic
button_add_reaction.setOnClickListener { button_add_reaction.setOnClickListener {
activity?.let { activity?.let {
KeyboardHelper.hideSoftKeyboard(it) val editor = text_message
val emojiBottomPicker = EmojiBottomPicker() val emojiFragment = EmojiFragment.getOrAttach(it, R.id.emoji_fragment_placeholder, composer)
text_message.apply { with(emojiFragment) {
addTextChangedListener(EmojiBottomPicker.EmojiTextWatcher(this)) if (!isShown()) {
show()
} else {
if (softKeyboardVisible) {
KeyboardHelper.hideSoftKeyboard(it)
} else {
KeyboardHelper.showSoftKeyboard(editor)
}
}
} }
emojiBottomPicker.show(it.supportFragmentManager, "EmojiBottomPicker")
} }
} }
addEmojiFragment()
text_message.addTextChangedListener(EmojiFragment.EmojiTextWatcher(text_message))
} }
} }
...@@ -297,6 +308,12 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiBottomPicker.OnEmojiClic ...@@ -297,6 +308,12 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiBottomPicker.OnEmojiClic
}) })
} }
private fun addEmojiFragment() {
activity?.let {
EmojiFragment.getOrAttach(it, R.id.emoji_fragment_placeholder, composer)
}
}
private fun clearActionMessage() { private fun clearActionMessage() {
citation = null citation = null
editingMessageId = null editingMessageId = null
......
...@@ -9,7 +9,7 @@ import android.view.View ...@@ -9,7 +9,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.widget.emoji.EmojiBottomPicker.OnEmojiClickCallback import chat.rocket.android.widget.emoji.EmojiFragment.OnEmojiClickCallback
import java.util.* import java.util.*
class CategoryPagerAdapter(val callback: OnEmojiClickCallback) : PagerAdapter() { class CategoryPagerAdapter(val callback: OnEmojiClickCallback) : PagerAdapter() {
...@@ -25,9 +25,9 @@ class CategoryPagerAdapter(val callback: OnEmojiClickCallback) : PagerAdapter() ...@@ -25,9 +25,9 @@ class CategoryPagerAdapter(val callback: OnEmojiClickCallback) : PagerAdapter()
val adapter = EmojiAdapter(layoutManager.spanCount, callback) val adapter = EmojiAdapter(layoutManager.spanCount, callback)
val category = EmojiCategory.values().get(position) val category = EmojiCategory.values().get(position)
val emojis = if (category != EmojiCategory.RECENTS) val emojis = if (category != EmojiCategory.RECENTS)
EmojiLoader.getEmojisByCategory(category) EmojiRepository.getEmojisByCategory(category)
else else
EmojiLoader.getRecents() EmojiRepository.getRecents()
adapter.addEmojis(emojis) adapter.addEmojis(emojis)
recycler.layoutManager = layoutManager recycler.layoutManager = layoutManager
recycler.itemAnimator = DefaultItemAnimator() recycler.itemAnimator = DefaultItemAnimator()
......
...@@ -35,7 +35,7 @@ enum class EmojiCategory { ...@@ -35,7 +35,7 @@ enum class EmojiCategory {
abstract fun icon(): CharSequence abstract fun icon(): CharSequence
protected fun getTextIconFor(text: String): CharSequence { protected fun getTextIconFor(text: String): CharSequence {
val span = EmojiTypefaceSpan("sans-serif", EmojiLoader.cachedTypeface) val span = EmojiTypefaceSpan("sans-serif", EmojiRepository.cachedTypeface)
return SpannableString.valueOf(text).apply { return SpannableString.valueOf(text).apply {
setSpan(span, 0, text.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE) setSpan(span, 0, text.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
} }
......
package chat.rocket.android.widget.emoji package chat.rocket.android.widget.emoji
import android.app.Dialog import android.graphics.Rect
import android.os.Bundle import android.os.Bundle
import android.support.design.widget.BottomSheetBehavior import android.support.annotation.IdRes
import android.support.design.widget.BottomSheetDialog
import android.support.design.widget.TabLayout import android.support.design.widget.TabLayout
import android.support.v4.app.DialogFragment import android.support.v4.app.Fragment
import android.support.v4.app.FragmentActivity
import android.support.v4.view.ViewPager import android.support.v4.view.ViewPager
import android.text.Editable import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
...@@ -16,45 +16,44 @@ import android.view.ViewTreeObserver ...@@ -16,45 +16,44 @@ import android.view.ViewTreeObserver
import android.widget.EditText import android.widget.EditText
import android.widget.TextView import android.widget.TextView
import chat.rocket.android.R import chat.rocket.android.R
import java.util.concurrent.atomic.AtomicReference import chat.rocket.android.util.extensions.setVisible
import java.util.function.UnaryOperator
open class EmojiBottomPicker : DialogFragment() { class EmojiFragment : Fragment() {
private lateinit var viewPager: ViewPager private lateinit var viewPager: ViewPager
private lateinit var tabLayout: TabLayout private lateinit var tabLayout: TabLayout
private lateinit var editor: View
internal lateinit var parentContainer: ViewGroup
var softKeyboardVisible = false
companion object { companion object {
const val PREF_EMOJI_RECENTS = "PREF_EMOJI_RECENTS" const val PREF_EMOJI_RECENTS = "PREF_EMOJI_RECENTS"
const val PREF_KEYBOARD_HEIGHT = "PREF_KEYBOARD_HEIGHT"
val TAG: String = EmojiFragment::class.java.simpleName
fun newInstance(editor: View) = EmojiFragment().apply { this.editor = editor }
fun getOrAttach(activity: FragmentActivity, @IdRes containerId: Int, editor: View): EmojiFragment {
val fragmentManager = activity.supportFragmentManager
var fragment: Fragment? = fragmentManager.findFragmentByTag(TAG)
return if (fragment == null) {
fragment = newInstance(editor)
fragment.parentContainer = activity.findViewById(containerId)
fragmentManager.beginTransaction()
.replace(containerId, fragment, TAG)
.commit()
fragment
} else {
fragment as EmojiFragment
}
}
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val view = inflater.inflate(R.layout.emoji_popup_layout, container, false) val view = inflater.inflate(R.layout.emoji_popup_layout, container, false)
parentContainer = view.findViewById(R.id.emoji_keyboard_container)
viewPager = view.findViewById(R.id.pager_categories) viewPager = view.findViewById(R.id.pager_categories)
tabLayout = view.findViewById(R.id.tabs) tabLayout = view.findViewById(R.id.tabs)
tabLayout.setupWithViewPager(viewPager) 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 return view
} }
...@@ -70,10 +69,47 @@ open class EmojiBottomPicker : DialogFragment() { ...@@ -70,10 +69,47 @@ open class EmojiBottomPicker : DialogFragment() {
} }
} }
activity?.let {
val decorView = it.getWindow().decorView
decorView.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
private val windowVisibleDisplayFrame = Rect()
private var lastVisibleDecorViewHeight: Int = 0
override fun onGlobalLayout() {
// Retrieve visible rectangle inside window.
decorView.getWindowVisibleDisplayFrame(windowVisibleDisplayFrame)
val visibleDecorViewHeight = windowVisibleDisplayFrame.height()
// Decide whether keyboard is visible from changing decor view height.
if (lastVisibleDecorViewHeight != 0) {
if (lastVisibleDecorViewHeight > visibleDecorViewHeight + 150) {
// Calculate current keyboard height (this includes also navigation bar height when in fullscreen mode).
val currentKeyboardHeight = decorView.height - windowVisibleDisplayFrame.bottom - editor.measuredHeight
// Notify listener about keyboard being shown.
EmojiRepository.saveKeyboardHeight(currentKeyboardHeight)
setKeyboardHeight(currentKeyboardHeight)
softKeyboardVisible = true
show()
} else if (lastVisibleDecorViewHeight + 150 < visibleDecorViewHeight) {
// Notify listener about keyboard being hidden.
softKeyboardVisible = false
hide()
}
}
// Save current decor view height for the next call.
lastVisibleDecorViewHeight = visibleDecorViewHeight
}
})
}
val storedHeight = EmojiRepository.getKeyboardHeight()
if (storedHeight > 0) {
setKeyboardHeight(storedHeight)
}
viewPager.adapter = CategoryPagerAdapter(object : OnEmojiClickCallback { viewPager.adapter = CategoryPagerAdapter(object : OnEmojiClickCallback {
override fun onEmojiAdded(emoji: Emoji) { override fun onEmojiAdded(emoji: Emoji) {
dismiss() EmojiRepository.addToRecents(emoji)
EmojiLoader.addToRecents(emoji)
callback.onEmojiAdded(emoji) callback.onEmojiAdded(emoji)
} }
}) })
...@@ -86,13 +122,14 @@ open class EmojiBottomPicker : DialogFragment() { ...@@ -86,13 +122,14 @@ open class EmojiBottomPicker : DialogFragment() {
textView.text = category.icon() textView.text = category.icon()
} }
val currentTab = if (EmojiLoader.getRecents().isEmpty()) EmojiCategory.PEOPLE.ordinal else val currentTab = if (EmojiRepository.getRecents().isEmpty()) EmojiCategory.PEOPLE.ordinal else
EmojiCategory.RECENTS.ordinal EmojiCategory.RECENTS.ordinal
viewPager.setCurrentItem(currentTab) viewPager.setCurrentItem(currentTab)
} }
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { private fun setKeyboardHeight(height: Int) {
return BottomSheetDialog(context!!, theme) parentContainer.layoutParams.height = height
parentContainer.requestLayout()
} }
class EmojiTextWatcher(val editor: EditText) : TextWatcher { class EmojiTextWatcher(val editor: EditText) : TextWatcher {
...@@ -139,6 +176,16 @@ open class EmojiBottomPicker : DialogFragment() { ...@@ -139,6 +176,16 @@ open class EmojiBottomPicker : DialogFragment() {
} }
} }
fun show() {
parentContainer.setVisible(true)
}
fun hide() {
parentContainer.setVisible(false)
}
fun isShown() = parentContainer.visibility == View.VISIBLE
interface OnEmojiClickCallback { interface OnEmojiClickCallback {
/** /**
* Callback triggered after an emoji is selected on the picker. * Callback triggered after an emoji is selected on the picker.
......
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: CharSequence, removeIfUnsupported: Boolean): String {
val matcher = SHORTNAME_PATTERN.matcher(input)
val supported = Build.VERSION.SDK_INT >= 16
var result: String = input.toString()
while (matcher.find()) {
val unicode = shortNameToUnicode.get(":${matcher.group(1)}:")
if (unicode == null) {
continue
}
if (supported) {
result = result.replace(":" + matcher.group(1) + ":", unicode)
} else if (!supported && removeIfUnsupported) {
result = result.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
...@@ -14,7 +14,7 @@ class EmojiParser { ...@@ -14,7 +14,7 @@ class EmojiParser {
* @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): CharSequence {
val unicodedText = EmojiLoader.shortnameToUnicode(text, true) val unicodedText = EmojiRepository.shortnameToUnicode(text, true)
val spannableString = SpannableString.valueOf(unicodedText) val spannableString = SpannableString.valueOf(unicodedText)
// Look for groups of emojis, set a CustomTypefaceSpan with the emojione font // Look for groups of emojis, set a CustomTypefaceSpan with the emojione font
val length = spannableString.length val length = spannableString.length
...@@ -31,14 +31,14 @@ class EmojiParser { ...@@ -31,14 +31,14 @@ class EmojiParser {
inEmoji = true inEmoji = true
} else { } else {
if (inEmoji) { if (inEmoji) {
spannableString.setSpan(EmojiTypefaceSpan("sans-serif", EmojiLoader.cachedTypeface), spannableString.setSpan(EmojiTypefaceSpan("sans-serif", EmojiRepository.cachedTypeface),
emojiStart, offset, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) emojiStart, offset, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
} }
inEmoji = false inEmoji = false
} }
offset += count offset += count
if (offset >= length && inEmoji) { if (offset >= length && inEmoji) {
spannableString.setSpan(EmojiTypefaceSpan("sans-serif", EmojiLoader.cachedTypeface), spannableString.setSpan(EmojiTypefaceSpan("sans-serif", EmojiRepository.cachedTypeface),
emojiStart, offset, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) emojiStart, offset, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
} }
} }
......
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
object EmojiRepository {
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(EmojiFragment.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(EmojiFragment.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(EmojiFragment.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
}
/**
* Store current soft keyboard height for later reference.
*/
fun saveKeyboardHeight(height: Int) {
if (height <= 0) {
return
}
preferences.edit()
.putInt(EmojiFragment.PREF_KEYBOARD_HEIGHT, height)
.apply()
}
/**
* Get stored keyboard height.
*
* @return Height of the current soft keyboard.
*/
fun getKeyboardHeight() = preferences.getInt(EmojiFragment.PREF_KEYBOARD_HEIGHT, 0)
/**
* Replace shortnames to unicode characters.
*/
fun shortnameToUnicode(input: CharSequence, removeIfUnsupported: Boolean): String {
val matcher = SHORTNAME_PATTERN.matcher(input)
val supported = Build.VERSION.SDK_INT >= 16
var result: String = input.toString()
while (matcher.find()) {
val unicode = shortNameToUnicode.get(":${matcher.group(1)}:")
if (unicode == null) {
continue
}
if (supported) {
result = result.replace(":" + matcher.group(1) + ":", unicode)
} else if (!supported && removeIfUnsupported) {
result = result.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
...@@ -5,9 +5,11 @@ ...@@ -5,9 +5,11 @@
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<android.support.v7.widget.RecyclerView <android.support.v7.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/emojiRecyclerView" android:id="@+id/emojiRecyclerView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
......
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/emoji_keyboard_container"
android:visibility="gone"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="250dp"> android:layout_height="200dp">
<View
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/colorDividerMessageComposer"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toTopOf="@+id/tabs" />
<android.support.design.widget.TabLayout <android.support.design.widget.TabLayout
android:id="@+id/tabs" android:id="@+id/tabs"
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@+id/pager_categories" app:layout_constraintBottom_toTopOf="@+id/pager_categories"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:tabGravity="fill"
app:tabBackground="@color/whitesmoke" app:tabBackground="@color/whitesmoke"
app:tabGravity="fill"
app:tabMaxWidth="48dp"
app:tabMode="scrollable" /> app:tabMode="scrollable" />
<android.support.v4.view.ViewPager <android.support.v4.view.ViewPager
android:id="@+id/pager_categories" android:id="@+id/pager_categories"
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="200dp" android:layout_height="0dp"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:background="@color/white" android:background="@color/white"
......
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content"
android:orientation="vertical">
<View <android.support.constraint.ConstraintLayout
android:id="@+id/divider" android:id="@+id/composer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="1dp" android:layout_height="wrap_content">
android:background="@color/colorDividerMessageComposer" />
<TextView <View
android:id="@+id/text_room_is_read_only" android:id="@+id/divider"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="45dp" android:layout_height="1dp"
android:background="@color/white" android:background="@color/colorDividerMessageComposer" />
android:gravity="center"
android:text="@string/msg_this_room_is_read_only"
android:textColor="@color/black"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@+id/divider" />
<LinearLayout <TextView
android:id="@+id/input_container" android:id="@+id/text_room_is_read_only"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="45dp"
android:layout_marginEnd="10dp" android:background="@color/white"
android:layout_marginStart="10dp" android:gravity="center"
android:layout_marginTop="10dp" android:text="@string/msg_this_room_is_read_only"
android:orientation="horizontal" android:textColor="@color/black"
android:paddingBottom="10dp" android:visibility="gone"
app:layout_constraintTop_toBottomOf="@+id/divider"> app:layout_constraintTop_toBottomOf="@+id/divider" />
<ImageButton <LinearLayout
android:id="@+id/button_add_reaction" android:id="@+id/input_container"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="16dp" android:layout_marginEnd="10dp"
android:background="?attr/selectableItemBackgroundBorderless" android:layout_marginStart="10dp"
android:contentDescription="@string/msg_content_description_show_attachment_options" android:gravity="center_vertical"
android:src="@drawable/ic_reaction_24dp" /> android:orientation="horizontal"
android:paddingBottom="10dp"
android:paddingTop="10dp"
app:layout_constraintTop_toBottomOf="@+id/divider">
<EditText <ImageButton
android:id="@+id/text_message" android:id="@+id/button_add_reaction"
android:layout_width="0dp" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:layout_weight="1" android:background="?attr/selectableItemBackgroundBorderless"
android:background="@android:color/transparent" android:contentDescription="@string/msg_content_description_show_attachment_options"
android:hint="@string/msg_message" android:clickable="false"
android:maxLines="4" /> android:src="@drawable/ic_reaction_24dp" />
<ImageButton <EditText
android:id="@+id/button_show_attachment_options" android:id="@+id/text_message"
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless" android:layout_marginEnd="16dp"
android:contentDescription="@string/msg_content_description_show_attachment_options" android:layout_weight="1"
android:src="@drawable/ic_add_24dp" /> android:background="@android:color/transparent"
android:hint="@string/msg_message"
android:maxLines="4"
android:scrollbars="vertical" />
<ImageButton <ImageButton
android:id="@+id/button_send" android:id="@+id/button_show_attachment_options"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless" android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/msg_content_description_send_message" android:contentDescription="@string/msg_content_description_show_attachment_options"
android:src="@drawable/ic_send_24dp" android:src="@drawable/ic_add_24dp" />
android:visibility="gone" />
</LinearLayout> <ImageButton
android:id="@+id/button_send"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/msg_content_description_send_message"
android:src="@drawable/ic_send_24dp"
android:visibility="gone" />
</LinearLayout>
</android.support.constraint.ConstraintLayout>
<FrameLayout
android:id="@+id/emoji_fragment_placeholder"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</android.support.constraint.ConstraintLayout> </LinearLayout>
\ No newline at end of file \ 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