Add bottomseet utils module
This commit is contained in:
parent
54d2482064
commit
f0c8e3f1d7
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
apply from: "../android-configs/lib-config.gradle"
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
dependencies {
|
||||
implementation project(":navigation-base")
|
||||
|
||||
implementation 'androidx.core:core-ktx'
|
||||
implementation 'com.google.android.material:material'
|
||||
|
||||
implementation("androidx.core:core-ktx") {
|
||||
version {
|
||||
require '1.9.0'
|
||||
}
|
||||
}
|
||||
implementation("com.google.android.material:material") {
|
||||
version {
|
||||
require '1.4.0'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
<manifest package="ru.touchin.roboswag.bottomsheet" />
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
package ru.touchin.roboswag.bottomsheet
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Dialog
|
||||
import android.content.res.Resources
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import kotlin.math.abs
|
||||
|
||||
abstract class BaseBottomSheet : BottomSheetDialogFragment() {
|
||||
|
||||
protected abstract val layoutId: Int
|
||||
|
||||
protected open val bottomSheetOptions = BottomSheetOptions()
|
||||
|
||||
protected val decorView: View
|
||||
get() = checkNotNull(dialog?.window?.decorView)
|
||||
|
||||
protected val bottomSheetView: FrameLayout
|
||||
get() = decorView.findViewById(com.google.android.material.R.id.design_bottom_sheet)
|
||||
|
||||
protected val touchOutsideView: View
|
||||
get() = decorView.findViewById(com.google.android.material.R.id.touch_outside)
|
||||
|
||||
protected val behavior: BottomSheetBehavior<FrameLayout>
|
||||
get() = BottomSheetBehavior.from(bottomSheetView)
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
|
||||
inflater.inflate(layoutId, container)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
bottomSheetOptions.styleId?.let { setStyle(DialogFragment.STYLE_NORMAL, it) }
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
bottomSheetOptions.defaultDimAmount?.let { dialog?.window?.setDimAmount(it) }
|
||||
bottomSheetOptions.animatedMaxDimAmount?.let { setupDimAmountChanges(it) }
|
||||
|
||||
if (bottomSheetOptions.isShiftedWithKeyboard) setupShiftWithKeyboard()
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
isCancelable = bottomSheetOptions.canDismiss
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
if (bottomSheetOptions.isSkipCollapsed) {
|
||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
behavior.skipCollapsed = true
|
||||
}
|
||||
|
||||
if (bottomSheetOptions.isFullscreen) {
|
||||
bottomSheetView.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
|
||||
}
|
||||
|
||||
if (bottomSheetOptions.canTouchOutside) {
|
||||
setupTouchOutside()
|
||||
}
|
||||
|
||||
bottomSheetOptions.fadeAnimationOptions?.let {
|
||||
setupFadeAnimationOnHeightChanges(it)
|
||||
}
|
||||
|
||||
bottomSheetOptions.heightStatesOptions?.let {
|
||||
setupHeightOptions(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupDimAmountChanges(maxDimAmount: Float) {
|
||||
behavior.peekHeight = 0
|
||||
dialog?.window?.setDimAmount(maxDimAmount)
|
||||
|
||||
behavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
|
||||
override fun onStateChanged(bottomSheet: View, newState: Int) = Unit
|
||||
|
||||
override fun onSlide(bottomSheet: View, slideOffset: Float) {
|
||||
dialog?.window?.setDimAmount(abs(slideOffset) * maxDimAmount)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun setupFadeAnimationOnHeightChanges(options: ContentFadeAnimationOptions) {
|
||||
val foreground = checkNotNull(
|
||||
ContextCompat.getDrawable(requireContext(), options.foregroundRes)
|
||||
).apply {
|
||||
alpha = 0
|
||||
bottomSheetView.foreground = this
|
||||
}
|
||||
|
||||
bottomSheetView.addOnLayoutChangeListener { _, _, top, _, _, _, oldTop, _, _ ->
|
||||
if (top != oldTop) showFadeAnimation(foreground, options)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showFadeAnimation(foreground: Drawable, options: ContentFadeAnimationOptions) {
|
||||
val maxAlpha = 255
|
||||
foreground.alpha = maxAlpha
|
||||
bottomSheetView.alpha = options.minAlpha
|
||||
|
||||
ValueAnimator.ofInt(maxAlpha, 0).apply {
|
||||
duration = options.duration
|
||||
addUpdateListener {
|
||||
val value = it.animatedValue as Int
|
||||
foreground.alpha = value
|
||||
bottomSheetView.alpha = (1 - value.toFloat() / maxAlpha).coerceAtLeast(options.minAlpha)
|
||||
}
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupHeightOptions(options: HeightStatesOptions) = with(behavior) {
|
||||
isFitToContents = false
|
||||
peekHeight = options.collapsedHeightPx
|
||||
halfExpandedRatio = options.halfExpandedHalfPx / Resources.getSystem().displayMetrics.heightPixels.toFloat()
|
||||
state = BottomSheetBehavior.STATE_COLLAPSED
|
||||
if (options.canTouchOutsideWhenCollapsed) setupTouchOutsideWhenCollapsed()
|
||||
}
|
||||
|
||||
private fun setupTouchOutsideWhenCollapsed() {
|
||||
setupTouchOutside()
|
||||
|
||||
behavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
|
||||
override fun onSlide(bottomSheet: View, slideOffset: Float) = Unit
|
||||
|
||||
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
||||
when (newState) {
|
||||
BottomSheetBehavior.STATE_COLLAPSED -> setupTouchOutside()
|
||||
else -> touchOutsideView.setOnTouchListener(null)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun setupShiftWithKeyboard() {
|
||||
dialog?.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private fun setupTouchOutside() {
|
||||
touchOutsideView.setOnTouchListener { _, event ->
|
||||
requireActivity().dispatchTouchEvent(event)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package ru.touchin.roboswag.bottomsheet
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StyleRes
|
||||
|
||||
data class BottomSheetOptions(
|
||||
@StyleRes val styleId: Int? = null,
|
||||
val canDismiss: Boolean = true,
|
||||
val canTouchOutside: Boolean = false,
|
||||
val isSkipCollapsed: Boolean = true,
|
||||
val isFullscreen: Boolean = false,
|
||||
val isShiftedWithKeyboard: Boolean = false,
|
||||
val defaultDimAmount: Float? = null,
|
||||
val animatedMaxDimAmount: Float? = null,
|
||||
val fadeAnimationOptions: ContentFadeAnimationOptions? = null,
|
||||
val heightStatesOptions: HeightStatesOptions? = null
|
||||
)
|
||||
|
||||
data class ContentFadeAnimationOptions(
|
||||
@DrawableRes val foregroundRes: Int,
|
||||
val duration: Long,
|
||||
val minAlpha: Float
|
||||
)
|
||||
|
||||
data class HeightStatesOptions(
|
||||
val collapsedHeightPx: Int,
|
||||
val halfExpandedHalfPx: Int,
|
||||
val canTouchOutsideWhenCollapsed: Boolean = true
|
||||
)
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package ru.touchin.roboswag.bottomsheet
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import ru.touchin.roboswag.bottomsheet.databinding.DefaultBottomSheetBinding
|
||||
import ru.touchin.roboswag.navigation_base.fragments.viewBinding
|
||||
|
||||
abstract class DefaultBottomSheet : BaseBottomSheet() {
|
||||
|
||||
abstract fun createContentView(inflater: LayoutInflater): View
|
||||
|
||||
final override val layoutId = R.layout.default_bottom_sheet
|
||||
|
||||
override val bottomSheetOptions = BottomSheetOptions(
|
||||
styleId = R.style.RoundedBottomSheetStyle,
|
||||
fadeAnimationOptions = ContentFadeAnimationOptions(
|
||||
foregroundRes = R.drawable.bottom_sheet_background_rounded_16,
|
||||
duration = 150,
|
||||
minAlpha = 0.5f
|
||||
)
|
||||
)
|
||||
|
||||
protected val rootBinding by viewBinding(DefaultBottomSheetBinding::bind)
|
||||
|
||||
protected val contentView: View get() = rootBinding.linearRoot.getChildAt(1)
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
.also {
|
||||
DefaultBottomSheetBinding.bind(checkNotNull(it))
|
||||
.linearRoot.addView(createContentView(inflater))
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
rootBinding.closeText.setOnClickListener { dismiss() }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<shape
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<corners
|
||||
android:topLeftRadius="16dp"
|
||||
android:topRightRadius="16dp" />
|
||||
|
||||
<solid android:color="@android:color/white" />
|
||||
|
||||
</shape>
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/linear_root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/top_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="top|center"
|
||||
tools:ignore="ContentDescription"
|
||||
tools:src="@android:mipmap/sym_def_app_icon" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/close_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="top|left"
|
||||
tools:text="Закрыть" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="RoundedBottomSheetStyle" parent="Theme.Design.Light.BottomSheetDialog">
|
||||
<item name="android:windowIsFloating">false</item>
|
||||
<item name="bottomSheetStyle">@style/RoundedBackground</item>
|
||||
</style>
|
||||
|
||||
<style name="RoundedBackground" parent="Widget.Design.BottomSheet.Modal">
|
||||
<item name="android:background">@drawable/bottom_sheet_background_rounded_16</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
Loading…
Reference in New Issue