Use SideSheet instead of BottomSheet on landscape
parent
3d05541f61
commit
0c132a521e
@ -0,0 +1,123 @@
|
||||
package org.koitharu.kotatsu.core.ui.sheet
|
||||
|
||||
import android.app.Dialog
|
||||
import android.view.View
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.sidesheet.SideSheetBehavior
|
||||
import com.google.android.material.sidesheet.SideSheetCallback
|
||||
import com.google.android.material.sidesheet.SideSheetDialog
|
||||
import java.util.LinkedList
|
||||
|
||||
sealed class AdaptiveSheetBehavior {
|
||||
|
||||
@JvmField
|
||||
protected val callbacks = LinkedList<AdaptiveSheetCallback>()
|
||||
|
||||
abstract var state: Int
|
||||
|
||||
abstract var isDraggable: Boolean
|
||||
|
||||
open val isHideable: Boolean = true
|
||||
|
||||
fun addCallback(callback: AdaptiveSheetCallback) {
|
||||
callbacks.add(callback)
|
||||
}
|
||||
|
||||
fun removeCallback(callback: AdaptiveSheetCallback) {
|
||||
callbacks.remove(callback)
|
||||
}
|
||||
|
||||
class Bottom(
|
||||
private val delegate: BottomSheetBehavior<*>,
|
||||
) : AdaptiveSheetBehavior() {
|
||||
|
||||
override var state: Int
|
||||
get() = delegate.state
|
||||
set(value) {
|
||||
delegate.state = value
|
||||
}
|
||||
|
||||
override var isDraggable: Boolean
|
||||
get() = delegate.isDraggable
|
||||
set(value) {
|
||||
delegate.isDraggable = value
|
||||
}
|
||||
|
||||
override val isHideable: Boolean
|
||||
get() = delegate.isHideable
|
||||
|
||||
var isFitToContents: Boolean
|
||||
get() = delegate.isFitToContents
|
||||
set(value) {
|
||||
delegate.isFitToContents = value
|
||||
}
|
||||
|
||||
init {
|
||||
delegate.addBottomSheetCallback(
|
||||
object : BottomSheetCallback() {
|
||||
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
||||
callbacks.forEach { it.onStateChanged(bottomSheet, newState) }
|
||||
}
|
||||
|
||||
override fun onSlide(bottomSheet: View, slideOffset: Float) {
|
||||
callbacks.forEach { it.onSlide(bottomSheet, slideOffset) }
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class Side(
|
||||
private val delegate: SideSheetBehavior<*>,
|
||||
) : AdaptiveSheetBehavior() {
|
||||
|
||||
override var state: Int
|
||||
get() = delegate.state
|
||||
set(value) {
|
||||
delegate.state = value
|
||||
}
|
||||
|
||||
override var isDraggable: Boolean
|
||||
get() = delegate.isDraggable
|
||||
set(value) {
|
||||
delegate.isDraggable = value
|
||||
}
|
||||
|
||||
init {
|
||||
delegate.addCallback(
|
||||
object : SideSheetCallback() {
|
||||
override fun onStateChanged(sheet: View, newState: Int) {
|
||||
callbacks.forEach { it.onStateChanged(sheet, newState) }
|
||||
}
|
||||
|
||||
override fun onSlide(sheet: View, slideOffset: Float) {
|
||||
callbacks.forEach { it.onSlide(sheet, slideOffset) }
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val STATE_EXPANDED = SideSheetBehavior.STATE_EXPANDED
|
||||
const val STATE_SETTLING = SideSheetBehavior.STATE_SETTLING
|
||||
const val STATE_DRAGGING = SideSheetBehavior.STATE_DRAGGING
|
||||
const val STATE_HIDDEN = SideSheetBehavior.STATE_HIDDEN
|
||||
|
||||
fun from(dialog: Dialog?): AdaptiveSheetBehavior? = when (dialog) {
|
||||
is BottomSheetDialog -> Bottom(dialog.behavior)
|
||||
is SideSheetDialog -> Side(dialog.behavior)
|
||||
else -> null
|
||||
}
|
||||
|
||||
fun from(lp: CoordinatorLayout.LayoutParams): AdaptiveSheetBehavior? = when (val behavior = lp.behavior) {
|
||||
is BottomSheetBehavior<*> -> Bottom(behavior)
|
||||
is SideSheetBehavior<*> -> Side(behavior)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
package org.koitharu.kotatsu.core.ui.sheet
|
||||
|
||||
import android.view.View
|
||||
|
||||
interface AdaptiveSheetCallback {
|
||||
|
||||
/**
|
||||
* Called when the sheet changes its state.
|
||||
*
|
||||
* @param sheet The sheet view.
|
||||
* @param newState The new state.
|
||||
*/
|
||||
fun onStateChanged(sheet: View, newState: Int)
|
||||
|
||||
/**
|
||||
* Called when the sheet is being dragged.
|
||||
*
|
||||
* @param sheet The sheet view.
|
||||
* @param slideOffset The new offset of this sheet.
|
||||
*/
|
||||
fun onSlide(sheet: View, slideOffset: Float) = Unit
|
||||
}
|
||||
@ -0,0 +1,101 @@
|
||||
package org.koitharu.kotatsu.core.ui.sheet
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.WindowInsets
|
||||
import android.widget.LinearLayout
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.content.withStyledAttributes
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.util.ext.parents
|
||||
import org.koitharu.kotatsu.databinding.LayoutSheetHeaderAdaptiveBinding
|
||||
|
||||
class AdaptiveSheetHeaderBar @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
@AttrRes defStyleAttr: Int = 0,
|
||||
) : LinearLayout(context, attrs, defStyleAttr), AdaptiveSheetCallback {
|
||||
|
||||
private val binding = LayoutSheetHeaderAdaptiveBinding.inflate(LayoutInflater.from(context), this)
|
||||
private var sheetBehavior: AdaptiveSheetBehavior? = null
|
||||
|
||||
var title: CharSequence?
|
||||
get() = binding.textViewTitle.text
|
||||
set(value) {
|
||||
binding.textViewTitle.text = value
|
||||
}
|
||||
|
||||
val isExpanded: Boolean
|
||||
get() = binding.dragHandle.isGone
|
||||
|
||||
init {
|
||||
orientation = VERTICAL
|
||||
binding.buttonClose.setOnClickListener { dismissSheet() }
|
||||
context.withStyledAttributes(
|
||||
attrs,
|
||||
R.styleable.AdaptiveSheetHeaderBar, defStyleAttr,
|
||||
) {
|
||||
title = getText(R.styleable.AdaptiveSheetHeaderBar_title)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
dispatchInsets(ViewCompat.getRootWindowInsets(this))
|
||||
setBottomSheetBehavior(findParentSheetBehavior())
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
setBottomSheetBehavior(null)
|
||||
super.onDetachedFromWindow()
|
||||
}
|
||||
|
||||
override fun onApplyWindowInsets(insets: WindowInsets?): WindowInsets {
|
||||
dispatchInsets(if (insets != null) WindowInsetsCompat.toWindowInsetsCompat(insets) else null)
|
||||
return super.onApplyWindowInsets(insets)
|
||||
}
|
||||
|
||||
override fun onStateChanged(sheet: View, newState: Int) {
|
||||
|
||||
}
|
||||
|
||||
fun setTitle(@StringRes resId: Int) {
|
||||
binding.textViewTitle.setText(resId)
|
||||
}
|
||||
|
||||
private fun dispatchInsets(insets: WindowInsetsCompat?) {
|
||||
|
||||
}
|
||||
|
||||
private fun setBottomSheetBehavior(behavior: AdaptiveSheetBehavior?) {
|
||||
binding.dragHandle.isVisible = behavior is AdaptiveSheetBehavior.Bottom
|
||||
binding.layoutSidesheet.isVisible = behavior is AdaptiveSheetBehavior.Side
|
||||
sheetBehavior?.removeCallback(this)
|
||||
sheetBehavior = behavior
|
||||
behavior?.addCallback(this)
|
||||
}
|
||||
|
||||
private fun dismissSheet() {
|
||||
sheetBehavior?.state = AdaptiveSheetBehavior.STATE_HIDDEN
|
||||
}
|
||||
|
||||
private fun findParentSheetBehavior(): AdaptiveSheetBehavior? {
|
||||
for (p in parents) {
|
||||
val layoutParams = (p as? View)?.layoutParams
|
||||
if (layoutParams is CoordinatorLayout.LayoutParams) {
|
||||
AdaptiveSheetBehavior.from(layoutParams)?.let {
|
||||
return it
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,161 @@
|
||||
package org.koitharu.kotatsu.core.ui.sheet
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.LayoutParams
|
||||
import androidx.activity.ComponentDialog
|
||||
import androidx.activity.OnBackPressedDispatcher
|
||||
import androidx.appcompat.app.AppCompatDialogFragment
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.viewbinding.ViewBinding
|
||||
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 com.google.android.material.R as materialR
|
||||
|
||||
abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment() {
|
||||
|
||||
private var waitingForDismissAllowingStateLoss = false
|
||||
|
||||
var viewBinding: B? = null
|
||||
private set
|
||||
|
||||
@Deprecated("", ReplaceWith("requireViewBinding()"))
|
||||
protected val binding: B
|
||||
get() = requireViewBinding()
|
||||
|
||||
protected val behavior: AdaptiveSheetBehavior?
|
||||
get() = AdaptiveSheetBehavior.from(dialog)
|
||||
|
||||
val isExpanded: Boolean
|
||||
get() = behavior?.state == AdaptiveSheetBehavior.STATE_EXPANDED
|
||||
|
||||
val onBackPressedDispatcher: OnBackPressedDispatcher
|
||||
get() = (requireDialog() as ComponentDialog).onBackPressedDispatcher
|
||||
|
||||
final override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?,
|
||||
): View {
|
||||
val binding = onCreateViewBinding(inflater, container)
|
||||
viewBinding = binding
|
||||
return binding.root
|
||||
}
|
||||
|
||||
final override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
val binding = requireViewBinding()
|
||||
onViewBindingCreated(binding, savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
viewBinding = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val context = requireContext()
|
||||
return if (context.resources.getBoolean(R.bool.is_tablet)) {
|
||||
SideSheetDialog(context, theme)
|
||||
} else {
|
||||
BottomSheetDialog(context, theme)
|
||||
}
|
||||
}
|
||||
|
||||
fun addSheetCallback(callback: AdaptiveSheetCallback) {
|
||||
val b = behavior ?: return
|
||||
b.addCallback(callback)
|
||||
val rootView = dialog?.findViewById<View>(materialR.id.design_bottom_sheet)
|
||||
?: dialog?.findViewById(materialR.id.coordinator)
|
||||
if (rootView != null) {
|
||||
callback.onStateChanged(rootView, b.state)
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): B
|
||||
|
||||
protected open fun onViewBindingCreated(binding: B, savedInstanceState: Bundle?) = Unit
|
||||
|
||||
protected fun setExpanded(isExpanded: Boolean, isLocked: Boolean) {
|
||||
val b = behavior ?: return
|
||||
if (isExpanded) {
|
||||
b.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
}
|
||||
if (b is AdaptiveSheetBehavior.Bottom) {
|
||||
b.isFitToContents = !isExpanded
|
||||
val rootView = dialog?.findViewById<View>(materialR.id.design_bottom_sheet)
|
||||
rootView?.updateLayoutParams {
|
||||
height = if (isExpanded) LayoutParams.MATCH_PARENT else LayoutParams.WRAP_CONTENT
|
||||
}
|
||||
}
|
||||
b.isDraggable = !isLocked
|
||||
}
|
||||
|
||||
fun requireViewBinding(): B = checkNotNull(viewBinding) {
|
||||
"Fragment $this did not return a ViewBinding from onCreateView() or this was called before onCreateView()."
|
||||
}
|
||||
|
||||
override fun dismiss() {
|
||||
if (!tryDismissWithAnimation(false)) {
|
||||
super.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
override fun dismissAllowingStateLoss() {
|
||||
if (!tryDismissWithAnimation(true)) {
|
||||
super.dismissAllowingStateLoss()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to dismiss the dialog fragment with the bottom sheet animation. Returns true if possible,
|
||||
* false otherwise.
|
||||
*/
|
||||
private fun tryDismissWithAnimation(allowingStateLoss: Boolean): Boolean {
|
||||
val shouldDismissWithAnimation = when (val dialog = dialog) {
|
||||
is BottomSheetDialog -> dialog.dismissWithAnimation
|
||||
is SideSheetDialog -> dialog.isDismissWithSheetAnimationEnabled
|
||||
else -> false
|
||||
}
|
||||
val behavior = behavior ?: return false
|
||||
return if (shouldDismissWithAnimation && behavior.isHideable) {
|
||||
dismissWithAnimation(behavior, allowingStateLoss)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun dismissWithAnimation(behavior: AdaptiveSheetBehavior, allowingStateLoss: Boolean) {
|
||||
waitingForDismissAllowingStateLoss = allowingStateLoss
|
||||
if (behavior.state == AdaptiveSheetBehavior.STATE_HIDDEN) {
|
||||
dismissAfterAnimation()
|
||||
} else {
|
||||
behavior.addCallback(SheetDismissCallback())
|
||||
behavior.state = AdaptiveSheetBehavior.STATE_HIDDEN
|
||||
}
|
||||
}
|
||||
|
||||
private fun dismissAfterAnimation() {
|
||||
if (waitingForDismissAllowingStateLoss) {
|
||||
super.dismissAllowingStateLoss()
|
||||
} else {
|
||||
super.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private inner class SheetDismissCallback : AdaptiveSheetCallback {
|
||||
override fun onStateChanged(sheet: View, newState: Int) {
|
||||
if (newState == BottomSheetBehavior.STATE_HIDDEN) {
|
||||
dismissAfterAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSlide(sheet: View, slideOffset: Float) {}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
package org.koitharu.kotatsu.favourites.ui.categories.select.adapter
|
||||
|
||||
import android.view.View
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.databinding.ItemCategoriesHeaderBinding
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.select.model.CategoriesHeaderItem
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
fun categoriesHeaderAD() = adapterDelegateViewBinding<CategoriesHeaderItem, ListModel, ItemCategoriesHeaderBinding>(
|
||||
{ inflater, parent -> ItemCategoriesHeaderBinding.inflate(inflater, parent, false) },
|
||||
) {
|
||||
|
||||
val onClickListener = View.OnClickListener { v ->
|
||||
val intent = when (v.id) {
|
||||
R.id.button_create -> FavouritesCategoryEditActivity.newIntent(v.context)
|
||||
R.id.button_manage -> FavouriteCategoriesActivity.newIntent(v.context)
|
||||
else -> return@OnClickListener
|
||||
}
|
||||
v.context.startActivity(intent)
|
||||
}
|
||||
|
||||
binding.buttonCreate.setOnClickListener(onClickListener)
|
||||
binding.buttonManage.setOnClickListener(onClickListener)
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
package org.koitharu.kotatsu.favourites.ui.categories.select.model
|
||||
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
class CategoriesHeaderItem : ListModel {
|
||||
|
||||
override fun equals(other: Any?): Boolean = other?.javaClass == CategoriesHeaderItem::class.java
|
||||
}
|
||||
@ -1,7 +1,9 @@
|
||||
package org.koitharu.kotatsu.favourites.ui.categories.select.model
|
||||
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
data class MangaCategoryItem(
|
||||
val id: Long,
|
||||
val name: String,
|
||||
val isChecked: Boolean
|
||||
)
|
||||
val isChecked: Boolean,
|
||||
) : ListModel
|
||||
|
||||
@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="12dp"
|
||||
android:paddingVertical="8dp"
|
||||
app:layout_scrollFlags="scroll|exitUntilCollapsed">
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_create"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:singleLine="true"
|
||||
android:text="@string/create_category"
|
||||
app:icon="@drawable/ic_add" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_manage"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:ellipsize="end"
|
||||
android:singleLine="true"
|
||||
android:text="@string/manage"
|
||||
app:icon="@drawable/ic_edit" />
|
||||
|
||||
</LinearLayout>
|
||||
@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge
|
||||
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"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:orientation="vertical"
|
||||
tools:parentTag="android.widget.LinearLayout">
|
||||
|
||||
<com.google.android.material.bottomsheet.BottomSheetDragHandleView
|
||||
android:id="@+id/dragHandle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_sidesheet"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:baselineAligned="false"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingVertical="8dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_weight="1"
|
||||
android:ellipsize="end"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="?textAppearanceBodyLarge"
|
||||
tools:text="@string/filter" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/button_close"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:padding="16dp"
|
||||
app:srcCompat="?actionModeCloseDrawable"
|
||||
app:tint="?colorControlActivated" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</merge>
|
||||
Loading…
Reference in New Issue