From 63b53d2244f18d73506d8afad40698877b8bf790 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Mon, 29 Apr 2024 10:16:03 +0300 Subject: [PATCH] Non-modal bottom sheet on details activity --- .../core/ui/sheet/AdaptiveSheetBehavior.kt | 19 ++++++++- .../core/ui/sheet/BaseAdaptiveSheet.kt | 24 +++++++---- .../ui/util/BottomSheetClollapseCallback.kt | 31 ++++++++++++++ .../kotatsu/details/ui/DetailsActivity.kt | 37 ++++++++++------- .../details/ui/pager/ChaptersPagesSheet.kt | 16 ++++++-- .../koitharu/kotatsu/main/ui/MainActivity.kt | 3 +- app/src/main/res/layout/activity_details.xml | 41 ++++++++++++++----- app/src/main/res/layout/item_chapter.xml | 2 +- app/src/main/res/values/dimens.xml | 1 + 9 files changed, 135 insertions(+), 39 deletions(-) create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/BottomSheetClollapseCallback.kt diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/AdaptiveSheetBehavior.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/AdaptiveSheetBehavior.kt index 077eca144..7e7599b7a 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/AdaptiveSheetBehavior.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/AdaptiveSheetBehavior.kt @@ -2,7 +2,10 @@ package org.koitharu.kotatsu.core.ui.sheet import android.app.Dialog import android.view.View +import android.view.ViewParent import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.view.ancestors +import androidx.fragment.app.DialogFragment import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback import com.google.android.material.bottomsheet.BottomSheetDialog @@ -109,7 +112,16 @@ sealed class AdaptiveSheetBehavior { const val STATE_DRAGGING = SideSheetBehavior.STATE_DRAGGING const val STATE_HIDDEN = SideSheetBehavior.STATE_HIDDEN - fun from(dialog: Dialog?): AdaptiveSheetBehavior? = when (dialog) { + fun from(fragment: DialogFragment): AdaptiveSheetBehavior? { + from(fragment.dialog)?.let { return it } + val rootView = fragment.view ?: return null + for (parent in rootView.ancestors) { + from(parent)?.let { return it } + } + return null + } + + private fun from(dialog: Dialog?): AdaptiveSheetBehavior? = when (dialog) { is BottomSheetDialog -> Bottom(dialog.behavior) is SideSheetDialog -> Side(dialog.behavior) else -> null @@ -121,5 +133,10 @@ sealed class AdaptiveSheetBehavior { is SideSheetBehavior<*> -> Side(behavior) else -> null } + + private fun from(parent: ViewParent): AdaptiveSheetBehavior? { + val lp = ((parent as? View)?.layoutParams as? CoordinatorLayout.LayoutParams) ?: return null + return from(lp) + } } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/BaseAdaptiveSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/BaseAdaptiveSheet.kt index 82d209335..f53f5b220 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/BaseAdaptiveSheet.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/BaseAdaptiveSheet.kt @@ -12,6 +12,7 @@ import android.view.ViewGroup.LayoutParams import androidx.activity.ComponentDialog import androidx.activity.OnBackPressedDispatcher import androidx.annotation.CallSuper +import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDialog import androidx.appcompat.app.AppCompatDialogFragment import androidx.appcompat.view.ActionMode @@ -26,6 +27,7 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.sidesheet.SideSheetDialog import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.ui.BaseActivity import org.koitharu.kotatsu.core.ui.util.ActionModeDelegate import org.koitharu.kotatsu.core.util.ext.getThemeColor import com.google.android.material.R as materialR @@ -44,10 +46,10 @@ abstract class BaseAdaptiveSheet : AppCompatDialogFragment() { get() = requireViewBinding() protected val behavior: AdaptiveSheetBehavior? - get() = AdaptiveSheetBehavior.from(dialog) + get() = AdaptiveSheetBehavior.from(this) - @JvmField - val actionModeDelegate = ActionModeDelegate() + var actionModeDelegate: ActionModeDelegate? = null + private set val isExpanded: Boolean get() = behavior?.state == AdaptiveSheetBehavior.STATE_EXPANDED @@ -72,11 +74,15 @@ abstract class BaseAdaptiveSheet : AppCompatDialogFragment() { final override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val binding = requireViewBinding() + if (actionModeDelegate == null) { + actionModeDelegate = (activity as? BaseActivity<*>)?.actionModeDelegate + } onViewBindingCreated(binding, savedInstanceState) } override fun onDestroyView() { viewBinding = null + actionModeDelegate = null super.onDestroyView() } @@ -87,13 +93,15 @@ abstract class BaseAdaptiveSheet : AppCompatDialogFragment() { } else { BottomSheetDialogImpl(context, theme) } - dialog.onBackPressedDispatcher.addCallback(actionModeDelegate) + actionModeDelegate = ActionModeDelegate().also { + dialog.onBackPressedDispatcher.addCallback(it) + } return dialog } @CallSuper protected open fun dispatchSupportActionModeStarted(mode: ActionMode) { - actionModeDelegate.onSupportActionModeStarted(mode) + actionModeDelegate?.onSupportActionModeStarted(mode) val ctx = requireContext() val actionModeColor = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { ColorUtils.compositeColors( @@ -119,7 +127,7 @@ abstract class BaseAdaptiveSheet : AppCompatDialogFragment() { @CallSuper protected open fun dispatchSupportActionModeFinished(mode: ActionMode) { - actionModeDelegate.onSupportActionModeFinished(mode) + actionModeDelegate?.onSupportActionModeFinished(mode) dialog?.window?.statusBarColor = defaultStatusBarColor } @@ -138,8 +146,8 @@ abstract class BaseAdaptiveSheet : AppCompatDialogFragment() { protected open fun onViewBindingCreated(binding: B, savedInstanceState: Bundle?) = Unit fun startSupportActionMode(callback: ActionMode.Callback): ActionMode? { - val appCompatDialog = dialog as? AppCompatDialog ?: return null - return appCompatDialog.delegate.startSupportActionMode(callback) + val delegate = (dialog as? AppCompatDialog)?.delegate ?: (activity as? AppCompatActivity)?.delegate ?: return null + return delegate.startSupportActionMode(callback) } protected fun setExpanded(isExpanded: Boolean, isLocked: Boolean) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/BottomSheetClollapseCallback.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/BottomSheetClollapseCallback.kt new file mode 100644 index 000000000..4daac0aa4 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/BottomSheetClollapseCallback.kt @@ -0,0 +1,31 @@ +package org.koitharu.kotatsu.core.ui.util + +import android.view.View +import androidx.activity.OnBackPressedCallback +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED +import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED +import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HALF_EXPANDED +import org.koitharu.kotatsu.core.util.ext.doOnExpansionsChanged + +class BottomSheetClollapseCallback( + private val behavior: BottomSheetBehavior<*>, +) : OnBackPressedCallback(behavior.state == STATE_EXPANDED) { + + init { + behavior.addBottomSheetCallback( + object : BottomSheetBehavior.BottomSheetCallback() { + + override fun onStateChanged(view: View, state: Int) { + isEnabled = state == STATE_EXPANDED || state == STATE_HALF_EXPANDED + } + + override fun onSlide(p0: View, p1: Float) = Unit + }, + ) + } + + override fun handleOnBackPressed() { + behavior.state = STATE_COLLAPSED + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt index bd74df54c..1006f0e65 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt @@ -31,6 +31,7 @@ import coil.request.ImageRequest import coil.request.SuccessResult import coil.transform.CircleCropTransformation import coil.util.CoilUtils +import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.chip.Chip import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint @@ -52,10 +53,10 @@ import org.koitharu.kotatsu.core.ui.BaseListAdapter import org.koitharu.kotatsu.core.ui.image.ChipIconTarget import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.ui.util.BottomSheetClollapseCallback import org.koitharu.kotatsu.core.ui.util.MenuInvalidator import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver import org.koitharu.kotatsu.core.ui.widgets.ChipsView -import org.koitharu.kotatsu.core.util.Event import org.koitharu.kotatsu.core.util.FileSize import org.koitharu.kotatsu.core.util.ViewBadge import org.koitharu.kotatsu.core.util.ext.crossfade @@ -68,6 +69,7 @@ import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.parentView import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf +import org.koitharu.kotatsu.core.util.ext.setNavigationBarTransparentCompat import org.koitharu.kotatsu.core.util.ext.setOnContextClickListenerCompat import org.koitharu.kotatsu.core.util.ext.source import org.koitharu.kotatsu.core.util.ext.textAndVisible @@ -123,6 +125,7 @@ class DetailsActivity : private val viewModel: DetailsViewModel by viewModels() private lateinit var chaptersBadge: ViewBadge + private lateinit var menuProvider: DetailsMenuProvider override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -134,7 +137,7 @@ class DetailsActivity : viewBinding.buttonRead.setOnClickListener(this) viewBinding.buttonRead.setOnLongClickListener(this) viewBinding.buttonRead.setOnContextClickListenerCompat(this) - viewBinding.buttonChapters?.setOnClickListener(this) + viewBinding.buttonDownload?.setOnClickListener(this) viewBinding.infoLayout.chipBranch.setOnClickListener(this) viewBinding.infoLayout.chipSize.setOnClickListener(this) viewBinding.infoLayout.chipSource.setOnClickListener(this) @@ -152,8 +155,10 @@ class DetailsActivity : viewBinding.textViewDescription.movementMethod = LinkMovementMethodCompat.getInstance() viewBinding.chipsTags.onChipClickListener = this TitleScrollCoordinator(viewBinding.textViewTitle).attach(viewBinding.scrollView) - - chaptersBadge = ViewBadge(viewBinding.buttonChapters ?: viewBinding.buttonRead, this) + viewBinding.containerBottomSheet?.let { BottomSheetBehavior.from(it) }?.let { behavior -> + onBackPressedDispatcher.addCallback(BottomSheetClollapseCallback(behavior)) + } + chaptersBadge = ViewBadge(viewBinding.buttonRead, this) viewModel.details.filterNotNull().observe(this, ::onMangaUpdated) viewModel.onMangaRemoved.observeEvent(this, ::onMangaRemoved) @@ -187,21 +192,20 @@ class DetailsActivity : .filterNot { ChaptersPagesSheet.isShown(supportFragmentManager) } .observeEvent(this, DownloadStartedObserver(viewBinding.scrollView)) - addMenuProvider( - DetailsMenuProvider( - activity = this, - viewModel = viewModel, - snackbarHost = viewBinding.scrollView, - appShortcutManager = shortcutManager, - ), + menuProvider = DetailsMenuProvider( + activity = this, + viewModel = viewModel, + snackbarHost = viewBinding.scrollView, + appShortcutManager = shortcutManager, ) + addMenuProvider(menuProvider) } override fun onClick(v: View) { when (v.id) { R.id.button_read -> openReader(isIncognitoMode = false) R.id.chip_branch -> showBranchPopupMenu(v) - R.id.button_chapters -> ChaptersPagesSheet.show(supportFragmentManager) + R.id.button_download -> DownloadDialogHelper(v, viewModel).show(menuProvider) R.id.chip_author -> { val manga = viewModel.manga.value ?: return @@ -417,7 +421,7 @@ class DetailsActivity : } private fun onLoadingStateChanged(isLoading: Boolean) { - val button = viewBinding.buttonChapters ?: return + val button = viewBinding.buttonDownload ?: return if (isLoading) { button.setImageDrawable( CircularProgressDrawable(this).also { @@ -427,7 +431,7 @@ class DetailsActivity : }, ) } else { - button.setImageResource(R.drawable.ic_list_sheet) + button.setImageResource(R.drawable.ic_download) } } @@ -525,6 +529,9 @@ class DetailsActivity : viewBinding.scrollView.updatePadding( bottom = insets.bottom, ) + viewBinding.containerBottomSheet?.let { bs -> + window.setNavigationBarTransparentCompat(this, bs.elevation, 0.9f) + } } private fun onHistoryChanged(info: HistoryInfo, isLoading: Boolean) = with(viewBinding) { @@ -538,7 +545,7 @@ class DetailsActivity : else -> resources.getQuantityString(R.plurals.chapters, info.totalChapters, info.totalChapters) } buttonRead.setProgress(info.history?.percent?.coerceIn(0f, 1f) ?: 0f, true) - buttonChapters?.isEnabled = info.isValid + buttonDownload?.isEnabled = info.isValid buttonRead.isEnabled = info.isValid } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/ChaptersPagesSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/ChaptersPagesSheet.kt index d9c483b44..7f6d349c6 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/ChaptersPagesSheet.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/ChaptersPagesSheet.kt @@ -17,8 +17,8 @@ import org.koitharu.kotatsu.core.ui.util.ActionModeListener import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver import org.koitharu.kotatsu.core.util.ext.doOnPageChanged import org.koitharu.kotatsu.core.util.ext.menuView +import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observeEvent -import org.koitharu.kotatsu.core.util.ext.recyclerView import org.koitharu.kotatsu.core.util.ext.setTabsEnabled import org.koitharu.kotatsu.core.util.ext.showDistinct import org.koitharu.kotatsu.core.util.ext.withArgs @@ -46,7 +46,6 @@ class ChaptersPagesSheet : BaseAdaptiveSheet(), Actio val args = arguments ?: Bundle.EMPTY val defaultTab = args.getInt(ARG_TAB, settings.defaultDetailsTab) val adapter = ChaptersPagesAdapter(this, settings.isPagesTabEnabled || defaultTab == TAB_PAGES) - binding.pager.recyclerView?.isNestedScrollingEnabled = false binding.pager.offscreenPageLimit = adapter.itemCount binding.pager.adapter = adapter binding.pager.doOnPageChanged(::onPageChanged) @@ -58,11 +57,12 @@ class ChaptersPagesSheet : BaseAdaptiveSheet(), Actio onBackPressedDispatcher.addCallback(viewLifecycleOwner, menuProvider) binding.toolbar.addMenuProvider(menuProvider) - actionModeDelegate.addListener(this, viewLifecycleOwner) + actionModeDelegate?.addListener(this, viewLifecycleOwner) viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.pager, this)) viewModel.onActionDone.observeEvent(viewLifecycleOwner, ReversibleActionObserver(binding.pager, null)) viewModel.onDownloadStarted.observeEvent(viewLifecycleOwner, DownloadStartedObserver(binding.pager)) + viewModel.newChaptersCount.observe(viewLifecycleOwner, ::onNewChaptersChanged) } override fun onActionModeStarted(mode: ActionMode) { @@ -97,6 +97,16 @@ class ChaptersPagesSheet : BaseAdaptiveSheet(), Actio settings.lastDetailsTab = position } + private fun onNewChaptersChanged(counter: Int) { + val tab = viewBinding?.tabs?.getTabAt(0) ?: return + if (counter == 0) { + tab.removeBadge() + } else { + val badge = tab.orCreateBadge + badge.number = counter + } + } + companion object { const val TAB_CHAPTERS = 0 diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActivity.kt index 90f4cc37c..394f1acae 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActivity.kt @@ -420,7 +420,8 @@ class MainActivity : BaseActivity(), AppBarOwner, BottomNav } viewBinding.container.updateLayoutParams { bottomMargin = if (isPinned) { - bottomNavBar?.measureHeight() ?: 0 + (bottomNavBar?.measureHeight() ?: 0) + .coerceAtLeast(resources.getDimensionPixelSize(materialR.dimen.m3_bottom_nav_min_height)) } else { 0 } diff --git a/app/src/main/res/layout/activity_details.xml b/app/src/main/res/layout/activity_details.xml index 0a3bb8330..e64cd889b 100644 --- a/app/src/main/res/layout/activity_details.xml +++ b/app/src/main/res/layout/activity_details.xml @@ -1,5 +1,5 @@ - + android:elevation="0dp" + android:fitsSystemWindows="true" + app:elevation="0dp" + app:liftOnScroll="false"> + android:scrollbars="vertical" + app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"> + android:layout_height="match_parent" + android:paddingBottom="@dimen/details_bs_peek_height"> + app:srcCompat="@drawable/ic_download" /> - + + + + diff --git a/app/src/main/res/layout/item_chapter.xml b/app/src/main/res/layout/item_chapter.xml index 3bb39d35c..43ac59102 100644 --- a/app/src/main/res/layout/item_chapter.xml +++ b/app/src/main/res/layout/item_chapter.xml @@ -57,6 +57,6 @@ android:layout_height="wrap_content" android:layout_marginEnd="?android:listPreferredItemPaddingEnd" android:contentDescription="@string/downloaded" - app:srcCompat="@drawable/ic_save_ok" /> + app:srcCompat="@drawable/ic_storage" /> diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index fa3c2f89a..0db529f60 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -35,6 +35,7 @@ 80dp 8dp 24dp + 92dp 142dp 6dp