Temporarily replace ViewPager2 within ViewPager in favourites

This commit is contained in:
Koitharu
2023-07-18 16:22:02 +03:00
parent e7bd74429e
commit 83cf6aa997
12 changed files with 174 additions and 63 deletions

View File

@@ -152,7 +152,7 @@ class KotatsuApp : Application(), Configuration.Provider {
FragmentStrictMode.defaultPolicy = FragmentStrictMode.Policy.Builder()
.penaltyDeath()
.detectFragmentReuse()
.detectWrongFragmentContainer()
// .detectWrongFragmentContainer() FIXME: migrate to ViewPager2
.detectRetainInstanceUsage()
.detectSetUserVisibleHint()
.detectFragmentTagUsage()

View File

@@ -0,0 +1,30 @@
package org.koitharu.kotatsu.core.ui.widgets
import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import androidx.viewpager.widget.ViewPager
@SuppressLint("ClickableViewAccessibility")
class EnhancedViewPager @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
) : ViewPager(context, attrs) {
var isUserInputEnabled: Boolean = true
set(value) {
field = value
if (!value) {
cancelPendingInputEvents()
}
}
override fun onTouchEvent(event: MotionEvent): Boolean {
return isUserInputEnabled && super.onTouchEvent(event)
}
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
return isUserInputEnabled && super.onInterceptTouchEvent(event)
}
}

View File

