├── demo ├── .gitignore ├── src │ └── main │ │ ├── res │ │ ├── values │ │ │ ├── strings.xml │ │ │ ├── colors.xml │ │ │ └── themes.xml │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ └── drawable │ │ │ └── ic_launcher_foreground.xml │ │ ├── ic_launcher-playstore.png │ │ ├── kotlin │ │ └── eu │ │ │ └── wewox │ │ │ └── modalsheet │ │ │ ├── ui │ │ │ ├── theme │ │ │ │ ├── Color.kt │ │ │ │ ├── Spacing.kt │ │ │ │ ├── Shape.kt │ │ │ │ └── Theme.kt │ │ │ └── components │ │ │ │ ├── TopBar.kt │ │ │ │ └── BottomNavigation.kt │ │ │ ├── Example.kt │ │ │ ├── screens │ │ │ ├── SimpleModalSheetScreen.kt │ │ │ ├── ModalSheetPaddingScreen.kt │ │ │ ├── BackHandlerInModalSheetScreen.kt │ │ │ ├── DynamicModalSheetScreen.kt │ │ │ ├── CustomModalSheetScreen.kt │ │ │ ├── SheetStateModalSheetScreen.kt │ │ │ ├── SheetAboveBottomBarScreen.kt │ │ │ └── ScrollableModalSheetScreen.kt │ │ │ └── MainActivity.kt │ │ └── AndroidManifest.xml ├── proguard-rules.pro └── build.gradle.kts ├── modalsheet ├── .gitignore ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── eu │ │ └── wewox │ │ └── modalsheet │ │ ├── ExperimentalSheetApi.kt │ │ ├── FullscreenPopup.kt │ │ └── ModalSheet.kt └── build.gradle.kts ├── .idea ├── .gitignore ├── codeStyles │ ├── codeStyleConfig.xml │ └── Project.xml ├── compiler.xml ├── vcs.xml ├── migrations.xml ├── misc.xml └── gradle.xml ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── settings.gradle.kts ├── .gitignore ├── .github └── workflows │ ├── static-analysis.yml │ └── publish.yml ├── gradle.properties ├── gradlew.bat ├── README.md ├── gradlew ├── LICENSE └── config └── detekt └── detekt.yml /demo/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /modalsheet/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /modalsheet/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /demo/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ModalSheet 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleksandrbalan/modalsheet/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /demo/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleksandrbalan/modalsheet/HEAD/demo/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleksandrbalan/modalsheet/HEAD/demo/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleksandrbalan/modalsheet/HEAD/demo/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleksandrbalan/modalsheet/HEAD/demo/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleksandrbalan/modalsheet/HEAD/demo/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleksandrbalan/modalsheet/HEAD/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleksandrbalan/modalsheet/HEAD/demo/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleksandrbalan/modalsheet/HEAD/demo/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleksandrbalan/modalsheet/HEAD/demo/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleksandrbalan/modalsheet/HEAD/demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleksandrbalan/modalsheet/HEAD/demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /demo/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 4 | 5 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /demo/src/main/kotlin/eu/wewox/modalsheet/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("UndocumentedPublicProperty") 2 | 3 | package eu.wewox.modalsheet.ui.theme 4 | 5 | import androidx.compose.ui.graphics.Color 6 | 7 | val LightBlue = Color(0xFF6DD3FF) 8 | val LightYellow = Color(0xFFFFF281) 9 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Mar 08 12:30:23 CET 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /demo/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /demo/src/main/kotlin/eu/wewox/modalsheet/ui/theme/Spacing.kt: -------------------------------------------------------------------------------- 1 | package eu.wewox.modalsheet.ui.theme 2 | 3 | import androidx.compose.ui.unit.dp 4 | 5 | /** 6 | * The small spacing. 7 | */ 8 | val SpacingSmall = 8.dp 9 | 10 | /** 11 | * The medium spacing. 12 | */ 13 | val SpacingMedium = 16.dp 14 | -------------------------------------------------------------------------------- /.idea/migrations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /modalsheet/src/main/java/eu/wewox/modalsheet/ExperimentalSheetApi.kt: -------------------------------------------------------------------------------- 1 | package eu.wewox.modalsheet 2 | 3 | /** 4 | * Used for annotating experimental modal sheet API that is likely to change or be removed in the 5 | * future. 6 | */ 7 | @Retention(AnnotationRetention.BINARY) 8 | @RequiresOptIn( 9 | "This API is experimental and is likely to change or to be removed in the future." 10 | ) 11 | public annotation class ExperimentalSheetApi 12 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | gradlePluginPortal() 6 | } 7 | } 8 | 9 | dependencyResolutionManagement { 10 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 11 | repositories { 12 | google() 13 | mavenCentral() 14 | } 15 | } 16 | 17 | rootProject.name = "ModalSheet" 18 | include(":demo") 19 | include(":modalsheet") 20 | -------------------------------------------------------------------------------- /demo/src/main/kotlin/eu/wewox/modalsheet/ui/theme/Shape.kt: -------------------------------------------------------------------------------- 1 | package eu.wewox.modalsheet.ui.theme 2 | 3 | import androidx.compose.foundation.shape.RoundedCornerShape 4 | import androidx.compose.material.Shapes 5 | import androidx.compose.ui.unit.dp 6 | 7 | /** 8 | * The application shapes. 9 | */ 10 | val Shapes = Shapes( 11 | small = RoundedCornerShape(50), 12 | medium = RoundedCornerShape(8.dp), 13 | large = RoundedCornerShape(16.dp) 14 | ) 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/.name 5 | /.idea/caches 6 | /.idea/libraries 7 | /.idea/inspectionProfiles 8 | /.idea/modules.xml 9 | /.idea/workspace.xml 10 | /.idea/navEditor.xml 11 | /.idea/assetWizardSettings.xml 12 | /.idea/kotlinc.xml 13 | /.idea/deploymentTargetDropDown.xml 14 | /.idea/deploymentTargetSelector.xml 15 | /.idea/other.xml 16 | .DS_Store 17 | /build 18 | /captures 19 | .externalNativeBuild 20 | .cxx 21 | local.properties 22 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | 12 | 13 | 15 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 19 | 20 | -------------------------------------------------------------------------------- /demo/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /demo/src/main/kotlin/eu/wewox/modalsheet/ui/components/TopBar.kt: -------------------------------------------------------------------------------- 1 | package eu.wewox.modalsheet.ui.components 2 | 3 | import androidx.compose.foundation.layout.padding 4 | import androidx.compose.foundation.layout.statusBarsPadding 5 | import androidx.compose.material.MaterialTheme 6 | import androidx.compose.material.Text 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import eu.wewox.modalsheet.ui.theme.SpacingMedium 10 | 11 | /** 12 | * The reusable component for top bar. 13 | * 14 | * @param title The text to show in top bar. 15 | */ 16 | @Composable 17 | fun TopBar(title: String) { 18 | Text( 19 | text = title, 20 | style = MaterialTheme.typography.h4, 21 | modifier = Modifier 22 | .padding(SpacingMedium) 23 | .statusBarsPadding() 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /demo/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 14 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /demo/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 16 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /demo/src/main/kotlin/eu/wewox/modalsheet/Example.kt: -------------------------------------------------------------------------------- 1 | package eu.wewox.modalsheet 2 | 3 | /** 4 | * Enumeration of available demo examples. 5 | * 6 | * @param label Example name. 7 | * @param description Brief description. 8 | */ 9 | enum class Example( 10 | val label: String, 11 | val description: String, 12 | ) { 13 | SimpleModalSheet( 14 | "Simple Modal Sheet", 15 | "Basic modal sheet usage" 16 | ), 17 | SheetAboveBottomBar( 18 | "Sheet Above Bottom Bar", 19 | "Showcases the fact, that modal is displayed above bottom bar" 20 | ), 21 | DynamicModalSheet( 22 | "Dynamic Content Modal Sheet", 23 | "Example of modal sheet with dynamic content" 24 | ), 25 | ScrollableModalSheet( 26 | "Scrollable Modal Sheet", 27 | "Example of scrollable modal sheet" 28 | ), 29 | CustomModalSheet( 30 | "Custom Modal Sheet", 31 | "Example of customizable input arguments" 32 | ), 33 | SheetStateModalSheet( 34 | "Sheet State Modal Sheet", 35 | "Example of modal sheet driven by sheet state" 36 | ), 37 | ModalSheetPadding( 38 | "Padding In Modal Sheet", 39 | "Example of the sheet padding parameter usage" 40 | ), 41 | BackHandlerInModalSheet( 42 | "Back Handler In Modal Sheet", 43 | "Example of modal sheet with overridden system back button behavior" 44 | ), 45 | } 46 | -------------------------------------------------------------------------------- /modalsheet/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.library) 3 | alias(libs.plugins.kotlin) 4 | alias(libs.plugins.mavenpublish) 5 | } 6 | 7 | android { 8 | namespace = "eu.wewox.modalsheet" 9 | 10 | compileSdk = libs.versions.sdk.compile.get().toInt() 11 | 12 | defaultConfig { 13 | minSdk = libs.versions.sdk.min.get().toInt() 14 | } 15 | compileOptions { 16 | sourceCompatibility = JavaVersion.toVersion(libs.versions.java.sourceCompatibility.get()) 17 | targetCompatibility = JavaVersion.toVersion(libs.versions.java.targetCompatibility.get()) 18 | } 19 | buildFeatures { 20 | compose = true 21 | } 22 | composeOptions { 23 | kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() 24 | } 25 | kotlinOptions { 26 | jvmTarget = libs.versions.java.jvmTarget.get() 27 | freeCompilerArgs = freeCompilerArgs + 28 | "-Xexplicit-api=strict" + 29 | "-opt-in=androidx.compose.material.ExperimentalMaterialApi" 30 | } 31 | } 32 | 33 | dependencies { 34 | implementation(platform(libs.compose.bom)) 35 | implementation(libs.compose.foundation) 36 | implementation(libs.compose.ui) 37 | implementation(libs.compose.material2) 38 | implementation(libs.androidx.activitycompose) 39 | implementation(libs.androidx.savedstate) 40 | implementation(libs.androidx.viewmodel) 41 | } 42 | -------------------------------------------------------------------------------- /.github/workflows/static-analysis.yml: -------------------------------------------------------------------------------- 1 | # This is a workflow to verify PRs with static code analysis tools 2 | name: Static Analysis 3 | 4 | # Controls when the workflow will run 5 | on: 6 | pull_request: 7 | branches: [main, develop] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | jobs: 13 | detekt: 14 | name: Detekt 15 | runs-on: ubuntu-latest 16 | 17 | # Steps represent a sequence of tasks that will be executed as part of the job 18 | steps: 19 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 20 | - uses: actions/checkout@v3 21 | - name: Set up JDK 17 22 | uses: actions/setup-java@v3 23 | with: 24 | java-version: '17' 25 | distribution: 'temurin' 26 | 27 | # Runs a single command using the runners shell 28 | - name: detekt 29 | run: ./gradlew detekt 30 | 31 | spotless: 32 | name: Spotless 33 | runs-on: ubuntu-latest 34 | 35 | # Steps represent a sequence of tasks that will be executed as part of the job 36 | steps: 37 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 38 | - uses: actions/checkout@v3 39 | - name: Set up JDK 17 40 | uses: actions/setup-java@v3 41 | with: 42 | java-version: '17' 43 | distribution: 'temurin' 44 | 45 | # Runs a single command using the runners shell 46 | - name: spotless 47 | run: ./gradlew spotlessCheck 48 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: Publish To Maven Central 4 | 5 | # Controls when the workflow will run 6 | on: 7 | # Triggers the workflow when tag is pushed 8 | push: 9 | tags: 10 | - 'v*' 11 | 12 | # Allows you to run this workflow manually from the Actions tab 13 | workflow_dispatch: 14 | 15 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 16 | jobs: 17 | # This workflow contains a single job called "publish" 18 | publish: 19 | # The type of runner that the job will run on 20 | runs-on: ubuntu-latest 21 | 22 | # Steps represent a sequence of tasks that will be executed as part of the job 23 | steps: 24 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 25 | - uses: actions/checkout@v3 26 | - name: Set up JDK 17 27 | uses: actions/setup-java@v3 28 | with: 29 | java-version: '17' 30 | distribution: 'temurin' 31 | 32 | # Runs a single command using the runners shell 33 | - name: publish 34 | run: ./gradlew publish --no-daemon --no-parallel 35 | env: 36 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }} 37 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} 38 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_KEY }} 39 | ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.SIGNING_KEY_ID }} 40 | ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_KEY_PASSWORD }} 41 | -------------------------------------------------------------------------------- /demo/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.application) 3 | alias(libs.plugins.kotlin) 4 | } 5 | 6 | android { 7 | namespace = "eu.wewox.modalsheet" 8 | 9 | compileSdk = libs.versions.sdk.compile.get().toInt() 10 | 11 | defaultConfig { 12 | applicationId = "eu.wewox.modalsheet" 13 | 14 | minSdk = libs.versions.sdk.min.get().toInt() 15 | targetSdk = libs.versions.sdk.target.get().toInt() 16 | 17 | versionCode = 1 18 | versionName = "1.0" 19 | 20 | vectorDrawables { 21 | useSupportLibrary = true 22 | } 23 | } 24 | 25 | buildTypes { 26 | release { 27 | isMinifyEnabled = false 28 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 29 | } 30 | } 31 | compileOptions { 32 | sourceCompatibility = JavaVersion.toVersion(libs.versions.java.sourceCompatibility.get()) 33 | targetCompatibility = JavaVersion.toVersion(libs.versions.java.targetCompatibility.get()) 34 | } 35 | kotlinOptions { 36 | jvmTarget = libs.versions.java.jvmTarget.get() 37 | } 38 | buildFeatures { 39 | compose = true 40 | } 41 | composeOptions { 42 | kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() 43 | } 44 | packaging { 45 | resources { 46 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 47 | } 48 | } 49 | } 50 | 51 | dependencies { 52 | implementation(project(":modalsheet")) 53 | 54 | implementation(platform(libs.compose.bom)) 55 | implementation(libs.compose.ui) 56 | implementation(libs.compose.material2) 57 | implementation(libs.androidx.activitycompose) 58 | implementation(libs.accompanist.systemuicontroller) 59 | } 60 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | sdk-compile = "34" 3 | sdk-min = "21" 4 | sdk-target = "34" 5 | 6 | compose-bom = "2024.06.00" 7 | compose-compiler = "1.5.14" 8 | accompanist = "0.34.0" 9 | activity-compose = "1.9.0" 10 | lifecycle = "2.8.3" 11 | 12 | plugin-android-gradle = "8.1.4" 13 | plugin-kotlin = "1.9.24" 14 | plugin-detekt = "1.23.6" 15 | plugin-spotless = "6.5.1" 16 | plugin-mavenpublish = "0.28.0" 17 | 18 | java-jvmTarget = "1.8" 19 | java-sourceCompatibility = "1.8" 20 | java-targetCompatibility = "1.8" 21 | 22 | [libraries] 23 | compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" } 24 | compose-foundation = { module = "androidx.compose.foundation:foundation" } 25 | compose-material2 = { module = "androidx.compose.material:material" } 26 | compose-ui = { module = "androidx.compose.ui:ui" } 27 | androidx-activitycompose = { module = "androidx.activity:activity-compose", version.ref = "activity-compose" } 28 | androidx-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel", version.ref = "lifecycle" } 29 | androidx-savedstate = { module = "androidx.lifecycle:lifecycle-viewmodel-savedstate", version.ref = "lifecycle" } 30 | accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist" } 31 | 32 | [plugins] 33 | android-application = { id = "com.android.application", version.ref = "plugin-android-gradle" } 34 | android-library = { id = "com.android.library", version.ref = "plugin-android-gradle" } 35 | kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "plugin-kotlin" } 36 | detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "plugin-detekt" } 37 | spotless = { id = "com.diffplug.spotless", version.ref = "plugin-spotless" } 38 | mavenpublish = { id = "com.vanniktech.maven.publish", version.ref = "plugin-mavenpublish" } 39 | -------------------------------------------------------------------------------- /demo/src/main/kotlin/eu/wewox/modalsheet/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package eu.wewox.modalsheet.ui.theme 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.material.MaterialTheme 5 | import androidx.compose.material.Typography 6 | import androidx.compose.material.darkColors 7 | import androidx.compose.material.lightColors 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.SideEffect 10 | import androidx.compose.ui.graphics.Color 11 | import com.google.accompanist.systemuicontroller.rememberSystemUiController 12 | 13 | private val DarkColorPalette = darkColors( 14 | primary = LightBlue, 15 | primaryVariant = LightBlue, 16 | secondary = LightYellow, 17 | secondaryVariant = LightYellow, 18 | 19 | onPrimary = Color.Black, 20 | onSecondary = Color.Black, 21 | ) 22 | 23 | private val LightColorPalette = lightColors( 24 | primary = LightBlue, 25 | primaryVariant = LightBlue, 26 | secondary = LightYellow, 27 | secondaryVariant = LightYellow, 28 | 29 | onPrimary = Color.Black, 30 | onSecondary = Color.Black, 31 | ) 32 | 33 | /** 34 | * The theme to use for deo application. 35 | */ 36 | @Composable 37 | fun ModalSheetDemoTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) { 38 | val sysUiController = rememberSystemUiController() 39 | SideEffect { 40 | sysUiController.setSystemBarsColor( 41 | color = Color.Transparent, 42 | darkIcons = !darkTheme, 43 | isNavigationBarContrastEnforced = false 44 | ) 45 | } 46 | 47 | val colors = if (darkTheme) { 48 | DarkColorPalette 49 | } else { 50 | LightColorPalette 51 | } 52 | 53 | MaterialTheme( 54 | colors = colors, 55 | typography = Typography(), 56 | shapes = Shapes, 57 | content = content 58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /demo/src/main/kotlin/eu/wewox/modalsheet/ui/components/BottomNavigation.kt: -------------------------------------------------------------------------------- 1 | package eu.wewox.modalsheet.ui.components 2 | 3 | import androidx.compose.foundation.layout.navigationBarsPadding 4 | import androidx.compose.material.BottomNavigationDefaults 5 | import androidx.compose.material.BottomNavigationItem 6 | import androidx.compose.material.Icon 7 | import androidx.compose.material.MaterialTheme 8 | import androidx.compose.material.Surface 9 | import androidx.compose.material.Text 10 | import androidx.compose.material.icons.Icons 11 | import androidx.compose.material.icons.filled.AddCircle 12 | import androidx.compose.material.icons.filled.Home 13 | import androidx.compose.material.icons.filled.Person 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.graphics.Color 17 | import androidx.compose.ui.graphics.vector.ImageVector 18 | import androidx.compose.ui.unit.dp 19 | 20 | /** 21 | * The reusable bottom navigation component. 22 | * 23 | * @param current The currently selected item. 24 | * @param onClick Called when user selects the bottom navigation item. 25 | */ 26 | @Composable 27 | fun BottomNavigation( 28 | current: BottomNavItem, 29 | onClick: (BottomNavItem) -> Unit, 30 | ) { 31 | Surface( 32 | color = MaterialTheme.colors.primary, 33 | elevation = BottomNavigationDefaults.Elevation, 34 | ) { 35 | androidx.compose.material.BottomNavigation( 36 | backgroundColor = Color.Transparent, 37 | contentColor = MaterialTheme.colors.onPrimary, 38 | elevation = 0.dp, 39 | modifier = Modifier.navigationBarsPadding(), 40 | ) { 41 | BottomNavItem.values().forEach { item -> 42 | BottomNavigationItem( 43 | icon = { Icon(item.imageVector, item.title) }, 44 | label = { Text(item.title) }, 45 | selected = current == item, 46 | onClick = { onClick(item) }, 47 | ) 48 | } 49 | } 50 | } 51 | } 52 | 53 | /** 54 | * The bottom navigation item with [title] for label and [imageVector] for icon. 55 | */ 56 | enum class BottomNavItem(var title: String, var imageVector: ImageVector) { 57 | Home("Home", Icons.Default.Home), 58 | Counter("Counter", Icons.Default.AddCircle), 59 | Profile("Profile", Icons.Default.Person), 60 | } 61 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true 24 | 25 | # Maven setup with https://github.com/vanniktech/gradle-maven-publish-plugin 26 | SONATYPE_HOST=S01 27 | RELEASE_SIGNING_ENABLED=true 28 | 29 | GROUP=io.github.oleksandrbalan 30 | POM_ARTIFACT_ID=modalsheet 31 | VERSION_NAME=0.7.0 32 | 33 | POM_NAME=Modal Sheet 34 | POM_DESCRIPTION=This library allows to create real modal bottom sheets which could be placed anywhere in the hierarchy and which are drawn above all other UI. 35 | POM_INCEPTION_YEAR=2022 36 | POM_URL=https://github.com/oleksandrbalan/modalsheet 37 | 38 | POM_LICENSE_NAME=The Apache Software License, Version 2.0 39 | POM_LICENSE_URL=https://www.apache.org/licenses/LICENSE-2.0.txt 40 | POM_LICENSE_DIST=repo 41 | 42 | POM_SCM_URL=https://github.com/oleksandrbalan/modalsheet 43 | POM_SCM_CONNECTION=scm:git:git://github.com/oleksandrbalan/modalsheet.git 44 | POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/oleksandrbalan/modalsheet.git 45 | 46 | POM_DEVELOPER_ID=oleksandrbalan 47 | POM_DEVELOPER_NAME=Oleksandr Balan 48 | POM_DEVELOPER_URL=https://github.com/oleksandrbalan 49 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /demo/src/main/kotlin/eu/wewox/modalsheet/screens/SimpleModalSheetScreen.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalSheetApi::class) 2 | 3 | package eu.wewox.modalsheet.screens 4 | 5 | import androidx.compose.foundation.layout.Arrangement 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.foundation.layout.Column 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.foundation.layout.navigationBarsPadding 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.material.Button 13 | import androidx.compose.material.MaterialTheme 14 | import androidx.compose.material.Scaffold 15 | import androidx.compose.material.Text 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.runtime.getValue 18 | import androidx.compose.runtime.mutableStateOf 19 | import androidx.compose.runtime.saveable.rememberSaveable 20 | import androidx.compose.runtime.setValue 21 | import androidx.compose.ui.Alignment 22 | import androidx.compose.ui.Modifier 23 | import eu.wewox.modalsheet.Example 24 | import eu.wewox.modalsheet.ExperimentalSheetApi 25 | import eu.wewox.modalsheet.ModalSheet 26 | import eu.wewox.modalsheet.ui.components.TopBar 27 | import eu.wewox.modalsheet.ui.theme.SpacingMedium 28 | import eu.wewox.modalsheet.ui.theme.SpacingSmall 29 | 30 | /** 31 | * Showcases the most simple usage of the modal sheet composable. 32 | * Shows the bottom sheet on button click. 33 | */ 34 | @Composable 35 | fun SimpleModalSheetScreen() { 36 | Scaffold( 37 | topBar = { TopBar(Example.SimpleModalSheet.label) } 38 | ) { padding -> 39 | var visible by rememberSaveable { mutableStateOf(false) } 40 | 41 | Box( 42 | contentAlignment = Alignment.Center, 43 | modifier = Modifier 44 | .fillMaxSize() 45 | .padding(padding) 46 | ) { 47 | Button(onClick = { visible = true }) { 48 | Text(text = "Show modal sheet") 49 | } 50 | } 51 | 52 | SimpleModalSheet( 53 | title = "Hello there \uD83D\uDC4B", 54 | visible = visible, 55 | onVisibleChange = { visible = it } 56 | ) 57 | } 58 | } 59 | 60 | /** 61 | * Simple [ModalSheet] example which contains only texts and button to close bottom sheet. 62 | * 63 | * @param title The title to show inside [ModalSheet]. 64 | * @param visible True if modal should be visible. 65 | * @param onVisibleChange Called when visibility changes. 66 | */ 67 | @Composable 68 | fun SimpleModalSheet( 69 | title: String, 70 | visible: Boolean, 71 | onVisibleChange: (Boolean) -> Unit, 72 | ) { 73 | ModalSheet( 74 | visible = visible, 75 | onVisibleChange = onVisibleChange 76 | ) { 77 | Column( 78 | horizontalAlignment = Alignment.CenterHorizontally, 79 | verticalArrangement = Arrangement.spacedBy(SpacingSmall), 80 | modifier = Modifier 81 | .fillMaxWidth() 82 | .navigationBarsPadding() 83 | .padding(SpacingMedium) 84 | ) { 85 | Text( 86 | text = title, 87 | style = MaterialTheme.typography.h4 88 | ) 89 | Text( 90 | text = "Swipe down, tap on scrim above, tap on system " + 91 | "back button or use a button below to close modal.", 92 | ) 93 | Button(onClick = { onVisibleChange(false) }) { 94 | Text(text = "Close modal sheet") 95 | } 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Maven Central](https://img.shields.io/maven-central/v/io.github.oleksandrbalan/modalsheet.svg?label=Maven%20Central)](https://search.maven.org/artifact/io.github.oleksandrbalan/modalsheet) 2 | 3 | 4 | 5 | # Modal Sheet 6 | 7 | Modal Sheet library for Jetpack Compose. 8 | 9 | ## Motivation 10 | 11 | Sometimes we want bottom sheets to behave as real modals, thus overlaying all content on the screen. Official `ModalBottomSheetState` however, is laid out only in bounds of the parent Composable. This means that to display a bottom sheet above, for example bottom bar navigation, it must be placed "higher" in the hierarchy than usage in one of the tab screen. 12 | 13 | The `ModalSheet` Composable from this library allows creating real modal bottom sheets which could be placed anywhere in the hierarchy. 14 | 15 | ## Solution 16 | 17 | The `ModalSheet` Composable internally uses `FullScreenPopup` which is basically a window placed above the current window and allows placing Composables inside. Then `ModalSheet` adds swipe-to-dismiss, scrim and shaping to simulate bottom sheet behavior. 18 | 19 | ## Usage 20 | 21 | ### Get a dependency 22 | 23 | **Step 1.** Add the MavenCentral repository to your build file. 24 | Add it in your root `build.gradle` at the end of repositories: 25 | ``` 26 | allprojects { 27 | repositories { 28 | ... 29 | mavenCentral() 30 | } 31 | } 32 | ``` 33 | 34 | Or in `settings.gradle`: 35 | ``` 36 | dependencyResolutionManagement { 37 | repositories { 38 | ... 39 | mavenCentral() 40 | } 41 | } 42 | ``` 43 | 44 | **Step 2.** Add the dependency. 45 | Check latest version on the [releases page](https://github.com/oleksandrbalan/modalsheet/releases). 46 | ``` 47 | dependencies { 48 | implementation 'io.github.oleksandrbalan:modalsheet:$version' 49 | } 50 | ``` 51 | 52 | ### Use in Composable 53 | 54 | The `ModalSheet` has 2 mandatory arguments: 55 | * **visible** - True if modal should be visible. 56 | * **onVisibleChange** - Called when visibility changes. 57 | 58 | ``` 59 | var visible by remember { mutableStateOf(false) } 60 | 61 | Button(onClick = { visible = true }) { 62 | Text(text = "Show modal sheet") 63 | } 64 | 65 | ModalSheet( 66 | visible = visible, 67 | onVisibleChange = { visible = it }, 68 | ) { 69 | Box(Modifier.height(200.dp)) 70 | } 71 | ``` 72 | 73 | See Demo application and [examples](demo/src/main/kotlin/eu/wewox/modalsheet/screens) for more usage examples. 74 | 75 | 76 | 77 | 78 | #### Alternative overloads 79 | 80 | Library also has a `ModalSheet` overload which receives `data` to be shown in the bottom sheet. When `data` is not null, bottom sheet is shown; when `null`, bottom sheet is hidden. 81 | 82 | This is useful when bottom sheet content dependents on some state, which may be change with a time, for example the bottom sheet which shows an error message. See [DynamicModalSheetScreen](demo/src/main/kotlin/eu/wewox/modalsheet/screens/DynamicModalSheetScreen.kt). 83 | 84 | Also there is an option to use `ModalSheet` overload with `ModalBottomSheetState` to have a full control when sheet should be shown or hidden. See [SheetStateModalSheetScreen](demo/src/main/kotlin/eu/wewox/modalsheet/screens/SheetStateModalSheetScreen.kt). 85 | 86 | ## Authors 87 | 88 | * [Filip Wiesner](https://github.com/wooodenleg) Initial modal sheet idea and implementation. 89 | 90 | * [Oleksandr Balan](https://github.com/oleksandrbalan) Popup enhancements and support. 91 | 92 | 93 | ## TODO list 94 | 95 | * Provide swipe state to `ModalSheetScope` 96 | -------------------------------------------------------------------------------- /demo/src/main/kotlin/eu/wewox/modalsheet/screens/ModalSheetPaddingScreen.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalSheetApi::class) 2 | 3 | package eu.wewox.modalsheet.screens 4 | 5 | import androidx.compose.foundation.layout.Arrangement 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.PaddingValues 8 | import androidx.compose.foundation.layout.WindowInsets 9 | import androidx.compose.foundation.layout.WindowInsetsSides 10 | import androidx.compose.foundation.layout.asPaddingValues 11 | import androidx.compose.foundation.layout.fillMaxSize 12 | import androidx.compose.foundation.layout.fillMaxWidth 13 | import androidx.compose.foundation.layout.only 14 | import androidx.compose.foundation.layout.padding 15 | import androidx.compose.foundation.layout.safeContent 16 | import androidx.compose.material.Button 17 | import androidx.compose.material.MaterialTheme 18 | import androidx.compose.material.Scaffold 19 | import androidx.compose.material.Text 20 | import androidx.compose.runtime.Composable 21 | import androidx.compose.runtime.getValue 22 | import androidx.compose.runtime.mutableStateOf 23 | import androidx.compose.runtime.saveable.rememberSaveable 24 | import androidx.compose.runtime.setValue 25 | import androidx.compose.ui.Alignment 26 | import androidx.compose.ui.Modifier 27 | import eu.wewox.modalsheet.Example 28 | import eu.wewox.modalsheet.ExperimentalSheetApi 29 | import eu.wewox.modalsheet.ModalSheet 30 | import eu.wewox.modalsheet.ui.components.TopBar 31 | import eu.wewox.modalsheet.ui.theme.SpacingMedium 32 | import eu.wewox.modalsheet.ui.theme.SpacingSmall 33 | 34 | /** 35 | * Example of the sheet padding parameter usage. 36 | */ 37 | @Composable 38 | fun ModalSheetPaddingScreen() { 39 | Scaffold( 40 | topBar = { TopBar(Example.ModalSheetPadding.label) } 41 | ) { padding -> 42 | var visibleSafeContent by rememberSaveable { mutableStateOf(false) } 43 | var visibleOnlyBottom by rememberSaveable { mutableStateOf(false) } 44 | 45 | Column( 46 | verticalArrangement = Arrangement.Center, 47 | horizontalAlignment = Alignment.CenterHorizontally, 48 | modifier = Modifier 49 | .fillMaxSize() 50 | .padding(padding) 51 | ) { 52 | Button(onClick = { visibleSafeContent = true }) { 53 | Text(text = "Show with safe content") 54 | } 55 | Button(onClick = { visibleOnlyBottom = true }) { 56 | Text(text = "Show with only bottom") 57 | } 58 | } 59 | 60 | ModalSheetWithPadding( 61 | visible = visibleSafeContent, 62 | onVisibleChange = { visibleSafeContent = it }, 63 | sheetPadding = WindowInsets.safeContent.asPaddingValues() 64 | ) 65 | 66 | ModalSheetWithPadding( 67 | visible = visibleOnlyBottom, 68 | onVisibleChange = { visibleOnlyBottom = it }, 69 | sheetPadding = WindowInsets.safeContent.only(WindowInsetsSides.Bottom).asPaddingValues() 70 | ) 71 | } 72 | } 73 | 74 | @Composable 75 | private fun ModalSheetWithPadding( 76 | visible: Boolean, 77 | onVisibleChange: (Boolean) -> Unit, 78 | sheetPadding: PaddingValues, 79 | ) { 80 | ModalSheet( 81 | visible = visible, 82 | onVisibleChange = onVisibleChange, 83 | sheetPadding = sheetPadding, 84 | ) { 85 | Column( 86 | horizontalAlignment = Alignment.CenterHorizontally, 87 | verticalArrangement = Arrangement.spacedBy(SpacingSmall), 88 | modifier = Modifier 89 | .fillMaxWidth() 90 | .padding(SpacingMedium) 91 | ) { 92 | Text( 93 | text = "Sheet padding", 94 | style = MaterialTheme.typography.h4 95 | ) 96 | Text( 97 | text = "M3 modal sheet draws a sheet and scrim above and below (on y axis) system bars. To mimic " + 98 | "such behavior it is possible to set sheetPadding, for example WindowInsets.safeContent.", 99 | ) 100 | Button(onClick = { onVisibleChange(false) }) { 101 | Text(text = "Close modal sheet") 102 | } 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /demo/src/main/kotlin/eu/wewox/modalsheet/screens/BackHandlerInModalSheetScreen.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalSheetApi::class) 2 | 3 | package eu.wewox.modalsheet.screens 4 | 5 | import androidx.activity.compose.BackHandler 6 | import androidx.compose.animation.AnimatedContent 7 | import androidx.compose.foundation.layout.Arrangement 8 | import androidx.compose.foundation.layout.Box 9 | import androidx.compose.foundation.layout.Column 10 | import androidx.compose.foundation.layout.Row 11 | import androidx.compose.foundation.layout.fillMaxSize 12 | import androidx.compose.foundation.layout.fillMaxWidth 13 | import androidx.compose.foundation.layout.navigationBarsPadding 14 | import androidx.compose.foundation.layout.padding 15 | import androidx.compose.material.Button 16 | import androidx.compose.material.MaterialTheme 17 | import androidx.compose.material.Scaffold 18 | import androidx.compose.material.Text 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.runtime.getValue 21 | import androidx.compose.runtime.mutableStateOf 22 | import androidx.compose.runtime.remember 23 | import androidx.compose.runtime.saveable.rememberSaveable 24 | import androidx.compose.runtime.setValue 25 | import androidx.compose.ui.Alignment 26 | import androidx.compose.ui.Modifier 27 | import eu.wewox.modalsheet.Example 28 | import eu.wewox.modalsheet.ExperimentalSheetApi 29 | import eu.wewox.modalsheet.ModalSheet 30 | import eu.wewox.modalsheet.ui.components.TopBar 31 | import eu.wewox.modalsheet.ui.theme.SpacingMedium 32 | import eu.wewox.modalsheet.ui.theme.SpacingSmall 33 | 34 | /** 35 | * Example of modal sheet with overridden system back button behavior. 36 | */ 37 | @Composable 38 | fun BackHandlerInModalSheetScreen() { 39 | Scaffold( 40 | topBar = { TopBar(Example.BackHandlerInModalSheet.label) } 41 | ) { padding -> 42 | var visible by rememberSaveable { mutableStateOf(false) } 43 | 44 | Box( 45 | contentAlignment = Alignment.Center, 46 | modifier = Modifier 47 | .fillMaxSize() 48 | .padding(padding) 49 | ) { 50 | Button(onClick = { visible = true }) { 51 | Text(text = "Show modal sheet") 52 | } 53 | } 54 | 55 | ThreeStatesModalSheet( 56 | visible = visible, 57 | onVisibleChange = { visible = it } 58 | ) 59 | } 60 | } 61 | 62 | @Composable 63 | private fun ThreeStatesModalSheet( 64 | visible: Boolean, 65 | onVisibleChange: (Boolean) -> Unit, 66 | ) { 67 | ModalSheet( 68 | visible = visible, 69 | onVisibleChange = onVisibleChange, 70 | onSystemBack = null, 71 | ) { 72 | var state by remember { mutableStateOf(0) } 73 | 74 | AnimatedContent(targetState = state, label = "State animation") { currentState -> 75 | Column( 76 | horizontalAlignment = Alignment.CenterHorizontally, 77 | verticalArrangement = Arrangement.spacedBy(SpacingSmall), 78 | modifier = Modifier 79 | .fillMaxWidth() 80 | .navigationBarsPadding() 81 | .padding(SpacingMedium) 82 | ) { 83 | Text( 84 | text = "State #${currentState + 1}", 85 | style = MaterialTheme.typography.h4 86 | ) 87 | 88 | Text( 89 | text = "Switch between states with next and prev buttons. Also system back button returns to the " + 90 | "previous state or dismisses a modal sheet.", 91 | ) 92 | 93 | Row( 94 | horizontalArrangement = Arrangement.spacedBy(SpacingSmall) 95 | ) { 96 | if (currentState > 0) { 97 | Button(onClick = { state -= 1 }) { 98 | Text(text = "Prev") 99 | } 100 | } 101 | if (currentState < 2) { 102 | Button(onClick = { state += 1 }) { 103 | Text(text = "Next") 104 | } 105 | } 106 | } 107 | } 108 | } 109 | 110 | BackHandler(visible) { 111 | if (state == 0) { 112 | onVisibleChange(false) 113 | } else { 114 | state -= 1 115 | } 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /demo/src/main/kotlin/eu/wewox/modalsheet/screens/DynamicModalSheetScreen.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalSheetApi::class) 2 | 3 | package eu.wewox.modalsheet.screens 4 | 5 | import androidx.compose.foundation.layout.Arrangement 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.foundation.layout.Column 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.foundation.layout.navigationBarsPadding 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.material.Button 13 | import androidx.compose.material.MaterialTheme 14 | import androidx.compose.material.Scaffold 15 | import androidx.compose.material.Text 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.runtime.MutableState 18 | import androidx.compose.runtime.getValue 19 | import androidx.compose.runtime.mutableStateOf 20 | import androidx.compose.runtime.saveable.Saver 21 | import androidx.compose.runtime.saveable.listSaver 22 | import androidx.compose.runtime.saveable.rememberSaveable 23 | import androidx.compose.runtime.setValue 24 | import androidx.compose.ui.Alignment 25 | import androidx.compose.ui.Modifier 26 | import eu.wewox.modalsheet.Example 27 | import eu.wewox.modalsheet.ExperimentalSheetApi 28 | import eu.wewox.modalsheet.ModalSheet 29 | import eu.wewox.modalsheet.ui.components.TopBar 30 | import eu.wewox.modalsheet.ui.theme.SpacingMedium 31 | import eu.wewox.modalsheet.ui.theme.SpacingSmall 32 | 33 | /** 34 | * Showcases the use-case when modal sheet content is dynamic. 35 | * For example an error modal which should be shown when error is not null. 36 | */ 37 | @Composable 38 | fun DynamicModalSheetScreen() { 39 | Scaffold( 40 | topBar = { TopBar(Example.DynamicModalSheet.label) } 41 | ) { padding -> 42 | var error by rememberSaveable(saver = ErrorDataSaver) { mutableStateOf(null) } 43 | 44 | Box( 45 | contentAlignment = Alignment.Center, 46 | modifier = Modifier 47 | .fillMaxSize() 48 | .padding(padding) 49 | ) { 50 | Button(onClick = { 51 | error = ErrorData( 52 | title = "Error", 53 | message = LoremIpsum 54 | ) 55 | }) { 56 | Text(text = "Make an error") 57 | } 58 | } 59 | 60 | ErrorModalSheet( 61 | error = error, 62 | onDataChange = { error = it } 63 | ) 64 | } 65 | } 66 | 67 | @Composable 68 | private fun ErrorModalSheet( 69 | error: ErrorData?, 70 | onDataChange: (ErrorData?) -> Unit, 71 | ) { 72 | ModalSheet( 73 | data = error, 74 | onDataChange = onDataChange 75 | ) { data -> 76 | Column( 77 | horizontalAlignment = Alignment.CenterHorizontally, 78 | verticalArrangement = Arrangement.spacedBy(SpacingSmall), 79 | modifier = Modifier 80 | .fillMaxWidth() 81 | .navigationBarsPadding() 82 | .padding(SpacingMedium) 83 | ) { 84 | Text( 85 | text = data.title, 86 | style = MaterialTheme.typography.h4 87 | ) 88 | Text( 89 | text = data.message, 90 | ) 91 | Button(onClick = { onDataChange(null) }) { 92 | Text(text = "Close modal sheet") 93 | } 94 | } 95 | } 96 | } 97 | 98 | private data class ErrorData(val title: String, val message: String) 99 | 100 | private val ErrorDataSaver: Saver, Any> = listSaver( 101 | save = { listOf(it.value?.title, it.value?.message) }, 102 | restore = { 103 | val title = it[0] 104 | val message = it[1] 105 | mutableStateOf(if (title != null && message != null) ErrorData(title, message) else null) 106 | } 107 | ) 108 | 109 | private val LoremIpsum = """ 110 | Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Mauris dictum facilisis augue. 111 | Vivamus porttitor turpis ac leo. Quisque porta. Nulla pulvinar eleifend sem. Maecenas sollicitudin. 112 | Morbi imperdiet, mauris ac auctor dictum, nisl ligula egestas nulla, et sollicitudin sem purus in lacus. 113 | Phasellus rhoncus. Nulla quis diam. Ut tempus purus at lorem. Vestibulum fermentum tortor id mi. 114 | Nullam feugiat, turpis at pulvinar vulputate, erat libero tristique tellus, nec bibendum odio risus sit amet ante. 115 | """.trimIndent() 116 | -------------------------------------------------------------------------------- /demo/src/main/kotlin/eu/wewox/modalsheet/screens/CustomModalSheetScreen.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalSheetApi::class) 2 | 3 | package eu.wewox.modalsheet.screens 4 | 5 | import androidx.compose.foundation.layout.Arrangement 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.foundation.layout.Column 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.foundation.layout.navigationBarsPadding 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.foundation.layout.size 13 | import androidx.compose.foundation.shape.GenericShape 14 | import androidx.compose.material.Button 15 | import androidx.compose.material.ButtonDefaults 16 | import androidx.compose.material.Icon 17 | import androidx.compose.material.MaterialTheme 18 | import androidx.compose.material.Scaffold 19 | import androidx.compose.material.Text 20 | import androidx.compose.material.icons.Icons 21 | import androidx.compose.material.icons.filled.ThumbUp 22 | import androidx.compose.runtime.Composable 23 | import androidx.compose.runtime.getValue 24 | import androidx.compose.runtime.mutableStateOf 25 | import androidx.compose.runtime.saveable.rememberSaveable 26 | import androidx.compose.runtime.setValue 27 | import androidx.compose.ui.Alignment 28 | import androidx.compose.ui.Modifier 29 | import androidx.compose.ui.unit.dp 30 | import eu.wewox.modalsheet.Example 31 | import eu.wewox.modalsheet.ExperimentalSheetApi 32 | import eu.wewox.modalsheet.ModalSheet 33 | import eu.wewox.modalsheet.ui.components.TopBar 34 | import eu.wewox.modalsheet.ui.theme.SpacingMedium 35 | import eu.wewox.modalsheet.ui.theme.SpacingSmall 36 | 37 | /** 38 | * Showcases the custom [ModalSheet], which has custom background, shape, scrim and can be dismissed only with button. 39 | */ 40 | @Composable 41 | fun CustomModalSheetScreen() { 42 | Scaffold( 43 | topBar = { TopBar(Example.CustomModalSheet.label) } 44 | ) { padding -> 45 | var visible by rememberSaveable { mutableStateOf(false) } 46 | 47 | Box( 48 | contentAlignment = Alignment.Center, 49 | modifier = Modifier 50 | .fillMaxSize() 51 | .padding(padding) 52 | ) { 53 | Button(onClick = { visible = true }) { 54 | Text(text = "Show modal sheet") 55 | } 56 | } 57 | 58 | CustomModalSheet( 59 | visible = visible, 60 | onDismiss = { visible = false } 61 | ) 62 | } 63 | } 64 | 65 | @Composable 66 | private fun CustomModalSheet( 67 | visible: Boolean, 68 | onDismiss: () -> Unit, 69 | ) { 70 | ModalSheet( 71 | visible = visible, 72 | cancelable = false, 73 | shape = createWaveShape(), 74 | elevation = 0.dp, 75 | backgroundColor = MaterialTheme.colors.secondary, 76 | scrimColor = MaterialTheme.colors.primary.copy(alpha = 0.65f), 77 | onVisibleChange = { /* NoOp */ } 78 | ) { 79 | Column( 80 | horizontalAlignment = Alignment.CenterHorizontally, 81 | verticalArrangement = Arrangement.spacedBy(SpacingSmall), 82 | modifier = Modifier 83 | .fillMaxWidth() 84 | .navigationBarsPadding() 85 | .padding(SpacingMedium) 86 | ) { 87 | Icon( 88 | imageVector = Icons.Default.ThumbUp, 89 | contentDescription = null, 90 | tint = MaterialTheme.colors.primary, 91 | modifier = Modifier 92 | .padding(24.dp) 93 | .size(96.dp) 94 | ) 95 | Text( 96 | text = "Feel free to modify modal's shape, background color, scrim and interaction flags.", 97 | ) 98 | Button( 99 | onClick = onDismiss, 100 | elevation = ButtonDefaults.elevation(0.dp, 0.dp), 101 | ) { 102 | Text(text = "Close modal sheet") 103 | } 104 | } 105 | } 106 | } 107 | 108 | private fun createWaveShape(multiplier: Int = 8) = GenericShape { size, _ -> 109 | moveTo(0f, 40f * multiplier) 110 | lineTo(size.width / 2f - 60f * multiplier, 40f * multiplier) 111 | quadraticBezierTo( 112 | size.width / 2f - 40f * multiplier, 40f * multiplier, 113 | size.width / 2f - 20f * multiplier, 20f * multiplier 114 | ) 115 | quadraticBezierTo(size.width / 2f, 0f, size.width / 2f + 20f * multiplier, 20f * multiplier) 116 | quadraticBezierTo( 117 | size.width / 2f + 40f * multiplier, 40f * multiplier, 118 | size.width / 2f + 60f * multiplier, 40f * multiplier 119 | ) 120 | lineTo(size.width, 40f * multiplier) 121 | lineTo(size.width, size.height) 122 | lineTo(0f, size.height) 123 | } 124 | -------------------------------------------------------------------------------- /demo/src/main/kotlin/eu/wewox/modalsheet/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package eu.wewox.modalsheet 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.BackHandler 6 | import androidx.activity.compose.setContent 7 | import androidx.compose.animation.Crossfade 8 | import androidx.compose.foundation.clickable 9 | import androidx.compose.foundation.layout.Column 10 | import androidx.compose.foundation.layout.Row 11 | import androidx.compose.foundation.layout.fillMaxWidth 12 | import androidx.compose.foundation.layout.padding 13 | import androidx.compose.foundation.lazy.LazyColumn 14 | import androidx.compose.foundation.lazy.items 15 | import androidx.compose.material.Icon 16 | import androidx.compose.material.MaterialTheme 17 | import androidx.compose.material.Scaffold 18 | import androidx.compose.material.Text 19 | import androidx.compose.material.icons.Icons 20 | import androidx.compose.material.icons.filled.KeyboardArrowRight 21 | import androidx.compose.runtime.Composable 22 | import androidx.compose.runtime.getValue 23 | import androidx.compose.runtime.mutableStateOf 24 | import androidx.compose.runtime.saveable.rememberSaveable 25 | import androidx.compose.runtime.setValue 26 | import androidx.compose.ui.Alignment 27 | import androidx.compose.ui.Modifier 28 | import androidx.core.view.WindowCompat 29 | import eu.wewox.modalsheet.screens.BackHandlerInModalSheetScreen 30 | import eu.wewox.modalsheet.screens.CustomModalSheetScreen 31 | import eu.wewox.modalsheet.screens.DynamicModalSheetScreen 32 | import eu.wewox.modalsheet.screens.ModalSheetPaddingScreen 33 | import eu.wewox.modalsheet.screens.ScrollableModalSheetScreen 34 | import eu.wewox.modalsheet.screens.SheetAboveBottomBarScreen 35 | import eu.wewox.modalsheet.screens.SheetStateModalSheetScreen 36 | import eu.wewox.modalsheet.screens.SimpleModalSheetScreen 37 | import eu.wewox.modalsheet.ui.components.TopBar 38 | import eu.wewox.modalsheet.ui.theme.ModalSheetDemoTheme 39 | import eu.wewox.modalsheet.ui.theme.SpacingMedium 40 | 41 | /** 42 | * Main activity for demo application. 43 | * Contains simple "Crossfade" based navigation to various examples. 44 | */ 45 | class MainActivity : ComponentActivity() { 46 | override fun onCreate(savedInstanceState: Bundle?) { 47 | super.onCreate(savedInstanceState) 48 | 49 | WindowCompat.setDecorFitsSystemWindows(window, false) 50 | 51 | setContent { 52 | ModalSheetDemoTheme { 53 | var example by rememberSaveable { mutableStateOf(null) } 54 | 55 | BackHandler(enabled = example != null) { 56 | example = null 57 | } 58 | 59 | Crossfade(targetState = example) { selected -> 60 | when (selected) { 61 | null -> RootScreen(onExampleClick = { example = it }) 62 | Example.SimpleModalSheet -> SimpleModalSheetScreen() 63 | Example.SheetAboveBottomBar -> SheetAboveBottomBarScreen() 64 | Example.DynamicModalSheet -> DynamicModalSheetScreen() 65 | Example.ScrollableModalSheet -> ScrollableModalSheetScreen() 66 | Example.CustomModalSheet -> CustomModalSheetScreen() 67 | Example.SheetStateModalSheet -> SheetStateModalSheetScreen() 68 | Example.ModalSheetPadding -> ModalSheetPaddingScreen() 69 | Example.BackHandlerInModalSheet -> BackHandlerInModalSheetScreen() 70 | } 71 | } 72 | } 73 | } 74 | } 75 | } 76 | 77 | @Composable 78 | private fun RootScreen(onExampleClick: (Example) -> Unit) { 79 | Scaffold( 80 | topBar = { TopBar("Modal Sheet Demo") } 81 | ) { padding -> 82 | LazyColumn(Modifier.padding(padding)) { 83 | items(Example.values()) { 84 | Row( 85 | verticalAlignment = Alignment.CenterVertically, 86 | modifier = Modifier 87 | .fillMaxWidth() 88 | .clickable { onExampleClick(it) } 89 | .padding(SpacingMedium) 90 | ) { 91 | Column(modifier = Modifier.weight(1f)) { 92 | Text( 93 | text = it.label, 94 | style = MaterialTheme.typography.h6 95 | ) 96 | Text( 97 | text = it.description, 98 | style = MaterialTheme.typography.body2 99 | ) 100 | } 101 | Icon( 102 | imageVector = Icons.Default.KeyboardArrowRight, 103 | contentDescription = null 104 | ) 105 | } 106 | } 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /demo/src/main/kotlin/eu/wewox/modalsheet/screens/SheetStateModalSheetScreen.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalSheetApi::class, ExperimentalMaterialApi::class) 2 | 3 | package eu.wewox.modalsheet.screens 4 | 5 | import androidx.compose.foundation.layout.Arrangement 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.foundation.layout.Column 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.foundation.layout.navigationBarsPadding 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.foundation.layout.size 13 | import androidx.compose.material.Button 14 | import androidx.compose.material.ExperimentalMaterialApi 15 | import androidx.compose.material.Icon 16 | import androidx.compose.material.MaterialTheme 17 | import androidx.compose.material.ModalBottomSheetState 18 | import androidx.compose.material.ModalBottomSheetValue 19 | import androidx.compose.material.Scaffold 20 | import androidx.compose.material.Text 21 | import androidx.compose.material.icons.Icons 22 | import androidx.compose.material.icons.rounded.KeyboardArrowDown 23 | import androidx.compose.material.icons.rounded.KeyboardArrowLeft 24 | import androidx.compose.material.icons.rounded.KeyboardArrowRight 25 | import androidx.compose.material.icons.rounded.KeyboardArrowUp 26 | import androidx.compose.material.rememberModalBottomSheetState 27 | import androidx.compose.runtime.Composable 28 | import androidx.compose.runtime.getValue 29 | import androidx.compose.runtime.mutableStateOf 30 | import androidx.compose.runtime.remember 31 | import androidx.compose.runtime.rememberCoroutineScope 32 | import androidx.compose.runtime.setValue 33 | import androidx.compose.ui.Alignment 34 | import androidx.compose.ui.Modifier 35 | import androidx.compose.ui.graphics.vector.ImageVector 36 | import androidx.compose.ui.unit.dp 37 | import eu.wewox.modalsheet.Example 38 | import eu.wewox.modalsheet.ExperimentalSheetApi 39 | import eu.wewox.modalsheet.ModalSheet 40 | import eu.wewox.modalsheet.ui.components.TopBar 41 | import eu.wewox.modalsheet.ui.theme.SpacingMedium 42 | import eu.wewox.modalsheet.ui.theme.SpacingSmall 43 | import kotlinx.coroutines.launch 44 | 45 | /** 46 | * Showcases the most lower-level [ModalSheet] usage with [ModalBottomSheetState]. 47 | */ 48 | @Composable 49 | fun SheetStateModalSheetScreen() { 50 | Scaffold( 51 | topBar = { TopBar(Example.SheetStateModalSheet.label) } 52 | ) { padding -> 53 | val scope = rememberCoroutineScope() 54 | var pageIndex by remember { mutableStateOf(0) } 55 | val sheetState = rememberModalBottomSheetState( 56 | skipHalfExpanded = true, 57 | initialValue = ModalBottomSheetValue.Hidden, 58 | ) 59 | 60 | Box( 61 | contentAlignment = Alignment.Center, 62 | modifier = Modifier 63 | .fillMaxSize() 64 | .padding(padding) 65 | ) { 66 | Button( 67 | onClick = { 68 | scope.launch { 69 | pageIndex = 0 70 | sheetState.show() 71 | } 72 | } 73 | ) { 74 | Text(text = "Show modal sheet") 75 | } 76 | } 77 | 78 | PageModalSheet( 79 | sheetState = sheetState, 80 | pageIndex = pageIndex, 81 | onNext = { 82 | scope.launch { 83 | sheetState.hide() 84 | if (pageIndex < Pages.lastIndex) { 85 | pageIndex += 1 86 | sheetState.show() 87 | } 88 | } 89 | }, 90 | onPrev = { 91 | scope.launch { 92 | sheetState.hide() 93 | if (pageIndex > 0) { 94 | pageIndex -= 1 95 | sheetState.show() 96 | } 97 | } 98 | }, 99 | ) 100 | } 101 | } 102 | 103 | @Composable 104 | private fun PageModalSheet( 105 | sheetState: ModalBottomSheetState, 106 | pageIndex: Int, 107 | onNext: () -> Unit, 108 | onPrev: () -> Unit, 109 | ) { 110 | ModalSheet( 111 | sheetState = sheetState, 112 | onSystemBack = if (sheetState.isVisible || sheetState.currentValue != sheetState.targetValue) onPrev else null 113 | ) { 114 | Column( 115 | horizontalAlignment = Alignment.CenterHorizontally, 116 | verticalArrangement = Arrangement.spacedBy(SpacingSmall), 117 | modifier = Modifier 118 | .fillMaxWidth() 119 | .navigationBarsPadding() 120 | .padding(SpacingMedium) 121 | ) { 122 | val page = Pages[pageIndex] 123 | Text( 124 | text = page.title, 125 | style = MaterialTheme.typography.h4 126 | ) 127 | Icon( 128 | imageVector = page.icon, 129 | contentDescription = page.title, 130 | tint = MaterialTheme.colors.primary, 131 | modifier = Modifier.size(256.dp) 132 | ) 133 | Button( 134 | onClick = onNext 135 | ) { 136 | Text(text = if (pageIndex < Pages.lastIndex) "Show next" else "Close") 137 | } 138 | } 139 | } 140 | } 141 | 142 | private data class Page( 143 | val title: String, 144 | val icon: ImageVector, 145 | ) 146 | 147 | private val Pages = listOf( 148 | Page("Left", Icons.Rounded.KeyboardArrowLeft), 149 | Page("Up", Icons.Rounded.KeyboardArrowUp), 150 | Page("Right", Icons.Rounded.KeyboardArrowRight), 151 | Page("Down", Icons.Rounded.KeyboardArrowDown), 152 | ) 153 | -------------------------------------------------------------------------------- /modalsheet/src/main/java/eu/wewox/modalsheet/FullscreenPopup.kt: -------------------------------------------------------------------------------- 1 | package eu.wewox.modalsheet 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.Activity 5 | import android.content.Context 6 | import android.content.ContextWrapper 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import androidx.activity.compose.BackHandler 10 | import androidx.compose.foundation.layout.Box 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.CompositionContext 13 | import androidx.compose.runtime.DisposableEffect 14 | import androidx.compose.runtime.SideEffect 15 | import androidx.compose.runtime.getValue 16 | import androidx.compose.runtime.mutableStateOf 17 | import androidx.compose.runtime.remember 18 | import androidx.compose.runtime.rememberCompositionContext 19 | import androidx.compose.runtime.rememberUpdatedState 20 | import androidx.compose.runtime.saveable.rememberSaveable 21 | import androidx.compose.runtime.setValue 22 | import androidx.compose.ui.Modifier 23 | import androidx.compose.ui.R 24 | import androidx.compose.ui.platform.AbstractComposeView 25 | import androidx.compose.ui.platform.LocalView 26 | import androidx.compose.ui.platform.ViewRootForInspector 27 | import androidx.compose.ui.semantics.popup 28 | import androidx.compose.ui.semantics.semantics 29 | import androidx.core.view.children 30 | import androidx.lifecycle.findViewTreeLifecycleOwner 31 | import androidx.lifecycle.findViewTreeViewModelStoreOwner 32 | import androidx.lifecycle.setViewTreeLifecycleOwner 33 | import androidx.lifecycle.setViewTreeViewModelStoreOwner 34 | import androidx.savedstate.findViewTreeSavedStateRegistryOwner 35 | import androidx.savedstate.setViewTreeSavedStateRegistryOwner 36 | import java.util.UUID 37 | 38 | /** 39 | * Opens a popup with the given content. 40 | * The popup is visible as long as it is part of the composition hierarchy. 41 | * 42 | * Note: This is highly reduced version of the official Popup composable with some changes: 43 | * * Fixes an issue with action mode (copy-paste) menu, see https://issuetracker.google.com/issues/216662636 44 | * * Adds the view to the decor view of the window, instead of the window itself. 45 | * * Do not have properties, as Popup is laid out as fullscreen. 46 | * 47 | * @param onSystemBack The action invoked by pressing the system back button. 48 | * @param content The content to be displayed inside the popup. 49 | */ 50 | @ExperimentalSheetApi 51 | @Composable 52 | internal fun FullscreenPopup( 53 | onSystemBack: (() -> Unit)? = null, 54 | content: @Composable () -> Unit 55 | ) { 56 | val view = LocalView.current 57 | val parentComposition = rememberCompositionContext() 58 | val currentContent by rememberUpdatedState(content) 59 | val popupId = rememberSaveable { UUID.randomUUID() } 60 | val popupLayout = remember { 61 | PopupLayout( 62 | onSystemBack = onSystemBack, 63 | composeView = view, 64 | popupId = popupId 65 | ).apply { 66 | setContent(parentComposition) { 67 | Box(Modifier.semantics { this.popup() }) { 68 | currentContent() 69 | } 70 | } 71 | } 72 | } 73 | 74 | DisposableEffect(popupLayout) { 75 | popupLayout.show() 76 | popupLayout.updateParameters( 77 | onSystemBack = onSystemBack 78 | ) 79 | onDispose { 80 | popupLayout.disposeComposition() 81 | // Remove the window 82 | popupLayout.dismiss() 83 | } 84 | } 85 | 86 | SideEffect { 87 | popupLayout.updateParameters( 88 | onSystemBack = onSystemBack 89 | ) 90 | } 91 | } 92 | 93 | /** 94 | * The layout the popup uses to display its content. 95 | */ 96 | @SuppressLint("ViewConstructor") 97 | private class PopupLayout( 98 | onSystemBack: (() -> Unit)?, 99 | composeView: View, 100 | popupId: UUID 101 | ) : AbstractComposeView(composeView.context), 102 | ViewRootForInspector { 103 | 104 | private val decorView = findOwner(composeView.context)?.window?.decorView as ViewGroup 105 | 106 | override val subCompositionView: AbstractComposeView get() = this 107 | 108 | init { 109 | id = android.R.id.content 110 | setViewTreeLifecycleOwner(composeView.findViewTreeLifecycleOwner()) 111 | setViewTreeViewModelStoreOwner(composeView.findViewTreeViewModelStoreOwner()) 112 | setViewTreeSavedStateRegistryOwner(composeView.findViewTreeSavedStateRegistryOwner()) 113 | // Set unique id for AbstractComposeView. This allows state restoration for the state 114 | // defined inside the Popup via rememberSaveable() 115 | setTag(R.id.compose_view_saveable_id_tag, "Popup:$popupId") 116 | setTag(R.id.consume_window_insets_tag, false) 117 | } 118 | 119 | private var content: @Composable () -> Unit by mutableStateOf({}) 120 | 121 | private var onSystemBackState by mutableStateOf(onSystemBack) 122 | 123 | override var shouldCreateCompositionOnAttachedToWindow: Boolean = false 124 | private set 125 | 126 | fun show() { 127 | // Place popup above all current views 128 | z = decorView.children.maxOf { it.z } + 1 129 | decorView.addView( 130 | this, 131 | 0, 132 | MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) 133 | ) 134 | 135 | requestFocus() 136 | } 137 | 138 | fun setContent(parent: CompositionContext, content: @Composable () -> Unit) { 139 | setParentCompositionContext(parent) 140 | this.content = content 141 | shouldCreateCompositionOnAttachedToWindow = true 142 | } 143 | 144 | @Composable 145 | override fun Content() { 146 | BackHandler(onSystemBackState != null) { 147 | onSystemBackState?.invoke() 148 | } 149 | content() 150 | } 151 | 152 | fun updateParameters( 153 | onSystemBack: (() -> Unit)? 154 | ) { 155 | onSystemBackState = onSystemBack 156 | } 157 | 158 | fun dismiss() { 159 | setViewTreeLifecycleOwner(null) 160 | decorView.removeView(this) 161 | } 162 | } 163 | 164 | private inline fun findOwner(context: Context): T? { 165 | var innerContext = context 166 | while (innerContext is ContextWrapper) { 167 | if (innerContext is T) { 168 | return innerContext 169 | } 170 | innerContext = innerContext.baseContext 171 | } 172 | return null 173 | } 174 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /demo/src/main/kotlin/eu/wewox/modalsheet/screens/SheetAboveBottomBarScreen.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalSheetApi::class) 2 | 3 | package eu.wewox.modalsheet.screens 4 | 5 | import androidx.compose.animation.Crossfade 6 | import androidx.compose.foundation.clickable 7 | import androidx.compose.foundation.layout.Arrangement 8 | import androidx.compose.foundation.layout.Box 9 | import androidx.compose.foundation.layout.Column 10 | import androidx.compose.foundation.layout.Row 11 | import androidx.compose.foundation.layout.fillMaxSize 12 | import androidx.compose.foundation.layout.fillMaxWidth 13 | import androidx.compose.foundation.layout.navigationBarsPadding 14 | import androidx.compose.foundation.layout.padding 15 | import androidx.compose.material.Button 16 | import androidx.compose.material.ButtonDefaults 17 | import androidx.compose.material.LocalTextStyle 18 | import androidx.compose.material.MaterialTheme 19 | import androidx.compose.material.Scaffold 20 | import androidx.compose.material.Text 21 | import androidx.compose.runtime.Composable 22 | import androidx.compose.runtime.getValue 23 | import androidx.compose.runtime.mutableStateOf 24 | import androidx.compose.runtime.remember 25 | import androidx.compose.runtime.saveable.rememberSaveable 26 | import androidx.compose.runtime.setValue 27 | import androidx.compose.ui.Alignment 28 | import androidx.compose.ui.Modifier 29 | import androidx.compose.ui.text.font.FontWeight 30 | import eu.wewox.modalsheet.ExperimentalSheetApi 31 | import eu.wewox.modalsheet.ModalSheet 32 | import eu.wewox.modalsheet.ui.components.BottomNavItem 33 | import eu.wewox.modalsheet.ui.components.BottomNavigation 34 | import eu.wewox.modalsheet.ui.components.TopBar 35 | import eu.wewox.modalsheet.ui.theme.SpacingMedium 36 | import eu.wewox.modalsheet.ui.theme.SpacingSmall 37 | 38 | /** 39 | * Showcases that [ModalSheet] could be displayed above the bottom bar navigation. 40 | */ 41 | @Composable 42 | fun SheetAboveBottomBarScreen() { 43 | var item by remember { mutableStateOf(BottomNavItem.Home) } 44 | Scaffold( 45 | bottomBar = { 46 | BottomNavigation( 47 | current = item, 48 | onClick = { item = it }, 49 | ) 50 | } 51 | ) { padding -> 52 | Crossfade(targetState = item, label = "Tab animation") { current -> 53 | Scaffold( 54 | topBar = { TopBar(current.title) }, 55 | modifier = Modifier.padding(padding) 56 | ) { 57 | Box( 58 | contentAlignment = Alignment.Center, 59 | modifier = Modifier 60 | .fillMaxSize() 61 | .padding(it) 62 | ) { 63 | when (current) { 64 | BottomNavItem.Home -> HomeScreen() 65 | BottomNavItem.Counter -> CounterScreen() 66 | BottomNavItem.Profile -> ProfileScreen() 67 | } 68 | } 69 | } 70 | } 71 | } 72 | } 73 | 74 | @Composable 75 | private fun HomeScreen() { 76 | var visible by rememberSaveable { mutableStateOf(false) } 77 | 78 | Button(onClick = { visible = true }) { 79 | Text(text = "Show modal sheet") 80 | } 81 | 82 | SimpleModalSheet( 83 | title = "Hello from Home", 84 | visible = visible, 85 | onVisibleChange = { visible = it } 86 | ) 87 | } 88 | 89 | @Composable 90 | private fun CounterScreen() { 91 | var visible by rememberSaveable { mutableStateOf(false) } 92 | var counter by rememberSaveable { mutableStateOf(0) } 93 | 94 | Column { 95 | Counter( 96 | counter = counter, 97 | onPlus = { counter++ }, 98 | onMinus = { counter-- }, 99 | ) 100 | Button(onClick = { visible = true }) { 101 | Text(text = "Show modal sheet") 102 | } 103 | } 104 | 105 | ModalSheet( 106 | visible = visible, 107 | onVisibleChange = { visible = it } 108 | ) { 109 | Column( 110 | horizontalAlignment = Alignment.CenterHorizontally, 111 | verticalArrangement = Arrangement.spacedBy(SpacingSmall), 112 | modifier = Modifier 113 | .fillMaxWidth() 114 | .navigationBarsPadding() 115 | .padding(SpacingMedium) 116 | ) { 117 | Text( 118 | text = "Hello from Counter", 119 | style = MaterialTheme.typography.h4 120 | ) 121 | Text( 122 | text = "Modal also can share state with a scope where it is placed. For example a counter value.", 123 | ) 124 | Counter( 125 | counter = counter, 126 | onPlus = { counter++ }, 127 | onMinus = { counter-- }, 128 | ) 129 | } 130 | } 131 | } 132 | 133 | @Composable 134 | private fun ProfileScreen() { 135 | val statuses = listOf("Not set", "Online", "Offline", "Away", "Busy") 136 | 137 | var visible by rememberSaveable { mutableStateOf(false) } 138 | var status by rememberSaveable { mutableStateOf(statuses.first()) } 139 | 140 | Column( 141 | horizontalAlignment = Alignment.CenterHorizontally, 142 | ) { 143 | Text( 144 | text = status, 145 | style = LocalTextStyle.current.copy(fontWeight = FontWeight.Bold), 146 | ) 147 | Button(onClick = { visible = true }) { 148 | Text(text = "Change status") 149 | } 150 | } 151 | 152 | ModalSheet( 153 | visible = visible, 154 | onVisibleChange = { visible = it } 155 | ) { 156 | Column( 157 | modifier = Modifier 158 | .fillMaxWidth() 159 | .navigationBarsPadding() 160 | ) { 161 | Column( 162 | horizontalAlignment = Alignment.CenterHorizontally, 163 | verticalArrangement = Arrangement.spacedBy(SpacingSmall), 164 | modifier = Modifier.padding(SpacingMedium) 165 | ) { 166 | Text( 167 | text = "Hello from Profile", 168 | style = MaterialTheme.typography.h4 169 | ) 170 | Text( 171 | text = "Modal can access scope states and modify them, thus is it convenient to use it as picker.", 172 | ) 173 | } 174 | statuses.forEach { 175 | Text( 176 | text = it, 177 | style = LocalTextStyle.current.copy(fontWeight = FontWeight.Bold), 178 | modifier = Modifier 179 | .fillMaxWidth() 180 | .clickable { 181 | status = it 182 | visible = false 183 | } 184 | .padding(SpacingMedium) 185 | ) 186 | } 187 | } 188 | } 189 | } 190 | 191 | @Composable 192 | private fun Counter( 193 | counter: Int, 194 | onPlus: () -> Unit, 195 | onMinus: () -> Unit, 196 | ) { 197 | Row( 198 | verticalAlignment = Alignment.CenterVertically, 199 | horizontalArrangement = Arrangement.spacedBy(SpacingSmall) 200 | ) { 201 | Button(onClick = onMinus, colors = ButtonDefaults.buttonColors(MaterialTheme.colors.secondary)) { 202 | Text(text = "-") 203 | } 204 | Text(text = counter.toString()) 205 | Button(onClick = onPlus, colors = ButtonDefaults.buttonColors(MaterialTheme.colors.secondary)) { 206 | Text(text = "+") 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 207 | -------------------------------------------------------------------------------- /demo/src/main/kotlin/eu/wewox/modalsheet/screens/ScrollableModalSheetScreen.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalSheetApi::class) 2 | 3 | package eu.wewox.modalsheet.screens 4 | 5 | import androidx.compose.foundation.background 6 | import androidx.compose.foundation.layout.Arrangement 7 | import androidx.compose.foundation.layout.Box 8 | import androidx.compose.foundation.layout.Column 9 | import androidx.compose.foundation.layout.fillMaxSize 10 | import androidx.compose.foundation.layout.fillMaxWidth 11 | import androidx.compose.foundation.layout.navigationBarsPadding 12 | import androidx.compose.foundation.layout.padding 13 | import androidx.compose.foundation.layout.statusBarsPadding 14 | import androidx.compose.foundation.layout.systemBarsPadding 15 | import androidx.compose.foundation.lazy.LazyColumn 16 | import androidx.compose.foundation.lazy.grid.GridCells 17 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid 18 | import androidx.compose.foundation.rememberScrollState 19 | import androidx.compose.foundation.verticalScroll 20 | import androidx.compose.material.Button 21 | import androidx.compose.material.MaterialTheme 22 | import androidx.compose.material.Scaffold 23 | import androidx.compose.material.Text 24 | import androidx.compose.runtime.Composable 25 | import androidx.compose.runtime.getValue 26 | import androidx.compose.runtime.mutableStateOf 27 | import androidx.compose.runtime.saveable.rememberSaveable 28 | import androidx.compose.runtime.setValue 29 | import androidx.compose.ui.Alignment 30 | import androidx.compose.ui.Modifier 31 | import androidx.compose.ui.unit.dp 32 | import eu.wewox.modalsheet.Example 33 | import eu.wewox.modalsheet.ExperimentalSheetApi 34 | import eu.wewox.modalsheet.ModalSheet 35 | import eu.wewox.modalsheet.ui.components.TopBar 36 | import eu.wewox.modalsheet.ui.theme.SpacingMedium 37 | import eu.wewox.modalsheet.ui.theme.SpacingSmall 38 | 39 | /** 40 | * Showcases the scroll support in the [ModalSheet] body. 41 | */ 42 | @Composable 43 | fun ScrollableModalSheetScreen() { 44 | Scaffold( 45 | topBar = { TopBar(Example.ScrollableModalSheet.label) } 46 | ) { padding -> 47 | Column( 48 | modifier = Modifier 49 | .fillMaxSize() 50 | .padding(padding) 51 | .padding(SpacingMedium) 52 | ) { 53 | var verticalScrollVisible by rememberSaveable { mutableStateOf(false) } 54 | var verticalScrollWithFixedVisible by rememberSaveable { mutableStateOf(false) } 55 | var lazyColumnVisible by rememberSaveable { mutableStateOf(false) } 56 | var lazyGridVisible by rememberSaveable { mutableStateOf(false) } 57 | 58 | Column( 59 | horizontalAlignment = Alignment.CenterHorizontally, 60 | verticalArrangement = Arrangement.Center, 61 | modifier = Modifier 62 | .fillMaxWidth() 63 | .weight(1f) 64 | ) { 65 | Button(onClick = { verticalScrollVisible = true }) { 66 | Text(text = "Vertical scroll") 67 | } 68 | 69 | Button(onClick = { verticalScrollWithFixedVisible = true }) { 70 | Text(text = "Vertical scroll with fixed") 71 | } 72 | 73 | Button(onClick = { lazyColumnVisible = true }) { 74 | Text(text = "Lazy list") 75 | } 76 | 77 | Button(onClick = { lazyGridVisible = true }) { 78 | Text(text = "Lazy grid") 79 | } 80 | } 81 | 82 | ScrollableModalSheet( 83 | visible = verticalScrollVisible, 84 | onVisibleChange = { verticalScrollVisible = it } 85 | ) 86 | 87 | ScrollableWithFixedPartsModalSheet( 88 | visible = verticalScrollWithFixedVisible, 89 | onVisibleChange = { verticalScrollWithFixedVisible = it } 90 | ) 91 | 92 | LazyColumnModalSheet( 93 | visible = lazyColumnVisible, 94 | onVisibleChange = { lazyColumnVisible = it } 95 | ) 96 | 97 | LazyGridModalSheet( 98 | visible = lazyGridVisible, 99 | onVisibleChange = { lazyGridVisible = it } 100 | ) 101 | } 102 | } 103 | } 104 | 105 | @Composable 106 | private fun ScrollableModalSheet( 107 | visible: Boolean, 108 | onVisibleChange: (Boolean) -> Unit, 109 | ) { 110 | ModalSheet( 111 | visible = visible, 112 | onVisibleChange = onVisibleChange 113 | ) { 114 | Column( 115 | horizontalAlignment = Alignment.CenterHorizontally, 116 | verticalArrangement = Arrangement.spacedBy(SpacingSmall), 117 | modifier = Modifier 118 | .verticalScroll(rememberScrollState()) 119 | .fillMaxWidth() 120 | .padding(SpacingMedium) 121 | .systemBarsPadding() 122 | ) { 123 | Text( 124 | text = "This Scrolls", 125 | style = MaterialTheme.typography.h4 126 | ) 127 | repeat(100) { 128 | Text( 129 | text = "Item #$it", 130 | modifier = Modifier.fillMaxWidth() 131 | ) 132 | } 133 | } 134 | } 135 | } 136 | 137 | @Composable 138 | private fun ScrollableWithFixedPartsModalSheet( 139 | visible: Boolean, 140 | onVisibleChange: (Boolean) -> Unit, 141 | ) { 142 | ModalSheet( 143 | visible = visible, 144 | onVisibleChange = onVisibleChange 145 | ) { 146 | Box( 147 | modifier = Modifier 148 | .fillMaxWidth() 149 | ) { 150 | Column( 151 | modifier = Modifier 152 | .verticalScroll(rememberScrollState()) 153 | .systemBarsPadding() 154 | .padding( 155 | top = 56.dp, 156 | bottom = 64.dp, 157 | start = SpacingMedium, 158 | end = SpacingMedium 159 | ) 160 | ) { 161 | repeat(100) { 162 | Text( 163 | text = "Item #$it", 164 | modifier = Modifier.fillMaxWidth() 165 | ) 166 | } 167 | } 168 | Text( 169 | text = "Fixed Header", 170 | style = MaterialTheme.typography.h4, 171 | modifier = Modifier 172 | .background(MaterialTheme.colors.surface) 173 | .padding(SpacingMedium) 174 | .statusBarsPadding() 175 | ) 176 | Button( 177 | onClick = { onVisibleChange(false) }, 178 | modifier = Modifier 179 | .fillMaxWidth() 180 | .align(Alignment.BottomCenter) 181 | .padding(SpacingMedium) 182 | .navigationBarsPadding() 183 | ) { 184 | Text(text = "Fixed Button") 185 | } 186 | } 187 | } 188 | } 189 | 190 | @Composable 191 | private fun LazyColumnModalSheet( 192 | visible: Boolean, 193 | onVisibleChange: (Boolean) -> Unit, 194 | ) { 195 | ModalSheet( 196 | visible = visible, 197 | onVisibleChange = onVisibleChange 198 | ) { 199 | LazyColumn( 200 | horizontalAlignment = Alignment.CenterHorizontally, 201 | verticalArrangement = Arrangement.spacedBy(SpacingSmall), 202 | modifier = Modifier 203 | .fillMaxWidth() 204 | .padding(SpacingMedium) 205 | .systemBarsPadding() 206 | ) { 207 | item { 208 | Text( 209 | text = "This Scrolls", 210 | style = MaterialTheme.typography.h4 211 | ) 212 | } 213 | 214 | items(100) { 215 | Text( 216 | text = "Item #$it", 217 | modifier = Modifier.fillMaxWidth() 218 | ) 219 | } 220 | } 221 | } 222 | } 223 | 224 | @Composable 225 | private fun LazyGridModalSheet( 226 | visible: Boolean, 227 | onVisibleChange: (Boolean) -> Unit, 228 | ) { 229 | ModalSheet( 230 | visible = visible, 231 | onVisibleChange = onVisibleChange 232 | ) { 233 | LazyVerticalGrid( 234 | columns = GridCells.Fixed(2), 235 | verticalArrangement = Arrangement.spacedBy(SpacingSmall), 236 | modifier = Modifier 237 | .fillMaxWidth() 238 | .padding(SpacingMedium) 239 | .systemBarsPadding() 240 | ) { 241 | items(100) { 242 | Text( 243 | text = "Item #$it", 244 | modifier = Modifier.fillMaxWidth() 245 | ) 246 | } 247 | } 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /modalsheet/src/main/java/eu/wewox/modalsheet/ModalSheet.kt: -------------------------------------------------------------------------------- 1 | package eu.wewox.modalsheet 2 | 3 | import androidx.compose.foundation.layout.ColumnScope 4 | import androidx.compose.foundation.layout.PaddingValues 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.material.MaterialTheme 7 | import androidx.compose.material.ModalBottomSheetDefaults 8 | import androidx.compose.material.ModalBottomSheetLayout 9 | import androidx.compose.material.ModalBottomSheetState 10 | import androidx.compose.material.ModalBottomSheetValue 11 | import androidx.compose.material.contentColorFor 12 | import androidx.compose.material.rememberModalBottomSheetState 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.runtime.DisposableEffect 15 | import androidx.compose.runtime.LaunchedEffect 16 | import androidx.compose.runtime.getValue 17 | import androidx.compose.runtime.mutableStateOf 18 | import androidx.compose.runtime.remember 19 | import androidx.compose.runtime.setValue 20 | import androidx.compose.ui.Modifier 21 | import androidx.compose.ui.draw.clipToBounds 22 | import androidx.compose.ui.graphics.Color 23 | import androidx.compose.ui.graphics.Shape 24 | import androidx.compose.ui.unit.Dp 25 | import androidx.compose.ui.unit.dp 26 | 27 | /** 28 | * Modal sheet that behaves like bottom sheet and draws over system UI. 29 | * 30 | * @param data Value that determines the content of the sheet. Sheet is closed (or remains closed) when null is passed. 31 | * @param onDataChange Called when data changes as a result of the visibility change. 32 | * @param cancelable When true, this modal sheet can be closed with swipe gesture, tap on scrim or tap on system back 33 | * button. Note: passing 'false' does not disable the interaction with the sheet. Only the resulting state after the 34 | * sheet settles. 35 | * @param onSystemBack The action invoked by pressing the system back button. By default sets the data to 'null' if 36 | * [cancelable] is set to true. Could be overridden or set to 'null' to react on system back button press. 37 | * @param shape The shape of the bottom sheet. 38 | * @param elevation The elevation of the bottom sheet. 39 | * @param backgroundColor The background color of the bottom sheet. 40 | * @param contentColor The preferred content color provided by the bottom sheet to its 41 | * children. Defaults to the matching content color for [backgroundColor], or if that is not 42 | * a color from the theme, this will keep the same content color set above the bottom sheet. 43 | * @param scrimColor The color of the scrim that is applied to the rest of the screen when the 44 | * bottom sheet is visible. If the color passed is [Color.Unspecified], then a scrim will no 45 | * longer be applied and the bottom sheet will not block interaction with the rest of the screen 46 | * when visible. 47 | * @param sheetPadding The padding to apply on the whole modal sheet. Could be used to draw above (on y axis) system UI. 48 | * @param content The content of the bottom sheet. 49 | */ 50 | @ExperimentalSheetApi 51 | @Composable 52 | public fun ModalSheet( 53 | data: T?, 54 | onDataChange: (T?) -> Unit, 55 | cancelable: Boolean = true, 56 | onSystemBack: (() -> Unit)? = { onDataChange(null) }, 57 | shape: Shape = ModalSheetDefaults.shape, 58 | elevation: Dp = ModalSheetDefaults.elevation, 59 | backgroundColor: Color = ModalSheetDefaults.backgroundColor, 60 | contentColor: Color = contentColorFor(backgroundColor), 61 | scrimColor: Color = ModalSheetDefaults.scrimColor, 62 | sheetPadding: PaddingValues = PaddingValues(0.dp), 63 | content: @Composable ColumnScope.(T) -> Unit, 64 | ) { 65 | var lastNonNullData by remember { mutableStateOf(data) } 66 | DisposableEffect(data) { 67 | if (data != null) lastNonNullData = data 68 | onDispose {} 69 | } 70 | 71 | ModalSheet( 72 | visible = data != null, 73 | onVisibleChange = { visible -> 74 | if (visible) { 75 | onDataChange(lastNonNullData) 76 | } else { 77 | onDataChange(null) 78 | } 79 | }, 80 | onSystemBack = onSystemBack, 81 | cancelable = cancelable, 82 | shape = shape, 83 | elevation = elevation, 84 | backgroundColor = backgroundColor, 85 | contentColor = contentColor, 86 | scrimColor = scrimColor, 87 | sheetPadding = sheetPadding, 88 | ) { 89 | lastNonNullData?.let { 90 | content(it) 91 | } 92 | } 93 | } 94 | 95 | /** 96 | * Modal sheet that behaves like bottom sheet and draws over system UI. 97 | * Should be used on with the content which is not dependent on the outer data. For dynamic content use [ModalSheet] 98 | * overload with a 'data' parameter. 99 | * 100 | * @param visible True if modal should be visible. 101 | * @param onVisibleChange Called when visibility changes. 102 | * @param cancelable When true, this modal sheet can be closed with swipe gesture, tap on scrim or tap on system back 103 | * button. Note: passing 'false' does not disable the interaction with the sheet. Only the resulting state after the 104 | * sheet settles. 105 | * @param onSystemBack The action invoked by pressing the system back button. By default sets the visibility to false 106 | * if [cancelable] is set to true. Could be overridden or set to 'null' to react on system back button press. 107 | * @param shape The shape of the bottom sheet. 108 | * @param elevation The elevation of the bottom sheet. 109 | * @param backgroundColor The background color of the bottom sheet. 110 | * @param contentColor The preferred content color provided by the bottom sheet to its 111 | * children. Defaults to the matching content color for [backgroundColor], or if that is not 112 | * a color from the theme, this will keep the same content color set above the bottom sheet. 113 | * @param scrimColor The color of the scrim that is applied to the rest of the screen when the 114 | * bottom sheet is visible. If the color passed is [Color.Unspecified], then a scrim will no 115 | * longer be applied and the bottom sheet will not block interaction with the rest of the screen 116 | * when visible. 117 | * @param sheetPadding The padding to apply on the whole modal sheet. Could be used to draw above (on y axis) system UI. 118 | * @param content The content of the bottom sheet. 119 | */ 120 | @ExperimentalSheetApi 121 | @Composable 122 | public fun ModalSheet( 123 | visible: Boolean, 124 | onVisibleChange: (Boolean) -> Unit, 125 | cancelable: Boolean = true, 126 | onSystemBack: (() -> Unit)? = { onVisibleChange(false) }, 127 | shape: Shape = ModalSheetDefaults.shape, 128 | elevation: Dp = ModalSheetDefaults.elevation, 129 | backgroundColor: Color = ModalSheetDefaults.backgroundColor, 130 | contentColor: Color = contentColorFor(backgroundColor), 131 | scrimColor: Color = ModalSheetDefaults.scrimColor, 132 | sheetPadding: PaddingValues = PaddingValues(0.dp), 133 | content: @Composable ColumnScope.() -> Unit, 134 | ) { 135 | // Hold cancelable flag internally and set to true when modal sheet is dismissed via "visible" property in 136 | // non-cancellable modal sheet. This ensures that "confirmValueChange" will return true when sheet is set to hidden 137 | // state. 138 | val internalCancelable = remember { mutableStateOf(cancelable) } 139 | val sheetState = rememberModalBottomSheetState( 140 | skipHalfExpanded = true, 141 | initialValue = ModalBottomSheetValue.Hidden, 142 | confirmValueChange = { 143 | // Intercept and disallow hide gesture / action 144 | if (it == ModalBottomSheetValue.Hidden && !internalCancelable.value) { 145 | return@rememberModalBottomSheetState false 146 | } 147 | true 148 | }, 149 | ) 150 | 151 | LaunchedEffect(visible, cancelable) { 152 | if (visible) { 153 | internalCancelable.value = cancelable 154 | sheetState.show() 155 | } else { 156 | internalCancelable.value = true 157 | sheetState.hide() 158 | } 159 | } 160 | 161 | LaunchedEffect(sheetState.currentValue, sheetState.targetValue, sheetState.progress) { 162 | if (sheetState.progress == 1f && sheetState.currentValue == sheetState.targetValue) { 163 | val newVisible = sheetState.isVisible 164 | if (newVisible != visible) { 165 | onVisibleChange(newVisible) 166 | } 167 | } 168 | } 169 | 170 | if (!visible && sheetState.currentValue == sheetState.targetValue && !sheetState.isVisible) { 171 | return 172 | } 173 | 174 | ModalSheet( 175 | sheetState = sheetState, 176 | onSystemBack = if (onSystemBack != null) { { if (cancelable) onSystemBack() } } else { null }, 177 | shape = shape, 178 | elevation = elevation, 179 | backgroundColor = backgroundColor, 180 | contentColor = contentColor, 181 | scrimColor = scrimColor, 182 | sheetPadding = sheetPadding, 183 | content = content, 184 | ) 185 | } 186 | 187 | /** 188 | * Modal sheet that behaves like bottom sheet and draws over system UI. 189 | * Takes [ModalBottomSheetState] as parameter to fine-tune sheet behavior. 190 | * 191 | * Note: In this case [ModalSheet] is always added to the composition. See [ModalSheet] overload with visible parameter, 192 | * or data object to conditionally add / remove modal sheet to / from the composition. 193 | * 194 | * @param sheetState The state of the underlying Material bottom sheet. 195 | * @param onSystemBack The action invoked by pressing the system back button. 196 | * @param shape The shape of the bottom sheet. 197 | * @param elevation The elevation of the bottom sheet. 198 | * @param backgroundColor The background color of the bottom sheet. 199 | * @param contentColor The preferred content color provided by the bottom sheet to its 200 | * children. Defaults to the matching content color for [backgroundColor], or if that is not 201 | * a color from the theme, this will keep the same content color set above the bottom sheet. 202 | * @param scrimColor The color of the scrim that is applied to the rest of the screen when the 203 | * bottom sheet is visible. If the color passed is [Color.Unspecified], then a scrim will no 204 | * longer be applied and the bottom sheet will not block interaction with the rest of the screen 205 | * when visible. 206 | * @param sheetPadding The padding to apply on the whole modal sheet. Could be used to draw above (on y axis) system UI. 207 | * @param content The content of the bottom sheet. 208 | */ 209 | @ExperimentalSheetApi 210 | @Composable 211 | public fun ModalSheet( 212 | sheetState: ModalBottomSheetState, 213 | onSystemBack: (() -> Unit)?, 214 | shape: Shape = ModalSheetDefaults.shape, 215 | elevation: Dp = ModalSheetDefaults.elevation, 216 | backgroundColor: Color = ModalSheetDefaults.backgroundColor, 217 | contentColor: Color = contentColorFor(backgroundColor), 218 | scrimColor: Color = ModalSheetDefaults.scrimColor, 219 | sheetPadding: PaddingValues = PaddingValues(0.dp), 220 | content: @Composable ColumnScope.() -> Unit, 221 | ) { 222 | FullscreenPopup( 223 | onSystemBack = onSystemBack, 224 | ) { 225 | ModalBottomSheetLayout( 226 | sheetState = sheetState, 227 | sheetShape = shape, 228 | sheetElevation = elevation, 229 | sheetBackgroundColor = backgroundColor, 230 | sheetContentColor = contentColor, 231 | scrimColor = scrimColor, 232 | sheetContent = content, 233 | content = { /* Empty */ }, 234 | modifier = Modifier 235 | .padding(sheetPadding) 236 | .clipToBounds(), 237 | ) 238 | } 239 | } 240 | 241 | /** 242 | * Contains the default values used by [ModalSheet]. 243 | */ 244 | @ExperimentalSheetApi 245 | public object ModalSheetDefaults { 246 | 247 | /** 248 | * Default shape of the bottom sheet. 249 | */ 250 | public val shape: Shape 251 | @Composable 252 | get() = MaterialTheme.shapes.large 253 | 254 | /** 255 | * Default elevation of the bottom sheet. 256 | */ 257 | public val elevation: Dp 258 | @Composable 259 | get() = ModalBottomSheetDefaults.Elevation 260 | 261 | /** 262 | * Default background color of the bottom sheet. 263 | */ 264 | public val backgroundColor: Color 265 | @Composable 266 | get() = MaterialTheme.colors.surface 267 | 268 | /** 269 | * Default color of the scrim that is applied to the rest of the screen when the bottom sheet 270 | * is visible. 271 | */ 272 | public val scrimColor: Color 273 | @Composable 274 | get() = ModalBottomSheetDefaults.scrimColor 275 | } 276 | -------------------------------------------------------------------------------- /config/detekt/detekt.yml: -------------------------------------------------------------------------------- 1 | build: 2 | maxIssues: 0 3 | excludeCorrectable: false 4 | weights: 5 | # complexity: 2 6 | # LongParameterList: 1 7 | # style: 1 8 | # comments: 1 9 | 10 | config: 11 | validation: true 12 | warningsAsErrors: false 13 | # when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]' 14 | excludes: '' 15 | 16 | processors: 17 | active: true 18 | exclude: 19 | - 'DetektProgressListener' 20 | # - 'KtFileCountProcessor' 21 | # - 'PackageCountProcessor' 22 | # - 'ClassCountProcessor' 23 | # - 'FunctionCountProcessor' 24 | # - 'PropertyCountProcessor' 25 | # - 'ProjectComplexityProcessor' 26 | # - 'ProjectCognitiveComplexityProcessor' 27 | # - 'ProjectLLOCProcessor' 28 | # - 'ProjectCLOCProcessor' 29 | # - 'ProjectLOCProcessor' 30 | # - 'ProjectSLOCProcessor' 31 | # - 'LicenseHeaderLoaderExtension' 32 | 33 | console-reports: 34 | active: true 35 | exclude: 36 | - 'ProjectStatisticsReport' 37 | - 'ComplexityReport' 38 | - 'NotificationReport' 39 | - 'FindingsReport' 40 | - 'FileBasedFindingsReport' 41 | # - 'LiteFindingsReport' 42 | 43 | output-reports: 44 | active: true 45 | exclude: 46 | # - 'TxtOutputReport' 47 | # - 'XmlOutputReport' 48 | # - 'HtmlOutputReport' 49 | 50 | comments: 51 | active: true 52 | AbsentOrWrongFileLicense: 53 | active: false 54 | licenseTemplateFile: 'license.template' 55 | licenseTemplateIsRegex: false 56 | CommentOverPrivateFunction: 57 | active: false 58 | CommentOverPrivateProperty: 59 | active: false 60 | DeprecatedBlockTag: 61 | active: false 62 | EndOfSentenceFormat: 63 | active: false 64 | endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)' 65 | OutdatedDocumentation: 66 | active: false 67 | matchTypeParameters: true 68 | matchDeclarationsOrder: true 69 | allowParamOnConstructorProperties: false 70 | UndocumentedPublicClass: 71 | active: true 72 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] 73 | searchInNestedClass: true 74 | searchInInnerClass: true 75 | searchInInnerObject: true 76 | searchInInnerInterface: true 77 | UndocumentedPublicFunction: 78 | active: true 79 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] 80 | UndocumentedPublicProperty: 81 | active: true 82 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] 83 | 84 | complexity: 85 | active: true 86 | ComplexCondition: 87 | active: true 88 | threshold: 4 89 | ComplexInterface: 90 | active: false 91 | threshold: 10 92 | includeStaticDeclarations: false 93 | includePrivateDeclarations: false 94 | ComplexMethod: 95 | active: true 96 | threshold: 15 97 | ignoreSingleWhenExpression: false 98 | ignoreSimpleWhenEntries: false 99 | ignoreNestingFunctions: false 100 | nestingFunctions: 101 | - 'also' 102 | - 'apply' 103 | - 'forEach' 104 | - 'isNotNull' 105 | - 'ifNull' 106 | - 'let' 107 | - 'run' 108 | - 'use' 109 | - 'with' 110 | LabeledExpression: 111 | active: false 112 | ignoredLabels: [] 113 | LargeClass: 114 | active: true 115 | threshold: 600 116 | LongMethod: 117 | active: true 118 | threshold: 60 119 | LongParameterList: 120 | active: true 121 | functionThreshold: 6 122 | constructorThreshold: 7 123 | ignoreDefaultParameters: false 124 | ignoreDataClasses: true 125 | ignoreAnnotated: [ 'Composable' ] 126 | ignoreAnnotatedParameter: [] 127 | MethodOverloading: 128 | active: false 129 | threshold: 6 130 | NamedArguments: 131 | active: false 132 | threshold: 3 133 | ignoreArgumentsMatchingNames: false 134 | NestedBlockDepth: 135 | active: true 136 | threshold: 4 137 | ReplaceSafeCallChainWithRun: 138 | active: false 139 | StringLiteralDuplication: 140 | active: false 141 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] 142 | threshold: 3 143 | ignoreAnnotation: true 144 | excludeStringsWithLessThan5Characters: true 145 | ignoreStringsRegex: '$^' 146 | TooManyFunctions: 147 | active: true 148 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] 149 | thresholdInFiles: 11 150 | thresholdInClasses: 11 151 | thresholdInInterfaces: 11 152 | thresholdInObjects: 11 153 | thresholdInEnums: 11 154 | ignoreDeprecated: false 155 | ignorePrivate: false 156 | ignoreOverridden: false 157 | 158 | coroutines: 159 | active: true 160 | GlobalCoroutineUsage: 161 | active: false 162 | InjectDispatcher: 163 | active: false 164 | dispatcherNames: 165 | - 'IO' 166 | - 'Default' 167 | - 'Unconfined' 168 | RedundantSuspendModifier: 169 | active: false 170 | SleepInsteadOfDelay: 171 | active: false 172 | SuspendFunWithCoroutineScopeReceiver: 173 | active: false 174 | SuspendFunWithFlowReturnType: 175 | active: false 176 | 177 | empty-blocks: 178 | active: true 179 | EmptyCatchBlock: 180 | active: true 181 | allowedExceptionNameRegex: '_|(ignore|expected).*' 182 | EmptyClassBlock: 183 | active: true 184 | EmptyDefaultConstructor: 185 | active: true 186 | EmptyDoWhileBlock: 187 | active: true 188 | EmptyElseBlock: 189 | active: true 190 | EmptyFinallyBlock: 191 | active: true 192 | EmptyForBlock: 193 | active: true 194 | EmptyFunctionBlock: 195 | active: true 196 | ignoreOverridden: false 197 | EmptyIfBlock: 198 | active: true 199 | EmptyInitBlock: 200 | active: true 201 | EmptyKtFile: 202 | active: true 203 | EmptySecondaryConstructor: 204 | active: true 205 | EmptyTryBlock: 206 | active: true 207 | EmptyWhenBlock: 208 | active: true 209 | EmptyWhileBlock: 210 | active: true 211 | 212 | exceptions: 213 | active: true 214 | ExceptionRaisedInUnexpectedLocation: 215 | active: true 216 | methodNames: 217 | - 'equals' 218 | - 'finalize' 219 | - 'hashCode' 220 | - 'toString' 221 | InstanceOfCheckForException: 222 | active: false 223 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] 224 | NotImplementedDeclaration: 225 | active: false 226 | ObjectExtendsThrowable: 227 | active: false 228 | PrintStackTrace: 229 | active: true 230 | RethrowCaughtException: 231 | active: true 232 | ReturnFromFinally: 233 | active: true 234 | ignoreLabeled: false 235 | SwallowedException: 236 | active: true 237 | ignoredExceptionTypes: 238 | - 'InterruptedException' 239 | - 'MalformedURLException' 240 | - 'NumberFormatException' 241 | - 'ParseException' 242 | allowedExceptionNameRegex: '_|(ignore|expected).*' 243 | ThrowingExceptionFromFinally: 244 | active: true 245 | ThrowingExceptionInMain: 246 | active: false 247 | ThrowingExceptionsWithoutMessageOrCause: 248 | active: true 249 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] 250 | exceptions: 251 | - 'ArrayIndexOutOfBoundsException' 252 | - 'Exception' 253 | - 'IllegalArgumentException' 254 | - 'IllegalMonitorStateException' 255 | - 'IllegalStateException' 256 | - 'IndexOutOfBoundsException' 257 | - 'NullPointerException' 258 | - 'RuntimeException' 259 | - 'Throwable' 260 | ThrowingNewInstanceOfSameException: 261 | active: true 262 | TooGenericExceptionCaught: 263 | active: true 264 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] 265 | exceptionNames: 266 | - 'ArrayIndexOutOfBoundsException' 267 | - 'Error' 268 | - 'Exception' 269 | - 'IllegalMonitorStateException' 270 | - 'IndexOutOfBoundsException' 271 | - 'NullPointerException' 272 | - 'RuntimeException' 273 | - 'Throwable' 274 | allowedExceptionNameRegex: '_|(ignore|expected).*' 275 | TooGenericExceptionThrown: 276 | active: true 277 | exceptionNames: 278 | - 'Error' 279 | - 'Exception' 280 | - 'RuntimeException' 281 | - 'Throwable' 282 | 283 | naming: 284 | active: true 285 | BooleanPropertyNaming: 286 | active: false 287 | allowedPattern: '^(is|has|are)' 288 | ignoreOverridden: true 289 | ClassNaming: 290 | active: true 291 | classPattern: '[A-Z][a-zA-Z0-9]*' 292 | ConstructorParameterNaming: 293 | active: true 294 | parameterPattern: '[a-z][A-Za-z0-9]*' 295 | privateParameterPattern: '[a-z][A-Za-z0-9]*' 296 | excludeClassPattern: '$^' 297 | ignoreOverridden: true 298 | EnumNaming: 299 | active: true 300 | enumEntryPattern: '[A-Z][_a-zA-Z0-9]*' 301 | ForbiddenClassName: 302 | active: false 303 | forbiddenName: [] 304 | FunctionMaxLength: 305 | active: false 306 | maximumFunctionNameLength: 30 307 | FunctionMinLength: 308 | active: false 309 | minimumFunctionNameLength: 3 310 | FunctionNaming: 311 | active: true 312 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] 313 | functionPattern: '[a-z][a-zA-Z0-9]*' 314 | excludeClassPattern: '$^' 315 | ignoreOverridden: true 316 | ignoreAnnotated: [ 'Composable' ] 317 | FunctionParameterNaming: 318 | active: true 319 | parameterPattern: '[a-z][A-Za-z0-9]*' 320 | excludeClassPattern: '$^' 321 | ignoreOverridden: true 322 | InvalidPackageDeclaration: 323 | active: false 324 | rootPackage: '' 325 | requireRootInDeclaration: false 326 | LambdaParameterNaming: 327 | active: false 328 | parameterPattern: '[a-z][A-Za-z0-9]*|_' 329 | MatchingDeclarationName: 330 | active: true 331 | mustBeFirst: true 332 | MemberNameEqualsClassName: 333 | active: true 334 | ignoreOverridden: true 335 | NoNameShadowing: 336 | active: false 337 | NonBooleanPropertyPrefixedWithIs: 338 | active: false 339 | ObjectPropertyNaming: 340 | active: true 341 | constantPattern: '[A-Za-z][_A-Za-z0-9]*' 342 | propertyPattern: '[A-Za-z][_A-Za-z0-9]*' 343 | privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*' 344 | PackageNaming: 345 | active: true 346 | packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*' 347 | TopLevelPropertyNaming: 348 | active: true 349 | constantPattern: '[A-Z][A-Za-z0-9]*' 350 | propertyPattern: '[A-Za-z][_A-Za-z0-9]*' 351 | privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*' 352 | VariableMaxLength: 353 | active: false 354 | maximumVariableNameLength: 64 355 | VariableMinLength: 356 | active: false 357 | minimumVariableNameLength: 1 358 | VariableNaming: 359 | active: true 360 | variablePattern: '[a-z][A-Za-z0-9]*' 361 | privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' 362 | excludeClassPattern: '$^' 363 | ignoreOverridden: true 364 | 365 | performance: 366 | active: true 367 | ArrayPrimitive: 368 | active: true 369 | ForEachOnRange: 370 | active: true 371 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] 372 | SpreadOperator: 373 | active: false 374 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] 375 | UnnecessaryTemporaryInstantiation: 376 | active: true 377 | 378 | potential-bugs: 379 | active: true 380 | AvoidReferentialEquality: 381 | active: false 382 | forbiddenTypePatterns: 383 | - 'kotlin.String' 384 | CastToNullableType: 385 | active: false 386 | Deprecation: 387 | active: false 388 | DontDowncastCollectionTypes: 389 | active: false 390 | DoubleMutabilityForCollection: 391 | active: false 392 | mutableTypes: 393 | - 'kotlin.collections.MutableList' 394 | - 'kotlin.collections.MutableMap' 395 | - 'kotlin.collections.MutableSet' 396 | - 'java.util.ArrayList' 397 | - 'java.util.LinkedHashSet' 398 | - 'java.util.HashSet' 399 | - 'java.util.LinkedHashMap' 400 | - 'java.util.HashMap' 401 | DuplicateCaseInWhenExpression: 402 | active: true 403 | ElseCaseInsteadOfExhaustiveWhen: 404 | active: false 405 | EqualsAlwaysReturnsTrueOrFalse: 406 | active: true 407 | EqualsWithHashCodeExist: 408 | active: true 409 | ExitOutsideMain: 410 | active: false 411 | ExplicitGarbageCollectionCall: 412 | active: true 413 | HasPlatformType: 414 | active: false 415 | IgnoredReturnValue: 416 | active: false 417 | restrictToAnnotatedMethods: true 418 | returnValueAnnotations: 419 | - '*.CheckResult' 420 | - '*.CheckReturnValue' 421 | ignoreReturnValueAnnotations: 422 | - '*.CanIgnoreReturnValue' 423 | ignoreFunctionCall: [] 424 | ImplicitDefaultLocale: 425 | active: true 426 | ImplicitUnitReturnType: 427 | active: false 428 | allowExplicitReturnType: true 429 | InvalidRange: 430 | active: true 431 | IteratorHasNextCallsNextMethod: 432 | active: true 433 | IteratorNotThrowingNoSuchElementException: 434 | active: true 435 | LateinitUsage: 436 | active: false 437 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] 438 | ignoreOnClassesPattern: '' 439 | MapGetWithNotNullAssertionOperator: 440 | active: false 441 | MissingPackageDeclaration: 442 | active: false 443 | excludes: ['**/*.kts'] 444 | MissingWhenCase: 445 | active: true 446 | allowElseExpression: true 447 | NullCheckOnMutableProperty: 448 | active: false 449 | NullableToStringCall: 450 | active: false 451 | RedundantElseInWhen: 452 | active: true 453 | UnconditionalJumpStatementInLoop: 454 | active: false 455 | UnnecessaryNotNullOperator: 456 | active: true 457 | UnnecessarySafeCall: 458 | active: true 459 | UnreachableCatchBlock: 460 | active: false 461 | UnreachableCode: 462 | active: true 463 | UnsafeCallOnNullableType: 464 | active: true 465 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] 466 | UnsafeCast: 467 | active: true 468 | UnusedUnaryOperator: 469 | active: false 470 | UselessPostfixExpression: 471 | active: false 472 | WrongEqualsTypeParameter: 473 | active: true 474 | 475 | style: 476 | active: true 477 | CanBeNonNullable: 478 | active: false 479 | ClassOrdering: 480 | active: false 481 | CollapsibleIfStatements: 482 | active: false 483 | DataClassContainsFunctions: 484 | active: false 485 | conversionFunctionPrefix: 'to' 486 | DataClassShouldBeImmutable: 487 | active: false 488 | DestructuringDeclarationWithTooManyEntries: 489 | active: false 490 | maxDestructuringEntries: 3 491 | EqualsNullCall: 492 | active: true 493 | EqualsOnSignatureLine: 494 | active: false 495 | ExplicitCollectionElementAccessMethod: 496 | active: false 497 | ExplicitItLambdaParameter: 498 | active: false 499 | ExpressionBodySyntax: 500 | active: false 501 | includeLineWrapping: false 502 | ForbiddenComment: 503 | active: true 504 | values: 505 | - 'FIXME:' 506 | - 'STOPSHIP:' 507 | - 'TODO:' 508 | allowedPatterns: '' 509 | customMessage: '' 510 | ForbiddenImport: 511 | active: false 512 | imports: [] 513 | forbiddenPatterns: '' 514 | ForbiddenMethodCall: 515 | active: false 516 | methods: 517 | - 'kotlin.io.print' 518 | - 'kotlin.io.println' 519 | ForbiddenPublicDataClass: 520 | active: true 521 | excludes: ['**'] 522 | ignorePackages: 523 | - '*.internal' 524 | - '*.internal.*' 525 | ForbiddenVoid: 526 | active: false 527 | ignoreOverridden: false 528 | ignoreUsageInGenerics: false 529 | FunctionOnlyReturningConstant: 530 | active: true 531 | ignoreOverridableFunction: true 532 | ignoreActualFunction: true 533 | excludedFunctions: '' 534 | LibraryCodeMustSpecifyReturnType: 535 | active: true 536 | excludes: ['**'] 537 | LibraryEntitiesShouldNotBePublic: 538 | active: true 539 | excludes: ['**'] 540 | LoopWithTooManyJumpStatements: 541 | active: true 542 | maxJumpCount: 1 543 | MagicNumber: 544 | active: false 545 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] 546 | ignoreNumbers: 547 | - '-1' 548 | - '0' 549 | - '1' 550 | - '2' 551 | ignoreHashCodeFunction: true 552 | ignorePropertyDeclaration: false 553 | ignoreLocalVariableDeclaration: false 554 | ignoreConstantDeclaration: true 555 | ignoreCompanionObjectPropertyDeclaration: true 556 | ignoreAnnotation: false 557 | ignoreNamedArgument: true 558 | ignoreEnums: false 559 | ignoreRanges: false 560 | ignoreExtensionFunctions: true 561 | MandatoryBracesIfStatements: 562 | active: false 563 | MandatoryBracesLoops: 564 | active: false 565 | MaxLineLength: 566 | active: true 567 | maxLineLength: 120 568 | excludePackageStatements: true 569 | excludeImportStatements: true 570 | excludeCommentStatements: false 571 | MayBeConst: 572 | active: true 573 | ModifierOrder: 574 | active: true 575 | MultilineLambdaItParameter: 576 | active: false 577 | NestedClassesVisibility: 578 | active: true 579 | NewLineAtEndOfFile: 580 | active: true 581 | NoTabs: 582 | active: false 583 | ObjectLiteralToLambda: 584 | active: false 585 | OptionalAbstractKeyword: 586 | active: true 587 | OptionalUnit: 588 | active: false 589 | OptionalWhenBraces: 590 | active: false 591 | PreferToOverPairSyntax: 592 | active: false 593 | ProtectedMemberInFinalClass: 594 | active: true 595 | RedundantExplicitType: 596 | active: false 597 | RedundantHigherOrderMapUsage: 598 | active: false 599 | RedundantVisibilityModifierRule: 600 | active: false 601 | ReturnCount: 602 | active: true 603 | max: 2 604 | excludedFunctions: 'equals' 605 | excludeLabeled: false 606 | excludeReturnFromLambda: true 607 | excludeGuardClauses: false 608 | SafeCast: 609 | active: true 610 | SerialVersionUIDInSerializableClass: 611 | active: true 612 | SpacingBetweenPackageAndImports: 613 | active: false 614 | ThrowsCount: 615 | active: true 616 | max: 2 617 | excludeGuardClauses: false 618 | TrailingWhitespace: 619 | active: false 620 | UnderscoresInNumericLiterals: 621 | active: false 622 | acceptableLength: 4 623 | allowNonStandardGrouping: false 624 | UnnecessaryAbstractClass: 625 | active: true 626 | UnnecessaryAnnotationUseSiteTarget: 627 | active: false 628 | UnnecessaryApply: 629 | active: true 630 | UnnecessaryFilter: 631 | active: false 632 | UnnecessaryInheritance: 633 | active: true 634 | UnnecessaryInnerClass: 635 | active: false 636 | UnnecessaryLet: 637 | active: false 638 | UnnecessaryParentheses: 639 | active: false 640 | UntilInsteadOfRangeTo: 641 | active: false 642 | UnusedImports: 643 | active: false 644 | UnusedPrivateClass: 645 | active: true 646 | UnusedPrivateMember: 647 | active: true 648 | allowedNames: '(_|ignored|expected|serialVersionUID)' 649 | UseAnyOrNoneInsteadOfFind: 650 | active: false 651 | UseArrayLiteralsInAnnotations: 652 | active: false 653 | UseCheckNotNull: 654 | active: false 655 | UseCheckOrError: 656 | active: false 657 | UseDataClass: 658 | active: false 659 | allowVars: false 660 | UseEmptyCounterpart: 661 | active: false 662 | UseIfEmptyOrIfBlank: 663 | active: false 664 | UseIfInsteadOfWhen: 665 | active: false 666 | UseIsNullOrEmpty: 667 | active: false 668 | UseOrEmpty: 669 | active: false 670 | UseRequire: 671 | active: false 672 | UseRequireNotNull: 673 | active: false 674 | UselessCallOnNotNull: 675 | active: true 676 | UtilityClassWithPublicConstructor: 677 | active: true 678 | VarCouldBeVal: 679 | active: true 680 | WildcardImport: 681 | active: true 682 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] 683 | excludeImports: 684 | - 'java.util.*' 685 | --------------------------------------------------------------------------------