Commit 18800044 authored by Leonardo Aramaki's avatar Leonardo Aramaki

Keyboard backpressing closes also the emoji keyboard

parent 2e563131
......@@ -6,6 +6,7 @@ import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity
import chat.rocket.android.R
import chat.rocket.android.helper.KeyboardHelper
import chat.rocket.android.util.extensions.addFragment
import chat.rocket.android.util.extensions.textContent
import chat.rocket.android.widget.emoji.EmojiFragment
......@@ -63,10 +64,12 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
}
override fun onBackPressed() {
super.onBackPressed()
val frag = supportFragmentManager.findFragmentByTag(EmojiFragment.TAG) as EmojiFragment?
if (frag != null && frag.isShown()) {
frag.hide()
} else {
KeyboardHelper.hideSoftKeyboard(this)
finishActivity()
}
}
......
......@@ -8,13 +8,13 @@ import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.support.annotation.DrawableRes
import android.support.v4.app.Fragment
import android.support.v7.widget.DefaultItemAnimator
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.text.method.ScrollingMovementMethod
import android.view.*
import android.widget.ImageButton
import chat.rocket.android.R
import chat.rocket.android.chatroom.presentation.ChatRoomPresenter
import chat.rocket.android.chatroom.presentation.ChatRoomView
......@@ -23,9 +23,11 @@ import chat.rocket.android.helper.EndlessRecyclerViewScrollListener
import chat.rocket.android.helper.KeyboardHelper
import chat.rocket.android.helper.MessageParser
import chat.rocket.android.util.extensions.*
import chat.rocket.android.widget.emoji.ComposerEditText
import chat.rocket.android.widget.emoji.Emoji
import chat.rocket.android.widget.emoji.EmojiFragment
import chat.rocket.android.widget.emoji.EmojiParser
import chat.rocket.common.util.ifNull
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_chat_room.*
import kotlinx.android.synthetic.main.message_attachment_options.*
......@@ -49,7 +51,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 REQUEST_CODE_FOR_PERFORM_SAF = 42
class ChatRoomFragment : Fragment(), ChatRoomView, EmojiFragment.OnEmojiClickCallback {
class ChatRoomFragment : Fragment(), ChatRoomView, EmojiFragment.EmojiKeyboardListener {
@Inject lateinit var presenter: ChatRoomPresenter
@Inject lateinit var parser: MessageParser
private lateinit var adapter: ChatRoomAdapter
......@@ -95,12 +97,24 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiFragment.OnEmojiClickCal
setupActionSnackbar()
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
attachOrGetEmojiFragment()
text_message.addTextChangedListener(EmojiFragment.EmojiTextWatcher(text_message))
text_message.requestFocus()
}
override fun onDestroyView() {
presenter.unsubscribeMessages()
handler.removeCallbacksAndMessages(null)
super.onDestroyView()
}
override fun onStop() {
super.onStop()
hideAllKeyboards()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
if (requestCode == REQUEST_CODE_FOR_PERFORM_SAF && resultCode == Activity.RESULT_OK) {
if (resultData != null) {
......@@ -134,6 +148,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiFragment.OnEmojiClickCal
adapter = ChatRoomAdapter(chatRoomType, chatRoomName, presenter)
recycler_view.adapter = adapter
val linearLayoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true)
linearLayoutManager.stackFromEnd = true
recycler_view.layoutManager = linearLayoutManager
recycler_view.itemAnimator = DefaultItemAnimator()
if (dataSet.size >= 30) {
......@@ -145,6 +160,9 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiFragment.OnEmojiClickCal
}
}
adapter.addDataSet(dataSet)
if (adapter.itemCount > 0) {
recycler_view.scrollToPosition(0)
}
}
}
......@@ -162,7 +180,6 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiFragment.OnEmojiClickCal
override fun showInvalidFileMessage() = showMessage(getString(R.string.msg_invalid_file))
override fun showNewMessage(message: MessageViewModel) {
text_message.textContent = ""
adapter.addItem(message)
recycler_view.smoothScrollToPosition(0)
}
......@@ -175,7 +192,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiFragment.OnEmojiClickCal
override fun enableMessageInput(clear: Boolean) {
button_send.isEnabled = true
text_message.isEnabled = true
if (clear) text_message.textContent = ""
if (clear) text_message.erase()
}
override fun dispatchUpdateMessage(index: Int, message: MessageViewModel) {
......@@ -220,13 +237,45 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiFragment.OnEmojiClickCal
text_message.textContent = text
editingMessageId = messageId
}
}
override fun onEmojiAdded(emoji: Emoji) {
val cursorPosition = text_message.selectionStart
text_message.text.insert(cursorPosition, EmojiParser.parse(emoji.shortname))
text_message.setSelection(cursorPosition + emoji.unicode.length)
if (cursorPosition > -1) {
text_message.text.insert(cursorPosition, EmojiParser.parse(emoji.shortname))
text_message.setSelection(cursorPosition + emoji.unicode.length)
}
}
override fun onSoftKeyboardHidden() {
setReactionButtonIcon(R.drawable.ic_keyboard_black_24dp)
}
override fun onSoftKeyboardShown() {
setReactionButtonIcon(R.drawable.ic_reaction_24dp)
recycler_view.scrollToPosition(0)
}
override fun onEmojiKeyboardHidden() {
setReactionButtonIcon(R.drawable.ic_reaction_24dp)
}
override fun onEmojiKeyboardShown() {
setReactionButtonIcon(R.drawable.ic_keyboard_black_24dp)
recycler_view.scrollToPosition(0)
}
private fun setReactionButtonIcon(@DrawableRes drawableId: Int) {
button_add_reaction.setImageResource(drawableId)
button_add_reaction.setTag(drawableId)
}
private fun hideAllKeyboards() {
activity?.let {
KeyboardHelper.hideSoftKeyboard(it)
attachOrGetEmojiFragment()?.hide()
setReactionButtonIcon(R.drawable.ic_reaction_24dp)
}
}
private fun setupComposer() {
......@@ -251,10 +300,28 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiFragment.OnEmojiClickCal
}
})
text_message.listener = object : ComposerEditText.ComposerEditTextListener {
override fun onKeyboardClose() {
activity?.let {
val fragment = EmojiFragment.getOrAttach(it, R.id.emoji_fragment_placeholder, composer)
if (fragment.isCollapsed()) {
it.onBackPressed()
} else {
hideAllKeyboards()
}
}
}
}
button_send.setOnClickListener {
var textMessage = citation ?: ""
textMessage += text_message.textContent
sendMessage(textMessage)
attachOrGetEmojiFragment()?.let {
if (it.softKeyboardVisible) {
it.hide()
}
}
clearActionMessage()
}
......@@ -263,6 +330,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiFragment.OnEmojiClickCal
if (layout_message_attachment_options.isShown) {
hideAttachmentOptions()
} else {
hideAllKeyboards()
showAttachmentOptions()
}
}
......@@ -282,28 +350,27 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiFragment.OnEmojiClickCal
button_add_reaction.setOnClickListener { view ->
activity?.let {
val editor = text_message
val emojiFragment = EmojiFragment.getOrAttach(it, R.id.emoji_fragment_placeholder, composer)
with(emojiFragment) {
if (!isShown()) {
show()
} else {
val button = view as ImageButton
val resourceId: Int
if (softKeyboardVisible) {
resourceId = R.drawable.ic_keyboard_black_24px
KeyboardHelper.hideSoftKeyboard(it)
} else {
resourceId = R.drawable.ic_reaction_24dp
KeyboardHelper.showSoftKeyboard(editor)
val emojiFragment = attachOrGetEmojiFragment()!!
val tag = if (view.tag == null) R.drawable.ic_reaction_24dp else view.tag as Int
when (tag) {
R.drawable.ic_reaction_24dp -> {
KeyboardHelper.hideSoftKeyboard(it)
if (!emojiFragment.isShown()) {
emojiFragment.show()
}
button.setImageResource(resourceId)
}
R.drawable.ic_keyboard_black_24dp -> KeyboardHelper.showSoftKeyboard(editor)
}
}
}
}
}
addEmojiFragment()
text_message.addTextChangedListener(EmojiFragment.EmojiTextWatcher(text_message))
private fun attachOrGetEmojiFragment(): EmojiFragment? {
return activity?.let {
val frag = EmojiFragment.getOrAttach(it, R.id.emoji_fragment_placeholder, composer)
frag.listener = this
frag
}
}
......@@ -314,12 +381,6 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiFragment.OnEmojiClickCal
})
}
private fun addEmojiFragment() {
activity?.let {
EmojiFragment.getOrAttach(it, R.id.emoji_fragment_placeholder, composer)
}
}
private fun clearActionMessage() {
citation = null
editingMessageId = null
......
......@@ -51,4 +51,11 @@ object KeyboardHelper {
inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0)
}
}
fun restart(view: View) {
if (view.requestFocus()) {
val inputMethodManager = view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.restartInput(view)
}
}
}
\ No newline at end of file
......@@ -10,7 +10,9 @@ import android.provider.MediaStore
import android.text.Spannable
import android.text.Spanned
import android.text.TextUtils
import android.widget.EditText
import chat.rocket.android.widget.emoji.EmojiParser
import chat.rocket.android.widget.emoji.EmojiTypefaceSpan
import ru.noties.markwon.Markwon
fun String.ifEmpty(value: String): String {
......@@ -27,6 +29,14 @@ fun CharSequence.ifEmpty(value: String): CharSequence {
return this
}
fun EditText.erase() {
this.text.clear()
val spans = this.text.getSpans(0, text.length, EmojiTypefaceSpan::class.java)
spans.forEach {
text.removeSpan(it)
}
}
var TextView.textContent: String
get() = text.toString()
set(value) {
......@@ -46,7 +56,8 @@ var TextView.content: CharSequence
Markwon.unscheduleTableRows(this)
if (value is Spanned) {
val result = EmojiParser.parse(value.toString()) as Spannable
TextUtils.copySpansFrom(value, 0, value.length, Any::class.java, result, 0)
val end = if (value.length > result.length) result.length else value.length
TextUtils.copySpansFrom(value, 0, end, Any::class.java, result, 0)
text = result
} else {
val result = EmojiParser.parse(value.toString()) as Spannable
......
......@@ -9,10 +9,10 @@ import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import chat.rocket.android.R
import chat.rocket.android.widget.emoji.EmojiFragment.OnEmojiClickCallback
import chat.rocket.android.widget.emoji.EmojiFragment.EmojiKeyboardListener
import java.util.*
class CategoryPagerAdapter(val callback: OnEmojiClickCallback) : PagerAdapter() {
class CategoryPagerAdapter(val listener: EmojiKeyboardListener) : PagerAdapter() {
override fun isViewFromObject(view: View, obj: Any): Boolean {
return view == obj
}
......@@ -20,9 +20,9 @@ class CategoryPagerAdapter(val callback: OnEmojiClickCallback) : PagerAdapter()
override fun instantiateItem(container: ViewGroup, position: Int): Any {
val view = LayoutInflater.from(container.context)
.inflate(R.layout.emoji_category_layout, container, false)
val layoutManager = GridLayoutManager(view.context, 5)
val layoutManager = GridLayoutManager(view.context, 8)
val recycler = view.findViewById(R.id.emojiRecyclerView) as RecyclerView
val adapter = EmojiAdapter(layoutManager.spanCount, callback)
val adapter = EmojiAdapter(layoutManager.spanCount, listener)
val category = EmojiCategory.values().get(position)
val emojis = if (category != EmojiCategory.RECENTS)
EmojiRepository.getEmojisByCategory(category)
......@@ -46,7 +46,7 @@ class CategoryPagerAdapter(val callback: OnEmojiClickCallback) : PagerAdapter()
override fun getPageTitle(position: Int) = EmojiCategory.values()[position].icon()
class EmojiAdapter(val spanCount: Int, val callback: OnEmojiClickCallback) : RecyclerView.Adapter<EmojiRowViewHolder>() {
class EmojiAdapter(val spanCount: Int, val listener: EmojiKeyboardListener) : RecyclerView.Adapter<EmojiRowViewHolder>() {
private var emojis: List<Emoji> = Collections.emptyList()
fun addEmojis(emojis: List<Emoji>) {
......@@ -60,13 +60,13 @@ class CategoryPagerAdapter(val callback: OnEmojiClickCallback) : PagerAdapter()
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, callback)
return EmojiRowViewHolder(view, itemCount, spanCount, listener)
}
override fun getItemCount(): Int = emojis.size
}
class EmojiRowViewHolder(itemView: View, val itemCount: Int, val spanCount: Int, val callback: OnEmojiClickCallback) : RecyclerView.ViewHolder(itemView) {
class EmojiRowViewHolder(itemView: View, val itemCount: Int, val spanCount: Int, val listener: EmojiKeyboardListener) : RecyclerView.ViewHolder(itemView) {
private val emojiView: TextView = itemView.findViewById(R.id.emoji)
fun bind(emoji: Emoji) {
......@@ -79,7 +79,7 @@ class CategoryPagerAdapter(val callback: OnEmojiClickCallback) : PagerAdapter()
itemView.setPadding(0, 0, 0, paddingBottom)
}
itemView.setOnClickListener {
callback.onEmojiAdded(emoji)
listener.onEmojiAdded(emoji)
}
}
}
......
package chat.rocket.android.widget.emoji
import android.content.Context
import android.support.v7.widget.AppCompatEditText
import android.util.AttributeSet
import android.view.KeyEvent
class ComposerEditText : AppCompatEditText {
var listener: ComposerEditTextListener? = null
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) :
super(context, attrs, defStyleAttr)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, android.support.v7.appcompat.R.attr.editTextStyle)
constructor(context: Context) : this(context, null)
override fun dispatchKeyEventPreIme(event: KeyEvent): Boolean {
if (event.keyCode == KeyEvent.KEYCODE_BACK) {
val state = getKeyDispatcherState()
if (state != null) {
if (event.action == KeyEvent.ACTION_DOWN) {
state.startTracking(event, this)
listener?.onKeyboardClose()
}
return true
}
}
return super.dispatchKeyEventPreIme(event)
}
interface ComposerEditTextListener {
fun onKeyboardClose()
}
}
\ No newline at end of file
......@@ -22,13 +22,16 @@ import chat.rocket.android.util.extensions.setVisible
class EmojiFragment : Fragment() {
private lateinit var viewPager: ViewPager
private lateinit var tabLayout: TabLayout
private lateinit var editor: View
internal lateinit var parentContainer: ViewGroup
private lateinit var parentContainer: ViewGroup
private var editor: View? = null
private var decorLayoutListener: ViewTreeObserver.OnGlobalLayoutListener? = null
var softKeyboardVisible = false
var listener: EmojiKeyboardListener? = null
companion object {
const val PREF_EMOJI_RECENTS = "PREF_EMOJI_RECENTS"
const val PREF_KEYBOARD_HEIGHT = "PREF_KEYBOARD_HEIGHT"
const val MIN_KEYBOARD_HEIGHT_PX = 150
val TAG: String = EmojiFragment::class.java.simpleName
fun newInstance(editor: View) = EmojiFragment().apply { this.editor = editor }
......@@ -57,40 +60,51 @@ class EmojiFragment : Fragment() {
return view
}
override fun onDetach() {
super.onDetach()
activity?.getWindow()?.decorView?.viewTreeObserver?.removeOnGlobalLayoutListener(decorLayoutListener)
listener = null
editor = null
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val callback = when (activity) {
is OnEmojiClickCallback -> activity as OnEmojiClickCallback
is EmojiKeyboardListener -> activity as EmojiKeyboardListener
else -> {
val fragments = activity?.supportFragmentManager?.fragments
if (fragments == null || fragments.size == 0 || !(fragments[0] is OnEmojiClickCallback)) {
throw IllegalStateException("activity/fragment should implement OnEmojiClickCallback interface")
if (fragments == null || fragments.size == 0 || !(fragments[0] is EmojiKeyboardListener)) {
throw IllegalStateException("activity/fragment should implement EmojiKeyboardListener interface")
}
fragments[0] as OnEmojiClickCallback
fragments[0] as EmojiKeyboardListener
}
}
activity?.let {
val decorView = it.getWindow().decorView
decorView.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
decorLayoutListener = object : ViewTreeObserver.OnGlobalLayoutListener {
private val windowVisibleDisplayFrame = Rect()
private var lastVisibleDecorViewHeight: Int = 0
override fun onGlobalLayout() {
if (editor == null) {
return
}
// 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) {
if (lastVisibleDecorViewHeight > visibleDecorViewHeight + MIN_KEYBOARD_HEIGHT_PX) {
// Calculate current keyboard height (this includes also navigation bar height when in fullscreen mode).
val currentKeyboardHeight = decorView.height - windowVisibleDisplayFrame.bottom - editor.measuredHeight
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) {
openHidden()
listener?.onSoftKeyboardShown()
} else if (lastVisibleDecorViewHeight + MIN_KEYBOARD_HEIGHT_PX < visibleDecorViewHeight) {
// Notify listener about keyboard being hidden.
softKeyboardVisible = false
}
......@@ -98,7 +112,8 @@ class EmojiFragment : Fragment() {
// Save current decor view height for the next call.
lastVisibleDecorViewHeight = visibleDecorViewHeight
}
})
}
decorView.viewTreeObserver.addOnGlobalLayoutListener(decorLayoutListener)
}
val storedHeight = EmojiRepository.getKeyboardHeight()
......@@ -106,7 +121,23 @@ class EmojiFragment : Fragment() {
setKeyboardHeight(storedHeight)
}
viewPager.adapter = CategoryPagerAdapter(object : OnEmojiClickCallback {
viewPager.adapter = CategoryPagerAdapter(object : EmojiKeyboardListener {
override fun onEmojiKeyboardShown() {
// Do nothing here.
}
override fun onEmojiKeyboardHidden() {
// Do nothing here.
}
override fun onSoftKeyboardHidden() {
// Do nothing here.
}
override fun onSoftKeyboardShown() {
// Do nothing here.
}
override fun onEmojiAdded(emoji: Emoji) {
EmojiRepository.addToRecents(emoji)
callback.onEmojiAdded(emoji)
......@@ -127,8 +158,11 @@ class EmojiFragment : Fragment() {
}
private fun setKeyboardHeight(height: Int) {
parentContainer.layoutParams.height = height
parentContainer.requestLayout()
val oldHeight = parentContainer.layoutParams.height
if (oldHeight != height) {
parentContainer.layoutParams.height = height
parentContainer.requestLayout()
}
}
class EmojiTextWatcher(val editor: EditText) : TextWatcher {
......@@ -175,22 +209,57 @@ class EmojiFragment : Fragment() {
}
}
/**
* Show the emoji keyboard.
*/
fun show() {
parentContainer.setVisible(true)
listener?.onEmojiKeyboardShown()
}
fun openHidden() {
parentContainer.visibility = View.INVISIBLE
}
/**
* Hide the emoji keyboard.
*/
fun hide() {
// Since the emoji keyboard is always behind the soft keyboard assume it's also dismissed
// when the emoji one is about to get close. Hence we should invoke our listener to update
// the UI as if the soft keyboard is hidden.
parentContainer.setVisible(false)
listener?.onEmojiKeyboardHidden()
}
/**
* Whether the emoji keyboard is visible.
*
* @return <code>true</code> if opened.
*/
fun isShown() = parentContainer.visibility == View.VISIBLE
interface OnEmojiClickCallback {
/**
* Whether the emoji keyboard is collapsed.
*
* @return false if the emoji keyboard is visible and not obscured
*/
fun isCollapsed() = parentContainer.visibility == View.GONE
interface EmojiKeyboardListener {
/**
* Callback triggered after an emoji is selected on the picker.
* Callback after an emoji is selected on the picker.
*
* @param emoji The selected emoji
*/
fun onEmojiAdded(emoji: Emoji)
fun onSoftKeyboardHidden()
fun onSoftKeyboardShown()
fun onEmojiKeyboardHidden()
fun onEmojiKeyboardShown()
}
}
\ No newline at end of file
......@@ -41,18 +41,18 @@
<ImageButton
android:id="@+id/button_add_reaction"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="16dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/msg_content_description_show_attachment_options"
android:clickable="false"
android:contentDescription="@string/msg_content_description_show_attachment_options"
android:src="@drawable/ic_reaction_24dp" />
<EditText
<chat.rocket.android.widget.emoji.ComposerEditText
android:id="@+id/text_message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_height="24dp"
android:layout_marginEnd="16dp"
android:layout_weight="1"
android:background="@android:color/transparent"
......
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