Add vertical scroll to Emoji Keyboard.
This commit is contained in:
parent
a71fe0fd75
commit
ed4bab1b8b
10 changed files with 334 additions and 121 deletions
|
@ -1,6 +1,7 @@
|
||||||
package org.thoughtcrime.securesms.emoji
|
package org.thoughtcrime.securesms.emoji
|
||||||
|
|
||||||
import androidx.annotation.AttrRes
|
import androidx.annotation.AttrRes
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -17,8 +18,30 @@ enum class EmojiCategory(val priority: Int, val key: String, @AttrRes val icon:
|
||||||
FLAGS(7, "Flags", R.attr.emoji_category_flags),
|
FLAGS(7, "Flags", R.attr.emoji_category_flags),
|
||||||
EMOTICONS(8, "Emoticons", R.attr.emoji_category_emoticons);
|
EMOTICONS(8, "Emoticons", R.attr.emoji_category_emoticons);
|
||||||
|
|
||||||
|
@StringRes
|
||||||
|
fun getCategoryLabel(): Int {
|
||||||
|
return getCategoryLabel(icon)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun forKey(key: String) = values().first { it.key == key }
|
fun forKey(key: String) = values().first { it.key == key }
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@StringRes
|
||||||
|
fun getCategoryLabel(@AttrRes iconAttr: Int): Int {
|
||||||
|
return when (iconAttr) {
|
||||||
|
R.attr.emoji_category_people -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__smileys_and_people
|
||||||
|
R.attr.emoji_category_nature -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__nature
|
||||||
|
R.attr.emoji_category_foods -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__food
|
||||||
|
R.attr.emoji_category_activity -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__activities
|
||||||
|
R.attr.emoji_category_places -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__places
|
||||||
|
R.attr.emoji_category_objects -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__objects
|
||||||
|
R.attr.emoji_category_symbols -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__symbols
|
||||||
|
R.attr.emoji_category_flags -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__flags
|
||||||
|
R.attr.emoji_category_emoticons -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__emoticons
|
||||||
|
else -> throw AssertionError()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
package org.thoughtcrime.securesms.keyboard;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.TypedArray;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.R;
|
||||||
|
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public final class BottomShadowBehavior extends CoordinatorLayout.Behavior<View> {
|
||||||
|
|
||||||
|
private int bottomBarId;
|
||||||
|
private boolean shown = true;
|
||||||
|
|
||||||
|
public BottomShadowBehavior(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
if (attrs != null) {
|
||||||
|
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.BottomShadowBehavior);
|
||||||
|
bottomBarId = a.getResourceId(R.styleable.BottomShadowBehavior_bottom_bar_id, 0);
|
||||||
|
a.recycle();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bottomBarId == 0) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
|
||||||
|
return dependency.getId() == bottomBarId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
|
||||||
|
float alpha = (dependency.getHeight() - (int) dependency.getTranslationY()) / (float) dependency.getHeight();
|
||||||
|
child.setAlpha(alpha);
|
||||||
|
|
||||||
|
float y = dependency.getY() - child.getHeight();
|
||||||
|
if (y != child.getY()) {
|
||||||
|
child.setY(y);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
package org.thoughtcrime.securesms.keyboard;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.TypedArray;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.R;
|
||||||
|
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public final class TopShadowBehavior extends CoordinatorLayout.Behavior<View> {
|
||||||
|
|
||||||
|
private int targetId;
|
||||||
|
private boolean shown = true;
|
||||||
|
|
||||||
|
public TopShadowBehavior(int targetId) {
|
||||||
|
super();
|
||||||
|
this.targetId = targetId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TopShadowBehavior(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
if (attrs != null) {
|
||||||
|
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TopShadowBehavior);
|
||||||
|
targetId = a.getResourceId(R.styleable.TopShadowBehavior_app_bar_layout_id, 0);
|
||||||
|
a.recycle();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetId == 0) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
|
||||||
|
return dependency instanceof RecyclerView;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
|
||||||
|
boolean shouldShow = dependency.getY() != parent.findViewById(targetId).getHeight();
|
||||||
|
|
||||||
|
if (shouldShow != shown) {
|
||||||
|
if (shouldShow) {
|
||||||
|
show(child);
|
||||||
|
} else {
|
||||||
|
hide(child);
|
||||||
|
}
|
||||||
|
shown = shouldShow;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (child.getY() != 0) {
|
||||||
|
child.setY(0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void show(View child) {
|
||||||
|
child.animate()
|
||||||
|
.setDuration(250)
|
||||||
|
.alpha(1f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hide(View child) {
|
||||||
|
child.animate()
|
||||||
|
.setDuration(250)
|
||||||
|
.alpha(0f);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,35 +0,0 @@
|
||||||
package org.thoughtcrime.securesms.keyboard.emoji
|
|
||||||
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider
|
|
||||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageView
|
|
||||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter
|
|
||||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
|
||||||
import org.thoughtcrime.securesms.util.MappingViewHolder
|
|
||||||
|
|
||||||
class EmojiKeyboardPageAdapter(
|
|
||||||
private val emojiSelectionListener: EmojiKeyboardProvider.EmojiEventListener,
|
|
||||||
private val variationSelectorListener: EmojiPageViewGridAdapter.VariationSelectorListener
|
|
||||||
) : MappingAdapter() {
|
|
||||||
|
|
||||||
init {
|
|
||||||
registerFactory(EmojiPageMappingModel::class.java) { parent ->
|
|
||||||
val pageView = EmojiPageView(parent.context, emojiSelectionListener, variationSelectorListener, true)
|
|
||||||
|
|
||||||
val layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
|
|
||||||
pageView.layoutParams = layoutParams
|
|
||||||
pageView.presentForEmojiKeyboard()
|
|
||||||
|
|
||||||
ViewHolder(pageView)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ViewHolder(
|
|
||||||
private val emojiPageView: EmojiPageView,
|
|
||||||
) : MappingViewHolder<EmojiPageMappingModel>(emojiPageView) {
|
|
||||||
|
|
||||||
override fun bind(model: EmojiPageMappingModel) {
|
|
||||||
emojiPageView.bindSearchableAdapter(model.emojiPageModel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -6,28 +6,36 @@ import android.view.KeyEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.ViewModelProviders
|
import androidx.lifecycle.ViewModelProviders
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
|
||||||
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider
|
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider
|
||||||
|
import org.thoughtcrime.securesms.components.emoji.EmojiPageView
|
||||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter
|
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter
|
||||||
|
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.EmojiHeader
|
||||||
import org.thoughtcrime.securesms.keyboard.findListener
|
import org.thoughtcrime.securesms.keyboard.findListener
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
import org.thoughtcrime.securesms.util.MappingModel
|
||||||
|
import java.util.Optional
|
||||||
|
|
||||||
private val DELETE_KEY_EVENT: KeyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL)
|
private val DELETE_KEY_EVENT: KeyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL)
|
||||||
|
|
||||||
class EmojiKeyboardPageFragment : Fragment(R.layout.keyboard_pager_emoji_page_fragment), EmojiKeyboardProvider.EmojiEventListener, EmojiPageViewGridAdapter.VariationSelectorListener {
|
class EmojiKeyboardPageFragment : Fragment(R.layout.keyboard_pager_emoji_page_fragment), EmojiKeyboardProvider.EmojiEventListener, EmojiPageViewGridAdapter.VariationSelectorListener {
|
||||||
|
|
||||||
private lateinit var viewModel: EmojiKeyboardPageViewModel
|
private lateinit var viewModel: EmojiKeyboardPageViewModel
|
||||||
private lateinit var emojiPager: ViewPager2
|
private lateinit var emojiPageView: EmojiPageView
|
||||||
private lateinit var searchView: View
|
private lateinit var searchView: View
|
||||||
private lateinit var emojiCategoriesRecycler: RecyclerView
|
private lateinit var emojiCategoriesRecycler: RecyclerView
|
||||||
private lateinit var backspaceView: View
|
private lateinit var backspaceView: View
|
||||||
private lateinit var eventListener: EmojiKeyboardProvider.EmojiEventListener
|
private lateinit var eventListener: EmojiKeyboardProvider.EmojiEventListener
|
||||||
private lateinit var callback: Callback
|
private lateinit var callback: Callback
|
||||||
private lateinit var pagesAdapter: EmojiKeyboardPageAdapter
|
|
||||||
private lateinit var categoriesAdapter: EmojiKeyboardPageCategoriesAdapter
|
private lateinit var categoriesAdapter: EmojiKeyboardPageCategoriesAdapter
|
||||||
private lateinit var searchBar: KeyboardPageSearchView
|
private lateinit var searchBar: KeyboardPageSearchView
|
||||||
|
private lateinit var appBarLayout: AppBarLayout
|
||||||
|
|
||||||
|
private val categoryUpdateOnScroll = UpdateCategorySelectionOnScroll()
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
override fun onAttach(context: Context) {
|
||||||
super.onAttach(context)
|
super.onAttach(context)
|
||||||
|
@ -37,33 +45,28 @@ class EmojiKeyboardPageFragment : Fragment(R.layout.keyboard_pager_emoji_page_fr
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
emojiPager = view.findViewById(R.id.emoji_pager)
|
emojiPageView = view.findViewById(R.id.emoji_page_view)
|
||||||
|
emojiPageView.initialize(this, this, true)
|
||||||
|
emojiPageView.addOnScrollListener(categoryUpdateOnScroll)
|
||||||
|
|
||||||
searchView = view.findViewById(R.id.emoji_search)
|
searchView = view.findViewById(R.id.emoji_search)
|
||||||
searchBar = view.findViewById(R.id.emoji_keyboard_search_text)
|
searchBar = view.findViewById(R.id.emoji_keyboard_search_text)
|
||||||
emojiCategoriesRecycler = view.findViewById(R.id.emoji_categories_recycler)
|
emojiCategoriesRecycler = view.findViewById(R.id.emoji_categories_recycler)
|
||||||
backspaceView = view.findViewById(R.id.emoji_backspace)
|
backspaceView = view.findViewById(R.id.emoji_backspace)
|
||||||
|
appBarLayout = view.findViewById(R.id.emoji_keyboard_search_appbar)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
super.onActivityCreated(savedInstanceState)
|
super.onActivityCreated(savedInstanceState)
|
||||||
|
|
||||||
viewModel = ViewModelProviders.of(requireActivity()).get(EmojiKeyboardPageViewModel::class.java)
|
viewModel = ViewModelProviders.of(requireActivity(), EmojiKeyboardPageViewModel.Factory(requireContext()))
|
||||||
|
.get(EmojiKeyboardPageViewModel::class.java)
|
||||||
pagesAdapter = EmojiKeyboardPageAdapter(this, this)
|
|
||||||
|
|
||||||
categoriesAdapter = EmojiKeyboardPageCategoriesAdapter { key ->
|
categoriesAdapter = EmojiKeyboardPageCategoriesAdapter { key ->
|
||||||
|
scrollTo(key)
|
||||||
viewModel.onKeySelected(key)
|
viewModel.onKeySelected(key)
|
||||||
|
|
||||||
val page = pagesAdapter.currentList.indexOfFirst {
|
|
||||||
(it as EmojiPageMappingModel).key == key
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (emojiPager.currentItem != page) {
|
|
||||||
emojiPager.currentItem = page
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
emojiPager.adapter = pagesAdapter
|
|
||||||
emojiCategoriesRecycler.adapter = categoriesAdapter
|
emojiCategoriesRecycler.adapter = categoriesAdapter
|
||||||
|
|
||||||
searchBar.callbacks = EmojiKeyboardPageSearchViewCallbacks()
|
searchBar.callbacks = EmojiKeyboardPageSearchViewCallbacks()
|
||||||
|
@ -75,22 +78,24 @@ class EmojiKeyboardPageFragment : Fragment(R.layout.keyboard_pager_emoji_page_fr
|
||||||
backspaceView.setOnClickListener { eventListener.onKeyEvent(DELETE_KEY_EVENT) }
|
backspaceView.setOnClickListener { eventListener.onKeyEvent(DELETE_KEY_EVENT) }
|
||||||
|
|
||||||
viewModel.categories.observe(viewLifecycleOwner) { categories ->
|
viewModel.categories.observe(viewLifecycleOwner) { categories ->
|
||||||
categoriesAdapter.submitList(categories)
|
categoriesAdapter.submitList(categories) {
|
||||||
|
(emojiCategoriesRecycler.parent as View).invalidate()
|
||||||
|
emojiCategoriesRecycler.parent.requestLayout()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.pages.observe(viewLifecycleOwner) { pages ->
|
viewModel.pages.observe(viewLifecycleOwner) { pages ->
|
||||||
val registerPageCallback: Boolean = pagesAdapter.currentList.isEmpty() && pages.isNotEmpty()
|
emojiPageView.setList(pages)
|
||||||
pagesAdapter.submitList(pages) { updatePagerPosition(registerPageCallback) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.selectedKey.observe(viewLifecycleOwner) { updateCategoryTab() }
|
viewModel.selectedKey.observe(viewLifecycleOwner) { updateCategoryTab(it) }
|
||||||
|
|
||||||
eventListener = findListener() ?: throw AssertionError("No emoji listener found")
|
eventListener = findListener() ?: throw AssertionError("No emoji listener found")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateCategoryTab() {
|
private fun updateCategoryTab(key: String) {
|
||||||
emojiCategoriesRecycler.post {
|
emojiCategoriesRecycler.post {
|
||||||
val index: Int = categoriesAdapter.currentList.indexOfFirst { (it as? EmojiKeyboardPageCategoryMappingModel)?.key == viewModel.selectedKey.value }
|
val index: Int = categoriesAdapter.indexOfFirst(EmojiKeyboardPageCategoryMappingModel::class.java) { it.key == key }
|
||||||
|
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
emojiCategoriesRecycler.smoothScrollToPosition(index)
|
emojiCategoriesRecycler.smoothScrollToPosition(index)
|
||||||
|
@ -98,17 +103,14 @@ class EmojiKeyboardPageFragment : Fragment(R.layout.keyboard_pager_emoji_page_fr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updatePagerPosition(registerPageCallback: Boolean) {
|
private fun scrollTo(key: String) {
|
||||||
val page = pagesAdapter.currentList.indexOfFirst {
|
emojiPageView.adapter?.let { adapter ->
|
||||||
(it as EmojiPageMappingModel).key == viewModel.selectedKey.value
|
val index = adapter.indexOfFirst(EmojiHeader::class.java) { it.key == key }
|
||||||
|
if (index != -1) {
|
||||||
|
appBarLayout.setExpanded(false, true)
|
||||||
|
categoryUpdateOnScroll.startAutoScrolling()
|
||||||
|
emojiPageView.smoothScrollToPositionTop(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (emojiPager.currentItem != page && page != -1) {
|
|
||||||
emojiPager.setCurrentItem(page, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (registerPageCallback) {
|
|
||||||
emojiPager.registerOnPageChangeCallback(PageChanged(pagesAdapter))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,16 +124,7 @@ class EmojiKeyboardPageFragment : Fragment(R.layout.keyboard_pager_emoji_page_fr
|
||||||
eventListener.onKeyEvent(keyEvent)
|
eventListener.onKeyEvent(keyEvent)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onVariationSelectorStateChanged(open: Boolean) {
|
override fun onVariationSelectorStateChanged(open: Boolean) = Unit
|
||||||
emojiPager.isUserInputEnabled = !open
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class PageChanged(private val adapter: EmojiKeyboardPageAdapter) : ViewPager2.OnPageChangeCallback() {
|
|
||||||
override fun onPageSelected(position: Int) {
|
|
||||||
val mappingModel: EmojiPageMappingModel = adapter.currentList[position] as EmojiPageMappingModel
|
|
||||||
viewModel.onKeySelected(mappingModel.key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class EmojiKeyboardPageSearchViewCallbacks : KeyboardPageSearchView.Callbacks {
|
private inner class EmojiKeyboardPageSearchViewCallbacks : KeyboardPageSearchView.Callbacks {
|
||||||
override fun onClicked() {
|
override fun onClicked() {
|
||||||
|
@ -139,6 +132,38 @@ class EmojiKeyboardPageFragment : Fragment(R.layout.keyboard_pager_emoji_page_fr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private inner class UpdateCategorySelectionOnScroll : RecyclerView.OnScrollListener() {
|
||||||
|
|
||||||
|
private var doneScrolling: Boolean = true
|
||||||
|
|
||||||
|
fun startAutoScrolling() {
|
||||||
|
doneScrolling = false
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||||
|
if (newState == SCROLL_STATE_IDLE && !doneScrolling) {
|
||||||
|
doneScrolling = true
|
||||||
|
onScrolled(recyclerView, 0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||||
|
if (recyclerView.layoutManager == null || !doneScrolling) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
emojiPageView.adapter?.let { adapter ->
|
||||||
|
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
|
||||||
|
val index = layoutManager.findFirstCompletelyVisibleItemPosition()
|
||||||
|
val item: Optional<MappingModel<*>> = adapter.getModel(index)
|
||||||
|
if (item.isPresent && item.get() is EmojiPageViewGridAdapter.HasKey) {
|
||||||
|
viewModel.onKeySelected((item.get() as EmojiPageViewGridAdapter.HasKey).key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface Callback {
|
interface Callback {
|
||||||
fun openEmojiSearch()
|
fun openEmojiSearch()
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
package org.thoughtcrime.securesms.keyboard.emoji
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import org.signal.core.util.concurrent.SignalExecutors
|
||||||
|
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider
|
||||||
|
import org.thoughtcrime.securesms.components.emoji.EmojiPageModel
|
||||||
|
import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel
|
||||||
|
import org.thoughtcrime.securesms.emoji.EmojiSource.Companion.latest
|
||||||
|
import java.util.function.Consumer
|
||||||
|
|
||||||
|
class EmojiKeyboardPageRepository(context: Context) {
|
||||||
|
|
||||||
|
private val recentEmojiPageModel: RecentEmojiPageModel = RecentEmojiPageModel(context, EmojiKeyboardProvider.RECENT_STORAGE_KEY)
|
||||||
|
|
||||||
|
fun getEmoji(consumer: Consumer<List<EmojiPageModel>>) {
|
||||||
|
SignalExecutors.BOUNDED.execute {
|
||||||
|
val list = mutableListOf<EmojiPageModel>()
|
||||||
|
list += recentEmojiPageModel
|
||||||
|
list += latest.displayPages
|
||||||
|
consumer.accept(list)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,38 +1,66 @@
|
||||||
package org.thoughtcrime.securesms.keyboard.emoji
|
package org.thoughtcrime.securesms.keyboard.emoji
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.Transformations
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider
|
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider
|
||||||
|
import org.thoughtcrime.securesms.components.emoji.EmojiPageModel
|
||||||
|
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter
|
||||||
|
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.EmojiHeader
|
||||||
import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel
|
import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||||
import org.thoughtcrime.securesms.emoji.EmojiCategory
|
import org.thoughtcrime.securesms.emoji.EmojiCategory
|
||||||
import org.thoughtcrime.securesms.emoji.EmojiSource
|
import org.thoughtcrime.securesms.emoji.EmojiSource
|
||||||
|
import org.thoughtcrime.securesms.keyboard.emoji.EmojiKeyboardPageCategoryMappingModel.EmojiCategoryMappingModel
|
||||||
import org.thoughtcrime.securesms.util.DefaultValueLiveData
|
import org.thoughtcrime.securesms.util.DefaultValueLiveData
|
||||||
|
import org.thoughtcrime.securesms.util.MappingModel
|
||||||
import org.thoughtcrime.securesms.util.MappingModelList
|
import org.thoughtcrime.securesms.util.MappingModelList
|
||||||
|
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil
|
||||||
|
|
||||||
class EmojiKeyboardPageViewModel : ViewModel() {
|
class EmojiKeyboardPageViewModel(repository: EmojiKeyboardPageRepository) : ViewModel() {
|
||||||
|
|
||||||
private val internalSelectedKey = DefaultValueLiveData<String>(getStartingTab())
|
private val internalSelectedKey = DefaultValueLiveData<String>(getStartingTab())
|
||||||
|
|
||||||
val selectedKey: LiveData<String>
|
val selectedKey: LiveData<String>
|
||||||
get() = internalSelectedKey
|
get() = internalSelectedKey
|
||||||
|
|
||||||
val categories: LiveData<MappingModelList> = Transformations.map(internalSelectedKey) { selected ->
|
val allEmojiModels: MutableLiveData<List<EmojiPageModel>> = MutableLiveData()
|
||||||
MappingModelList().apply {
|
val pages: LiveData<MappingModelList>
|
||||||
add(EmojiKeyboardPageCategoryMappingModel.RecentsMappingModel(selected == RecentEmojiPageModel.KEY))
|
val categories: LiveData<MappingModelList>
|
||||||
|
|
||||||
EmojiCategory.values().forEach {
|
init {
|
||||||
add(EmojiKeyboardPageCategoryMappingModel.EmojiCategoryMappingModel(it, it.key == selected))
|
repository.getEmoji(allEmojiModels::postValue)
|
||||||
}
|
|
||||||
}
|
pages = LiveDataUtil.mapAsync(allEmojiModels) { models ->
|
||||||
|
val list = MappingModelList()
|
||||||
|
models.forEach { pageModel ->
|
||||||
|
list += if (RecentEmojiPageModel.KEY == pageModel.key) {
|
||||||
|
EmojiHeader(pageModel.key, R.string.ReactWithAnyEmojiBottomSheetDialogFragment__recently_used)
|
||||||
|
} else {
|
||||||
|
val category = EmojiCategory.forKey(pageModel.key)
|
||||||
|
EmojiHeader(pageModel.key, category.getCategoryLabel())
|
||||||
}
|
}
|
||||||
|
|
||||||
val pages: LiveData<MappingModelList> = Transformations.map(categories) { categories ->
|
list += pageModel.toMappingModels()
|
||||||
MappingModelList().apply {
|
|
||||||
categories.forEach {
|
|
||||||
add(getPageForCategory(it as EmojiKeyboardPageCategoryMappingModel))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
list
|
||||||
|
}
|
||||||
|
|
||||||
|
categories = LiveDataUtil.combineLatest(allEmojiModels, internalSelectedKey) { models, selectedKey ->
|
||||||
|
val list = MappingModelList()
|
||||||
|
list += models.map { m ->
|
||||||
|
if (RecentEmojiPageModel.KEY == m.key) {
|
||||||
|
EmojiKeyboardPageCategoryMappingModel.RecentsMappingModel(m.key == selectedKey)
|
||||||
|
} else {
|
||||||
|
val category = EmojiCategory.forKey(m.key)
|
||||||
|
EmojiCategoryMappingModel(category, category.key == selectedKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
list
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,4 +91,21 @@ class EmojiKeyboardPageViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Factory(context: Context) : ViewModelProvider.Factory {
|
||||||
|
|
||||||
|
private val repository = EmojiKeyboardPageRepository(context)
|
||||||
|
|
||||||
|
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||||
|
return requireNotNull(modelClass.cast(EmojiKeyboardPageViewModel(repository)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun EmojiPageModel.toMappingModels(): List<MappingModel<*>> {
|
||||||
|
return if (EmojiCategory.EMOTICONS.key == key) {
|
||||||
|
displayEmoji.map { EmojiPageViewGridAdapter.EmojiTextModel(key, it) }
|
||||||
|
} else {
|
||||||
|
displayEmoji.map { EmojiPageViewGridAdapter.EmojiModel(key, it) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,7 +45,7 @@ final class ReactWithAnyEmojiRepository {
|
||||||
|
|
||||||
emojiPages.addAll(Stream.of(EmojiSource.getLatest().getDisplayPages())
|
emojiPages.addAll(Stream.of(EmojiSource.getLatest().getDisplayPages())
|
||||||
.filterNot(p -> p.getIconAttr() == EmojiCategory.EMOTICONS.getIcon())
|
.filterNot(p -> p.getIconAttr() == EmojiCategory.EMOTICONS.getIcon())
|
||||||
.map(page -> new ReactWithAnyEmojiPage(Collections.singletonList(new ReactWithAnyEmojiPageBlock(getCategoryLabel(page.getIconAttr()), page))))
|
.map(page -> new ReactWithAnyEmojiPage(Collections.singletonList(new ReactWithAnyEmojiPageBlock(EmojiCategory.getCategoryLabel(page.getIconAttr()), page))))
|
||||||
.toList());
|
.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,29 +89,4 @@ final class ReactWithAnyEmojiRepository {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private @StringRes int getCategoryLabel(@AttrRes int iconAttr) {
|
|
||||||
switch (iconAttr) {
|
|
||||||
case R.attr.emoji_category_people:
|
|
||||||
return R.string.ReactWithAnyEmojiBottomSheetDialogFragment__smileys_and_people;
|
|
||||||
case R.attr.emoji_category_nature:
|
|
||||||
return R.string.ReactWithAnyEmojiBottomSheetDialogFragment__nature;
|
|
||||||
case R.attr.emoji_category_foods:
|
|
||||||
return R.string.ReactWithAnyEmojiBottomSheetDialogFragment__food;
|
|
||||||
case R.attr.emoji_category_activity:
|
|
||||||
return R.string.ReactWithAnyEmojiBottomSheetDialogFragment__activities;
|
|
||||||
case R.attr.emoji_category_places:
|
|
||||||
return R.string.ReactWithAnyEmojiBottomSheetDialogFragment__places;
|
|
||||||
case R.attr.emoji_category_objects:
|
|
||||||
return R.string.ReactWithAnyEmojiBottomSheetDialogFragment__objects;
|
|
||||||
case R.attr.emoji_category_symbols:
|
|
||||||
return R.string.ReactWithAnyEmojiBottomSheetDialogFragment__symbols;
|
|
||||||
case R.attr.emoji_category_flags:
|
|
||||||
return R.string.ReactWithAnyEmojiBottomSheetDialogFragment__flags;
|
|
||||||
case R.attr.emoji_category_emoticons:
|
|
||||||
return R.string.ReactWithAnyEmojiBottomSheetDialogFragment__emoticons;
|
|
||||||
default:
|
|
||||||
throw new AssertionError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,22 +19,28 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:layout_marginTop="8dp"
|
android:paddingTop="8dp"
|
||||||
android:layout_marginBottom="8dp"
|
android:paddingBottom="8dp"
|
||||||
app:click_only="true"
|
app:click_only="true"
|
||||||
app:layout_scrollFlags="scroll|snap"
|
app:layout_scrollFlags="scroll|snap"
|
||||||
|
app:search_icon_tint="@color/signal_icon_tint_tab_unselected"
|
||||||
app:search_hint="@string/KeyboardPagerFragment_search_emoji"
|
app:search_hint="@string/KeyboardPagerFragment_search_emoji"
|
||||||
app:show_always="true" />
|
app:show_always="true" />
|
||||||
|
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
<androidx.viewpager2.widget.ViewPager2
|
<org.thoughtcrime.securesms.components.emoji.EmojiPageView
|
||||||
android:id="@+id/emoji_pager"
|
android:id="@+id/emoji_page_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:paddingStart="5dp"
|
||||||
|
android:paddingEnd="5dp"
|
||||||
|
android:paddingBottom="?actionBarSize"
|
||||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
android:id="@+id/emoji_keyboard_bottom_bar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="?actionBarSize"
|
android:layout_height="?actionBarSize"
|
||||||
android:layout_gravity="bottom"
|
android:layout_gravity="bottom"
|
||||||
|
@ -57,7 +63,7 @@
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/emoji_categories_recycler"
|
android:id="@+id/emoji_categories_recycler"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="48dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
|
@ -76,4 +82,20 @@
|
||||||
app:tint="@color/icon_tab_selector" />
|
app:tint="@color/icon_tab_selector" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/emoji_keyboard_top_shadow"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="2dp"
|
||||||
|
android:background="@drawable/toolbar_shadow"
|
||||||
|
app:app_bar_layout_id="@+id/emoji_keyboard_search_appbar"
|
||||||
|
app:layout_behavior=".keyboard.TopShadowBehavior" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/emoji_keyboard_bottom_shadow"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="3dp"
|
||||||
|
android:background="@drawable/bottom_toolbar_shadow"
|
||||||
|
app:bottom_bar_id="@+id/emoji_keyboard_bottom_bar"
|
||||||
|
app:layout_behavior=".keyboard.BottomShadowBehavior" />
|
||||||
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@ -294,4 +294,12 @@
|
||||||
<attr name="search_hint" format="string|reference" />
|
<attr name="search_hint" format="string|reference" />
|
||||||
<attr name="click_only" format="boolean" />
|
<attr name="click_only" format="boolean" />
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
|
|
||||||
|
<declare-styleable name="TopShadowBehavior">
|
||||||
|
<attr name="app_bar_layout_id" format="reference" />
|
||||||
|
</declare-styleable>
|
||||||
|
|
||||||
|
<declare-styleable name="BottomShadowBehavior">
|
||||||
|
<attr name="bottom_bar_id" format="reference" />
|
||||||
|
</declare-styleable>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Loading…
Add table
Reference in a new issue