├── app ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── values │ │ │ │ ├── strings.xml │ │ │ │ ├── colors.xml │ │ │ │ └── styles.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 │ │ │ │ ├── filters_sheet_header_shadow.xml │ │ │ │ ├── filters_sheet_background.xml │ │ │ │ ├── ic_expand_more.xml │ │ │ │ ├── map_marker_lounge.xml │ │ │ │ └── ic_launcher_background.xml │ │ │ ├── color │ │ │ │ └── collapsing_section.xml │ │ │ ├── animator │ │ │ │ └── active_alpha.xml │ │ │ ├── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ │ └── layout │ │ │ │ └── activity_main.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── com │ │ │ └── kienht │ │ │ └── demobottomsheetbehavior │ │ │ └── MainActivity.kt │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── kienht │ │ │ └── demobottomsheetbehavior │ │ │ └── ExampleUnitTest.kt │ └── androidTest │ │ └── java │ │ └── com │ │ └── kienht │ │ └── demobottomsheetbehavior │ │ └── ExampleInstrumentedTest.kt ├── proguard-rules.pro └── build.gradle ├── bottomsheetbehavior ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ └── values │ │ │ │ └── strings.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── com │ │ │ └── kienht │ │ │ └── bottomsheetbehavior │ │ │ ├── Extensions.kt │ │ │ └── BottomSheetBehavior.kt │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── kienht │ │ │ └── bottomsheetbehavior │ │ │ └── ExampleUnitTest.java │ └── androidTest │ │ └── java │ │ └── com │ │ └── kienht │ │ └── bottomsheetbehavior │ │ └── ExampleInstrumentedTest.java ├── proguard-rules.pro └── build.gradle ├── settings.gradle ├── image └── screenshot.png ├── .gitignore ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── README.md ├── gradlew.bat ├── gradlew └── LICENSE /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /bottomsheetbehavior/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':bottomsheetbehavior' 2 | -------------------------------------------------------------------------------- /image/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hantrungkien/BottomSheetBehavior/HEAD/image/screenshot.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | DemoBottomSheetBehavior 3 | 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hantrungkien/BottomSheetBehavior/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /bottomsheetbehavior/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | BottomSheetBehavior 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hantrungkien/BottomSheetBehavior/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hantrungkien/BottomSheetBehavior/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hantrungkien/BottomSheetBehavior/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hantrungkien/BottomSheetBehavior/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hantrungkien/BottomSheetBehavior/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hantrungkien/BottomSheetBehavior/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hantrungkien/BottomSheetBehavior/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hantrungkien/BottomSheetBehavior/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hantrungkien/BottomSheetBehavior/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /bottomsheetbehavior/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hantrungkien/BottomSheetBehavior/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Sep 05 18:20:24 ICT 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip 7 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/test/java/com/kienht/demobottomsheetbehavior/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.kienht.demobottomsheetbehavior 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /bottomsheetbehavior/src/test/java/com/kienht/bottomsheetbehavior/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.kienht.bottomsheetbehavior; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/kienht/demobottomsheetbehavior/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.kienht.demobottomsheetbehavior 2 | 3 | import android.support.test.InstrumentationRegistry 4 | import android.support.test.runner.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getTargetContext() 22 | assertEquals("com.kienht.demobottomsheetbehavior", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/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 22 | -------------------------------------------------------------------------------- /bottomsheetbehavior/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 22 | -------------------------------------------------------------------------------- /bottomsheetbehavior/src/androidTest/java/com/kienht/bottomsheetbehavior/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.kienht.bottomsheetbehavior; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.kienht.bottomsheetbehavior.test", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/filters_sheet_header_shadow.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 20 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/color/collapsing_section.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/filters_sheet_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 20 | 21 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_expand_more.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | 25 | 26 | -------------------------------------------------------------------------------- /bottomsheetbehavior/src/main/java/com/kienht/bottomsheetbehavior/Extensions.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.kienht.bottomsheetbehavior 18 | 19 | import android.os.Parcel 20 | 21 | /** Write a boolean to a Parcel (copied from Parcel, where this is @hidden). */ 22 | fun Parcel.writeBoolean(value: Boolean) = writeInt(if (value) 1 else 0) 23 | 24 | /** Read a boolean from a Parcel (copied from Parcel, where this is @hidden). */ 25 | fun Parcel.readBoolean() = readInt() != 0 -------------------------------------------------------------------------------- /bottomsheetbehavior/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | 4 | android { 5 | compileSdkVersion 28 6 | 7 | defaultConfig { 8 | minSdkVersion 21 9 | targetSdkVersion 28 10 | versionCode 1 11 | versionName "1.0" 12 | 13 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 14 | 15 | } 16 | 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | 24 | } 25 | 26 | dependencies { 27 | implementation fileTree(dir: 'libs', include: ['*.jar']) 28 | 29 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 30 | implementation 'androidx.appcompat:appcompat:1.1.0-alpha05' 31 | implementation 'com.google.android.material:material:1.1.0-alpha07' 32 | 33 | testImplementation 'junit:junit:4.12' 34 | androidTestImplementation 'androidx.test:runner:1.1.1' 35 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' 36 | } 37 | -------------------------------------------------------------------------------- /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=-Xmx1536m 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 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official 22 | -------------------------------------------------------------------------------- /app/src/main/res/animator/active_alpha.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 25 | 26 | 27 | 28 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | apply plugin: 'kotlin-android' 4 | 5 | apply plugin: 'kotlin-android-extensions' 6 | 7 | android { 8 | compileSdkVersion 28 9 | defaultConfig { 10 | applicationId "com.kienht.demobottomsheetbehavior" 11 | minSdkVersion 21 12 | targetSdkVersion 28 13 | versionCode 1 14 | versionName "1.0" 15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 16 | } 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | } 24 | 25 | dependencies { 26 | implementation fileTree(dir: 'libs', include: ['*.jar']) 27 | 28 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 29 | implementation 'androidx.appcompat:appcompat:1.1.0-alpha05' 30 | implementation 'com.google.android.material:material:1.1.0-alpha07' 31 | 32 | implementation project(":bottomsheetbehavior") 33 | 34 | testImplementation 'junit:junit:4.12' 35 | androidTestImplementation 'androidx.test:runner:1.1.1' 36 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/map_marker_lounge.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | 25 | 28 | 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BottomSheetBehavior 2 | BottomSheetBehavior is an android library extracted from the Google I/O 2018 application source code. 3 | 4 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 5 | [![Platform](https://img.shields.io/badge/platform-android-green.svg)](http://developer.android.com/index.html) 6 | [![](https://jitpack.io/v/hantrungkien/BottomSheetBehavior.svg)](https://jitpack.io/#hantrungkien/BottomSheetBehavior) 7 | 8 | 9 | 10 | ### install: 11 | 12 | **via JitPack (to get current code)** 13 | 14 | project/build.gradle 15 | ````gradle 16 | allprojects { 17 | repositories { 18 | maven { url "https://jitpack.io" } 19 | } 20 | } 21 | ```` 22 | module/build.gradle 23 | ````gradle 24 | implementation 'com.github.hantrungkien:BottomSheetBehavior:1.0.3' 25 | ```` 26 | 27 | #### How to use please review in the demo app 28 | 29 | ### Contribution 30 | 31 | If you've found an error, please file an issue. 32 | 33 | Patches and new samples are encouraged, and may be submitted by forking this project and submitting a pull request through GitHub. 34 | 35 | 36 | ### LICENCE 37 | 38 | Copyright 2018 Kien Han Trung 39 | 40 | Licensed under the Apache License, Version 2.0 (the "License"); 41 | you may not use this file except in compliance with the License. 42 | You may obtain a copy of the License at 43 | 44 | http://www.apache.org/licenses/LICENSE-2.0 45 | 46 | Unless required by applicable law or agreed to in writing, software 47 | distributed under the License is distributed on an "AS IS" BASIS, 48 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 49 | See the License for the specific language governing permissions and 50 | limitations under the License. 51 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /app/src/main/java/com/kienht/demobottomsheetbehavior/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.kienht.demobottomsheetbehavior 2 | 3 | import android.annotation.SuppressLint 4 | import android.os.Bundle 5 | import android.view.View 6 | import androidx.appcompat.app.AppCompatActivity 7 | import androidx.core.widget.NestedScrollView 8 | import com.kienht.bottomsheetbehavior.BottomSheetBehavior 9 | import kotlinx.android.synthetic.main.activity_main.* 10 | 11 | class MainActivity : AppCompatActivity() { 12 | 13 | companion object { 14 | val TAG = MainActivity::class.java.simpleName 15 | } 16 | 17 | @SuppressLint("SetTextI18n") 18 | override fun onCreate(savedInstanceState: Bundle?) { 19 | super.onCreate(savedInstanceState) 20 | setContentView(R.layout.activity_main) 21 | 22 | val bottomSheetBehavior = BottomSheetBehavior.from(bottom_sheet) 23 | 24 | bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED 25 | bottomSheetBehavior.skipCollapsed = true 26 | 27 | bottomSheetBehavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback { 28 | override fun onStateChanged(bottomSheet: View, newState: Int) { 29 | val rotation = when (newState) { 30 | BottomSheetBehavior.STATE_EXPANDED -> 0f 31 | BottomSheetBehavior.STATE_COLLAPSED -> 180f 32 | BottomSheetBehavior.STATE_HIDDEN -> 180f 33 | else -> return 34 | } 35 | 36 | expand_icon.animate().rotationX(rotation).start() 37 | } 38 | }) 39 | 40 | clickable.setOnClickListener { 41 | if (bottomSheetBehavior.state == BottomSheetBehavior.STATE_HIDDEN) { 42 | bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED 43 | } else { 44 | bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN 45 | } 46 | } 47 | 48 | text_test.setOnClickListener { 49 | if (bottomSheetBehavior.state == BottomSheetBehavior.STATE_HIDDEN) { 50 | bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED 51 | } else { 52 | bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN 53 | } 54 | } 55 | 56 | description_scrollview 57 | .setOnScrollChangeListener { v: NestedScrollView, _: Int, _: Int, _: Int, _: Int -> 58 | sheet_header_shadow.isActivated = v.canScrollVertically(-1) 59 | } 60 | 61 | marker_description.text = 62 | "OICSoft.com - Many start-ups in our portfolio had no technical staff, and we do know how to deal with it. Our team studies the specifics of your projects and can build the entire software development process for non-technical clients. Over the years, we've faced many problems and found numerous workarounds. That's how we polished our software development process, and now can confidently guarantee quality and timely results." 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 20 | 21 | 31 | 32 | 38 | 39 | 47 | 48 | 61 | 62 | 73 | 74 | 75 | 76 | 83 | 84 | 90 | 91 | 95 | 96 | 101 | 102 | 107 | 108 | 113 | 114 | 119 | 120 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /bottomsheetbehavior/src/main/java/com/kienht/bottomsheetbehavior/BottomSheetBehavior.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.kienht.bottomsheetbehavior 18 | 19 | import android.annotation.SuppressLint 20 | import android.content.Context 21 | import android.os.Bundle 22 | import android.os.Parcel 23 | import android.os.Parcelable 24 | import android.util.AttributeSet 25 | import android.view.MotionEvent 26 | import android.view.VelocityTracker 27 | import android.view.View 28 | import android.view.ViewConfiguration 29 | import android.view.ViewGroup 30 | import androidx.annotation.IntDef 31 | import androidx.annotation.VisibleForTesting 32 | import androidx.coordinatorlayout.widget.CoordinatorLayout 33 | import androidx.core.view.ViewCompat 34 | import androidx.customview.view.AbsSavedState 35 | import androidx.customview.widget.ViewDragHelper 36 | import java.lang.ref.WeakReference 37 | import kotlin.math.absoluteValue 38 | 39 | /** 40 | * Copy of material lib's BottomSheetBehavior that includes some bug fixes. 41 | */ 42 | // TODO remove when a fixed version in material lib is released. 43 | class BottomSheetBehavior : CoordinatorLayout.Behavior { 44 | 45 | companion object { 46 | /** The bottom sheet is dragging. */ 47 | const val STATE_DRAGGING = 1 48 | /** The bottom sheet is settling. */ 49 | const val STATE_SETTLING = 2 50 | /** The bottom sheet is expanded. */ 51 | const val STATE_EXPANDED = 3 52 | /** The bottom sheet is collapsed. */ 53 | const val STATE_COLLAPSED = 4 54 | /** The bottom sheet is hidden. */ 55 | const val STATE_HIDDEN = 5 56 | /** The bottom sheet is half-expanded (used when behavior_fitToContents is false). */ 57 | const val STATE_HALF_EXPANDED = 6 58 | 59 | /** 60 | * Peek at the 16:9 ratio keyline of its parent. This can be used as a parameter for 61 | * [setPeekHeight(Int)]. [getPeekHeight()] will return this when the value is set. 62 | */ 63 | const val PEEK_HEIGHT_AUTO = -1 64 | 65 | private const val HIDE_THRESHOLD = 0.5f 66 | private const val HIDE_FRICTION = 0.1f 67 | 68 | @IntDef( 69 | value = [STATE_DRAGGING, 70 | STATE_SETTLING, 71 | STATE_EXPANDED, 72 | STATE_COLLAPSED, 73 | STATE_HIDDEN, 74 | STATE_HALF_EXPANDED] 75 | ) 76 | @Retention(AnnotationRetention.SOURCE) 77 | annotation class State 78 | 79 | /** Utility to get the [BottomSheetBehavior] from a [view]. */ 80 | @JvmStatic 81 | fun from(view: View): BottomSheetBehavior<*> { 82 | val lp = view.layoutParams as? CoordinatorLayout.LayoutParams 83 | ?: throw IllegalArgumentException("view is not a child of CoordinatorLayout") 84 | return lp.behavior as? BottomSheetBehavior 85 | ?: throw IllegalArgumentException("view not associated with this behavior") 86 | } 87 | } 88 | 89 | /** Callback for monitoring events about bottom sheets. */ 90 | interface BottomSheetCallback { 91 | /** 92 | * Called when the bottom sheet changes its state. 93 | * 94 | * @param bottomSheet The bottom sheet view. 95 | * @param newState The new state. This will be one of link [STATE_DRAGGING], 96 | * [STATE_SETTLING], [STATE_EXPANDED], [STATE_COLLAPSED], [STATE_HIDDEN], or 97 | * [STATE_HALF_EXPANDED]. 98 | */ 99 | fun onStateChanged(bottomSheet: View, newState: Int) {} 100 | 101 | /** 102 | * Called when the bottom sheet is being dragged. 103 | * 104 | * @param bottomSheet The bottom sheet view. 105 | * @param slideOffset The new offset of this bottom sheet within [-1,1] range. Offset 106 | * increases as this bottom sheet is moving upward. From 0 to 1 the sheet is between 107 | * collapsed and expanded states and from -1 to 0 it is between hidden and collapsed states. 108 | */ 109 | fun onSlide(bottomSheet: View, slideOffset: Float) {} 110 | } 111 | 112 | /** The current state of the bottom sheet, backing property */ 113 | private var _state = STATE_COLLAPSED 114 | /** The current state of the bottom sheet */ 115 | @State 116 | var state 117 | get() = _state 118 | set(@State value) { 119 | if (_state == value) { 120 | return 121 | } 122 | if (viewRef == null) { 123 | // Child is not laid out yet. Set our state and let onLayoutChild() handle it later. 124 | if (value == STATE_COLLAPSED || 125 | value == STATE_EXPANDED || 126 | value == STATE_HALF_EXPANDED || 127 | (isHideable && value == STATE_HIDDEN) 128 | ) { 129 | _state = value 130 | } 131 | return 132 | } 133 | 134 | viewRef?.get()?.apply { 135 | // Start the animation; wait until a pending layout if there is one. 136 | if (parent != null && parent.isLayoutRequested && isAttachedToWindow) { 137 | post { 138 | startSettlingAnimation(this, value) 139 | } 140 | } else { 141 | startSettlingAnimation(this, value) 142 | } 143 | } 144 | } 145 | 146 | /** Whether to fit to contents. If false, the behavior will include [STATE_HALF_EXPANDED]. */ 147 | var isFitToContents = true 148 | set(value) { 149 | if (field != value) { 150 | field = value 151 | // If sheet is already laid out, recalculate the collapsed offset. 152 | // Otherwise onLayoutChild will handle this later. 153 | if (viewRef != null) { 154 | collapsedOffset = calculateCollapsedOffset() 155 | } 156 | // Fix incorrect expanded settings. 157 | setStateInternal( 158 | if (field && state == STATE_HALF_EXPANDED) STATE_EXPANDED else state 159 | ) 160 | } 161 | } 162 | 163 | /** Real peek height in pixels */ 164 | private var _peekHeight = 0 165 | /** Peek height in pixels, or [PEEK_HEIGHT_AUTO] */ 166 | var peekHeight 167 | get() = if (peekHeightAuto) PEEK_HEIGHT_AUTO else _peekHeight 168 | set(value) { 169 | var needLayout = false 170 | if (value == PEEK_HEIGHT_AUTO) { 171 | if (!peekHeightAuto) { 172 | peekHeightAuto = true 173 | needLayout = true 174 | } 175 | } else if (peekHeightAuto || _peekHeight != value) { 176 | peekHeightAuto = false 177 | _peekHeight = Math.max(0, value) 178 | collapsedOffset = parentHeight - value 179 | needLayout = true 180 | } 181 | if (needLayout && (state == STATE_COLLAPSED || state == STATE_HIDDEN)) { 182 | viewRef?.get()?.requestLayout() 183 | } 184 | } 185 | 186 | /** Whether the bottom sheet can be hidden. */ 187 | var isHideable = false 188 | set(value) { 189 | if (field != value) { 190 | field = value 191 | if (!value && state == STATE_HIDDEN) { 192 | // Fix invalid state by moving to collapsed 193 | state = STATE_COLLAPSED 194 | } 195 | } 196 | } 197 | 198 | /** Whether the bottom sheet can be dragged or not. */ 199 | var isDraggable = true 200 | 201 | /** Whether the bottom sheet should skip collapsed state after being expanded once. */ 202 | var skipCollapsed = false 203 | 204 | /** Whether animations should be disabled, to be used from UI tests. */ 205 | @VisibleForTesting 206 | var isAnimationDisabled = false 207 | 208 | /** Whether or not to use automatic peek height */ 209 | private var peekHeightAuto = false 210 | /** Minimum peek height allowed */ 211 | private var peekHeightMin = 0 212 | /** The last peek height calculated in onLayoutChild */ 213 | private var lastPeekHeight = 0 214 | 215 | private var parentHeight = 0 216 | /** Bottom sheet's top offset in [STATE_EXPANDED] state. */ 217 | private var fitToContentsOffset = 0 218 | /** Bottom sheet's top offset in [STATE_HALF_EXPANDED] state. */ 219 | private var halfExpandedOffset = 0 220 | /** Bottom sheet's top offset in [STATE_COLLAPSED] state. */ 221 | private var collapsedOffset = 0 222 | 223 | /** Keeps reference to the bottom sheet outside of Behavior callbacks */ 224 | private var viewRef: WeakReference? = null 225 | /** Controls movement of the bottom sheet */ 226 | private lateinit var dragHelper: ViewDragHelper 227 | 228 | // Touch event handling, etc 229 | private var lastTouchX = 0 230 | private var lastTouchY = 0 231 | private var initialTouchY = 0 232 | private var activePointerId = MotionEvent.INVALID_POINTER_ID 233 | private var acceptTouches = true 234 | 235 | private var minimumVelocity = 0 236 | private var maximumVelocity = 0 237 | private var velocityTracker: VelocityTracker? = null 238 | 239 | private var nestedScrolled = false 240 | private var nestedScrollingChildRef: WeakReference? = null 241 | 242 | private val callbacks: MutableSet = mutableSetOf() 243 | 244 | constructor() : super() 245 | 246 | @SuppressLint("PrivateResource") 247 | constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { 248 | // Re-use BottomSheetBehavior's attrs 249 | val a = context.obtainStyledAttributes(attrs, R.styleable.BottomSheetBehavior_Layout) 250 | val value = a.peekValue(R.styleable.BottomSheetBehavior_Layout_behavior_peekHeight) 251 | peekHeight = if (value != null && value.data == PEEK_HEIGHT_AUTO) { 252 | value.data 253 | } else { 254 | a.getDimensionPixelSize( 255 | R.styleable.BottomSheetBehavior_Layout_behavior_peekHeight, PEEK_HEIGHT_AUTO 256 | ) 257 | } 258 | isHideable = a.getBoolean(R.styleable.BottomSheetBehavior_Layout_behavior_hideable, false) 259 | isFitToContents = 260 | a.getBoolean(R.styleable.BottomSheetBehavior_Layout_behavior_fitToContents, true) 261 | skipCollapsed = 262 | a.getBoolean(R.styleable.BottomSheetBehavior_Layout_behavior_skipCollapsed, false) 263 | a.recycle() 264 | val configuration = ViewConfiguration.get(context) 265 | minimumVelocity = configuration.scaledMinimumFlingVelocity 266 | maximumVelocity = configuration.scaledMaximumFlingVelocity 267 | } 268 | 269 | override fun onSaveInstanceState(parent: CoordinatorLayout, child: V): Parcelable { 270 | return SavedState( 271 | super.onSaveInstanceState(parent, child) ?: Bundle.EMPTY, 272 | state, 273 | peekHeight, 274 | isFitToContents, 275 | isHideable, 276 | skipCollapsed, 277 | isDraggable 278 | ) 279 | } 280 | 281 | override fun onRestoreInstanceState(parent: CoordinatorLayout, child: V, state: Parcelable) { 282 | val ss = state as SavedState 283 | super.onRestoreInstanceState(parent, child, ss.superState ?: Bundle.EMPTY) 284 | 285 | isDraggable = ss.isDraggable 286 | peekHeight = ss.peekHeight 287 | isFitToContents = ss.isFitToContents 288 | isHideable = ss.isHideable 289 | skipCollapsed = ss.skipCollapsed 290 | 291 | // Set state last. Intermediate states are restored as collapsed state. 292 | _state = if (ss.state == STATE_DRAGGING || ss.state == STATE_SETTLING) { 293 | STATE_COLLAPSED 294 | } else { 295 | ss.state 296 | } 297 | } 298 | 299 | fun addBottomSheetCallback(callback: BottomSheetCallback) { 300 | callbacks.add(callback) 301 | } 302 | 303 | fun removeBottomSheetCallback(callback: BottomSheetCallback) { 304 | callbacks.remove(callback) 305 | } 306 | 307 | private fun setStateInternal(@State state: Int) { 308 | if (_state != state) { 309 | _state = state 310 | viewRef?.get()?.let { view -> 311 | callbacks.forEach { callback -> 312 | callback.onStateChanged(view, state) 313 | } 314 | } 315 | } 316 | } 317 | 318 | // -- Layout 319 | 320 | override fun onLayoutChild( 321 | parent: CoordinatorLayout, 322 | child: V, 323 | layoutDirection: Int 324 | ): Boolean { 325 | if (parent.fitsSystemWindows && !child.fitsSystemWindows) { 326 | child.fitsSystemWindows = true 327 | } 328 | val savedTop = child.top 329 | // First let the parent lay it out 330 | parent.onLayoutChild(child, layoutDirection) 331 | parentHeight = parent.height 332 | 333 | // Calculate peek and offsets 334 | if (peekHeightAuto) { 335 | if (peekHeightMin == 0) { 336 | // init peekHeightMin 337 | @SuppressLint("PrivateResource") 338 | peekHeightMin = parent.resources.getDimensionPixelSize( 339 | R.dimen.design_bottom_sheet_peek_height_min 340 | ) 341 | } 342 | lastPeekHeight = Math.max(peekHeightMin, parentHeight - parent.width * 9 / 16) 343 | } else { 344 | lastPeekHeight = _peekHeight 345 | } 346 | fitToContentsOffset = Math.max(0, parentHeight - child.height) 347 | halfExpandedOffset = parentHeight / 2 348 | collapsedOffset = calculateCollapsedOffset() 349 | 350 | // Offset the bottom sheet 351 | when (state) { 352 | STATE_EXPANDED -> ViewCompat.offsetTopAndBottom(child, getExpandedOffset()) 353 | STATE_HALF_EXPANDED -> ViewCompat.offsetTopAndBottom(child, halfExpandedOffset) 354 | STATE_HIDDEN -> ViewCompat.offsetTopAndBottom(child, parentHeight) 355 | STATE_COLLAPSED -> ViewCompat.offsetTopAndBottom(child, collapsedOffset) 356 | STATE_DRAGGING, STATE_SETTLING -> ViewCompat.offsetTopAndBottom( 357 | child, savedTop - child.top 358 | ) 359 | } 360 | 361 | // Init these for later 362 | viewRef = WeakReference(child) 363 | if (!::dragHelper.isInitialized) { 364 | dragHelper = ViewDragHelper.create(parent, dragCallback) 365 | } 366 | return true 367 | } 368 | 369 | private fun calculateCollapsedOffset(): Int { 370 | return if (isFitToContents) { 371 | Math.max(parentHeight - lastPeekHeight, fitToContentsOffset) 372 | } else { 373 | parentHeight - lastPeekHeight 374 | } 375 | } 376 | 377 | private fun getExpandedOffset() = if (isFitToContents) fitToContentsOffset else 0 378 | 379 | // -- Touch events and scrolling 380 | 381 | override fun onInterceptTouchEvent( 382 | parent: CoordinatorLayout, 383 | child: V, 384 | event: MotionEvent 385 | ): Boolean { 386 | if (!isDraggable || !child.isShown) { 387 | acceptTouches = false 388 | return false 389 | } 390 | 391 | val action = event.actionMasked 392 | 393 | lastTouchX = event.x.toInt() 394 | lastTouchY = event.y.toInt() 395 | 396 | // Record velocity 397 | if (action == MotionEvent.ACTION_DOWN) { 398 | resetVelocityTracker() 399 | } 400 | if (velocityTracker == null) { 401 | velocityTracker = VelocityTracker.obtain() 402 | } 403 | velocityTracker?.addMovement(event) 404 | 405 | when (action) { 406 | MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { 407 | activePointerId = MotionEvent.INVALID_POINTER_ID 408 | if (!acceptTouches) { 409 | acceptTouches = true 410 | return false 411 | } 412 | } 413 | 414 | MotionEvent.ACTION_DOWN -> { 415 | activePointerId = event.getPointerId(event.actionIndex) 416 | initialTouchY = event.y.toInt() 417 | 418 | clearNestedScroll() 419 | 420 | if (!parent.isPointInChildBounds(child, lastTouchX, initialTouchY)) { 421 | // Not touching the sheet 422 | acceptTouches = false 423 | } 424 | } 425 | } 426 | 427 | return acceptTouches && 428 | // CoordinatorLayout can call us before the view is laid out. >_< 429 | ::dragHelper.isInitialized && 430 | dragHelper.shouldInterceptTouchEvent(event) 431 | } 432 | 433 | override fun onTouchEvent( 434 | parent: CoordinatorLayout, 435 | child: V, 436 | event: MotionEvent 437 | ): Boolean { 438 | if (!isDraggable || !child.isShown) { 439 | return false 440 | } 441 | 442 | val action = event.actionMasked 443 | if (action == MotionEvent.ACTION_DOWN && state == STATE_DRAGGING) { 444 | return true 445 | } 446 | 447 | lastTouchX = event.x.toInt() 448 | lastTouchY = event.y.toInt() 449 | 450 | // Record velocity 451 | if (action == MotionEvent.ACTION_DOWN) { 452 | resetVelocityTracker() 453 | } 454 | if (velocityTracker == null) { 455 | velocityTracker = VelocityTracker.obtain() 456 | } 457 | velocityTracker?.addMovement(event) 458 | 459 | // CoordinatorLayout can call us before the view is laid out. >_< 460 | if (::dragHelper.isInitialized) { 461 | dragHelper.processTouchEvent(event) 462 | } 463 | 464 | if (acceptTouches && 465 | action == MotionEvent.ACTION_MOVE && 466 | exceedsTouchSlop(initialTouchY, lastTouchY) 467 | ) { 468 | // Manually capture the sheet since nothing beneath us is scrolling. 469 | dragHelper.captureChildView(child, event.getPointerId(event.actionIndex)) 470 | } 471 | 472 | return acceptTouches 473 | } 474 | 475 | private fun resetVelocityTracker() { 476 | activePointerId = MotionEvent.INVALID_POINTER_ID 477 | velocityTracker?.recycle() 478 | velocityTracker = null 479 | } 480 | 481 | private fun exceedsTouchSlop(p1: Int, p2: Int) = Math.abs(p1 - p2) >= dragHelper.touchSlop 482 | 483 | // Nested scrolling 484 | 485 | override fun onStartNestedScroll( 486 | coordinatorLayout: CoordinatorLayout, 487 | child: V, 488 | directTargetChild: View, 489 | target: View, 490 | axes: Int, 491 | type: Int 492 | ): Boolean { 493 | nestedScrolled = false 494 | if (isDraggable && 495 | viewRef?.get() == directTargetChild && 496 | (axes and ViewCompat.SCROLL_AXIS_VERTICAL) != 0 497 | ) { 498 | // Scrolling view is a descendent of the sheet and scrolling vertically. 499 | // Let's follow along! 500 | nestedScrollingChildRef = WeakReference(target) 501 | return true 502 | } 503 | return false 504 | } 505 | 506 | override fun onNestedPreScroll( 507 | coordinatorLayout: CoordinatorLayout, 508 | child: V, 509 | target: View, 510 | dx: Int, 511 | dy: Int, 512 | consumed: IntArray, 513 | type: Int 514 | ) { 515 | if (type == ViewCompat.TYPE_NON_TOUCH) { 516 | return // Ignore fling here 517 | } 518 | if (target != nestedScrollingChildRef?.get()) { 519 | return 520 | } 521 | 522 | val currentTop = child.top 523 | val newTop = currentTop - dy 524 | if (dy > 0) { // Upward 525 | if (newTop < getExpandedOffset()) { 526 | consumed[1] = currentTop - getExpandedOffset() 527 | ViewCompat.offsetTopAndBottom(child, -consumed[1]) 528 | setStateInternal(STATE_EXPANDED) 529 | } else { 530 | consumed[1] = dy 531 | ViewCompat.offsetTopAndBottom(child, -dy) 532 | setStateInternal(STATE_DRAGGING) 533 | } 534 | } else if (dy < 0) { // Downward 535 | if (!target.canScrollVertically(-1)) { 536 | if (newTop <= collapsedOffset || isHideable) { 537 | consumed[1] = dy 538 | ViewCompat.offsetTopAndBottom(child, -dy) 539 | setStateInternal(STATE_DRAGGING) 540 | } else { 541 | consumed[1] = currentTop - collapsedOffset 542 | ViewCompat.offsetTopAndBottom(child, -consumed[1]) 543 | setStateInternal(STATE_COLLAPSED) 544 | } 545 | } 546 | } 547 | dispatchOnSlide(child.top) 548 | nestedScrolled = true 549 | } 550 | 551 | override fun onStopNestedScroll( 552 | coordinatorLayout: CoordinatorLayout, 553 | child: V, 554 | target: View, 555 | type: Int 556 | ) { 557 | if (child.top == getExpandedOffset()) { 558 | setStateInternal(STATE_EXPANDED) 559 | return 560 | } 561 | if (target != nestedScrollingChildRef?.get() || !nestedScrolled) { 562 | return 563 | } 564 | 565 | settleBottomSheet(child, getYVelocity(), true) 566 | clearNestedScroll() 567 | } 568 | 569 | override fun onNestedPreFling( 570 | coordinatorLayout: CoordinatorLayout, 571 | child: V, 572 | target: View, 573 | velocityX: Float, 574 | velocityY: Float 575 | ): Boolean { 576 | return isDraggable && 577 | target == nestedScrollingChildRef?.get() && 578 | (state != STATE_EXPANDED || super.onNestedPreFling( 579 | coordinatorLayout, child, target, velocityX, velocityY 580 | )) 581 | } 582 | 583 | private fun clearNestedScroll() { 584 | nestedScrolled = false 585 | nestedScrollingChildRef = null 586 | } 587 | 588 | // Settling 589 | 590 | private fun getYVelocity(): Float { 591 | return velocityTracker?.run { 592 | computeCurrentVelocity(1000, maximumVelocity.toFloat()) 593 | getYVelocity(activePointerId) 594 | } ?: 0f 595 | } 596 | 597 | private fun settleBottomSheet(sheet: View, yVelocity: Float, isNestedScroll: Boolean) { 598 | val top: Int 599 | @State val targetState: Int 600 | 601 | val flinging = yVelocity.absoluteValue > minimumVelocity 602 | if (flinging && yVelocity < 0) { // Moving up 603 | if (isFitToContents) { 604 | top = fitToContentsOffset 605 | targetState = STATE_EXPANDED 606 | } else { 607 | if (sheet.top > halfExpandedOffset) { 608 | top = halfExpandedOffset 609 | targetState = STATE_HALF_EXPANDED 610 | } else { 611 | top = 0 612 | targetState = STATE_EXPANDED 613 | } 614 | } 615 | } else if (isHideable && shouldHide(sheet, yVelocity)) { 616 | top = parentHeight 617 | targetState = STATE_HIDDEN 618 | } else if (flinging && yVelocity > 0) { // Moving down 619 | top = collapsedOffset 620 | targetState = STATE_COLLAPSED 621 | } else { 622 | val currentTop = sheet.top 623 | if (isFitToContents) { 624 | if (Math.abs(currentTop - fitToContentsOffset) 625 | < Math.abs(currentTop - collapsedOffset) 626 | ) { 627 | top = fitToContentsOffset 628 | targetState = STATE_EXPANDED 629 | } else { 630 | top = collapsedOffset 631 | targetState = STATE_COLLAPSED 632 | } 633 | } else { 634 | if (currentTop < halfExpandedOffset) { 635 | if (currentTop < Math.abs(currentTop - collapsedOffset)) { 636 | top = 0 637 | targetState = STATE_EXPANDED 638 | } else { 639 | top = halfExpandedOffset 640 | targetState = STATE_HALF_EXPANDED 641 | } 642 | } else { 643 | if (Math.abs(currentTop - halfExpandedOffset) 644 | < Math.abs(currentTop - collapsedOffset) 645 | ) { 646 | top = halfExpandedOffset 647 | targetState = STATE_HALF_EXPANDED 648 | } else { 649 | top = collapsedOffset 650 | targetState = STATE_COLLAPSED 651 | } 652 | } 653 | } 654 | } 655 | 656 | val startedSettling = if (isNestedScroll) { 657 | dragHelper.smoothSlideViewTo(sheet, sheet.left, top) 658 | } else { 659 | dragHelper.settleCapturedViewAt(sheet.left, top) 660 | } 661 | 662 | if (startedSettling) { 663 | setStateInternal(STATE_SETTLING) 664 | ViewCompat.postOnAnimation(sheet, SettleRunnable(sheet, targetState)) 665 | } else { 666 | setStateInternal(targetState) 667 | } 668 | } 669 | 670 | private fun shouldHide(child: View, yVelocity: Float): Boolean { 671 | if (skipCollapsed) { 672 | return true 673 | } 674 | if (child.top < collapsedOffset) { 675 | return false // it should not hide, but collapse. 676 | } 677 | val newTop = child.top + yVelocity * HIDE_FRICTION 678 | return Math.abs(newTop - collapsedOffset) / _peekHeight.toFloat() > HIDE_THRESHOLD 679 | } 680 | 681 | private fun startSettlingAnimation(child: View, state: Int) { 682 | var top: Int 683 | var finalState = state 684 | 685 | when { 686 | state == STATE_COLLAPSED -> top = collapsedOffset 687 | state == STATE_EXPANDED -> top = getExpandedOffset() 688 | state == STATE_HALF_EXPANDED -> { 689 | top = halfExpandedOffset 690 | // Skip to expanded state if we would scroll past the height of the contents. 691 | if (isFitToContents && top <= fitToContentsOffset) { 692 | finalState = STATE_EXPANDED 693 | top = fitToContentsOffset 694 | } 695 | } 696 | state == STATE_HIDDEN && isHideable -> top = parentHeight 697 | else -> throw IllegalArgumentException("Invalid state: $state") 698 | } 699 | 700 | if (isAnimationDisabled) { 701 | // Prevent animations 702 | ViewCompat.offsetTopAndBottom(child, top - child.top) 703 | } 704 | 705 | if (dragHelper.smoothSlideViewTo(child, child.left, top)) { 706 | setStateInternal(STATE_SETTLING) 707 | ViewCompat.postOnAnimation(child, SettleRunnable(child, finalState)) 708 | } else { 709 | setStateInternal(finalState) 710 | } 711 | } 712 | 713 | private fun dispatchOnSlide(top: Int) { 714 | viewRef?.get()?.let { sheet -> 715 | val denom = if (top > collapsedOffset) { 716 | parentHeight - collapsedOffset 717 | } else { 718 | collapsedOffset - getExpandedOffset() 719 | } 720 | callbacks.forEach { callback -> 721 | callback.onSlide(sheet, (collapsedOffset - top).toFloat() / denom) 722 | } 723 | } 724 | } 725 | 726 | private inner class SettleRunnable( 727 | private val view: View, 728 | @State private val state: Int 729 | ) : Runnable { 730 | override fun run() { 731 | if (dragHelper.continueSettling(true)) { 732 | view.postOnAnimation(this) 733 | } else { 734 | setStateInternal(state) 735 | } 736 | } 737 | } 738 | 739 | private val dragCallback: ViewDragHelper.Callback = object : ViewDragHelper.Callback() { 740 | 741 | override fun tryCaptureView(child: View, pointerId: Int): Boolean { 742 | when { 743 | // Sanity check 744 | state == STATE_DRAGGING -> return false 745 | // recapture a settling sheet 746 | dragHelper.viewDragState == ViewDragHelper.STATE_SETTLING -> return true 747 | // let nested scroll handle this 748 | nestedScrollingChildRef?.get() != null -> return false 749 | } 750 | 751 | val dy = lastTouchY - initialTouchY 752 | if (dy == 0) { 753 | // ViewDragHelper tries to capture in onTouch for the ACTION_DOWN event, but there's 754 | // really no way to check for a scrolling child without a direction, so wait. 755 | return false 756 | } 757 | 758 | if (state == STATE_COLLAPSED) { 759 | if (isHideable) { 760 | // Any drag should capture in order to expand or hide the sheet 761 | return true 762 | } 763 | if (dy < 0) { 764 | // Expand on upward movement, even if there's scrolling content underneath 765 | return true 766 | } 767 | } 768 | 769 | // Check for scrolling content underneath the touch point that can scroll in the 770 | // appropriate direction. 771 | val scrollingChild = findScrollingChildUnder(child, lastTouchX, lastTouchY, -dy) 772 | return scrollingChild == null 773 | } 774 | 775 | private fun findScrollingChildUnder(view: View, x: Int, y: Int, direction: Int): View? { 776 | if (view.visibility == View.VISIBLE && dragHelper.isViewUnder(view, x, y)) { 777 | if (view.canScrollVertically(direction)) { 778 | return view 779 | } 780 | if (view is ViewGroup) { 781 | // TODO this doesn't account for elevation or child drawing order. 782 | for (i in (view.childCount - 1) downTo 0) { 783 | val child = view.getChildAt(i) 784 | val found = 785 | findScrollingChildUnder(child, x - child.left, y - child.top, direction) 786 | if (found != null) { 787 | return found 788 | } 789 | } 790 | } 791 | } 792 | return null 793 | } 794 | 795 | override fun getViewVerticalDragRange(child: View): Int { 796 | return if (isHideable) parentHeight else collapsedOffset 797 | } 798 | 799 | override fun clampViewPositionVertical(child: View, top: Int, dy: Int): Int { 800 | val maxOffset = if (isHideable) parentHeight else collapsedOffset 801 | return top.coerceIn(getExpandedOffset(), maxOffset) 802 | } 803 | 804 | override fun clampViewPositionHorizontal(child: View, left: Int, dx: Int) = child.left 805 | 806 | override fun onViewDragStateChanged(state: Int) { 807 | if (state == ViewDragHelper.STATE_DRAGGING) { 808 | setStateInternal(STATE_DRAGGING) 809 | } 810 | } 811 | 812 | override fun onViewPositionChanged(child: View, left: Int, top: Int, dx: Int, dy: Int) { 813 | dispatchOnSlide(top) 814 | } 815 | 816 | override fun onViewReleased(releasedChild: View, xvel: Float, yvel: Float) { 817 | settleBottomSheet(releasedChild, yvel, false) 818 | } 819 | } 820 | 821 | /** SavedState implementation */ 822 | internal class SavedState : AbsSavedState { 823 | 824 | @State 825 | internal val state: Int 826 | internal val peekHeight: Int 827 | internal val isFitToContents: Boolean 828 | internal val isHideable: Boolean 829 | internal val skipCollapsed: Boolean 830 | internal val isDraggable: Boolean 831 | 832 | constructor(source: Parcel) : this(source, null) 833 | 834 | constructor(source: Parcel, loader: ClassLoader?) : super(source, loader) { 835 | state = source.readInt() 836 | peekHeight = source.readInt() 837 | isFitToContents = source.readBoolean() 838 | isHideable = source.readBoolean() 839 | skipCollapsed = source.readBoolean() 840 | isDraggable = source.readBoolean() 841 | } 842 | 843 | constructor( 844 | superState: Parcelable, 845 | @State state: Int, 846 | peekHeight: Int, 847 | isFitToContents: Boolean, 848 | isHideable: Boolean, 849 | skipCollapsed: Boolean, 850 | isDraggable: Boolean 851 | ) : super(superState) { 852 | this.state = state 853 | this.peekHeight = peekHeight 854 | this.isFitToContents = isFitToContents 855 | this.isHideable = isHideable 856 | this.skipCollapsed = skipCollapsed 857 | this.isDraggable = isDraggable 858 | } 859 | 860 | override fun writeToParcel(dest: Parcel, flags: Int) { 861 | super.writeToParcel(dest, flags) 862 | dest.apply { 863 | writeInt(state) 864 | writeInt(peekHeight) 865 | writeBoolean(isFitToContents) 866 | writeBoolean(isHideable) 867 | writeBoolean(skipCollapsed) 868 | writeBoolean(isDraggable) 869 | } 870 | } 871 | 872 | companion object { 873 | @JvmField 874 | val CREATOR: Parcelable.Creator = 875 | object : Parcelable.ClassLoaderCreator { 876 | override fun createFromParcel(source: Parcel): SavedState { 877 | return SavedState(source, null) 878 | } 879 | 880 | override fun createFromParcel(source: Parcel, loader: ClassLoader): SavedState { 881 | return SavedState(source, loader) 882 | } 883 | 884 | override fun newArray(size: Int): Array { 885 | return arrayOfNulls(size) 886 | } 887 | } 888 | } 889 | } 890 | } 891 | --------------------------------------------------------------------------------