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.
- package/LICENSE +20 -0
- package/NitroModal.podspec +29 -0
- package/README.md +39 -0
- package/android/CMakeLists.txt +24 -0
- package/android/build.gradle +124 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/cpp-adapter.cpp +11 -0
- package/android/src/main/java/com/margelo/nitro/nitromodal/BottomSheetPresenter.kt +284 -0
- package/android/src/main/java/com/margelo/nitro/nitromodal/HybridNitroModal.kt +312 -0
- package/android/src/main/java/com/margelo/nitro/nitromodal/ModalConfig.kt +50 -0
- package/android/src/main/java/com/margelo/nitro/nitromodal/ModalContentRoot.kt +115 -0
- package/android/src/main/java/com/margelo/nitro/nitromodal/ModalPresenter.kt +253 -0
- package/android/src/main/java/com/margelo/nitro/nitromodal/NitroModalHostView.kt +52 -0
- package/android/src/main/java/com/margelo/nitro/nitromodal/NitroModalPackage.kt +28 -0
- package/android/src/main/java/com/margelo/nitro/nitromodal/NitroModalViewManager.kt +67 -0
- package/android/src/main/java/com/margelo/nitro/nitromodal/PopupPresenter.kt +156 -0
- package/android/src/main/res/values/styles.xml +17 -0
- package/ios/BackdropView.swift +64 -0
- package/ios/HybridNitroModal.swift +329 -0
- package/ios/HybridNitroModalComponent+Children.mm +77 -0
- package/ios/ModalConfig.swift +73 -0
- package/ios/NitroModalHostView.swift +91 -0
- package/ios/PopupViewController.swift +223 -0
- package/ios/SheetViewController.swift +244 -0
- package/lib/module/NitroModal.js +188 -0
- package/lib/module/NitroModal.js.map +1 -0
- package/lib/module/NitroModal.nitro.js +4 -0
- package/lib/module/NitroModal.nitro.js.map +1 -0
- package/lib/module/NitroModalView.js +17 -0
- package/lib/module/NitroModalView.js.map +1 -0
- package/lib/module/NitroModalView.native.js +6 -0
- package/lib/module/NitroModalView.native.js.map +1 -0
- package/lib/module/index.js +4 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NitroModal.d.ts +59 -0
- package/lib/typescript/src/NitroModal.d.ts.map +1 -0
- package/lib/typescript/src/NitroModal.nitro.d.ts +78 -0
- package/lib/typescript/src/NitroModal.nitro.d.ts.map +1 -0
- package/lib/typescript/src/NitroModalView.d.ts +4 -0
- package/lib/typescript/src/NitroModalView.d.ts.map +1 -0
- package/lib/typescript/src/NitroModalView.native.d.ts +3 -0
- package/lib/typescript/src/NitroModalView.native.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +3 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/nitro.json +23 -0
- package/nitrogen/generated/android/c++/JDismissReason.hpp +64 -0
- package/nitrogen/generated/android/c++/JFunc_void.hpp +75 -0
- package/nitrogen/generated/android/c++/JFunc_void_DismissReason.hpp +77 -0
- package/nitrogen/generated/android/c++/JFunc_void_ModalContentArea.hpp +77 -0
- package/nitrogen/generated/android/c++/JFunc_void_double.hpp +75 -0
- package/nitrogen/generated/android/c++/JHybridNitroModalSpec.cpp +344 -0
- package/nitrogen/generated/android/c++/JHybridNitroModalSpec.hpp +106 -0
- package/nitrogen/generated/android/c++/JKeyboardBehavior.hpp +61 -0
- package/nitrogen/generated/android/c++/JModalContentArea.hpp +61 -0
- package/nitrogen/generated/android/c++/JModalMode.hpp +58 -0
- package/nitrogen/generated/android/c++/JPopupAnimation.hpp +61 -0
- package/nitrogen/generated/android/c++/JSheetDetent.hpp +64 -0
- package/nitrogen/generated/android/c++/views/JHybridNitroModalStateUpdater.cpp +174 -0
- package/nitrogen/generated/android/c++/views/JHybridNitroModalStateUpdater.hpp +54 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/DismissReason.kt +25 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/Func_void.kt +78 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/Func_void_DismissReason.kt +78 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/Func_void_ModalContentArea.kt +78 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/Func_void_double.kt +78 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/HybridNitroModalSpec.kt +237 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/KeyboardBehavior.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/ModalContentArea.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/ModalMode.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/PopupAnimation.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/SheetDetent.kt +25 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/nitromodalOnLoad.kt +35 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/views/HybridNitroModalManager.kt +96 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/views/HybridNitroModalStateUpdater.kt +23 -0
- package/nitrogen/generated/android/nitromodal+autolinking.cmake +83 -0
- package/nitrogen/generated/android/nitromodal+autolinking.gradle +27 -0
- package/nitrogen/generated/android/nitromodalOnLoad.cpp +64 -0
- package/nitrogen/generated/android/nitromodalOnLoad.hpp +34 -0
- package/nitrogen/generated/ios/NitroModal+autolinking.rb +62 -0
- package/nitrogen/generated/ios/NitroModal-Swift-Cxx-Bridge.cpp +65 -0
- package/nitrogen/generated/ios/NitroModal-Swift-Cxx-Bridge.hpp +237 -0
- package/nitrogen/generated/ios/NitroModal-Swift-Cxx-Umbrella.hpp +64 -0
- package/nitrogen/generated/ios/NitroModalAutolinking.mm +33 -0
- package/nitrogen/generated/ios/NitroModalAutolinking.swift +26 -0
- package/nitrogen/generated/ios/c++/HybridNitroModalSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridNitroModalSpecSwift.hpp +247 -0
- package/nitrogen/generated/ios/c++/views/HybridNitroModalComponent.mm +264 -0
- package/nitrogen/generated/ios/swift/DismissReason.swift +48 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_DismissReason.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_ModalContentArea.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_double.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridNitroModalSpec.swift +77 -0
- package/nitrogen/generated/ios/swift/HybridNitroModalSpec_cxx.swift +554 -0
- package/nitrogen/generated/ios/swift/KeyboardBehavior.swift +44 -0
- package/nitrogen/generated/ios/swift/ModalContentArea.swift +34 -0
- package/nitrogen/generated/ios/swift/ModalMode.swift +40 -0
- package/nitrogen/generated/ios/swift/PopupAnimation.swift +44 -0
- package/nitrogen/generated/ios/swift/SheetDetent.swift +48 -0
- package/nitrogen/generated/shared/c++/DismissReason.hpp +84 -0
- package/nitrogen/generated/shared/c++/HybridNitroModalSpec.cpp +65 -0
- package/nitrogen/generated/shared/c++/HybridNitroModalSpec.hpp +124 -0
- package/nitrogen/generated/shared/c++/KeyboardBehavior.hpp +80 -0
- package/nitrogen/generated/shared/c++/ModalContentArea.hpp +87 -0
- package/nitrogen/generated/shared/c++/ModalMode.hpp +76 -0
- package/nitrogen/generated/shared/c++/PopupAnimation.hpp +80 -0
- package/nitrogen/generated/shared/c++/SheetDetent.hpp +84 -0
- package/nitrogen/generated/shared/c++/views/HybridNitroModalComponent.cpp +74 -0
- package/nitrogen/generated/shared/c++/views/HybridNitroModalComponent.hpp +151 -0
- package/nitrogen/generated/shared/json/NitroModalConfig.json +30 -0
- package/package.json +183 -0
- package/src/NitroModal.nitro.ts +92 -0
- package/src/NitroModal.tsx +292 -0
- package/src/NitroModalView.native.tsx +8 -0
- package/src/NitroModalView.tsx +24 -0
- 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,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
|
+
}
|