react-native-nitro-modal 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/LICENSE +20 -0
  2. package/NitroModal.podspec +29 -0
  3. package/README.md +39 -0
  4. package/android/CMakeLists.txt +24 -0
  5. package/android/build.gradle +124 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/cpp/cpp-adapter.cpp +11 -0
  8. package/android/src/main/java/com/margelo/nitro/nitromodal/BottomSheetPresenter.kt +284 -0
  9. package/android/src/main/java/com/margelo/nitro/nitromodal/HybridNitroModal.kt +312 -0
  10. package/android/src/main/java/com/margelo/nitro/nitromodal/ModalConfig.kt +50 -0
  11. package/android/src/main/java/com/margelo/nitro/nitromodal/ModalContentRoot.kt +115 -0
  12. package/android/src/main/java/com/margelo/nitro/nitromodal/ModalPresenter.kt +253 -0
  13. package/android/src/main/java/com/margelo/nitro/nitromodal/NitroModalHostView.kt +52 -0
  14. package/android/src/main/java/com/margelo/nitro/nitromodal/NitroModalPackage.kt +28 -0
  15. package/android/src/main/java/com/margelo/nitro/nitromodal/NitroModalViewManager.kt +67 -0
  16. package/android/src/main/java/com/margelo/nitro/nitromodal/PopupPresenter.kt +156 -0
  17. package/android/src/main/res/values/styles.xml +17 -0
  18. package/ios/BackdropView.swift +64 -0
  19. package/ios/HybridNitroModal.swift +329 -0
  20. package/ios/HybridNitroModalComponent+Children.mm +77 -0
  21. package/ios/ModalConfig.swift +73 -0
  22. package/ios/NitroModalHostView.swift +91 -0
  23. package/ios/PopupViewController.swift +223 -0
  24. package/ios/SheetViewController.swift +244 -0
  25. package/lib/module/NitroModal.js +188 -0
  26. package/lib/module/NitroModal.js.map +1 -0
  27. package/lib/module/NitroModal.nitro.js +4 -0
  28. package/lib/module/NitroModal.nitro.js.map +1 -0
  29. package/lib/module/NitroModalView.js +17 -0
  30. package/lib/module/NitroModalView.js.map +1 -0
  31. package/lib/module/NitroModalView.native.js +6 -0
  32. package/lib/module/NitroModalView.native.js.map +1 -0
  33. package/lib/module/index.js +4 -0
  34. package/lib/module/index.js.map +1 -0
  35. package/lib/module/package.json +1 -0
  36. package/lib/typescript/package.json +1 -0
  37. package/lib/typescript/src/NitroModal.d.ts +59 -0
  38. package/lib/typescript/src/NitroModal.d.ts.map +1 -0
  39. package/lib/typescript/src/NitroModal.nitro.d.ts +78 -0
  40. package/lib/typescript/src/NitroModal.nitro.d.ts.map +1 -0
  41. package/lib/typescript/src/NitroModalView.d.ts +4 -0
  42. package/lib/typescript/src/NitroModalView.d.ts.map +1 -0
  43. package/lib/typescript/src/NitroModalView.native.d.ts +3 -0
  44. package/lib/typescript/src/NitroModalView.native.d.ts.map +1 -0
  45. package/lib/typescript/src/index.d.ts +3 -0
  46. package/lib/typescript/src/index.d.ts.map +1 -0
  47. package/nitro.json +23 -0
  48. package/nitrogen/generated/android/c++/JDismissReason.hpp +64 -0
  49. package/nitrogen/generated/android/c++/JFunc_void.hpp +75 -0
  50. package/nitrogen/generated/android/c++/JFunc_void_DismissReason.hpp +77 -0
  51. package/nitrogen/generated/android/c++/JFunc_void_ModalContentArea.hpp +77 -0
  52. package/nitrogen/generated/android/c++/JFunc_void_double.hpp +75 -0
  53. package/nitrogen/generated/android/c++/JHybridNitroModalSpec.cpp +344 -0
  54. package/nitrogen/generated/android/c++/JHybridNitroModalSpec.hpp +106 -0
  55. package/nitrogen/generated/android/c++/JKeyboardBehavior.hpp +61 -0
  56. package/nitrogen/generated/android/c++/JModalContentArea.hpp +61 -0
  57. package/nitrogen/generated/android/c++/JModalMode.hpp +58 -0
  58. package/nitrogen/generated/android/c++/JPopupAnimation.hpp +61 -0
  59. package/nitrogen/generated/android/c++/JSheetDetent.hpp +64 -0
  60. package/nitrogen/generated/android/c++/views/JHybridNitroModalStateUpdater.cpp +174 -0
  61. package/nitrogen/generated/android/c++/views/JHybridNitroModalStateUpdater.hpp +54 -0
  62. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/DismissReason.kt +25 -0
  63. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/Func_void.kt +78 -0
  64. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/Func_void_DismissReason.kt +78 -0
  65. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/Func_void_ModalContentArea.kt +78 -0
  66. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/Func_void_double.kt +78 -0
  67. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/HybridNitroModalSpec.kt +237 -0
  68. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/KeyboardBehavior.kt +24 -0
  69. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/ModalContentArea.kt +56 -0
  70. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/ModalMode.kt +23 -0
  71. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/PopupAnimation.kt +24 -0
  72. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/SheetDetent.kt +25 -0
  73. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/nitromodalOnLoad.kt +35 -0
  74. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/views/HybridNitroModalManager.kt +96 -0
  75. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/views/HybridNitroModalStateUpdater.kt +23 -0
  76. package/nitrogen/generated/android/nitromodal+autolinking.cmake +83 -0
  77. package/nitrogen/generated/android/nitromodal+autolinking.gradle +27 -0
  78. package/nitrogen/generated/android/nitromodalOnLoad.cpp +64 -0
  79. package/nitrogen/generated/android/nitromodalOnLoad.hpp +34 -0
  80. package/nitrogen/generated/ios/NitroModal+autolinking.rb +62 -0
  81. package/nitrogen/generated/ios/NitroModal-Swift-Cxx-Bridge.cpp +65 -0
  82. package/nitrogen/generated/ios/NitroModal-Swift-Cxx-Bridge.hpp +237 -0
  83. package/nitrogen/generated/ios/NitroModal-Swift-Cxx-Umbrella.hpp +64 -0
  84. package/nitrogen/generated/ios/NitroModalAutolinking.mm +33 -0
  85. package/nitrogen/generated/ios/NitroModalAutolinking.swift +26 -0
  86. package/nitrogen/generated/ios/c++/HybridNitroModalSpecSwift.cpp +11 -0
  87. package/nitrogen/generated/ios/c++/HybridNitroModalSpecSwift.hpp +247 -0
  88. package/nitrogen/generated/ios/c++/views/HybridNitroModalComponent.mm +264 -0
  89. package/nitrogen/generated/ios/swift/DismissReason.swift +48 -0
  90. package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
  91. package/nitrogen/generated/ios/swift/Func_void_DismissReason.swift +46 -0
  92. package/nitrogen/generated/ios/swift/Func_void_ModalContentArea.swift +46 -0
  93. package/nitrogen/generated/ios/swift/Func_void_double.swift +46 -0
  94. package/nitrogen/generated/ios/swift/HybridNitroModalSpec.swift +77 -0
  95. package/nitrogen/generated/ios/swift/HybridNitroModalSpec_cxx.swift +554 -0
  96. package/nitrogen/generated/ios/swift/KeyboardBehavior.swift +44 -0
  97. package/nitrogen/generated/ios/swift/ModalContentArea.swift +34 -0
  98. package/nitrogen/generated/ios/swift/ModalMode.swift +40 -0
  99. package/nitrogen/generated/ios/swift/PopupAnimation.swift +44 -0
  100. package/nitrogen/generated/ios/swift/SheetDetent.swift +48 -0
  101. package/nitrogen/generated/shared/c++/DismissReason.hpp +84 -0
  102. package/nitrogen/generated/shared/c++/HybridNitroModalSpec.cpp +65 -0
  103. package/nitrogen/generated/shared/c++/HybridNitroModalSpec.hpp +124 -0
  104. package/nitrogen/generated/shared/c++/KeyboardBehavior.hpp +80 -0
  105. package/nitrogen/generated/shared/c++/ModalContentArea.hpp +87 -0
  106. package/nitrogen/generated/shared/c++/ModalMode.hpp +76 -0
  107. package/nitrogen/generated/shared/c++/PopupAnimation.hpp +80 -0
  108. package/nitrogen/generated/shared/c++/SheetDetent.hpp +84 -0
  109. package/nitrogen/generated/shared/c++/views/HybridNitroModalComponent.cpp +74 -0
  110. package/nitrogen/generated/shared/c++/views/HybridNitroModalComponent.hpp +151 -0
  111. package/nitrogen/generated/shared/json/NitroModalConfig.json +30 -0
  112. package/package.json +183 -0
  113. package/src/NitroModal.nitro.ts +92 -0
  114. package/src/NitroModal.tsx +292 -0
  115. package/src/NitroModalView.native.tsx +8 -0
  116. package/src/NitroModalView.tsx +24 -0
  117. package/src/index.tsx +12 -0
