Add vertical scrolling to Sticker Keyboard.

fork-5.53.8
Cody Henthorne 2021-06-28 17:18:04 -04:00 zatwierdzone przez Greyson Parrelli
rodzic aba5774446
commit d4a3b442f4
13 zmienionych plików z 565 dodań i 82 usunięć

Wyświetl plik

@ -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" />

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -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" />

Wyświetl plik

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

Wyświetl plik

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