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" />
|
||||
</JavaCodeStyleSettings>
|
||||
<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_FOR_MEMBERS" value="2147483647" />
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
|
|
|
@ -76,6 +76,7 @@ public abstract class Database {
|
|||
}
|
||||
|
||||
protected void notifyStickerPackListeners() {
|
||||
ApplicationDependencies.getDatabaseObserver().notifyStickerPackObservers();
|
||||
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 Set<Observer> allPaymentsObservers;
|
||||
private final Set<Observer> chatColorsObservers;
|
||||
private final Set<Observer> stickerPackObservers;
|
||||
|
||||
public DatabaseObserver(Application application) {
|
||||
this.application = application;
|
||||
|
@ -40,6 +41,7 @@ public final class DatabaseObserver {
|
|||
this.paymentObservers = new HashMap<>();
|
||||
this.allPaymentsObservers = new HashSet<>();
|
||||
this.chatColorsObservers = new HashSet<>();
|
||||
this.stickerPackObservers = new HashSet<>();
|
||||
}
|
||||
|
||||
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) {
|
||||
executor.execute(() -> {
|
||||
conversationListObservers.remove(listener);
|
||||
|
@ -85,6 +93,7 @@ public final class DatabaseObserver {
|
|||
unregisterMapped(verboseConversationObservers, listener);
|
||||
unregisterMapped(paymentObservers, 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) {
|
||||
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.view.View
|
||||
import androidx.annotation.Px
|
||||
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.viewpager.widget.PagerAdapter
|
||||
import androidx.viewpager.widget.ViewPager
|
||||
import androidx.recyclerview.widget.RecyclerView.SmoothScroller
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import org.thoughtcrime.securesms.LoggingFragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.emoji.MediaKeyboardBottomTabAdapter
|
||||
import org.thoughtcrime.securesms.components.emoji.MediaKeyboardProvider
|
||||
import org.thoughtcrime.securesms.database.DatabaseObserver
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.keyboard.emoji.KeyboardPageSearchView
|
||||
import org.thoughtcrime.securesms.keyboard.findListener
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.stickers.StickerKeyboardProvider
|
||||
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 provider: StickerKeyboardProvider
|
||||
|
||||
private lateinit var stickerPager: ViewPager
|
||||
private lateinit var stickerList: RecyclerView
|
||||
private lateinit var keyboardStickerListAdapter: KeyboardStickerListAdapter
|
||||
private lateinit var layoutManager: GridLayoutManager
|
||||
private lateinit var listTouchListener: StickerRolloverTouchListener
|
||||
private lateinit var stickerPacksRecycler: RecyclerView
|
||||
private lateinit var manageStickers: View
|
||||
private lateinit var tabAdapter: MediaKeyboardBottomTabAdapter
|
||||
private lateinit var appBarLayout: AppBarLayout
|
||||
private lateinit var stickerPacksAdapter: StickerPackListAdapter
|
||||
|
||||
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?) {
|
||||
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)
|
||||
|
||||
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 {
|
||||
override fun onClicked() {
|
||||
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_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?) {
|
||||
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)
|
||||
stickerPacksRecycler.adapter = tabAdapter
|
||||
viewModel.stickers.observe(viewLifecycleOwner, keyboardStickerListAdapter::submitList)
|
||||
viewModel.packs.observe(viewLifecycleOwner, stickerPacksAdapter::submitList)
|
||||
viewModel.getSelectedPack().observe(viewLifecycleOwner, this::updateCategoryTab)
|
||||
|
||||
provider = StickerKeyboardProvider(requireActivity(), findListener() ?: throw AssertionError("No sticker listener"))
|
||||
provider.requestPresentation(presenter, true)
|
||||
viewModel.refreshStickers()
|
||||
}
|
||||
|
||||
private fun findListener(): StickerEventListener? {
|
||||
return parentFragment as? StickerEventListener ?: requireActivity() as? StickerEventListener
|
||||
private fun onTabSelected(stickerPack: StickerPackListAdapter.StickerPack) {
|
||||
scrollTo(stickerPack.packRecord.packId)
|
||||
viewModel.selectPack(stickerPack.packRecord.packId)
|
||||
}
|
||||
|
||||
private fun onTabSelected(index: Int) {
|
||||
stickerPager.currentItem = index
|
||||
stickerPacksRecycler.smoothScrollToPosition(index)
|
||||
viewModel.selectedTab = index
|
||||
}
|
||||
private fun updateCategoryTab(packId: String) {
|
||||
stickerPacksRecycler.post {
|
||||
val index: Int = stickerPacksAdapter.indexOfFirst(StickerPackListAdapter.StickerPack::class.java) { it.packRecord.packId == packId }
|
||||
|
||||
private inner class StickerPresenter : MediaKeyboardProvider.Presenter {
|
||||
override fun present(
|
||||
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
|
||||
if (index != -1) {
|
||||
stickerPacksRecycler.smoothScrollToPosition(index)
|
||||
}
|
||||
stickerPager.currentItem = viewModel.selectedTab
|
||||
}
|
||||
}
|
||||
|
||||
stickerPager.clearOnPageChangeListeners()
|
||||
stickerPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
|
||||
override fun onPageSelected(position: Int) {
|
||||
tabAdapter.setActivePosition(position)
|
||||
stickerPacksRecycler.smoothScrollToPosition(position)
|
||||
provider.setCurrentPosition(position)
|
||||
private fun scrollTo(packId: String) {
|
||||
val index = keyboardStickerListAdapter.indexOfFirst(KeyboardStickerListAdapter.StickerHeader::class.java) { it.packId == packId }
|
||||
if (index != -1) {
|
||||
appBarLayout.setExpanded(false, true)
|
||||
packIdSelectionOnScroll.startAutoScrolling()
|
||||
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 onPageScrollStateChanged(state: Int) = Unit
|
||||
})
|
||||
override fun onStickerClicked(sticker: KeyboardStickerListAdapter.Sticker) {
|
||||
stickerThrottler.publish { findListener<StickerEventListener>()?.onStickerSelected(sticker.stickerRecord) }
|
||||
}
|
||||
|
||||
tabAdapter.setTabIconProvider(iconProvider, pagerAdapter.count)
|
||||
tabAdapter.setActivePosition(stickerPager.currentItem)
|
||||
override fun onStickerLongClicked(sticker: KeyboardStickerListAdapter.Sticker) {
|
||||
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 stickerPager.currentItem
|
||||
return null
|
||||
}
|
||||
|
||||
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 fun isVisible(): Boolean = true
|
||||
@Override
|
||||
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
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
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() {
|
||||
var selectedTab: Int = 0
|
||||
private const val NO_SELECTED_PAGE = "no_selected_page"
|
||||
|
||||
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 org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.keyboard.sticker.KeyboardStickerListAdapter;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
|
@ -24,10 +25,10 @@ public class StickerRolloverTouchListener implements RecyclerView.OnItemTouchLis
|
|||
private WeakReference<View> currentView;
|
||||
private boolean hoverMode;
|
||||
|
||||
StickerRolloverTouchListener(@NonNull Context context,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull RolloverEventListener eventListener,
|
||||
@NonNull RolloverStickerRetriever stickerRetriever)
|
||||
public StickerRolloverTouchListener(@NonNull Context context,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull RolloverEventListener eventListener,
|
||||
@NonNull RolloverStickerRetriever stickerRetriever)
|
||||
{
|
||||
this.eventListener = eventListener;
|
||||
this.stickerRetriever = stickerRetriever;
|
||||
|
@ -57,7 +58,7 @@ public class StickerRolloverTouchListener implements RecyclerView.OnItemTouchLis
|
|||
View child = recyclerView.getChildAt(i);
|
||||
|
||||
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())
|
||||
{
|
||||
showStickerForView(recyclerView, child);
|
||||
|
@ -72,25 +73,35 @@ public class StickerRolloverTouchListener implements RecyclerView.OnItemTouchLis
|
|||
public void onRequestDisallowInterceptTouchEvent(boolean b) {
|
||||
}
|
||||
|
||||
void enterHoverMode(@NonNull RecyclerView recyclerView, View targetView) {
|
||||
public void enterHoverMode(@NonNull RecyclerView recyclerView, View targetView) {
|
||||
this.hoverMode = true;
|
||||
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) {
|
||||
Pair<Object, String> stickerData = stickerRetriever.getStickerDataFromView(view);
|
||||
|
||||
if (stickerData != null) {
|
||||
if (!popup.isShowing()) {
|
||||
popup.showAtLocation(recyclerView, Gravity.NO_GRAVITY, 0, 0);
|
||||
eventListener.onStickerPopupStarted();
|
||||
}
|
||||
popup.presentSticker(stickerData.first(), stickerData.second());
|
||||
showSticker(recyclerView, 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 {
|
||||
void onStickerPopupStarted();
|
||||
|
||||
void onStickerPopupEnded();
|
||||
}
|
||||
|
||||
|
|
|
@ -7,19 +7,21 @@
|
|||
tools:background="@color/signal_background_secondary">
|
||||
|
||||
<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_height="wrap_content"
|
||||
android:background="@color/signal_background_secondary"
|
||||
app:elevation="0dp">
|
||||
app:elevation="0dp"
|
||||
app:expanded="false"
|
||||
tools:expanded="true">
|
||||
|
||||
<org.thoughtcrime.securesms.keyboard.emoji.KeyboardPageSearchView
|
||||
android:id="@+id/sticker_keyboard_search_text"
|
||||
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_hint="@string/StickerSearchDialogFragment_search_stickers"
|
||||
|
@ -27,10 +29,14 @@
|
|||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.viewpager.widget.ViewPager
|
||||
android:id="@+id/sticker_pager"
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/sticker_keyboard_list"
|
||||
android:layout_width="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" />
|
||||
|
||||
<LinearLayout
|
||||
|
@ -38,7 +44,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="?actionBarSize"
|
||||
android:layout_gravity="bottom"
|
||||
android:background="@drawable/keyboard_bottom_bar_background"
|
||||
android:background="@color/signal_background_secondary"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
app:layout_behavior="@string/hide_bottom_view_on_scroll_behavior">
|
||||
|
@ -57,7 +63,7 @@
|
|||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/sticker_packs_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"
|
||||
|
@ -77,4 +83,18 @@
|
|||
|
||||
</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>
|
||||
|
|
|
@ -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"?>
|
||||
<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"
|
||||
android:id="@+id/sticker_keyboard_page_image"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?selectableItemBackground"
|
||||
tools:srcCompat="@tools:sample/avatars" />
|
||||
android:background="?selectableItemBackgroundBorderless">
|
||||
|
||||
<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__custom_notifications">Custom notifications</string>
|
||||
|
||||
<!-- StickerKeyboard -->
|
||||
<string name="StickerKeyboard__recently_used">Recently used</string>
|
||||
|
||||
<!-- EOF -->
|
||||
|
||||
</resources>
|
||||
|
|
Ładowanie…
Reference in New Issue