package/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mohammad Navabi
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
@@ -0,0 +1,29 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = "NitroModal"
7
+ s.version = package["version"]
8
+ s.summary = package["description"]
9
+ s.homepage = package["homepage"]
10
+ s.license = package["license"]
11
+ s.authors = package["author"]
12
+
13
+ s.platforms = { :ios => min_ios_version_supported }
14
+ s.source = { :git => "https://github.com/mohamadnavabi/react-native-nitro-modal.git", :tag => "#{s.version}" }
15
+
16
+ s.source_files = [
17
+ "ios/**/*.{swift}",
18
+ "ios/**/*.{m,mm}",
19
+ "cpp/**/*.{hpp,cpp}",
20
+ ]
21
+
22
+ s.dependency 'React-jsi'
23
+ s.dependency 'React-callinvoker'
24
+
25
+ load 'nitrogen/generated/ios/NitroModal+autolinking.rb'
26
+ add_nitrogen_files(s)
27
+
28
+ install_modules_dependencies(s)
29
+ end
package/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # react-native-nitro-modal
2
+
3
+ react native native modal
4
+
5
+ ## Installation
6
+
7
+
8
+ ```sh
9
+ npm install react-native-nitro-modal react-native-nitro-modules
10
+
11
+ > `react-native-nitro-modules` is required as this library relies on [Nitro Modules](https://nitro.margelo.com/).
12
+ ```
13
+
14
+
15
+ ## Usage
16
+
17
+
18
+ ```js
19
+ import { NitroModalView } from "react-native-nitro-modal";
20
+
21
+ // ...
22
+
23
+ <NitroModalView color="tomato" />
24
+ ```
25
+
26
+
27
+ ## Contributing
28
+
29
+ - [Development workflow](CONTRIBUTING.md#development-workflow)
30
+ - [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request)
31
+ - [Code of conduct](CODE_OF_CONDUCT.md)
32
+
33
+ ## License
34
+
35
+ MIT
36
+
37
+ ---
38
+
39
+ Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)
@@ -0,0 +1,24 @@
1
+ project(nitromodal)
2
+ cmake_minimum_required(VERSION 3.9.0)
3
+
4
+ set(PACKAGE_NAME nitromodal)
5
+ set(CMAKE_VERBOSE_MAKEFILE ON)
6
+ set(CMAKE_CXX_STANDARD 20)
7
+
8
+ # Define C++ library and add all sources
9
+ add_library(${PACKAGE_NAME} SHARED src/main/cpp/cpp-adapter.cpp)
10
+
11
+ # Add Nitrogen specs :)
12
+ include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/nitromodal+autolinking.cmake)
13
+
14
+ # Set up local includes
15
+ include_directories("src/main/cpp" "../cpp")
16
+
17
+ find_library(LOG_LIB log)
18
+
19
+ # Link all libraries together
20
+ target_link_libraries(
21
+ ${PACKAGE_NAME}
22
+ ${LOG_LIB}
23
+ android # <-- Android core
24
+ )
@@ -0,0 +1,124 @@
1
+ buildscript {
2
+ ext.NitroModal = [
3
+ kotlinVersion: "2.0.21",
4
+ minSdkVersion: 24,
5
+ compileSdkVersion: 36
6
+ ]
7
+
8
+ ext.getExtOrDefault = { prop ->
9
+ if (rootProject.ext.has(prop)) {
10
+ return rootProject.ext.get(prop)
11
+ }
12
+
13
+ return NitroModal[prop]
14
+ }
15
+
16
+ repositories {
17
+ google()
18
+ mavenCentral()
19
+ }
20
+
21
+ dependencies {
22
+ classpath "com.android.tools.build:gradle:8.7.2"
23
+ // noinspection DifferentKotlinGradleVersion
24
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
25
+ }
26
+ }
27
+
28
+ def reactNativeArchitectures() {
29
+ def value = rootProject.getProperties().get("reactNativeArchitectures")
30
+ return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
31
+ }
32
+
33
+ apply plugin: "com.android.library"
34
+
35
+ if (project.extensions.findByName("kotlin") == null) {
36
+ apply plugin: "kotlin-android"
37
+ }
38
+
39
+ apply from: '../nitrogen/generated/android/nitromodal+autolinking.gradle'
40
+
41
+ apply plugin: "com.facebook.react"
42
+
43
+ android {
44
+ namespace "com.margelo.nitro.nitromodal"
45
+
46
+ compileSdkVersion getExtOrDefault("compileSdkVersion")
47
+
48
+ defaultConfig {
49
+ minSdkVersion getExtOrDefault("minSdkVersion")
50
+
51
+ externalNativeBuild {
52
+ cmake {
53
+ cppFlags "-frtti -fexceptions -Wall -fstack-protector-all"
54
+ arguments "-DANDROID_STL=c++_shared", "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
55
+ abiFilters (*reactNativeArchitectures())
56
+
57
+ buildTypes {
58
+ debug {
59
+ cppFlags "-O1 -g"
60
+ }
61
+ release {
62
+ cppFlags "-O2"
63
+ }
64
+ }
65
+ }
66
+ }
67
+ }
68
+
69
+ if (rootProject.ext.has("ndkVersion")) {
70
+ ndkVersion rootProject.ext.ndkVersion
71
+ }
72
+
73
+ externalNativeBuild {
74
+ cmake {
75
+ path "CMakeLists.txt"
76
+ }
77
+ }
78
+
79
+ packaging {
80
+ resources {
81
+ excludes += [
82
+ "META-INF",
83
+ "META-INF/**"
84
+ ]
85
+ }
86
+
87
+ jniLibs {
88
+ excludes += [
89
+ "**/libc++_shared.so",
90
+ "**/libfbjni.so",
91
+ "**/libjsi.so",
92
+ "**/libfolly_json.so",
93
+ "**/libfolly_runtime.so",
94
+ "**/libglog.so",
95
+ "**/libhermes.so",
96
+ "**/libhermes-executor-debug.so",
97
+ "**/libhermes_executor.so",
98
+ "**/libreactnative.so",
99
+ "**/libreactnativejni.so",
100
+ "**/libturbomodulejsijni.so",
101
+ "**/libreact_nativemodule_core.so",
102
+ "**/libjscexecutor.so"
103
+ ]
104
+ }
105
+ }
106
+
107
+ buildFeatures {
108
+ prefab true
109
+ }
110
+
111
+ compileOptions {
112
+ sourceCompatibility JavaVersion.VERSION_17
113
+ targetCompatibility JavaVersion.VERSION_17
114
+ }
115
+ }
116
+
117
+ dependencies {
118
+ implementation "com.facebook.react:react-android"
119
+ implementation project(":react-native-nitro-modules")
120
+ // BottomSheetBehavior + CoordinatorLayout for `bottomSheet` mode.
121
+ implementation "com.google.android.material:material:1.12.0"
122
+ implementation "androidx.activity:activity-ktx:1.9.3"
123
+ implementation "androidx.core:core-ktx:1.13.1"
124
+ }
@@ -0,0 +1,2 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ </manifest>
@@ -0,0 +1,11 @@
1
+ #include <jni.h>
2
+ #include "nitromodalOnLoad.hpp"
3
+
4
+ #include <fbjni/fbjni.h>
5
+
6
+
7
+ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) {
8
+ return facebook::jni::initialize(vm, []() {
9
+ margelo::nitro::nitromodal::registerAllNatives();
10
+ });
11
+ }
@@ -0,0 +1,284 @@
1
+ package com.margelo.nitro.nitromodal
2
+
3
+ import android.app.Activity
4
+ import android.content.res.ColorStateList
5
+ import android.graphics.Color
6
+ import android.graphics.drawable.GradientDrawable
7
+ import android.view.Gravity
8
+ import android.view.View
9
+ import android.view.ViewGroup
10
+ import android.view.ViewOutlineProvider
11
+ import android.widget.FrameLayout
12
+ import androidx.coordinatorlayout.widget.CoordinatorLayout
13
+ import androidx.core.graphics.Insets
14
+ import androidx.core.view.doOnLayout
15
+ import com.facebook.react.uimanager.ThemedReactContext
16
+ import com.google.android.material.bottomsheet.BottomSheetBehavior
17
+ import com.google.android.material.shape.MaterialShapeDrawable
18
+ import com.google.android.material.shape.ShapeAppearanceModel
19
+
20
+ /**
21
+ * `bottomSheet` mode, driven by [BottomSheetBehavior]. Dragging, flinging,
22
+ * snapping and the backdrop fade all run on the UI thread with no JS.
23
+ *
24
+ * `BottomSheetBehavior` supports up to three resting states, so detents map
25
+ * onto collapsed / half-expanded / expanded, smallest first.
26
+ */
27
+ internal class BottomSheetPresenter(
28
+ activity: Activity,
29
+ reactContext: ThemedReactContext,
30
+ contentRoot: ModalContentRoot,
31
+ config: ModalConfig,
32
+ listener: Listener,
33
+ ) : ModalPresenter(activity, reactContext, contentRoot, config, listener) {
34
+
35
+ private class Detent(val index: Int, val height: Int)
36
+
37
+ private val coordinator = CoordinatorLayout(activity)
38
+ private val sheet = FrameLayout(activity)
39
+ private val grabber = View(activity)
40
+ private val behavior = BottomSheetBehavior<FrameLayout>()
41
+ private val sheetBackground = MaterialShapeDrawable()
42
+
43
+ /** Resting detents sorted by height; position i maps to [states][i]. */
44
+ private var sortedDetents: List<Detent> = emptyList()
45
+ private var maxContentHeight = 0
46
+ private var presented = false
47
+ private var lastDetentIndex = -1
48
+
49
+ private val callback = object : BottomSheetBehavior.BottomSheetCallback() {
50
+ override fun onStateChanged(bottomSheet: View, newState: Int) {
51
+ if (newState == BottomSheetBehavior.STATE_HIDDEN) {
52
+ finishUserDismissal(DismissReason.SWIPE)
53
+ return
54
+ }
55
+ val position = states().indexOf(newState)
56
+ if (position < 0 || isDismissing) return
57
+ if (!presented) {
58
+ presented = true
59
+ behavior.isHideable = this@BottomSheetPresenter.config.dismissOnSwipe
60
+ notifyPresented()
61
+ }
62
+ val index = sortedDetents.getOrNull(position)?.index ?: return
63
+ if (index != lastDetentIndex) {
64
+ lastDetentIndex = index
65
+ listener.onDetentChange(index)
66
+ }
67
+ }
68
+
69
+ override fun onSlide(bottomSheet: View, slideOffset: Float) {
70
+ // slideOffset is -1 when hidden and 0 at the lowest detent.
71
+ backdrop.alpha = (1f + slideOffset).coerceIn(0f, 1f)
72
+ }
73
+ }
74
+
75
+ override fun createLayout(): ViewGroup {
76
+ coordinator.addView(backdrop, CoordinatorLayout.LayoutParams(MATCH, MATCH))
77
+ coordinator.addOnLayoutChangeListener { _, l, t, r, b, oldL, oldT, oldR, oldB ->
78
+ // Rotation / window resize. Posted: detents change the sheet's layout params.
79
+ if (r - l != oldR - oldL || b - t != oldB - oldT) coordinator.post { updateDetents() }
80
+ }
81
+
82
+ sheet.background = sheetBackground
83
+ sheet.outlineProvider = ViewOutlineProvider.BACKGROUND
84
+ sheet.clipToOutline = true
85
+ sheet.addView(contentRoot, FrameLayout.LayoutParams(MATCH, MATCH))
86
+ sheet.addView(
87
+ grabber,
88
+ FrameLayout.LayoutParams(dp(32f), dp(4f), Gravity.TOP or Gravity.CENTER_HORIZONTAL).apply {
89
+ topMargin = dp(8f)
90
+ },
91
+ )
92
+ applyAppearance()
93
+
94
+ behavior.isGestureInsetBottomIgnored = true
95
+ behavior.maxWidth = dp(MAX_WIDTH_DP)
96
+ behavior.isHideable = true
97
+ behavior.state = BottomSheetBehavior.STATE_HIDDEN
98
+ // Registered after the initial HIDDEN state so it doesn't read as a dismissal.
99
+ behavior.addBottomSheetCallback(callback)
100
+
101
+ coordinator.addView(
102
+ sheet,
103
+ CoordinatorLayout.LayoutParams(MATCH, 0).apply {
104
+ gravity = Gravity.CENTER_HORIZONTAL
105
+ behavior = this@BottomSheetPresenter.behavior
106
+ },
107
+ )
108
+ return coordinator
109
+ }
110
+
111
+ override fun animateIn() {
112
+ coordinator.doOnLayout {
113
+ updateDetents()
114
+ // Settles from HIDDEN with the behavior's own animation.
115
+ behavior.state = stateFor(config.clampedInitialDetentIndex)
116
+ }
117
+ }
118
+
119
+ override fun animateOut(onEnd: () -> Unit) {
120
+ if (behavior.state == BottomSheetBehavior.STATE_HIDDEN || !coordinator.isLaidOut) {
121
+ onEnd()
122
+ return
123
+ }
124
+ behavior.isHideable = true
125
+ behavior.state = BottomSheetBehavior.STATE_HIDDEN
126
+ // STATE_HIDDEN reaches finishUserDismissal(), which keeps our reason.
127
+ }
128
+
129
+ override fun onConfigChanged(previous: ModalConfig) {
130
+ applyAppearance()
131
+ if (presented) behavior.isHideable = config.dismissOnSwipe
132
+ if (previous.detents != config.detents || previous.keyboardBehavior != config.keyboardBehavior) {
133
+ updateDetents()
134
+ }
135
+ }
136
+
137
+ override fun onInsetsChanged() {
138
+ sheet.setPadding(systemInsets.left, 0, systemInsets.right, systemInsets.bottom)
139
+ updateDetents()
140
+ applyKeyboardOffset()
141
+ }
142
+
143
+ override fun onKeyboardProgress() = applyKeyboardOffset()
144
+
145
+ override fun onContentSizeChanged() {
146
+ if (config.detents.contains(SheetDetent.FITCONTENT)) updateDetents()
147
+ }
148
+
149
+ override fun snapToDetent(index: Int) {
150
+ if (!presented || isDismissing) return
151
+ behavior.state = stateFor(index)
152
+ }
153
+
154
+ private fun applyAppearance() {
155
+ val radius = activity.dpToPx(config.cornerRadius ?: DEFAULT_CORNER_RADIUS_DP)
156
+ sheetBackground.shapeAppearanceModel = ShapeAppearanceModel.builder()
157
+ .setTopLeftCornerSize(radius)
158
+ .setTopRightCornerSize(radius)
159
+ .build()
160
+ sheetBackground.fillColor = ColorStateList.valueOf(config.surfaceColor(activity))
161
+ grabber.visibility = if (config.grabberVisible) View.VISIBLE else View.GONE
162
+ grabber.background = GradientDrawable().apply {
163
+ cornerRadius = activity.dpToPx(2f)
164
+ setColor(Color.argb(0x66, 0x80, 0x80, 0x80))
165
+ }
166
+ }
167
+
168
+ /** Recomputes detent heights and maps them onto the behavior's states. */
169
+ private fun updateDetents() {
170
+ val parentHeight = coordinator.height
171
+ if (parentHeight == 0) return
172
+ val bottomInset = systemInsets.bottom
173
+ maxContentHeight = (parentHeight - systemInsets.top - bottomInset).coerceAtLeast(1)
174
+
175
+ val requested = config.detents.ifEmpty { listOf(SheetDetent.FITCONTENT) }
176
+ val detents = requested
177
+ .mapIndexed { index, detent -> Detent(index, contentHeightFor(detent) + bottomInset) }
178
+ .distinctBy { it.height }
179
+ .sortedBy { it.height }
180
+ sortedDetents = when {
181
+ detents.size <= 3 -> detents
182
+ // BottomSheetBehavior only has three resting states.
183
+ else -> listOf(detents.first(), detents[detents.size / 2], detents.last())
184
+ }
185
+
186
+ val sheetHeight = sortedDetents.last().height
187
+ val params = sheet.layoutParams
188
+ if (params.height != sheetHeight) {
189
+ params.height = sheetHeight
190
+ sheet.layoutParams = params
191
+ }
192
+
193
+ val heights = sortedDetents.map { it.height }
194
+ when (heights.size) {
195
+ 1 -> {
196
+ behavior.isFitToContents = true
197
+ behavior.skipCollapsed = true
198
+ behavior.peekHeight = sheetHeight
199
+ }
200
+ 2 -> {
201
+ behavior.isFitToContents = true
202
+ behavior.skipCollapsed = false
203
+ behavior.peekHeight = heights[0]
204
+ }
205
+ else -> {
206
+ behavior.isFitToContents = false
207
+ behavior.skipCollapsed = false
208
+ behavior.peekHeight = heights[0]
209
+ behavior.halfExpandedRatio = (heights[1].toFloat() / parentHeight).coerceIn(0.01f, 0.99f)
210
+ behavior.expandedOffset = parentHeight - heights[2]
211
+ }
212
+ }
213
+
214
+ val areaHeight = sheetHeight - bottomInset -
215
+ if (config.keyboardBehavior == KeyboardBehavior.RESIZE) keyboardOverlap else 0
216
+ val sheetWidth = minOf(coordinator.width, behavior.maxWidth.takeIf { it > 0 } ?: Int.MAX_VALUE)
217
+ reportContentArea(sheetWidth - systemInsets.left - systemInsets.right, areaHeight)
218
+ }
219
+
220
+ private fun contentHeightFor(detent: SheetDetent): Int = when (detent) {
221
+ SheetDetent.SMALL -> (maxContentHeight * 0.25f).toInt()
222
+ SheetDetent.MEDIUM -> (maxContentHeight * 0.5f).toInt()
223
+ SheetDetent.LARGE -> maxContentHeight
224
+ SheetDetent.FITCONTENT -> contentRoot.contentHeight.coerceIn(1, maxContentHeight)
225
+ }
226
+
227
+ private fun states(): List<Int> = when (sortedDetents.size) {
228
+ 0 -> emptyList()
229
+ 1 -> listOf(BottomSheetBehavior.STATE_EXPANDED)
230
+ 2 -> listOf(BottomSheetBehavior.STATE_COLLAPSED, BottomSheetBehavior.STATE_EXPANDED)
231
+ else -> listOf(
232
+ BottomSheetBehavior.STATE_COLLAPSED,
233
+ BottomSheetBehavior.STATE_HALF_EXPANDED,
234
+ BottomSheetBehavior.STATE_EXPANDED,
235
+ )
236
+ }
237
+
238
+ /** Resting state for `detents[index]`, falling back to the nearest height. */
239
+ private fun stateFor(index: Int): Int {
240
+ val states = states()
241
+ if (states.isEmpty()) return BottomSheetBehavior.STATE_EXPANDED
242
+ val target = config.detents.getOrNull(index)?.let { contentHeightFor(it) + systemInsets.bottom }
243
+ val position = if (target == null) {
244
+ states.lastIndex
245
+ } else {
246
+ sortedDetents.indices.minBy { kotlin.math.abs(sortedDetents[it].height - target) }
247
+ }
248
+ return states[position]
249
+ }
250
+
251
+ /** Lifts the sheet above the keyboard without relayout (`pan` and `resize`). */
252
+ private fun applyKeyboardOffset() {
253
+ if (config.keyboardBehavior == KeyboardBehavior.NONE) {
254
+ sheet.translationY = 0f
255
+ return
256
+ }
257
+ val headroom = (sheet.top - systemInsets.top).coerceAtLeast(0)
258
+ sheet.translationY = -keyboardOverlap.coerceAtMost(headroom).toFloat()
259
+ }
260
+
261
+ companion object {
262
+ private const val MATCH = ViewGroup.LayoutParams.MATCH_PARENT
263
+ private const val DEFAULT_CORNER_RADIUS_DP = 28f
264
+ private const val MAX_WIDTH_DP = 640f
265
+
266
+ /** Content area before the sheet is on screen, in px. */
267
+ fun estimateContentArea(
268
+ activity: Activity,
269
+ detents: List<SheetDetent>,
270
+ width: Int,
271
+ height: Int,
272
+ insets: Insets,
273
+ ): Pair<Int, Int> {
274
+ val maxContent = height - insets.top - insets.bottom
275
+ val fraction = when {
276
+ detents.isEmpty() || SheetDetent.LARGE in detents || SheetDetent.FITCONTENT in detents -> 1f
277
+ SheetDetent.MEDIUM in detents -> 0.5f
278
+ else -> 0.25f
279
+ }
280
+ val sheetWidth = minOf(width, activity.dpToPx(MAX_WIDTH_DP).toInt())
281
+ return Pair(sheetWidth - insets.left - insets.right, (maxContent * fraction).toInt())
282
+ }
283
+ }
284
+ }