From f0c8e3f1d777f4c9f704a120b8bc0aa48037e84a Mon Sep 17 00:00:00 2001 From: Grigorii Date: Fri, 7 Apr 2023 11:39:46 +0400 Subject: [PATCH] Add bottomseet utils module --- bottom-sheet/.gitignore | 1 + bottom-sheet/build.gradle | 26 +++ bottom-sheet/src/main/AndroidManifest.xml | 1 + .../roboswag/bottomsheet/BaseBottomSheet.kt | 161 ++++++++++++++++++ .../bottomsheet/BottomSheetOptions.kt | 29 ++++ .../bottomsheet/DefaultBottomSheet.kt | 40 +++++ .../bottom_sheet_background_rounded_16.xml | 11 ++ .../main/res/layout/default_bottom_sheet.xml | 31 ++++ bottom-sheet/src/main/res/values/styles.xml | 13 ++ 9 files changed, 313 insertions(+) create mode 100644 bottom-sheet/.gitignore create mode 100644 bottom-sheet/build.gradle create mode 100644 bottom-sheet/src/main/AndroidManifest.xml create mode 100644 bottom-sheet/src/main/java/ru/touchin/roboswag/bottomsheet/BaseBottomSheet.kt create mode 100644 bottom-sheet/src/main/java/ru/touchin/roboswag/bottomsheet/BottomSheetOptions.kt create mode 100644 bottom-sheet/src/main/java/ru/touchin/roboswag/bottomsheet/DefaultBottomSheet.kt create mode 100644 bottom-sheet/src/main/res/drawable/bottom_sheet_background_rounded_16.xml create mode 100644 bottom-sheet/src/main/res/layout/default_bottom_sheet.xml create mode 100644 bottom-sheet/src/main/res/values/styles.xml 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/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..ae73d16 --- /dev/null +++ b/bottom-sheet/src/main/java/ru/touchin/roboswag/bottomsheet/BottomSheetOptions.kt @@ -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 +) 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 @@ + + + + + + + +