Non-modal bottom sheet on details activity

This commit is contained in:
Koitharu
2024-04-29 10:16:03 +03:00
parent aba6b64074
commit 63b53d2244
9 changed files with 135 additions and 39 deletions

View File

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

View File

@@ -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<B : ViewBinding> : 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<B : ViewBinding> : 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<B : ViewBinding> : 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<B : ViewBinding> : 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<B : ViewBinding> : 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) {

View File

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

View File

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

View File

@@ -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<SheetChaptersPagesBinding>(), 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<SheetChaptersPagesBinding>(), 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<SheetChaptersPagesBinding>(), 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

View File

@@ -420,7 +420,8 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
}
viewBinding.container.updateLayoutParams<MarginLayoutParams> {
bottomMargin = if (isPinned) {
bottomNavBar?.measureHeight() ?: 0
(bottomNavBar?.measureHeight() ?: 0)
.coerceAtLeast(resources.getDimensionPixelSize(materialR.dimen.m3_bottom_nav_min_height))
} else {
0
}

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
@@ -7,14 +7,16 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical"
tools:viewBindingType="android.view.ViewGroup">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
android:elevation="0dp"
android:fitsSystemWindows="true"
app:elevation="0dp"
app:liftOnScroll="false">
<com.google.android.material.appbar.MaterialToolbar
android:id="@id/toolbar"
@@ -29,11 +31,13 @@
android:layout_height="match_parent"
android:clipToPadding="false"
android:scrollIndicators="top"
android:scrollbars="vertical">
android:scrollbars="vertical"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:paddingBottom="@dimen/details_bs_peek_height">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/imageView_cover"
@@ -184,7 +188,7 @@
android:paddingHorizontal="6dp"
android:paddingVertical="8dp"
app:baseColor="?colorSecondaryContainer"
app:layout_constraintEnd_toStartOf="@id/button_chapters"
app:layout_constraintEnd_toStartOf="@id/button_download"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/info_layout"
app:progressColor="?colorPrimary"
@@ -196,19 +200,19 @@
tools:title="@string/read" />
<ImageView
android:id="@+id/button_chapters"
android:id="@+id/button_download"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginEnd="16dp"
android:background="@drawable/bg_circle_button"
android:backgroundTint="?colorSecondaryContainer"
android:contentDescription="@string/chapters"
android:contentDescription="@string/download"
android:scaleType="centerInside"
app:layout_constraintBottom_toBottomOf="@id/button_read"
app:layout_constraintDimensionRatio="1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/button_read"
app:srcCompat="@drawable/ic_list_sheet" />
app:srcCompat="@drawable/ic_download" />
<TextView
android:id="@+id/textView_description_title"
@@ -390,4 +394,21 @@
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/container_bottom_sheet"
android:name="org.koitharu.kotatsu.details.ui.pager.ChaptersPagesSheet"
style="@style/Widget.Material3.BottomSheet"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:elevation="6dp"
android:nestedScrollingEnabled="false"
android:outlineProvider="background"
app:behavior_fitToContents="false"
app:behavior_halfExpandedRatio="0.8"
app:behavior_hideable="false"
app:behavior_peekHeight="@dimen/details_bs_peek_height"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"
tools:layout="@layout/sheet_chapters_pages" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

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

View File

@@ -35,6 +35,7 @@
<dimen name="chapter_grid_width">80dp</dimen>
<dimen name="side_card_offset">8dp</dimen>
<dimen name="webtoon_pages_gap">24dp</dimen>
<dimen name="details_bs_peek_height">92dp</dimen>
<dimen name="search_suggestions_manga_height">142dp</dimen>
<dimen name="search_suggestions_manga_spacing">6dp</dimen>