Add vertical scroll to Emoji Keyboard.

This commit is contained in:
Cody Henthorne 2021-06-25 16:39:04 -04:00 committed by GitHub
parent a71fe0fd75
commit ed4bab1b8b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 334 additions and 121 deletions

View file

@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.emoji
import androidx.annotation.AttrRes
import androidx.annotation.StringRes
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),
EMOTICONS(8, "Emoticons", R.attr.emoji_category_emoticons);
@StringRes
fun getCategoryLabel(): Int {
return getCategoryLabel(icon)
}
companion object {
@JvmStatic
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()
}
}
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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)
}
}
}

View file

@ -6,28 +6,36 @@ import android.view.KeyEvent
import android.view.View
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.LinearLayoutManager
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.components.emoji.EmojiKeyboardProvider
import org.thoughtcrime.securesms.components.emoji.EmojiPageView
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.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)
class EmojiKeyboardPageFragment : Fragment(R.layout.keyboard_pager_emoji_page_fragment), EmojiKeyboardProvider.EmojiEventListener, EmojiPageViewGridAdapter.VariationSelectorListener {
private lateinit var viewModel: EmojiKeyboardPageViewModel
private lateinit var emojiPager: ViewPager2
private lateinit var emojiPageView: EmojiPageView
private lateinit var searchView: View
private lateinit var emojiCategoriesRecycler: RecyclerView
private lateinit var backspaceView: View
private lateinit var eventListener: EmojiKeyboardProvider.EmojiEventListener
private lateinit var callback: Callback
private lateinit var pagesAdapter: EmojiKeyboardPageAdapter
private lateinit var categoriesAdapter: EmojiKeyboardPageCategoriesAdapter
private lateinit var searchBar: KeyboardPageSearchView
private lateinit var appBarLayout: AppBarLayout
private val categoryUpdateOnScroll = UpdateCategorySelectionOnScroll()
override fun onAttach(context: 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?) {
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)
searchBar = view.findViewById(R.id.emoji_keyboard_search_text)
emojiCategoriesRecycler = view.findViewById(R.id.emoji_categories_recycler)
backspaceView = view.findViewById(R.id.emoji_backspace)
appBarLayout = view.findViewById(R.id.emoji_keyboard_search_appbar)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProviders.of(requireActivity()).get(EmojiKeyboardPageViewModel::class.java)
pagesAdapter = EmojiKeyboardPageAdapter(this, this)
viewModel = ViewModelProviders.of(requireActivity(), EmojiKeyboardPageViewModel.Factory(requireContext()))
.get(EmojiKeyboardPageViewModel::class.java)
categoriesAdapter = EmojiKeyboardPageCategoriesAdapter { key ->
scrollTo(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
searchBar.callbacks = EmojiKeyboardPageSearchViewCallbacks()
@ -75,22 +78,24 @@ class EmojiKeyboardPageFragment : Fragment(R.layout.keyboard_pager_emoji_page_fr
backspaceView.setOnClickListener { eventListener.onKeyEvent(DELETE_KEY_EVENT) }
viewModel.categories.observe(viewLifecycleOwner) { categories ->
categoriesAdapter.submitList(categories)
categoriesAdapter.submitList(categories) {
(emojiCategoriesRecycler.parent as View).invalidate()
emojiCategoriesRecycler.parent.requestLayout()
}
}
viewModel.pages.observe(viewLifecycleOwner) { pages ->
val registerPageCallback: Boolean = pagesAdapter.currentList.isEmpty() && pages.isNotEmpty()
pagesAdapter.submitList(pages) { updatePagerPosition(registerPageCallback) }
emojiPageView.setList(pages)
}
viewModel.selectedKey.observe(viewLifecycleOwner) { updateCategoryTab() }
viewModel.selectedKey.observe(viewLifecycleOwner) { updateCategoryTab(it) }
eventListener = findListener() ?: throw AssertionError("No emoji listener found")
}
private fun updateCategoryTab() {
private fun updateCategoryTab(key: String) {
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) {
emojiCategoriesRecycler.smoothScrollToPosition(index)
@ -98,17 +103,14 @@ class EmojiKeyboardPageFragment : Fragment(R.layout.keyboard_pager_emoji_page_fr
}
}
private fun updatePagerPosition(registerPageCallback: Boolean) {
val page = pagesAdapter.currentList.indexOfFirst {
(it as EmojiPageMappingModel).key == viewModel.selectedKey.value
private fun scrollTo(key: String) {
emojiPageView.adapter?.let { adapter ->
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)
}
override fun onVariationSelectorStateChanged(open: Boolean) {
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)
}
}
override fun onVariationSelectorStateChanged(open: Boolean) = Unit
private inner class EmojiKeyboardPageSearchViewCallbacks : KeyboardPageSearchView.Callbacks {
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 {
fun openEmojiSearch()
}

View file

@ -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)
}
}
}

View file

@ -1,38 +1,66 @@
package org.thoughtcrime.securesms.keyboard.emoji
import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import androidx.lifecycle.MutableLiveData
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.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.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.emoji.EmojiCategory
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.MappingModel
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())
val selectedKey: LiveData<String>
get() = internalSelectedKey
val categories: LiveData<MappingModelList> = Transformations.map(internalSelectedKey) { selected ->
MappingModelList().apply {
add(EmojiKeyboardPageCategoryMappingModel.RecentsMappingModel(selected == RecentEmojiPageModel.KEY))
val allEmojiModels: MutableLiveData<List<EmojiPageModel>> = MutableLiveData()
val pages: LiveData<MappingModelList>
val categories: LiveData<MappingModelList>
EmojiCategory.values().forEach {
add(EmojiKeyboardPageCategoryMappingModel.EmojiCategoryMappingModel(it, it.key == selected))
}
}
init {
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 ->
MappingModelList().apply {
categories.forEach {
add(getPageForCategory(it as EmojiKeyboardPageCategoryMappingModel))
list += pageModel.toMappingModels()
}
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) }
}
}

View file

@ -45,7 +45,7 @@ final class ReactWithAnyEmojiRepository {
emojiPages.addAll(Stream.of(EmojiSource.getLatest().getDisplayPages())
.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());
}
@ -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();
}
}
}

View file

@ -19,22 +19,28 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:paddingTop="8dp"
android:paddingBottom="8dp"
app:click_only="true"
app:layout_scrollFlags="scroll|snap"
app:search_icon_tint="@color/signal_icon_tint_tab_unselected"
app:search_hint="@string/KeyboardPagerFragment_search_emoji"
app:show_always="true" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/emoji_pager"
<org.thoughtcrime.securesms.components.emoji.EmojiPageView
android:id="@+id/emoji_page_view"
android:layout_width="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" />
<LinearLayout
android:id="@+id/emoji_keyboard_bottom_bar"
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
android:layout_gravity="bottom"
@ -57,7 +63,7 @@
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/emoji_categories_recycler"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_height="48dp"
android:layout_weight="1"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
@ -76,4 +82,20 @@
app:tint="@color/icon_tab_selector" />
</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>

View file

@ -294,4 +294,12 @@
<attr name="search_hint" format="string|reference" />
<attr name="click_only" format="boolean" />
</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>