kopia lustrzana https://github.com/ryukoposting/Signal-Android
Add vertical scrolling to Sticker Keyboard.
rodzic
aba5774446
commit
d4a3b442f4
|
@ -10,6 +10,9 @@
|
||||||
<option name="ALIGN_MULTILINE_TEXT_BLOCKS" value="true" />
|
<option name="ALIGN_MULTILINE_TEXT_BLOCKS" value="true" />
|
||||||
</JavaCodeStyleSettings>
|
</JavaCodeStyleSettings>
|
||||||
<JetCodeStyleSettings>
|
<JetCodeStyleSettings>
|
||||||
|
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
|
||||||
|
<value />
|
||||||
|
</option>
|
||||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
|
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
|
||||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
|
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
|
||||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
|
|
|
@ -76,6 +76,7 @@ public abstract class Database {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void notifyStickerPackListeners() {
|
protected void notifyStickerPackListeners() {
|
||||||
|
ApplicationDependencies.getDatabaseObserver().notifyStickerPackObservers();
|
||||||
context.getContentResolver().notifyChange(DatabaseContentProviders.StickerPack.CONTENT_URI, null);
|
context.getContentResolver().notifyChange(DatabaseContentProviders.StickerPack.CONTENT_URI, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,7 @@ public final class DatabaseObserver {
|
||||||
private final Map<UUID, Set<Observer>> paymentObservers;
|
private final Map<UUID, Set<Observer>> paymentObservers;
|
||||||
private final Set<Observer> allPaymentsObservers;
|
private final Set<Observer> allPaymentsObservers;
|
||||||
private final Set<Observer> chatColorsObservers;
|
private final Set<Observer> chatColorsObservers;
|
||||||
|
private final Set<Observer> stickerPackObservers;
|
||||||
|
|
||||||
public DatabaseObserver(Application application) {
|
public DatabaseObserver(Application application) {
|
||||||
this.application = application;
|
this.application = application;
|
||||||
|
@ -40,6 +41,7 @@ public final class DatabaseObserver {
|
||||||
this.paymentObservers = new HashMap<>();
|
this.paymentObservers = new HashMap<>();
|
||||||
this.allPaymentsObservers = new HashSet<>();
|
this.allPaymentsObservers = new HashSet<>();
|
||||||
this.chatColorsObservers = new HashSet<>();
|
this.chatColorsObservers = new HashSet<>();
|
||||||
|
this.stickerPackObservers = new HashSet<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void registerConversationListObserver(@NonNull Observer listener) {
|
public void registerConversationListObserver(@NonNull Observer listener) {
|
||||||
|
@ -78,6 +80,12 @@ public final class DatabaseObserver {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void registerStickerPackObserver(@NonNull Observer listener) {
|
||||||
|
executor.execute(() -> {
|
||||||
|
stickerPackObservers.add(listener);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public void unregisterObserver(@NonNull Observer listener) {
|
public void unregisterObserver(@NonNull Observer listener) {
|
||||||
executor.execute(() -> {
|
executor.execute(() -> {
|
||||||
conversationListObservers.remove(listener);
|
conversationListObservers.remove(listener);
|
||||||
|
@ -85,6 +93,7 @@ public final class DatabaseObserver {
|
||||||
unregisterMapped(verboseConversationObservers, listener);
|
unregisterMapped(verboseConversationObservers, listener);
|
||||||
unregisterMapped(paymentObservers, listener);
|
unregisterMapped(paymentObservers, listener);
|
||||||
chatColorsObservers.remove(listener);
|
chatColorsObservers.remove(listener);
|
||||||
|
stickerPackObservers.remove(listener);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,6 +169,12 @@ public final class DatabaseObserver {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void notifyStickerPackObservers() {
|
||||||
|
executor.execute(() -> {
|
||||||
|
notifySet(stickerPackObservers);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private <K> void registerMapped(@NonNull Map<K, Set<Observer>> map, @NonNull K key, @NonNull Observer listener) {
|
private <K> void registerMapped(@NonNull Map<K, Set<Observer>> map, @NonNull K key, @NonNull Observer listener) {
|
||||||
Set<Observer> listeners = map.get(key);
|
Set<Observer> listeners = map.get(key);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,95 @@
|
||||||
|
package org.thoughtcrime.securesms.keyboard.sticker
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.database.model.StickerRecord
|
||||||
|
import org.thoughtcrime.securesms.glide.cache.ApngOptions
|
||||||
|
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri
|
||||||
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
|
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||||
|
import org.thoughtcrime.securesms.util.MappingModel
|
||||||
|
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||||
|
|
||||||
|
class KeyboardStickerListAdapter(
|
||||||
|
private val glideRequests: GlideRequests,
|
||||||
|
private val eventListener: EventListener?,
|
||||||
|
private val allowApngAnimation: Boolean,
|
||||||
|
) : MappingAdapter() {
|
||||||
|
|
||||||
|
init {
|
||||||
|
registerFactory(Sticker::class.java, LayoutFactory(::StickerViewHolder, R.layout.sticker_keyboard_page_list_item))
|
||||||
|
registerFactory(StickerHeader::class.java, LayoutFactory(::StickerHeaderViewHolder, R.layout.sticker_grid_header))
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Sticker(override val packId: String, val stickerRecord: StickerRecord) : MappingModel<Sticker>, HasPackId {
|
||||||
|
val uri: DecryptableUri
|
||||||
|
get() = DecryptableUri(stickerRecord.uri)
|
||||||
|
|
||||||
|
override fun areItemsTheSame(newItem: Sticker): Boolean {
|
||||||
|
return packId == newItem.packId && stickerRecord.rowId == newItem.stickerRecord.rowId
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(newItem: Sticker): Boolean {
|
||||||
|
return areItemsTheSame(newItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class StickerViewHolder(itemView: View) : MappingViewHolder<Sticker>(itemView) {
|
||||||
|
|
||||||
|
private val image: ImageView = findViewById(R.id.sticker_keyboard_page_image)
|
||||||
|
|
||||||
|
override fun bind(model: Sticker) {
|
||||||
|
glideRequests.load(model.uri)
|
||||||
|
.set(ApngOptions.ANIMATE, allowApngAnimation)
|
||||||
|
.transition(DrawableTransitionOptions.withCrossFade())
|
||||||
|
.into(image)
|
||||||
|
|
||||||
|
if (eventListener != null) {
|
||||||
|
itemView.setOnClickListener { eventListener.onStickerClicked(model) }
|
||||||
|
itemView.setOnLongClickListener {
|
||||||
|
eventListener.onStickerLongClicked(model)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
itemView.setOnClickListener(null)
|
||||||
|
itemView.setOnLongClickListener(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class StickerHeader(override val packId: String, private val title: String?, private val titleResource: Int?) : MappingModel<StickerHeader>, HasPackId {
|
||||||
|
fun getTitle(context: Context): String {
|
||||||
|
return title ?: context.resources.getString(titleResource ?: R.string.StickerManagementAdapter_untitled)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areItemsTheSame(newItem: StickerHeader): Boolean {
|
||||||
|
return title == newItem.title
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(newItem: StickerHeader): Boolean {
|
||||||
|
return areItemsTheSame(newItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class StickerHeaderViewHolder(itemView: View) : MappingViewHolder<StickerHeader>(itemView) {
|
||||||
|
|
||||||
|
private val title: TextView = findViewById(R.id.sticker_grid_header_title)
|
||||||
|
|
||||||
|
override fun bind(model: StickerHeader) {
|
||||||
|
title.text = model.getTitle(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HasPackId {
|
||||||
|
val packId: String
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EventListener {
|
||||||
|
fun onStickerClicked(sticker: Sticker)
|
||||||
|
fun onStickerLongClicked(sticker: Sticker)
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,38 +2,84 @@ package org.thoughtcrime.securesms.keyboard.sticker
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.annotation.Px
|
||||||
import androidx.lifecycle.ViewModelProviders
|
import androidx.lifecycle.ViewModelProviders
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.LinearSmoothScroller
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.viewpager.widget.PagerAdapter
|
import androidx.recyclerview.widget.RecyclerView.SmoothScroller
|
||||||
import androidx.viewpager.widget.ViewPager
|
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
import org.thoughtcrime.securesms.LoggingFragment
|
import org.thoughtcrime.securesms.LoggingFragment
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
import org.thoughtcrime.securesms.components.emoji.MediaKeyboardBottomTabAdapter
|
import org.thoughtcrime.securesms.database.DatabaseObserver
|
||||||
import org.thoughtcrime.securesms.components.emoji.MediaKeyboardProvider
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||||
import org.thoughtcrime.securesms.keyboard.emoji.KeyboardPageSearchView
|
import org.thoughtcrime.securesms.keyboard.emoji.KeyboardPageSearchView
|
||||||
|
import org.thoughtcrime.securesms.keyboard.findListener
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp
|
import org.thoughtcrime.securesms.mms.GlideApp
|
||||||
import org.thoughtcrime.securesms.stickers.StickerKeyboardProvider
|
|
||||||
import org.thoughtcrime.securesms.stickers.StickerKeyboardProvider.StickerEventListener
|
import org.thoughtcrime.securesms.stickers.StickerKeyboardProvider.StickerEventListener
|
||||||
|
import org.thoughtcrime.securesms.stickers.StickerRolloverTouchListener
|
||||||
|
import org.thoughtcrime.securesms.stickers.StickerRolloverTouchListener.RolloverStickerRetriever
|
||||||
|
import org.thoughtcrime.securesms.util.DeviceProperties
|
||||||
|
import org.thoughtcrime.securesms.util.MappingModel
|
||||||
|
import org.thoughtcrime.securesms.util.Throttler
|
||||||
|
import org.whispersystems.libsignal.util.Pair
|
||||||
|
import java.util.Optional
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
class StickerKeyboardPageFragment : LoggingFragment(R.layout.keyboard_pager_sticker_page_fragment) {
|
class StickerKeyboardPageFragment :
|
||||||
|
LoggingFragment(R.layout.keyboard_pager_sticker_page_fragment),
|
||||||
|
KeyboardStickerListAdapter.EventListener,
|
||||||
|
StickerRolloverTouchListener.RolloverEventListener,
|
||||||
|
RolloverStickerRetriever,
|
||||||
|
DatabaseObserver.Observer,
|
||||||
|
View.OnLayoutChangeListener {
|
||||||
|
|
||||||
private val presenter: StickerPresenter = StickerPresenter()
|
private lateinit var stickerList: RecyclerView
|
||||||
private lateinit var provider: StickerKeyboardProvider
|
private lateinit var keyboardStickerListAdapter: KeyboardStickerListAdapter
|
||||||
|
private lateinit var layoutManager: GridLayoutManager
|
||||||
private lateinit var stickerPager: ViewPager
|
private lateinit var listTouchListener: StickerRolloverTouchListener
|
||||||
private lateinit var stickerPacksRecycler: RecyclerView
|
private lateinit var stickerPacksRecycler: RecyclerView
|
||||||
private lateinit var manageStickers: View
|
private lateinit var appBarLayout: AppBarLayout
|
||||||
private lateinit var tabAdapter: MediaKeyboardBottomTabAdapter
|
private lateinit var stickerPacksAdapter: StickerPackListAdapter
|
||||||
|
|
||||||
private lateinit var viewModel: StickerKeyboardPageViewModel
|
private lateinit var viewModel: StickerKeyboardPageViewModel
|
||||||
|
|
||||||
|
private val packIdSelectionOnScroll: UpdatePackSelectionOnScroll = UpdatePackSelectionOnScroll()
|
||||||
|
private val observerThrottler: Throttler = Throttler(500)
|
||||||
|
private val stickerThrottler: Throttler = Throttler(100)
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
stickerPager = view.findViewById(R.id.sticker_pager)
|
|
||||||
manageStickers = view.findViewById(R.id.sticker_manage)
|
val glideRequests = GlideApp.with(this)
|
||||||
|
keyboardStickerListAdapter = KeyboardStickerListAdapter(glideRequests, this, DeviceProperties.shouldAllowApngStickerAnimation(requireContext()))
|
||||||
|
layoutManager = GridLayoutManager(requireContext(), 2).apply {
|
||||||
|
spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
||||||
|
override fun getSpanSize(position: Int): Int {
|
||||||
|
val model: Optional<MappingModel<*>> = keyboardStickerListAdapter.getModel(position)
|
||||||
|
if (model.isPresent && model.get() is KeyboardStickerListAdapter.StickerHeader) {
|
||||||
|
return spanCount
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
listTouchListener = StickerRolloverTouchListener(requireContext(), glideRequests, this, this)
|
||||||
|
|
||||||
|
stickerList = view.findViewById(R.id.sticker_keyboard_list)
|
||||||
|
stickerList.layoutManager = layoutManager
|
||||||
|
stickerList.adapter = keyboardStickerListAdapter
|
||||||
|
stickerList.addOnItemTouchListener(listTouchListener)
|
||||||
|
stickerList.addOnScrollListener(packIdSelectionOnScroll)
|
||||||
|
|
||||||
stickerPacksRecycler = view.findViewById(R.id.sticker_packs_recycler)
|
stickerPacksRecycler = view.findViewById(R.id.sticker_packs_recycler)
|
||||||
|
|
||||||
|
stickerPacksAdapter = StickerPackListAdapter(glideRequests, DeviceProperties.shouldAllowApngStickerAnimation(requireContext()), this::onTabSelected)
|
||||||
|
stickerPacksRecycler.adapter = stickerPacksAdapter
|
||||||
|
|
||||||
|
appBarLayout = view.findViewById(R.id.sticker_keyboard_search_appbar)
|
||||||
|
|
||||||
view.findViewById<KeyboardPageSearchView>(R.id.sticker_keyboard_search_text).callbacks = object : KeyboardPageSearchView.Callbacks {
|
view.findViewById<KeyboardPageSearchView>(R.id.sticker_keyboard_search_text).callbacks = object : KeyboardPageSearchView.Callbacks {
|
||||||
override fun onClicked() {
|
override fun onClicked() {
|
||||||
StickerSearchDialogFragment.show(requireActivity().supportFragmentManager)
|
StickerSearchDialogFragment.show(requireActivity().supportFragmentManager)
|
||||||
|
@ -41,70 +87,139 @@ class StickerKeyboardPageFragment : LoggingFragment(R.layout.keyboard_pager_stic
|
||||||
}
|
}
|
||||||
|
|
||||||
view.findViewById<View>(R.id.sticker_search).setOnClickListener { StickerSearchDialogFragment.show(requireActivity().supportFragmentManager) }
|
view.findViewById<View>(R.id.sticker_search).setOnClickListener { StickerSearchDialogFragment.show(requireActivity().supportFragmentManager) }
|
||||||
|
view.findViewById<View>(R.id.sticker_manage).setOnClickListener { findListener<StickerEventListener>()?.onStickerManagementClicked() }
|
||||||
|
|
||||||
view.findViewById<AppBarLayout>(R.id.sticker_appbar).setExpanded(false)
|
ApplicationDependencies.getDatabaseObserver().registerStickerPackObserver(this)
|
||||||
|
|
||||||
|
view.addOnLayoutChangeListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
ApplicationDependencies.getDatabaseObserver().unregisterObserver(this)
|
||||||
|
requireView().removeOnLayoutChangeListener(this)
|
||||||
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
super.onActivityCreated(savedInstanceState)
|
super.onActivityCreated(savedInstanceState)
|
||||||
|
|
||||||
viewModel = ViewModelProviders.of(requireActivity()).get(StickerKeyboardPageViewModel::class.java)
|
viewModel = ViewModelProviders.of(requireActivity(), StickerKeyboardPageViewModel.Factory(requireContext()))
|
||||||
|
.get(StickerKeyboardPageViewModel::class.java)
|
||||||
|
|
||||||
tabAdapter = MediaKeyboardBottomTabAdapter(GlideApp.with(this), this::onTabSelected)
|
viewModel.stickers.observe(viewLifecycleOwner, keyboardStickerListAdapter::submitList)
|
||||||
stickerPacksRecycler.adapter = tabAdapter
|
viewModel.packs.observe(viewLifecycleOwner, stickerPacksAdapter::submitList)
|
||||||
|
viewModel.getSelectedPack().observe(viewLifecycleOwner, this::updateCategoryTab)
|
||||||
|
|
||||||
provider = StickerKeyboardProvider(requireActivity(), findListener() ?: throw AssertionError("No sticker listener"))
|
viewModel.refreshStickers()
|
||||||
provider.requestPresentation(presenter, true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun findListener(): StickerEventListener? {
|
private fun onTabSelected(stickerPack: StickerPackListAdapter.StickerPack) {
|
||||||
return parentFragment as? StickerEventListener ?: requireActivity() as? StickerEventListener
|
scrollTo(stickerPack.packRecord.packId)
|
||||||
|
viewModel.selectPack(stickerPack.packRecord.packId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onTabSelected(index: Int) {
|
private fun updateCategoryTab(packId: String) {
|
||||||
stickerPager.currentItem = index
|
stickerPacksRecycler.post {
|
||||||
stickerPacksRecycler.smoothScrollToPosition(index)
|
val index: Int = stickerPacksAdapter.indexOfFirst(StickerPackListAdapter.StickerPack::class.java) { it.packRecord.packId == packId }
|
||||||
viewModel.selectedTab = index
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class StickerPresenter : MediaKeyboardProvider.Presenter {
|
if (index != -1) {
|
||||||
override fun present(
|
stickerPacksRecycler.smoothScrollToPosition(index)
|
||||||
provider: MediaKeyboardProvider,
|
|
||||||
pagerAdapter: PagerAdapter,
|
|
||||||
iconProvider: MediaKeyboardProvider.TabIconProvider,
|
|
||||||
backspaceObserver: MediaKeyboardProvider.BackspaceObserver?,
|
|
||||||
addObserver: MediaKeyboardProvider.AddObserver?,
|
|
||||||
searchObserver: MediaKeyboardProvider.SearchObserver?,
|
|
||||||
startingIndex: Int
|
|
||||||
) {
|
|
||||||
if (stickerPager.adapter != pagerAdapter) {
|
|
||||||
stickerPager.adapter = pagerAdapter
|
|
||||||
}
|
}
|
||||||
stickerPager.currentItem = viewModel.selectedTab
|
}
|
||||||
|
}
|
||||||
|
|
||||||
stickerPager.clearOnPageChangeListeners()
|
private fun scrollTo(packId: String) {
|
||||||
stickerPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
|
val index = keyboardStickerListAdapter.indexOfFirst(KeyboardStickerListAdapter.StickerHeader::class.java) { it.packId == packId }
|
||||||
override fun onPageSelected(position: Int) {
|
if (index != -1) {
|
||||||
tabAdapter.setActivePosition(position)
|
appBarLayout.setExpanded(false, true)
|
||||||
stickerPacksRecycler.smoothScrollToPosition(position)
|
packIdSelectionOnScroll.startAutoScrolling()
|
||||||
provider.setCurrentPosition(position)
|
smoothScrollToPositionTop(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun smoothScrollToPositionTop(position: Int) {
|
||||||
|
val currentPosition = layoutManager.findFirstCompletelyVisibleItemPosition()
|
||||||
|
val shortTrip = abs(currentPosition - position) < 40
|
||||||
|
if (shortTrip) {
|
||||||
|
val smoothScroller: SmoothScroller = object : LinearSmoothScroller(context) {
|
||||||
|
override fun getVerticalSnapPreference(): Int {
|
||||||
|
return SNAP_TO_START
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
smoothScroller.targetPosition = position
|
||||||
|
layoutManager.startSmoothScroll(smoothScroller)
|
||||||
|
} else {
|
||||||
|
layoutManager.scrollToPositionWithOffset(position, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) = Unit
|
override fun onStickerClicked(sticker: KeyboardStickerListAdapter.Sticker) {
|
||||||
override fun onPageScrollStateChanged(state: Int) = Unit
|
stickerThrottler.publish { findListener<StickerEventListener>()?.onStickerSelected(sticker.stickerRecord) }
|
||||||
})
|
}
|
||||||
|
|
||||||
tabAdapter.setTabIconProvider(iconProvider, pagerAdapter.count)
|
override fun onStickerLongClicked(sticker: KeyboardStickerListAdapter.Sticker) {
|
||||||
tabAdapter.setActivePosition(stickerPager.currentItem)
|
listTouchListener.enterHoverMode(stickerList, sticker)
|
||||||
|
}
|
||||||
|
|
||||||
manageStickers.setOnClickListener { addObserver?.onAddClicked() }
|
override fun getStickerDataFromView(view: View): Pair<Any, String>? {
|
||||||
|
val position: Int = stickerList.getChildAdapterPosition(view)
|
||||||
|
val model: Optional<MappingModel<*>> = keyboardStickerListAdapter.getModel(position)
|
||||||
|
if (model.isPresent && model.get() is KeyboardStickerListAdapter.Sticker) {
|
||||||
|
val sticker = model.get() as KeyboardStickerListAdapter.Sticker
|
||||||
|
return Pair(sticker.uri, sticker.stickerRecord.emoji)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getCurrentPosition(): Int {
|
return null
|
||||||
return stickerPager.currentItem
|
}
|
||||||
|
|
||||||
|
override fun onStickerPopupStarted() = Unit
|
||||||
|
override fun onStickerPopupEnded() = Unit
|
||||||
|
|
||||||
|
override fun onChanged() {
|
||||||
|
observerThrottler.publish(viewModel::refreshStickers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLayoutChange(v: View?, left: Int, top: Int, right: Int, bottom: Int, oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int) {
|
||||||
|
onScreenWidthChanged(view?.width ?: 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onScreenWidthChanged(@Px newWidth: Int) {
|
||||||
|
layoutManager.spanCount = calculateColumnCount(newWidth)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun calculateColumnCount(@Px screenWidth: Int): Int {
|
||||||
|
val modifier = resources.getDimensionPixelOffset(R.dimen.sticker_page_item_padding).toFloat()
|
||||||
|
val divisor = resources.getDimensionPixelOffset(R.dimen.sticker_page_item_divisor).toFloat()
|
||||||
|
return ((screenWidth - modifier) / divisor).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class UpdatePackSelectionOnScroll : RecyclerView.OnScrollListener() {
|
||||||
|
|
||||||
|
private var doneScrolling: Boolean = true
|
||||||
|
|
||||||
|
fun startAutoScrolling() {
|
||||||
|
doneScrolling = false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun requestDismissal() = Unit
|
@Override
|
||||||
override fun isVisible(): Boolean = true
|
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||||
|
if (newState == RecyclerView.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
|
||||||
|
}
|
||||||
|
|
||||||
|
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
|
||||||
|
val index = layoutManager.findFirstCompletelyVisibleItemPosition()
|
||||||
|
val item: Optional<MappingModel<*>> = keyboardStickerListAdapter.getModel(index)
|
||||||
|
if (item.isPresent && item.get() is KeyboardStickerListAdapter.HasPackId) {
|
||||||
|
viewModel.selectPack((item.get() as KeyboardStickerListAdapter.HasPackId).packId)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,66 @@
|
||||||
package org.thoughtcrime.securesms.keyboard.sticker
|
package org.thoughtcrime.securesms.keyboard.sticker
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||||
|
import org.thoughtcrime.securesms.keyboard.sticker.StickerPackListAdapter.StickerPack
|
||||||
|
import org.thoughtcrime.securesms.util.MappingModelList
|
||||||
|
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil
|
||||||
|
|
||||||
class StickerKeyboardPageViewModel : ViewModel() {
|
private const val NO_SELECTED_PAGE = "no_selected_page"
|
||||||
var selectedTab: Int = 0
|
|
||||||
|
class StickerKeyboardPageViewModel(private val repository: StickerKeyboardRepository) : ViewModel() {
|
||||||
|
private val keyboardStickerPacks: MutableLiveData<List<StickerKeyboardRepository.KeyboardStickerPack>> = MutableLiveData()
|
||||||
|
|
||||||
|
private val selectedPack: MutableLiveData<String> = MutableLiveData(NO_SELECTED_PAGE)
|
||||||
|
|
||||||
|
val packs: LiveData<List<StickerPack>>
|
||||||
|
val stickers: LiveData<MappingModelList>
|
||||||
|
|
||||||
|
init {
|
||||||
|
val stickerPacks: LiveData<List<StickerPack>> = LiveDataUtil.mapAsync(keyboardStickerPacks) { packs ->
|
||||||
|
packs.map { StickerPack(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
packs = LiveDataUtil.combineLatest(selectedPack, stickerPacks) { selected, packs ->
|
||||||
|
if (packs.isEmpty()) {
|
||||||
|
packs
|
||||||
|
} else {
|
||||||
|
val actualSelected = if (selected == NO_SELECTED_PAGE) packs[0].packRecord.packId else selected
|
||||||
|
packs.map { it.copy(selected = it.packRecord.packId == actualSelected) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stickers = LiveDataUtil.mapAsync(keyboardStickerPacks) { packs ->
|
||||||
|
val list = MappingModelList()
|
||||||
|
|
||||||
|
packs.forEach { pack ->
|
||||||
|
list += KeyboardStickerListAdapter.StickerHeader(pack.packId, pack.title, pack.titleResource)
|
||||||
|
list += pack.stickers.map { KeyboardStickerListAdapter.Sticker(pack.packId, it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSelectedPack(): LiveData<String> = selectedPack
|
||||||
|
|
||||||
|
fun selectPack(packId: String) {
|
||||||
|
selectedPack.value = packId
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refreshStickers() {
|
||||||
|
repository.getStickerPacks { keyboardStickerPacks.postValue(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
class Factory(context: Context) : ViewModelProvider.Factory {
|
||||||
|
private val repository = StickerKeyboardRepository(DatabaseFactory.getStickerDatabase(context))
|
||||||
|
|
||||||
|
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||||
|
return requireNotNull(modelClass.cast(StickerKeyboardPageViewModel(repository)))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
package org.thoughtcrime.securesms.keyboard.sticker
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import org.signal.core.util.concurrent.SignalExecutors
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.database.StickerDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.StickerDatabase.StickerPackRecordReader
|
||||||
|
import org.thoughtcrime.securesms.database.StickerDatabase.StickerRecordReader
|
||||||
|
import org.thoughtcrime.securesms.database.model.StickerPackRecord
|
||||||
|
import org.thoughtcrime.securesms.database.model.StickerRecord
|
||||||
|
import java.util.function.Consumer
|
||||||
|
|
||||||
|
private const val RECENT_LIMIT = 24
|
||||||
|
private const val RECENT_PACK_ID = "RECENT"
|
||||||
|
|
||||||
|
class StickerKeyboardRepository(private val stickerDatabase: StickerDatabase) {
|
||||||
|
fun getStickerPacks(consumer: Consumer<List<KeyboardStickerPack>>) {
|
||||||
|
SignalExecutors.BOUNDED.execute {
|
||||||
|
val packs: MutableList<KeyboardStickerPack> = mutableListOf()
|
||||||
|
|
||||||
|
StickerPackRecordReader(stickerDatabase.installedStickerPacks).use { reader ->
|
||||||
|
var pack: StickerPackRecord? = reader.next
|
||||||
|
while (pack != null) {
|
||||||
|
packs += KeyboardStickerPack(packId = pack.packId, title = pack.title.orNull(), coverUri = pack.cover.uri)
|
||||||
|
pack = reader.next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val fullPacks: MutableList<KeyboardStickerPack> = packs.map { p ->
|
||||||
|
val stickers: MutableList<StickerRecord> = mutableListOf()
|
||||||
|
|
||||||
|
StickerRecordReader(stickerDatabase.getStickersForPack(p.packId)).use { reader ->
|
||||||
|
var sticker: StickerRecord? = reader.next
|
||||||
|
while (sticker != null) {
|
||||||
|
stickers.add(sticker)
|
||||||
|
sticker = reader.next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.copy(stickers = stickers)
|
||||||
|
}.toMutableList()
|
||||||
|
|
||||||
|
val recentStickerPack: KeyboardStickerPack = getRecentStickerPack()
|
||||||
|
if (recentStickerPack.stickers.isNotEmpty()) {
|
||||||
|
fullPacks.add(0, recentStickerPack)
|
||||||
|
}
|
||||||
|
consumer.accept(fullPacks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getRecentStickerPack(): KeyboardStickerPack {
|
||||||
|
val recentStickers: MutableList<StickerRecord> = mutableListOf()
|
||||||
|
|
||||||
|
StickerRecordReader(stickerDatabase.getRecentlyUsedStickers(RECENT_LIMIT)).use { reader ->
|
||||||
|
var recentSticker: StickerRecord? = reader.next
|
||||||
|
while (recentSticker != null) {
|
||||||
|
recentStickers.add(recentSticker)
|
||||||
|
recentSticker = reader.next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return KeyboardStickerPack(
|
||||||
|
packId = RECENT_PACK_ID,
|
||||||
|
title = null,
|
||||||
|
titleResource = R.string.StickerKeyboard__recently_used,
|
||||||
|
coverUri = null,
|
||||||
|
coverResource = R.drawable.ic_recent_20,
|
||||||
|
stickers = recentStickers
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class KeyboardStickerPack(
|
||||||
|
val packId: String,
|
||||||
|
val title: String?,
|
||||||
|
val titleResource: Int? = 0,
|
||||||
|
val coverUri: Uri?,
|
||||||
|
val coverResource: Int? = null,
|
||||||
|
val stickers: List<StickerRecord> = emptyList()
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
package org.thoughtcrime.securesms.keyboard.sticker
|
||||||
|
|
||||||
|
import android.content.res.ColorStateList
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.ImageView
|
||||||
|
import androidx.core.widget.ImageViewCompat
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.glide.cache.ApngOptions
|
||||||
|
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri
|
||||||
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
|
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||||
|
import org.thoughtcrime.securesms.util.MappingModel
|
||||||
|
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||||
|
|
||||||
|
class StickerPackListAdapter(private val glideRequests: GlideRequests, private val allowApngAnimation: Boolean, private val onTabSelected: (StickerPack) -> Unit) : MappingAdapter() {
|
||||||
|
|
||||||
|
init {
|
||||||
|
registerFactory(StickerPack::class.java, LayoutFactory(::StickerPackViewHolder, R.layout.keyboard_pager_category_icon))
|
||||||
|
}
|
||||||
|
|
||||||
|
data class StickerPack(val packRecord: StickerKeyboardRepository.KeyboardStickerPack, val selected: Boolean = false) : MappingModel<StickerPack> {
|
||||||
|
val loadImage: Boolean = packRecord.coverResource == null
|
||||||
|
val uri: DecryptableUri? = packRecord.coverUri?.let { DecryptableUri(packRecord.coverUri) }
|
||||||
|
val iconResource: Int = packRecord.coverResource ?: 0
|
||||||
|
|
||||||
|
override fun areItemsTheSame(newItem: StickerPack): Boolean {
|
||||||
|
return packRecord.packId == newItem.packRecord.packId
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(newItem: StickerPack): Boolean {
|
||||||
|
return areItemsTheSame(newItem) && selected == newItem.selected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class StickerPackViewHolder(itemView: View) : MappingViewHolder<StickerPack>(itemView) {
|
||||||
|
|
||||||
|
private val selected: View = findViewById(R.id.category_icon_selected)
|
||||||
|
private val icon: ImageView = findViewById(R.id.category_icon)
|
||||||
|
private val defaultTint: ColorStateList? = ImageViewCompat.getImageTintList(icon)
|
||||||
|
|
||||||
|
override fun bind(model: StickerPack) {
|
||||||
|
itemView.setOnClickListener { onTabSelected(model) }
|
||||||
|
|
||||||
|
selected.isSelected = model.selected
|
||||||
|
|
||||||
|
if (model.loadImage) {
|
||||||
|
ImageViewCompat.setImageTintList(icon, null)
|
||||||
|
icon.alpha = if (model.selected) 1f else 0.5f
|
||||||
|
glideRequests.load(model.uri)
|
||||||
|
.set(ApngOptions.ANIMATE, allowApngAnimation)
|
||||||
|
.into(icon)
|
||||||
|
} else {
|
||||||
|
ImageViewCompat.setImageTintList(icon, defaultTint)
|
||||||
|
icon.setImageResource(model.iconResource)
|
||||||
|
icon.alpha = 1f
|
||||||
|
icon.isSelected = model.selected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ import androidx.annotation.Nullable;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
|
import org.thoughtcrime.securesms.keyboard.sticker.KeyboardStickerListAdapter;
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||||
import org.whispersystems.libsignal.util.Pair;
|
import org.whispersystems.libsignal.util.Pair;
|
||||||
|
@ -24,10 +25,10 @@ public class StickerRolloverTouchListener implements RecyclerView.OnItemTouchLis
|
||||||
private WeakReference<View> currentView;
|
private WeakReference<View> currentView;
|
||||||
private boolean hoverMode;
|
private boolean hoverMode;
|
||||||
|
|
||||||
StickerRolloverTouchListener(@NonNull Context context,
|
public StickerRolloverTouchListener(@NonNull Context context,
|
||||||
@NonNull GlideRequests glideRequests,
|
@NonNull GlideRequests glideRequests,
|
||||||
@NonNull RolloverEventListener eventListener,
|
@NonNull RolloverEventListener eventListener,
|
||||||
@NonNull RolloverStickerRetriever stickerRetriever)
|
@NonNull RolloverStickerRetriever stickerRetriever)
|
||||||
{
|
{
|
||||||
this.eventListener = eventListener;
|
this.eventListener = eventListener;
|
||||||
this.stickerRetriever = stickerRetriever;
|
this.stickerRetriever = stickerRetriever;
|
||||||
|
@ -57,7 +58,7 @@ public class StickerRolloverTouchListener implements RecyclerView.OnItemTouchLis
|
||||||
View child = recyclerView.getChildAt(i);
|
View child = recyclerView.getChildAt(i);
|
||||||
|
|
||||||
if (ViewUtil.isPointInsideView(recyclerView, motionEvent.getRawX(), motionEvent.getRawY()) &&
|
if (ViewUtil.isPointInsideView(recyclerView, motionEvent.getRawX(), motionEvent.getRawY()) &&
|
||||||
ViewUtil.isPointInsideView(child, motionEvent.getRawX(), motionEvent.getRawY()) &&
|
ViewUtil.isPointInsideView(child, motionEvent.getRawX(), motionEvent.getRawY()) &&
|
||||||
child != currentView.get())
|
child != currentView.get())
|
||||||
{
|
{
|
||||||
showStickerForView(recyclerView, child);
|
showStickerForView(recyclerView, child);
|
||||||
|
@ -72,25 +73,35 @@ public class StickerRolloverTouchListener implements RecyclerView.OnItemTouchLis
|
||||||
public void onRequestDisallowInterceptTouchEvent(boolean b) {
|
public void onRequestDisallowInterceptTouchEvent(boolean b) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void enterHoverMode(@NonNull RecyclerView recyclerView, View targetView) {
|
public void enterHoverMode(@NonNull RecyclerView recyclerView, View targetView) {
|
||||||
this.hoverMode = true;
|
this.hoverMode = true;
|
||||||
showStickerForView(recyclerView, targetView);
|
showStickerForView(recyclerView, targetView);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void enterHoverMode(@NonNull RecyclerView recyclerView, @NonNull KeyboardStickerListAdapter.Sticker sticker) {
|
||||||
|
this.hoverMode = true;
|
||||||
|
showSticker(recyclerView, sticker.getUri(), sticker.getStickerRecord().getEmoji());
|
||||||
|
}
|
||||||
|
|
||||||
private void showStickerForView(@NonNull RecyclerView recyclerView, @NonNull View view) {
|
private void showStickerForView(@NonNull RecyclerView recyclerView, @NonNull View view) {
|
||||||
Pair<Object, String> stickerData = stickerRetriever.getStickerDataFromView(view);
|
Pair<Object, String> stickerData = stickerRetriever.getStickerDataFromView(view);
|
||||||
|
|
||||||
if (stickerData != null) {
|
if (stickerData != null) {
|
||||||
if (!popup.isShowing()) {
|
showSticker(recyclerView, stickerData.first(), stickerData.second());
|
||||||
popup.showAtLocation(recyclerView, Gravity.NO_GRAVITY, 0, 0);
|
|
||||||
eventListener.onStickerPopupStarted();
|
|
||||||
}
|
|
||||||
popup.presentSticker(stickerData.first(), stickerData.second());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void showSticker(@NonNull RecyclerView recyclerView, @NonNull Object toLoad, @NonNull String emoji) {
|
||||||
|
if (!popup.isShowing()) {
|
||||||
|
popup.showAtLocation(recyclerView, Gravity.NO_GRAVITY, 0, 0);
|
||||||
|
eventListener.onStickerPopupStarted();
|
||||||
|
}
|
||||||
|
popup.presentSticker(toLoad, emoji);
|
||||||
|
}
|
||||||
|
|
||||||
public interface RolloverEventListener {
|
public interface RolloverEventListener {
|
||||||
void onStickerPopupStarted();
|
void onStickerPopupStarted();
|
||||||
|
|
||||||
void onStickerPopupEnded();
|
void onStickerPopupEnded();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,19 +7,21 @@
|
||||||
tools:background="@color/signal_background_secondary">
|
tools:background="@color/signal_background_secondary">
|
||||||
|
|
||||||
<com.google.android.material.appbar.AppBarLayout
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
android:id="@+id/sticker_appbar"
|
android:id="@+id/sticker_keyboard_search_appbar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="@color/signal_background_secondary"
|
android:background="@color/signal_background_secondary"
|
||||||
app:elevation="0dp">
|
app:elevation="0dp"
|
||||||
|
app:expanded="false"
|
||||||
|
tools:expanded="true">
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.keyboard.emoji.KeyboardPageSearchView
|
<org.thoughtcrime.securesms.keyboard.emoji.KeyboardPageSearchView
|
||||||
android:id="@+id/sticker_keyboard_search_text"
|
android:id="@+id/sticker_keyboard_search_text"
|
||||||
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_hint="@string/StickerSearchDialogFragment_search_stickers"
|
app:search_hint="@string/StickerSearchDialogFragment_search_stickers"
|
||||||
|
@ -27,10 +29,14 @@
|
||||||
|
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
<androidx.viewpager.widget.ViewPager
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/sticker_pager"
|
android:id="@+id/sticker_keyboard_list"
|
||||||
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="16dp"
|
||||||
|
android:paddingEnd="16dp"
|
||||||
|
android:paddingBottom="?actionBarSize"
|
||||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
@ -38,7 +44,7 @@
|
||||||
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"
|
||||||
android:background="@drawable/keyboard_bottom_bar_background"
|
android:background="@color/signal_background_secondary"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
app:layout_behavior="@string/hide_bottom_view_on_scroll_behavior">
|
app:layout_behavior="@string/hide_bottom_view_on_scroll_behavior">
|
||||||
|
@ -57,7 +63,7 @@
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/sticker_packs_recycler"
|
android:id="@+id/sticker_packs_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"
|
||||||
|
@ -77,4 +83,18 @@
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="2dp"
|
||||||
|
android:background="@drawable/toolbar_shadow"
|
||||||
|
app:app_bar_layout_id="@+id/sticker_keyboard_search_appbar"
|
||||||
|
app:layout_behavior=".keyboard.TopShadowBehavior" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="3dp"
|
||||||
|
android:background="@drawable/bottom_toolbar_shadow"
|
||||||
|
app:bottom_bar_id="@+id/sticker_keyboard_packs_background"
|
||||||
|
app:layout_behavior=".keyboard.BottomShadowBehavior" />
|
||||||
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.appcompat.widget.AppCompatTextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/sticker_grid_header_title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:minHeight="48dp"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textAppearance="@style/TextAppearance.Signal.Body1.Bold"
|
||||||
|
android:textColor="@color/signal_text_primary"
|
||||||
|
tools:text="@string/ReactWithAnyEmojiBottomSheetDialogFragment__activities" />
|
|
@ -1,8 +1,16 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.appcompat.widget.AppCompatImageView xmlns:android="http://schemas.android.com/apk/res/android"
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/sticker_keyboard_page_image"
|
android:layout_width="match_parent"
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="?selectableItemBackground"
|
android:background="?selectableItemBackgroundBorderless">
|
||||||
tools:srcCompat="@tools:sample/avatars" />
|
|
||||||
|
<androidx.appcompat.widget.AppCompatImageView
|
||||||
|
android:id="@+id/sticker_keyboard_page_image"
|
||||||
|
android:layout_width="88dp"
|
||||||
|
android:layout_height="88dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:adjustViewBounds="true"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
tools:srcCompat="@tools:sample/avatars" />
|
||||||
|
</FrameLayout>
|
||||||
|
|
|
@ -3588,6 +3588,9 @@
|
||||||
<string name="SoundsAndNotificationsSettingsFragment__do_not_notify">Do not notify</string>
|
<string name="SoundsAndNotificationsSettingsFragment__do_not_notify">Do not notify</string>
|
||||||
<string name="SoundsAndNotificationsSettingsFragment__custom_notifications">Custom notifications</string>
|
<string name="SoundsAndNotificationsSettingsFragment__custom_notifications">Custom notifications</string>
|
||||||
|
|
||||||
|
<!-- StickerKeyboard -->
|
||||||
|
<string name="StickerKeyboard__recently_used">Recently used</string>
|
||||||
|
|
||||||
<!-- EOF -->
|
<!-- EOF -->
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Ładowanie…
Reference in New Issue