@@ -1,54 +1,33 @@
package org.koitharu.kotatsu.favourites.ui.container
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.AdapterListUpdateCallback
import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator.TabConfigurationStrategy
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asExecutor
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentStatePagerAdapter
import kotlinx.coroutines.flow.FlowCollector
import org.koitharu.kotatsu.core.util.ContinuationResumeRunnable
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import kotlin.coroutines.suspendCoroutine
import org.koitharu.kotatsu.parsers.util.replaceWith
class FavouritesContainerAdapter(fragment: Fragment) :
FragmentStateAdapter(fragment.childFragmentManager, fragment.viewLifecycleOwner.lifecycle),
TabConfigurationStrategy,
@Suppress("DEPRECATION")
class FavouritesContainerAdapter(
fm: FragmentManager
) : FragmentStatePagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT),
FlowCollector<List<FavouriteTabModel>> {
private val differ = AsyncListDiffer(
AdapterListUpdateCallback(this),
AsyncDifferConfig.Builder(ListModelDiffCallback<FavouriteTabModel>())
.setBackgroundThreadExecutor(Dispatchers.Default.limitedParallelism(2).asExecutor())
.build(),
)
private val dataSet = ArrayList<FavouriteTabModel>()
override fun getItemCount(): Int = differ.currentList.size
override fun getCount(): Int = dataSet.size
override fun getItemId(position: Int): Long {
return differ.currentList[position].id
}
override fun containsItem(itemId: Long): Boolean {
return differ.currentList.any { x -> x.id == itemId }
}
override fun createFragment(position: Int): Fragment {
val item = differ.currentList[position]
override fun getItem(position: Int): Fragment {
val item = dataSet[position]
return FavouritesListFragment.newInstance(item.id)
}
override fun onConfigureTab(tab: TabLayout.Tab, position: Int) {
val item = differ.currentList[position]
tab.text = item.title
tab.tag = item
override fun getPageTitle(position: Int): CharSequence {
return dataSet[position].title
}
override suspend fun emit(value: List<FavouriteTabModel>) = suspendCoroutine { cont ->
differ.submitList(value, ContinuationResumeRunnable(cont))
override suspend fun emit(value: List<FavouriteTabModel>) {
dataSet.replaceWith(value)
notifyDataSetChanged()
}
}

View File

@@ -0,0 +1,55 @@
package org.koitharu.kotatsu.favourites.ui.container
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.AdapterListUpdateCallback
import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator.TabConfigurationStrategy
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.flow.FlowCollector
import org.koitharu.kotatsu.core.util.ContinuationResumeRunnable
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import kotlin.coroutines.suspendCoroutine
// FIXME migrate to ViewPager2 in FavouritesContainerFragment
class FavouritesContainerAdapter2(fragment: Fragment) :
FragmentStateAdapter(fragment.childFragmentManager, fragment.viewLifecycleOwner.lifecycle),
TabConfigurationStrategy,
FlowCollector<List<FavouriteTabModel>> {
private val differ = AsyncListDiffer(
AdapterListUpdateCallback(this),
AsyncDifferConfig.Builder(ListModelDiffCallback<FavouriteTabModel>())
.setBackgroundThreadExecutor(Dispatchers.Default.limitedParallelism(2).asExecutor())
.build(),
)
override fun getItemCount(): Int = differ.currentList.size
override fun getItemId(position: Int): Long {
return differ.currentList[position].id
}
override fun containsItem(itemId: Long): Boolean {
return differ.currentList.any { x -> x.id == itemId }
}
override fun createFragment(position: Int): Fragment {
val item = differ.currentList[position]
return FavouritesListFragment.newInstance(item.id)
}
override fun onConfigureTab(tab: TabLayout.Tab, position: Int) {
val item = differ.currentList[position]
tab.text = item.title
tab.tag = item
}
override suspend fun emit(value: List<FavouriteTabModel>) = suspendCoroutine { cont ->
differ.submitList(value, ContinuationResumeRunnable(cont))
}
}

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.favourites.ui.container
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
@@ -7,10 +8,10 @@ import androidx.appcompat.view.ActionMode
import androidx.core.graphics.Insets
import androidx.core.view.updatePadding
import androidx.fragment.app.viewModels
import com.google.android.material.tabs.TabLayoutMediator
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.core.ui.BaseFragment
import org.koitharu.kotatsu.core.ui.util.ActionModeListener
import org.koitharu.kotatsu.core.util.ext.addMenuProvider
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.databinding.FragmentFavouritesContainerBinding
@@ -26,16 +27,13 @@ class FavouritesContainerFragment : BaseFragment<FragmentFavouritesContainerBind
override fun onViewBindingCreated(binding: FragmentFavouritesContainerBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
val adapter = FavouritesContainerAdapter(this)
val adapter = FavouritesContainerAdapter(childFragmentManager)
binding.pager.adapter = adapter
TabLayoutMediator(
binding.tabs,
binding.pager,
adapter,
).attach()
binding.tabs.setupWithViewPager(binding.pager)
binding.pager.offscreenPageLimit = 1
actionModeDelegate.addListener(this)
viewModel.categories.observe(viewLifecycleOwner, adapter)
addMenuProvider(FavouritesContainerMenuProvider(binding.root.context))
}
override fun onDestroyView() {
@@ -50,6 +48,7 @@ class FavouritesContainerFragment : BaseFragment<FragmentFavouritesContainerBind
)
}
@SuppressLint("ClickableViewAccessibility")
override fun onActionModeStarted(mode: ActionMode) {
viewBinding?.run {
pager.isUserInputEnabled = false
@@ -57,6 +56,7 @@ class FavouritesContainerFragment : BaseFragment<FragmentFavouritesContainerBind
}
}
@SuppressLint("ClickableViewAccessibility")
override fun onActionModeFinished(mode: ActionMode) {
viewBinding?.run {
pager.isUserInputEnabled = true

View File

@@ -0,0 +1,29 @@
package org.koitharu.kotatsu.favourites.ui.container
import android.content.Context
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.core.view.MenuProvider
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity
class FavouritesContainerMenuProvider(
private val context: Context,
) : MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.opt_favourites_container, menu)
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
when (menuItem.itemId) {
R.id.action_manage -> {
context.startActivity(FavouriteCategoriesActivity.newIntent(context))
}
else -> return false
}
return true
}
}

View File

@@ -4,7 +4,6 @@ import android.Manifest
import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.os.Build
import android.os.Bundle
import android.util.SparseIntArray
import android.view.Menu
import android.view.MenuItem
import android.view.View
@@ -14,7 +13,6 @@ import androidx.appcompat.view.ActionMode
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.graphics.Insets
import androidx.core.util.size
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.inputmethod.EditorInfoCompat
@@ -276,11 +274,10 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
startActivity(IntentBuilder(this).manga(manga).build(), options)
}
private fun onCountersChanged(counters: SparseIntArray) {
private fun onCountersChanged(counters: IntArray) {
repeat(counters.size) { i ->
val id = counters.keyAt(i)
val counter = counters.valueAt(i)
navigationDelegate.setCounter(id, counter)
val counter = counters[i]
navigationDelegate.setCounterAt(i, counter)
}
}

View File

@@ -66,6 +66,11 @@ class MainNavigationDelegate(
} ?: onNavigationItemSelected(navBar.selectedItemId)
}
fun setCounterAt(position: Int, counter: Int) {
val id = navBar.menu.getItem(position).itemId
setCounter(id, counter)
}
fun setCounter(@IdRes id: Int, counter: Int) {
if (counter == 0) {
navBar.getBadge(id)?.isVisible = false

View File

@@ -1,16 +1,16 @@
package org.koitharu.kotatsu.main.ui
import android.util.SparseIntArray
import androidx.core.util.set
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException
import org.koitharu.kotatsu.core.github.AppUpdateRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
@@ -51,16 +51,13 @@ class MainViewModel @Inject constructor(
val counters = combine(
trackingRepository.observeUpdatedMangaCount(),
flow { emit(settings.newSources.size) },
observeNewSourcesCount(),
) { tracks, newSources ->
val a = SparseIntArray(2)
a[R.id.nav_explore] = newSources
a[R.id.nav_feed] = tracks
a
intArrayOf(0, 0, newSources, tracks)
}.stateIn(
scope = viewModelScope + Dispatchers.Default,
started = SharingStarted.WhileSubscribed(5000),
initialValue = SparseIntArray(0),
initialValue = IntArray(4),
)
init {
@@ -79,4 +76,11 @@ class MainViewModel @Inject constructor(
fun setIncognitoMode(isEnabled: Boolean) {
settings.isIncognitoModeEnabled = isEnabled
}
private fun observeNewSourcesCount() = settings.observe()
.filter { it == AppSettings.KEY_SOURCES_ORDER || it == AppSettings.KEY_SOURCES_HIDDEN }
.onStart { emit("") }
.map { settings.newSources.size }
.distinctUntilChanged()
}

View File

@@ -13,7 +13,7 @@
app:tabGravity="start"
app:tabMode="scrollable" />
<androidx.viewpager2.widget.ViewPager2
<org.koitharu.kotatsu.core.ui.widgets.EnhancedViewPager
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent" />

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/action_manage"
android:orderInCategory="48"
android:title="@string/manage_favourites"
android:titleCondensed="@string/manage" />
</menu>

View File

@@ -101,7 +101,7 @@
<string name="text_empty_holder_primary">It\'s kind of empty here…</string>
<string name="text_search_holder_secondary">Try to reformulate the query.</string>
<string name="text_history_holder_primary">What you read will be displayed here</string>
<string name="text_history_holder_secondary">Find what to read in side menu.</string>
<string name="text_history_holder_secondary">Find what to read in the «Explore» section</string>
<string name="text_shelf_holder_primary">Your manga will be displayed here</string>
<string name="text_shelf_holder_secondary">Find what to read in the «Explore» section</string>
<string name="text_local_holder_primary">Save something first</string>
@@ -460,4 +460,5 @@
<string name="background">Background</string>
<string name="data_not_restored">Data was not restored</string>
<string name="data_not_restored_text">Make sure you have selected the correct backup file</string>
<string name="manage_favourites">Manage favourites</string>
</resources>