diff --git a/bottom-sheet/.gitignore b/bottom-sheet/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/bottom-sheet/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/bottom-sheet/build.gradle b/bottom-sheet/build.gradle
new file mode 100644
index 0000000..a30f045
--- /dev/null
+++ b/bottom-sheet/build.gradle
@@ -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
+ }
+}
diff --git a/bottom-sheet/readme.md b/bottom-sheet/readme.md
new file mode 100644
index 0000000..4ac7334
--- /dev/null
+++ b/bottom-sheet/readme.md
@@ -0,0 +1,29 @@
+# BottomSheet Utils
+
+- `BaseBottomSheet` - класс, содержащий парамерты `BottomSheetOptions`
+
+- `DefaultBottomSheet` - класс с классическим хедером и скруглением, в котором нужно переопределить `createContentView()`
+
+## BottomSheetOptions
+- `styleId` - xml-стиль, в котором можно задать скругление
+- `canDismiss` - может ли модалка быть срыта по тапу/свайпу/backButton
+- `canTouchOutside` - возможность передавать жесты под модалкой
+- `isSkipCollapsed` - убирает промежуточное состояние модалки
+- `isFullscreen` - модалка откроется на весь экран, даже при маленьком контенте
+- `isShiftedWithKeyboard` - модалка будет полностью подниматься при открытии клавиатуры
+- `defaultDimAmount` - константное затемнение
+- `animatedMaxDimAmount` - максимальное затемнение, при этом будет анимироваться в зависимости от offset
+- `fadeAnimationOptions` - позволяет настроить fade анимацию при изменении высоты
+- `heightStatesOptions` - позволяет задать 3 состояния высоты модалки
+
+## ContentFadeAnimationOptions
+- `foregroundRes` - drawableId, который будет показыватся сверху во время анимации
+- `duration` - длительность fade анимации
+- `minAlpha` - минимальная прозрачность во время анимации
+
+## HeightStatesOptions
+- `collapsedHeightPx` - высота минимального состояния
+- `halfExpandedHalfPx` - высота промежуточного состояния
+- `canTouchOutsideWhenCollapsed` - могут ли жесты передаватья под модалку в минимальном состоянии
+
+Тестовый проект: https://github.com/duwna/BottomSheets
diff --git a/bottom-sheet/src/main/AndroidManifest.xml b/bottom-sheet/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..c66f4e2
--- /dev/null
+++ b/bottom-sheet/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
diff --git a/bottom-sheet/src/main/java/ru/touchin/roboswag/bottomsheet/BaseBottomSheet.kt b/bottom-sheet/src/main/java/ru/touchin/roboswag/bottomsheet/BaseBottomSheet.kt
new file mode 100644
index 0000000..f42b19e
--- /dev/null
+++ b/bottom-sheet/src/main/java/ru/touchin/roboswag/bottomsheet/BaseBottomSheet.kt
@@ -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
+ 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
+ }
+ }
+}
diff --git a/bottom-sheet/src/main/java/ru/touchin/roboswag/bottomsheet/BottomSheetOptions.kt b/bottom-sheet/src/main/java/ru/touchin/roboswag/bottomsheet/BottomSheetOptions.kt
new file mode 100644
index 0000000..98a28db
--- /dev/null
+++ b/bottom-sheet/src/main/java/ru/touchin/roboswag/bottomsheet/BottomSheetOptions.kt
@@ -0,0 +1,32 @@
+package ru.touchin.roboswag.bottomsheet
+
+import androidx.annotation.DrawableRes
+import androidx.annotation.StyleRes
+
+/**
+ * See explanation in readme
+ * */
+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
+)
diff --git a/bottom-sheet/src/main/java/ru/touchin/roboswag/bottomsheet/DefaultBottomSheet.kt b/bottom-sheet/src/main/java/ru/touchin/roboswag/bottomsheet/DefaultBottomSheet.kt
new file mode 100644
index 0000000..0dc19d0
--- /dev/null
+++ b/bottom-sheet/src/main/java/ru/touchin/roboswag/bottomsheet/DefaultBottomSheet.kt
@@ -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() }
+ }
+}
diff --git a/bottom-sheet/src/main/res/drawable/bottom_sheet_background_rounded_16.xml b/bottom-sheet/src/main/res/drawable/bottom_sheet_background_rounded_16.xml
new file mode 100644
index 0000000..1146eb4
--- /dev/null
+++ b/bottom-sheet/src/main/res/drawable/bottom_sheet_background_rounded_16.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
diff --git a/bottom-sheet/src/main/res/layout/default_bottom_sheet.xml b/bottom-sheet/src/main/res/layout/default_bottom_sheet.xml
new file mode 100644
index 0000000..792de5b
--- /dev/null
+++ b/bottom-sheet/src/main/res/layout/default_bottom_sheet.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bottom-sheet/src/main/res/values/styles.xml b/bottom-sheet/src/main/res/values/styles.xml
new file mode 100644
index 0000000..d10936c
--- /dev/null
+++ b/bottom-sheet/src/main/res/values/styles.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+