├── .gitignore ├── LICENSE ├── Navigation-toolbar.gif ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── header.png ├── navigation-toolbar-example ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── kotlin │ └── com │ │ └── ramotion │ │ └── navigationtoolbar │ │ └── example │ │ ├── ExampleDataSet.kt │ │ ├── FABBehavior.kt │ │ ├── MainActivity.kt │ │ ├── header │ │ ├── HeaderAdapter.kt │ │ ├── HeaderItem.kt │ │ ├── HeaderItemTransformer.kt │ │ └── HeaderOverlayBehavior.kt │ │ └── pager │ │ ├── PageAdapter.kt │ │ ├── PageItem.kt │ │ └── ViewPagerAdapter.kt │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ ├── aaron_bradley.webp │ ├── barry_allen.webp │ ├── bella_holmes.webp │ ├── card_1_background.webp │ ├── card_1_gradient.xml │ ├── card_2_background.webp │ ├── card_2_gradient.xml │ ├── card_3_background.webp │ ├── card_3_gradient.xml │ ├── card_4_background.webp │ ├── card_4_gradient.xml │ ├── caroline_shaw.webp │ ├── connor_graham.webp │ ├── deann_hunt.webp │ ├── ella_cole.webp │ ├── header_background.webp │ ├── header_background_gradient.xml │ ├── ic_launcher_background.xml │ ├── jayden_shaw.webp │ ├── jerry_carrol.webp │ ├── lena_lucas.webp │ ├── leonrd_kim.webp │ ├── marc_baker.webp │ ├── marjorie_ellis.webp │ ├── mattew_jordan.webp │ ├── ross_rodriguez.webp │ ├── tina_caldwell.webp │ └── wallace_sutton.webp │ ├── layout │ ├── activity_main.xml │ ├── content_layout.xml │ ├── header_background.xml │ ├── header_item.xml │ ├── header_overlay.xml │ ├── list_item_image.xml │ ├── list_item_user.xml │ └── pager_item.xml │ ├── menu │ └── menu_main.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.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 │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── spinners_data.xml │ ├── strings.xml │ └── styles.xml ├── navigation-toolbar ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── kotlin │ └── com │ │ └── ramotion │ │ └── navigationtoolbar │ │ ├── DefaultItemTransformer.kt │ │ ├── HeaderLayout.kt │ │ ├── HeaderLayoutManager.kt │ │ ├── NavigationToolBarLayout.kt │ │ └── SimpleSnapHelper.kt │ └── res │ ├── layout │ └── navigation_layout.xml │ └── values │ ├── attrs.xml │ └── strings.xml └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Java compiled classes # 2 | *.class 3 | 4 | # Package Files # 5 | *.apk 6 | *.ap_ 7 | 8 | # IDEA Project files # 9 | *.iws 10 | *.iml 11 | *.ipr 12 | .idea/ 13 | 14 | # Build directory # 15 | target/ 16 | build/ 17 | 18 | # Generated files # 19 | bin/ 20 | gen/ 21 | out/ 22 | 23 | # Local props file # 24 | local.properties 25 | 26 | # Gradle cache # 27 | .gradle 28 | 29 | # OSX files # 30 | .DS_Store 31 | 32 | # NDK # 33 | obj/ 34 | 35 | # files for the dex VM # 36 | *.dex 37 | 38 | # Proguard folder generated by Eclipse 39 | proguard/ 40 | 41 | # Log Files 42 | *.log 43 | 44 | # Android Studio Navigation editor temp files 45 | .navigation/ 46 | 47 | # Android Studio captures folder 48 | captures/ 49 | 50 | # Keystore files 51 | *.jks 52 | 53 | # External native build folder generated in Android Studio 2.2 and later 54 | .externalNativeBuild 55 | 56 | # Google Services (e.g. APIs or Firebase) 57 | google-services.json 58 | 59 | # Freeline 60 | freeline.py 61 | freeline/ 62 | freeline_project_description.json 63 | fluid-slider-simple-example/libs/ 64 | fluid-slider/libs/ 65 | fluid-slider/src/main/java/ 66 | fluid-slider/src/main/res/drawable/ 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Ramotion 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Navigation-toolbar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/navigation-toolbar-android/4706c15209dff67e3f5ce191211fbb87dedfd13d/Navigation-toolbar.gif -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

NAVIGATION TOOLBAR

7 | 8 |

Navigation toolbar is a Kotlin slide-modeled UI navigation controller.

9 | 10 | 11 | ___ 12 | 13 | 14 |

We specialize in the designing and coding of custom UI for Mobile Apps and Websites.
15 | 16 | 17 |

18 |

Stay tuned for the latest updates:
19 | 20 |

21 | 22 | Inspired by [Aurélien Salomon](https://dribbble.com/aureliensalomon) [shot](https://dribbble.com/shots/2940231-Google-Newsstand-Navigation-Pattern) 23 | 24 |
25 | 26 | [![Twitter](https://img.shields.io/badge/Twitter-@Ramotion-blue.svg?style=flat)](http://twitter.com/Ramotion) 27 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/92bd2e49f7e543cd8748c670b9e52ca7)](https://app.codacy.com/app/dvg4000/navigation-toolbar-android/dashboard) 28 | [![Donate](https://img.shields.io/badge/Donate-PayPal-blue.svg)](https://paypal.me/Ramotion) 29 | 30 | ## Requirements 31 | 32 | - Android 5.0 Lollipop (API lvl 21) or greater 33 | - Your favorite IDE 34 | 35 | ## Installation 36 | ​ 37 | Just download the package from [here](http://central.maven.org/maven2/com/ramotion/navigationtoolbar/navigation-toolbar/0.1.3/navigation-toolbar-0.1.3.aar) and add it to your project classpath, or just use the maven repo: 38 | 39 | Gradle: 40 | ```groovy 41 | implementation 'com.ramotion.navigationtoolbar:navigation-toolbar:0.1.3' 42 | ``` 43 | SBT: 44 | ```scala 45 | libraryDependencies += "com.ramotion.navigationtoolbar" % "navitagiton-toolbar" % "0.1.3" 46 | ``` 47 | Maven: 48 | ```xml 49 | 50 | com.ramotion.navigationtoolbar 51 | navigation-toolbar 52 | 0.1.3 53 | aar 54 | 55 | ``` 56 | 57 | ## Basic usage 58 | 59 | NavigationToolBarLayout is the successor to CoordinatorLayout. Therefore, NavigationToolBarLayout 60 | must be the root element of your layout. Displayed content must be inside 61 | NavigationToolBarLayout, as shown below: 62 | 63 | ```xml 64 | 67 | 68 | 69 | 70 | 77 | 78 | 79 | ``` 80 | 81 | Next, you must specify an adapter for NavigationToolBarLayout, from which 82 | NavigationToolBarLayout will receive the displayed View. 83 | 84 | NavigationToolBarLayout contains `android.support.v7.widget.Toolbar` and 85 | `android.support.design.widget.AppBarLayout`, access to which can be obtained through 86 | the appropriate identifiers: 87 | ``` xml 88 | @id/com_ramotion_toolbar 89 | @id/com_ramotion_app_bar 90 | ``` 91 | or through the appropriate properties of the NavigationToolBarLayout class: 92 | ```kotlin 93 | val toolBar: Toolbar 94 | val appBarLayout: AppBarLayout 95 | ``` 96 | 97 | Here are the attributes you can specify through XML or related setters: 98 | * `headerOnScreenItemCount` - The maximum number of simultaneously displayed cards (items) in vertical orientation. 99 | * `headerCollapsingBySelectDuration` - Collapsing animation duration of header (HeaderLayout), when you click on the card in vertical orientation. 100 | * `headerTopBorderAtSystemBar` - Align the top card on the systembar or not. 101 | * `headerVerticalItemWidth` - Specifies the width of the vertical card. It can be equal to `match_parent`, then the width of the card will be equal to the width of NavigationToolBarLayout. 102 | * `headerVerticalGravity` - Specifies the alignment of the vertical card. Can take the values: left, center, or right. 103 | 104 | ## 🗂 Check this library on other language: 105 | 106 | 107 | 108 | 109 | ## 📄 License 110 | 111 | Navigation Toolbar Android is released under the MIT license. 112 | See [LICENSE](./LICENSE) for details. 113 | 114 | This library is a part of a selection of our best UI open-source projects 115 | 116 | If you use the open-source library in your project, please make sure to credit and backlink to www.ramotion.com 117 | 118 | ## 📱 Get the Showroom App for Android to give it a try 119 | Try this UI component and more like this in our Android app. Contact us if interested. 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.3.11' 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:3.3.1' 9 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 10 | classpath 'com.bmuschko:gradle-nexus-plugin:2.3.1' 11 | 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | google() 18 | jcenter() 19 | } 20 | } 21 | 22 | task clean(type: Delete) { 23 | delete rootProject.buildDir 24 | } 25 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | android.useAndroidX=true 11 | android.enableJetifier=true 12 | 13 | # Specifies the JVM arguments used for the daemon process. 14 | # The setting is particularly useful for tweaking memory settings. 15 | org.gradle.jvmargs=-Xmx1536m 16 | 17 | # When configured, Gradle will run in incubating parallel mode. 18 | # This option should only be used with decoupled projects. More details, visit 19 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 20 | # org.gradle.parallel=true 21 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/navigation-toolbar-android/4706c15209dff67e3f5ce191211fbb87dedfd13d/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Feb 01 09:49:09 MSK 2019 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-4.10.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /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 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 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 Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/navigation-toolbar-android/4706c15209dff67e3f5ce191211fbb87dedfd13d/header.png -------------------------------------------------------------------------------- /navigation-toolbar-example/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /navigation-toolbar-example/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | apply plugin: 'kotlin-kapt' 5 | 6 | android { 7 | compileSdkVersion 28 8 | defaultConfig { 9 | applicationId "com.ramotion.navigationtoolbar.example" 10 | minSdkVersion 21 11 | targetSdkVersion 28 12 | versionCode 3 13 | versionName "1.2" 14 | 15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 16 | 17 | } 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | sourceSets { 25 | main.java.srcDirs += 'src/main/kotlin' 26 | } 27 | buildToolsVersion '28.0.3' 28 | } 29 | 30 | dependencies { 31 | implementation fileTree(include: ['*.jar'], dir: 'libs') 32 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 33 | implementation 'androidx.appcompat:appcompat:1.0.2' 34 | implementation 'com.google.android.material:material:1.0.0' 35 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 36 | implementation 'androidx.cardview:cardview:1.0.0' 37 | implementation 'com.github.bumptech.glide:glide:4.8.0' 38 | kapt 'com.github.bumptech.glide:compiler:4.8.0' 39 | implementation project(':navigation-toolbar') 40 | testImplementation 'junit:junit:4.12' 41 | androidTestImplementation 'androidx.test:runner:1.1.1' 42 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' 43 | } 44 | -------------------------------------------------------------------------------- /navigation-toolbar-example/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 | -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/kotlin/com/ramotion/navigationtoolbar/example/ExampleDataSet.kt: -------------------------------------------------------------------------------- 1 | package com.ramotion.navigationtoolbar.example 2 | 3 | interface HeaderDataSet { 4 | data class ItemData(val gradient: Int, 5 | val background: Int, 6 | val title: String) 7 | 8 | fun getItemData(pos: Int): ItemData 9 | } 10 | 11 | interface PageDataSet { 12 | 13 | data class ItemData(val avatar: Int, 14 | val userName: String, 15 | val status: String) 16 | 17 | val secondItemImage: Int 18 | 19 | fun getItemData(pos: Int): ItemData 20 | } 21 | 22 | interface ViewPagerDataSet { 23 | fun getPageData(page: Int): PageDataSet 24 | } 25 | 26 | class ExampleDataSet { 27 | private val headerBackgrounds = intArrayOf(R.drawable.card_1_background, R.drawable.card_2_background, R.drawable.card_3_background, R.drawable.card_4_background).toTypedArray() 28 | private val headerGradients = intArrayOf(R.drawable.card_1_gradient, R.drawable.card_2_gradient, R.drawable.card_3_gradient, R.drawable.card_4_gradient).toTypedArray() 29 | private val headerTitles = arrayOf("TECHNOLOGY", "SCIENCE", "MOVIES", "GAMING") 30 | 31 | private val userNames = arrayOf("Aaron Bradley", "Barry Allen", "Bella Holmes", "Caroline Shaw", "Connor Graham", "Deann Hunt", "Ella Cole", "Jayden Shaw", "Jerry Carrol", "Lena Lucas", "Leonrd Kim", "Marc Baker", "Marjorie Ellis", "Mattew Jordan", "Ross Rodriguez", "Tina Caldwell", "Wallace Sutton") 32 | private val avatars = intArrayOf(R.drawable.aaron_bradley, R.drawable.barry_allen, R.drawable.bella_holmes, R.drawable.caroline_shaw, R.drawable.connor_graham, R.drawable.deann_hunt, R.drawable.ella_cole, R.drawable.jayden_shaw, R.drawable.jerry_carrol, R.drawable.lena_lucas, R.drawable.leonrd_kim, R.drawable.marc_baker, R.drawable.marjorie_ellis, R.drawable.mattew_jordan, R.drawable.ross_rodriguez, R.drawable.tina_caldwell, R.drawable.wallace_sutton) 33 | private val statuses = arrayOf( 34 | "When the sensor experiments for deep space, all mermaids accelerate mysterious, vital moons.", 35 | "It is a cold powerdrain, sir.", 36 | "Particle of a calm shield, control the alignment!", 37 | "The human kahless quickly promises the phenomenan.", 38 | "Ionic cannon at the infinity room was the sensor of voyage, imitated to a dead pathway.", 39 | "Vital particles, to the port.", 40 | "Stars fly with hypnosis at the boldly infinity room!", 41 | "Hypnosis, definition, and powerdrain.", 42 | "When the queen experiments for nowhere, all particles control reliable, cold captains.", 43 | "When the c-beam experiments for astral city, all cosmonauts acquire remarkable, virtual lieutenant commanders.", 44 | "Starships walk with love at the cold parallel universe!", 45 | "Friendship at the bridge that is when quirky green people yell.") 46 | 47 | internal val headerDataSet = object : HeaderDataSet { 48 | override fun getItemData(pos: Int) = 49 | HeaderDataSet.ItemData( 50 | gradient = headerGradients[pos % headerGradients.size], 51 | background = headerBackgrounds[pos % headerBackgrounds.size], 52 | title = headerTitles[pos % headerTitles.size]) 53 | } 54 | 55 | internal val viewPagerDataSet = object : ViewPagerDataSet { 56 | val pageItemCount = 5 57 | 58 | override fun getPageData(page: Int) = object : PageDataSet { 59 | override val secondItemImage = headerDataSet.getItemData(page).background 60 | 61 | override fun getItemData(pos: Int): PageDataSet.ItemData { 62 | val localPos = page * pageItemCount + pos 63 | return PageDataSet.ItemData( 64 | avatar = avatars[localPos % avatars.size], 65 | userName = userNames[localPos % userNames.size], 66 | status = statuses[localPos % statuses.size]) 67 | } 68 | } 69 | } 70 | 71 | } -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/kotlin/com/ramotion/navigationtoolbar/example/FABBehavior.kt: -------------------------------------------------------------------------------- 1 | package com.ramotion.navigationtoolbar.example 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.view.View 6 | import androidx.coordinatorlayout.widget.CoordinatorLayout 7 | import com.google.android.material.appbar.AppBarLayout 8 | import com.google.android.material.floatingactionbutton.FloatingActionButton 9 | 10 | class FABBehavior(context: Context, attrs: AttributeSet) : CoordinatorLayout.Behavior(context, attrs) { 11 | private val hideBorder = context.resources.displayMetrics.heightPixels / 2 12 | 13 | override fun layoutDependsOn(parent: CoordinatorLayout, child: FloatingActionButton, dependency: View): Boolean { 14 | return dependency is AppBarLayout 15 | } 16 | 17 | override fun onDependentViewChanged(parent: CoordinatorLayout, child: FloatingActionButton, dependency: View): Boolean { 18 | updateFABVisibility(dependency, child) 19 | return false 20 | } 21 | 22 | override fun onLayoutChild(parent: CoordinatorLayout, child: FloatingActionButton, layoutDirection: Int): Boolean { 23 | val dependencies = parent.getDependencies(child) 24 | for (i in 0 until dependencies.size) { 25 | val dependency = dependencies[i] 26 | if (dependency is AppBarLayout) { 27 | updateFABVisibility(dependency, child) 28 | break 29 | } 30 | } 31 | return super.onLayoutChild(parent, child, layoutDirection) 32 | } 33 | 34 | private fun updateFABVisibility(dependency: View, child: FloatingActionButton) { 35 | val show = dependency.bottom <= hideBorder 36 | if (show) child.show() else child.hide() 37 | } 38 | } -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/kotlin/com/ramotion/navigationtoolbar/example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.ramotion.navigationtoolbar.example 2 | 3 | import android.animation.ObjectAnimator 4 | import android.graphics.Rect 5 | import android.os.Bundle 6 | import android.view.Menu 7 | import android.view.MenuItem 8 | import android.widget.FrameLayout 9 | import androidx.appcompat.app.AppCompatActivity 10 | import androidx.appcompat.graphics.drawable.DrawerArrowDrawable 11 | import androidx.core.content.ContextCompat 12 | import androidx.viewpager.widget.ViewPager 13 | import com.google.android.material.snackbar.Snackbar 14 | import com.ramotion.navigationtoolbar.HeaderLayout 15 | import com.ramotion.navigationtoolbar.HeaderLayoutManager 16 | import com.ramotion.navigationtoolbar.NavigationToolBarLayout 17 | import com.ramotion.navigationtoolbar.SimpleSnapHelper 18 | import com.ramotion.navigationtoolbar.example.header.HeaderAdapter 19 | import com.ramotion.navigationtoolbar.example.header.HeaderItemTransformer 20 | import com.ramotion.navigationtoolbar.example.pager.ViewPagerAdapter 21 | import kotlinx.android.synthetic.main.activity_main.* 22 | import kotlin.math.ceil 23 | import kotlin.math.max 24 | 25 | 26 | class MainActivity : AppCompatActivity() { 27 | private val itemCount = 40 28 | private val dataSet = ExampleDataSet() 29 | 30 | private var isExpanded = true 31 | private var prevAnchorPosition = 0 32 | 33 | override fun onCreate(savedInstanceState: Bundle?) { 34 | super.onCreate(savedInstanceState) 35 | setContentView(R.layout.activity_main) 36 | 37 | fab.setOnClickListener { view -> 38 | Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG) 39 | .setAction("Action", null).show() 40 | } 41 | 42 | val header = findViewById(R.id.navigation_toolbar_layout) 43 | val viewPager = findViewById(R.id.pager) 44 | 45 | initActionBar() 46 | initViewPager(header, viewPager) 47 | initHeader(header, viewPager) 48 | } 49 | 50 | override fun onCreateOptionsMenu(menu: Menu): Boolean { 51 | // Inflate the menu; this adds items to the action bar if it is present. 52 | menuInflater.inflate(R.menu.menu_main, menu) 53 | return true 54 | } 55 | 56 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 57 | // Handle action bar item clicks here. The action bar will 58 | // automatically handle clicks on the Home/Up button, so long 59 | // as you specify a parent activity in AndroidManifest.xml. 60 | 61 | return when (item.itemId) { 62 | R.id.action_settings -> true 63 | else -> super.onOptionsItemSelected(item) 64 | } 65 | } 66 | 67 | private fun initActionBar() { 68 | val toolbar = navigation_toolbar_layout.toolBar 69 | setSupportActionBar(toolbar) 70 | supportActionBar?.apply { 71 | setDisplayShowTitleEnabled(false) 72 | setDisplayHomeAsUpEnabled(true) 73 | } 74 | } 75 | 76 | private fun initViewPager(header: NavigationToolBarLayout, viewPager: ViewPager) { 77 | viewPager.adapter = ViewPagerAdapter(itemCount, dataSet.viewPagerDataSet) 78 | viewPager.addOnPageChangeListener(object : ViewPager.SimpleOnPageChangeListener() { 79 | override fun onPageSelected(position: Int) { 80 | header.smoothScrollToPosition(position) 81 | } 82 | }) 83 | } 84 | 85 | private fun initHeader(header: NavigationToolBarLayout, viewPager: ViewPager) { 86 | val titleLeftOffset = resources.getDimensionPixelSize(R.dimen.title_left_offset) 87 | val lineRightOffset = resources.getDimensionPixelSize(R.dimen.line_right_offset) 88 | val lineBottomOffset = resources.getDimensionPixelSize(R.dimen.line_bottom_offset) 89 | val lineTitleOffset = resources.getDimensionPixelSize(R.dimen.line_title_offset) 90 | 91 | val headerOverlay = findViewById(R.id.header_overlay) 92 | header.setItemTransformer(HeaderItemTransformer(headerOverlay, 93 | titleLeftOffset, lineRightOffset, lineBottomOffset, lineTitleOffset)) 94 | header.setAdapter(HeaderAdapter(itemCount, dataSet.headerDataSet, headerOverlay)) 95 | 96 | header.addItemChangeListener(object : HeaderLayoutManager.ItemChangeListener { 97 | override fun onItemChangeStarted(position: Int) { 98 | prevAnchorPosition = position 99 | } 100 | 101 | override fun onItemChanged(position: Int) { 102 | viewPager.currentItem = position 103 | } 104 | }) 105 | 106 | header.addItemClickListener(object : HeaderLayoutManager.ItemClickListener { 107 | override fun onItemClicked(viewHolder: HeaderLayout.ViewHolder) { 108 | viewPager.currentItem = viewHolder.position 109 | } 110 | }) 111 | 112 | SimpleSnapHelper().attach(header) 113 | initDrawerArrow(header) 114 | initHeaderDecorator(header) 115 | } 116 | 117 | private fun initDrawerArrow(header: NavigationToolBarLayout) { 118 | val drawerArrow = DrawerArrowDrawable(this) 119 | drawerArrow.color = ContextCompat.getColor(this, android.R.color.white) 120 | drawerArrow.progress = 1f 121 | 122 | header.addHeaderChangeStateListener(object : HeaderLayoutManager.HeaderChangeStateListener() { 123 | private fun changeIcon(progress: Float) { 124 | ObjectAnimator.ofFloat(drawerArrow, "progress", progress).start() 125 | isExpanded = progress == 1f 126 | if (isExpanded) { 127 | prevAnchorPosition = header.getAnchorPos() 128 | } 129 | } 130 | 131 | override fun onMiddle() = changeIcon(0f) 132 | override fun onExpanded() = changeIcon(1f) 133 | }) 134 | 135 | val toolbar = header.toolBar 136 | toolbar.navigationIcon = drawerArrow 137 | toolbar.setNavigationOnClickListener { 138 | if (!isExpanded) { 139 | return@setNavigationOnClickListener 140 | } 141 | val anchorPos = header.getAnchorPos() 142 | if (anchorPos == HeaderLayout.INVALID_POSITION) { 143 | return@setNavigationOnClickListener 144 | } 145 | 146 | if (anchorPos == prevAnchorPosition) { 147 | header.collapse() 148 | } else { 149 | header.smoothScrollToPosition(prevAnchorPosition) 150 | } 151 | } 152 | } 153 | 154 | private fun initHeaderDecorator(header: NavigationToolBarLayout) { 155 | val decorator = object : 156 | HeaderLayoutManager.ItemDecoration, 157 | HeaderLayoutManager.HeaderChangeListener { 158 | 159 | private val dp5 = resources.getDimensionPixelSize(R.dimen.decor_bottom) 160 | 161 | private var bottomOffset = dp5 162 | 163 | override fun onHeaderChanged(lm: HeaderLayoutManager, header: HeaderLayout, headerBottom: Int) { 164 | val ratio = max(0f, headerBottom.toFloat() / header.height - 0.5f) / 0.5f 165 | bottomOffset = ceil(dp5 * ratio).toInt() 166 | } 167 | 168 | override fun getItemOffsets(outRect: Rect, viewHolder: HeaderLayout.ViewHolder) { 169 | outRect.bottom = bottomOffset 170 | } 171 | } 172 | 173 | header.addItemDecoration(decorator) 174 | header.addHeaderChangeListener(decorator) 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/kotlin/com/ramotion/navigationtoolbar/example/header/HeaderAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.ramotion.navigationtoolbar.example.header 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import android.widget.FrameLayout 7 | import android.widget.TextView 8 | import com.ramotion.navigationtoolbar.HeaderLayout 9 | import com.ramotion.navigationtoolbar.example.HeaderDataSet 10 | import com.ramotion.navigationtoolbar.example.R 11 | 12 | class HeaderAdapter( 13 | private val count: Int, 14 | private val dataSet: HeaderDataSet, 15 | overlay: FrameLayout) : HeaderLayout.Adapter() { 16 | 17 | private val textsLayout = overlay.findViewById(R.id.texts) 18 | private val linesLayout = overlay.findViewById(R.id.lines) 19 | 20 | override fun getItemCount() = count 21 | 22 | override fun onCreateViewHolder(parent: ViewGroup): HeaderItem { 23 | val view = LayoutInflater.from(parent.context).inflate(R.layout.header_item, parent, false) 24 | return HeaderItem(view) 25 | } 26 | 27 | override fun onBindViewHolder(holder: HeaderItem, position: Int) { 28 | holder.setContent(dataSet.getItemData(position), getNextOverlayTitle(), getNextOverlayLine()) 29 | } 30 | 31 | override fun onViewRecycled(holder: HeaderItem) { 32 | holder.clearContent() 33 | } 34 | 35 | private fun getNextOverlayTitle(): TextView? { 36 | for (i in 0 until textsLayout.childCount) { 37 | val child = textsLayout.getChildAt(i) 38 | if (child is TextView && child.getTag() == null) { 39 | return child 40 | } 41 | } 42 | return null 43 | } 44 | 45 | private fun getNextOverlayLine(): View? { 46 | for (i in 0 until linesLayout.childCount) { 47 | val child = linesLayout.getChildAt(i) 48 | if (child.getTag() == null) { 49 | return child 50 | } 51 | } 52 | return null 53 | } 54 | } -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/kotlin/com/ramotion/navigationtoolbar/example/header/HeaderItem.kt: -------------------------------------------------------------------------------- 1 | package com.ramotion.navigationtoolbar.example.header 2 | 3 | import android.view.View 4 | import android.widget.ImageView 5 | import android.widget.TextView 6 | import com.bumptech.glide.Glide 7 | import com.ramotion.navigationtoolbar.HeaderLayout 8 | import com.ramotion.navigationtoolbar.example.HeaderDataSet 9 | import com.ramotion.navigationtoolbar.example.R 10 | 11 | class HeaderItem(view: View) : HeaderLayout.ViewHolder(view) { 12 | private val gradient = view.findViewById(R.id.gradient) 13 | private val background = view.findViewById(R.id.image) 14 | 15 | internal val backgroundLayout = view.findViewById(R.id.backgroud_layout) 16 | 17 | internal var overlayTitle: TextView? = null 18 | internal var overlayLine: View? = null 19 | 20 | fun setContent(content: HeaderDataSet.ItemData, title: TextView?, line: View?) { 21 | gradient.setBackgroundResource(content.gradient) 22 | Glide.with(background).load(content.background).into(background) 23 | 24 | overlayTitle = title?.also { 25 | it.tag = position 26 | it.text = content.title 27 | it.visibility = View.VISIBLE 28 | } 29 | 30 | overlayLine = line 31 | overlayLine?.also { 32 | it.tag = position 33 | it.visibility = View.VISIBLE 34 | } 35 | } 36 | 37 | fun clearContent() { 38 | overlayTitle?.also { 39 | it.visibility = View.GONE 40 | it.tag = null 41 | } 42 | 43 | overlayLine?.also { 44 | it.tag = null 45 | it.visibility = View.GONE 46 | } 47 | 48 | overlayTitle = null 49 | overlayLine = null 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/kotlin/com/ramotion/navigationtoolbar/example/header/HeaderItemTransformer.kt: -------------------------------------------------------------------------------- 1 | package com.ramotion.navigationtoolbar.example.header 2 | 3 | import android.view.View 4 | import android.view.ViewOutlineProvider 5 | import android.view.ViewTreeObserver 6 | import android.widget.FrameLayout 7 | import com.ramotion.navigationtoolbar.DefaultItemTransformer 8 | import com.ramotion.navigationtoolbar.HeaderLayout 9 | import com.ramotion.navigationtoolbar.HeaderLayoutManager 10 | import kotlin.math.abs 11 | import kotlin.math.max 12 | import kotlin.math.min 13 | import kotlin.math.pow 14 | 15 | class HeaderItemTransformer( 16 | private val headerOverlay: FrameLayout, 17 | private val titleLeftOffset: Int, 18 | private val lineRightOffset: Int, 19 | private val lineBottomOffset: Int, 20 | private val lineTitleOffset: Int) : DefaultItemTransformer() { 21 | 22 | private var prevChildCount = Int.MIN_VALUE 23 | private var prevHScrollOffset = Int.MIN_VALUE 24 | private var prevVScrollOffset = Int.MIN_VALUE 25 | private var prevHeaderBottom = Int.MIN_VALUE 26 | 27 | private var isOverlayLaidout = false 28 | 29 | override fun transform(lm: HeaderLayoutManager, header: HeaderLayout, headerBottom: Int) { 30 | super.transform(lm, header, headerBottom) 31 | 32 | if (!isOverlayLaidout) { 33 | headerOverlay.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener { 34 | override fun onGlobalLayout() { 35 | headerOverlay.viewTreeObserver.removeOnGlobalLayoutListener(this) 36 | isOverlayLaidout = true 37 | transformOverlay(header) 38 | } 39 | }) 40 | return 41 | } 42 | 43 | if (!checkForChanges(header, headerBottom)) { 44 | return 45 | } 46 | 47 | transformOverlay(header) 48 | } 49 | 50 | private fun checkForChanges(header: HeaderLayout, headerBottom: Int): Boolean { 51 | val childCount = header.childCount 52 | if (childCount == 0) { 53 | return false 54 | } 55 | 56 | val (hs, vs) = header.getChildAt(0).let { it.left to it.top } 57 | val nothingChanged = 58 | hs == prevHScrollOffset && vs == prevVScrollOffset 59 | && childCount == prevChildCount 60 | && prevHeaderBottom == headerBottom 61 | if (nothingChanged) { 62 | return false 63 | } 64 | 65 | prevChildCount = childCount 66 | prevHScrollOffset = hs 67 | prevVScrollOffset = vs 68 | prevHeaderBottom = headerBottom 69 | 70 | return true 71 | } 72 | 73 | private fun transformOverlay(header: HeaderLayout) { 74 | val invertedBottomRatio = 1f - currentRatioBottomHalf 75 | val headerCenter = header.width / 2f 76 | 77 | val lineAlpha = (abs((min(0.8f, max(0.2f, currentRatioBottomHalf)) - 0.2f) / 0.6f - 0.5f) / 0.5f).pow(11) 78 | 79 | for (i in 0 until header.childCount) { 80 | val card = header.getChildAt(i) 81 | val holder = HeaderLayout.getChildViewHolder(card) as HeaderItem 82 | 83 | val cardWidth = card.width 84 | val cardHeight = card.height 85 | val cardCenterX = card.x + cardWidth / 2 86 | val cardCenterY = card.y + cardHeight / 2 87 | 88 | val ratioHorizontalPosition = (card.x / cardWidth) * invertedBottomRatio 89 | val ratioHorizontalOffset = (1f - min(headerCenter, abs(headerCenter - cardCenterX)) / headerCenter * invertedBottomRatio) 90 | val alphaTitle = 0.7f + 0.3f * ratioHorizontalOffset 91 | 92 | if (holder.overlayTitle?.text?.isNotEmpty() == true && holder.overlayLine?.width == 0) 93 | holder.overlayTitle?.requestLayout() 94 | transformTitle(holder, card, cardCenterX, cardCenterY, ratioHorizontalPosition, invertedBottomRatio, ratioHorizontalOffset, alphaTitle) 95 | 96 | if (holder.overlayLine?.width == 0 || holder.overlayLine?.height == 0) 97 | holder.overlayLine?.requestLayout() 98 | transformLine(holder, card, cardCenterX, cardCenterY, ratioHorizontalPosition, lineAlpha, alphaTitle) 99 | 100 | val background = holder.backgroundLayout 101 | if (currentRatioBottomHalf != 0f) { 102 | background.translationX = 0f 103 | background.alpha = 1f 104 | card.outlineProvider = ViewOutlineProvider.BOUNDS 105 | } else { 106 | card.outlineProvider = null 107 | if (ratioHorizontalPosition <= -1f || ratioHorizontalPosition >= 1f) { 108 | background.translationX = cardWidth * ratioHorizontalPosition 109 | background.alpha = 0f 110 | } else if (ratioHorizontalPosition == 0f) { 111 | background.translationX = cardWidth * ratioHorizontalPosition 112 | background.alpha = 1f 113 | } else { 114 | background.translationX = cardWidth * -ratioHorizontalPosition 115 | background.alpha = 1f - abs(ratioHorizontalPosition) 116 | } 117 | } 118 | } 119 | } 120 | 121 | private fun transformTitle(holder: HeaderItem, card: View, 122 | cardCenterX: Float, cardCenterY: Float, 123 | ratioHorizontalPosition: Float, invertedBottomRatio: Float, 124 | ratioHorizontalOffset: Float, alphaTitle: Float) { 125 | 126 | if (holder.overlayTitle?.text?.isNotEmpty() == true && holder.overlayLine?.width == 0) { 127 | holder.overlayTitle?.visibility = View.INVISIBLE 128 | holder.overlayTitle?.postDelayed({ 129 | transformTitle(holder, card, cardCenterX, cardCenterY, ratioHorizontalPosition, invertedBottomRatio, ratioHorizontalOffset, alphaTitle) 130 | }, 50) 131 | return 132 | } 133 | 134 | holder.overlayTitle?.also { title -> 135 | holder.overlayTitle?.visibility = View.VISIBLE 136 | val titleLeft = card.x + titleLeftOffset 137 | val titleCenter = cardCenterX - title.width / 2 138 | val titleCurrentLeft = titleLeft + (titleCenter - titleLeft) * invertedBottomRatio 139 | val titleTop = cardCenterY - title.height / 2 140 | val titleOffset = (-ratioHorizontalPosition * card.width / 2) * currentRatioTopHalf 141 | 142 | title.x = titleCurrentLeft + titleOffset 143 | title.y = titleTop 144 | title.alpha = alphaTitle 145 | title.scaleX = min(1f, 0.8f + 0.2f * ratioHorizontalOffset) 146 | title.scaleY = title.scaleX 147 | } 148 | } 149 | 150 | private fun transformLine(holder: HeaderItem, card: View, 151 | cardCenterX: Float, cardCenterY: Float, 152 | ratioHorizontalPosition: Float, 153 | lineAlpha: Float, alphaTitle: Float) { 154 | 155 | if (holder.overlayLine?.width == 0 || holder.overlayLine?.height == 0) { 156 | holder.overlayLine?.visibility = View.INVISIBLE 157 | holder.overlayLine?.postDelayed({ 158 | transformLine(holder, card, cardCenterX, cardCenterY, ratioHorizontalPosition, lineAlpha, alphaTitle) 159 | }, 50) 160 | return 161 | } 162 | 163 | holder.overlayLine?.also { line -> 164 | holder.overlayLine?.visibility = View.VISIBLE 165 | val lineWidth = line.width 166 | val lineHeight = line.height 167 | val lineLeft = cardCenterX - lineWidth / 2 168 | val lineTop = cardCenterY + (holder.overlayTitle?.let { it.height / 2 + lineTitleOffset } ?: 0) 169 | val hBottomOffset = ((card.right - lineRightOffset - lineWidth) - lineLeft) * currentRatioBottomHalf 170 | val hTopOffset = -ratioHorizontalPosition * card.width / 1.1f * (1f - currentRatioTopHalf) 171 | val vOffset = ((card.bottom - lineBottomOffset - lineHeight) - lineTop) * currentRatioBottomHalf 172 | line.x = lineLeft + hBottomOffset + hTopOffset 173 | line.y = lineTop + vOffset 174 | line.alpha = if (currentRatioTopHalf == 1f) lineAlpha else alphaTitle 175 | } 176 | } 177 | } -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/kotlin/com/ramotion/navigationtoolbar/example/header/HeaderOverlayBehavior.kt: -------------------------------------------------------------------------------- 1 | package com.ramotion.navigationtoolbar.example.header 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.view.View 6 | import android.widget.FrameLayout 7 | import androidx.coordinatorlayout.widget.CoordinatorLayout 8 | import com.google.android.material.appbar.AppBarLayout 9 | 10 | class HeaderOverlayBehavior(context: Context, attrs: AttributeSet) : 11 | CoordinatorLayout.Behavior(context, attrs) { 12 | 13 | override fun layoutDependsOn(parent: CoordinatorLayout, child: FrameLayout, dependency: View): Boolean { 14 | return dependency is AppBarLayout 15 | } 16 | 17 | override fun onDependentViewChanged(parent: CoordinatorLayout, child: FrameLayout, dependency: View): Boolean { 18 | update(dependency, child) 19 | return false 20 | } 21 | 22 | override fun onLayoutChild(parent: CoordinatorLayout, child: FrameLayout, layoutDirection: Int): Boolean { 23 | val dependencies = parent.getDependencies(child) 24 | for (i in 0 until dependencies.size) { 25 | val dependency = dependencies[i] 26 | if (dependency is AppBarLayout) { 27 | update(dependency, child) 28 | break 29 | } 30 | } 31 | return super.onLayoutChild(parent, child, layoutDirection) 32 | } 33 | 34 | private fun update(dependency: View, child: FrameLayout) { 35 | child.y = (dependency.bottom - child.height).toFloat() 36 | } 37 | } -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/kotlin/com/ramotion/navigationtoolbar/example/pager/PageAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.ramotion.navigationtoolbar.example.pager 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.recyclerview.widget.RecyclerView 6 | import com.ramotion.navigationtoolbar.example.PageDataSet 7 | import com.ramotion.navigationtoolbar.example.R 8 | 9 | 10 | class PageAdapter(private val count: Int, 11 | private val dataSet: PageDataSet) : RecyclerView.Adapter() { 12 | 13 | private enum class ItemType(val value: Int) { 14 | USER(1), 15 | IMAGE(2); 16 | 17 | companion object { 18 | private val map = ItemType.values().associateBy(ItemType::value) 19 | fun fromInt(type: Int, defaultValue: ItemType = USER) = map.getOrElse(type) {defaultValue} 20 | } 21 | } 22 | 23 | override fun getItemCount() = count 24 | 25 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PageItem { 26 | return when (ItemType.fromInt(viewType)) { 27 | ItemType.USER -> createItemUser(parent) 28 | ItemType.IMAGE -> createItemImage(parent) 29 | } 30 | } 31 | 32 | override fun onBindViewHolder(holder: PageItem, position: Int) { 33 | when (holder) { 34 | is ItemUser -> { holder.setContent(dataSet.getItemData(position)) } 35 | is ItemImage -> { holder.setImage(dataSet.secondItemImage) } 36 | } 37 | } 38 | 39 | override fun getItemViewType(position: Int): Int { 40 | return (if (position == 1) ItemType.IMAGE else ItemType.USER).value 41 | } 42 | 43 | override fun onViewRecycled(holder: PageItem) { 44 | super.onViewRecycled(holder) 45 | holder.clearContent() 46 | } 47 | 48 | private fun createItemUser(parent: ViewGroup): ItemUser { 49 | val view = LayoutInflater.from(parent.context).inflate(R.layout.list_item_user, parent, false) 50 | return ItemUser(view) 51 | } 52 | 53 | private fun createItemImage(parent: ViewGroup): ItemImage { 54 | val view = LayoutInflater.from(parent.context).inflate(R.layout.list_item_image, parent, false) 55 | return ItemImage(view) 56 | } 57 | } -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/kotlin/com/ramotion/navigationtoolbar/example/pager/PageItem.kt: -------------------------------------------------------------------------------- 1 | package com.ramotion.navigationtoolbar.example.pager 2 | 3 | import android.view.View 4 | import android.widget.ImageView 5 | import android.widget.TextView 6 | import androidx.recyclerview.widget.RecyclerView 7 | import com.bumptech.glide.Glide 8 | import com.ramotion.navigationtoolbar.example.PageDataSet 9 | import com.ramotion.navigationtoolbar.example.R 10 | 11 | sealed class PageItem(view: View) : RecyclerView.ViewHolder(view) { 12 | fun clearContent() {} 13 | } 14 | 15 | class ItemUser(view: View) : PageItem(view) { 16 | private val avatar = view.findViewById(R.id.avatar) 17 | private val userName = view.findViewById(R.id.user_name) 18 | private val status = view.findViewById(R.id.status) 19 | 20 | fun setContent(content: PageDataSet.ItemData) { 21 | userName.setText(content.userName) 22 | status.setText(content.status) 23 | avatar.setImageResource(content.avatar) 24 | 25 | Glide.with(avatar).load(content.avatar).into(avatar) 26 | } 27 | } 28 | 29 | class ItemImage(view: View) : PageItem(view) { 30 | private val imageView = view.findViewById(R.id.page_image) 31 | 32 | fun setImage(imgId: Int) { 33 | Glide.with(imageView).load(imgId).into(imageView) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/kotlin/com/ramotion/navigationtoolbar/example/pager/ViewPagerAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.ramotion.navigationtoolbar.example.pager 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import androidx.recyclerview.widget.RecyclerView 7 | import androidx.viewpager.widget.PagerAdapter 8 | import com.ramotion.navigationtoolbar.example.R 9 | import com.ramotion.navigationtoolbar.example.ViewPagerDataSet 10 | import java.util.* 11 | 12 | 13 | class ViewPagerAdapter(private val count: Int, 14 | private val dataSet: ViewPagerDataSet) : PagerAdapter() { 15 | 16 | private companion object { 17 | val random = Random() 18 | } 19 | 20 | override fun getCount(): Int = count 21 | 22 | override fun isViewFromObject(view: View, key: Any): Boolean = view == key 23 | 24 | override fun instantiateItem(container: ViewGroup, position: Int): Any { 25 | val view = LayoutInflater.from(container.context).inflate(R.layout.pager_item, container, false) 26 | initRecyclerView(view as RecyclerView, position) 27 | container.addView(view) 28 | return view 29 | } 30 | 31 | override fun destroyItem(container: ViewGroup, position: Int, key: Any) { 32 | container.removeView(key as View) 33 | } 34 | 35 | override fun getPageTitle(position: Int): CharSequence = position.toString() 36 | 37 | private fun initRecyclerView(recyclerView: RecyclerView, position: Int) { 38 | val adapter = PageAdapter(random.nextInt(10) + 5, dataSet.getPageData(position)) 39 | recyclerView.adapter = adapter 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/drawable/aaron_bradley.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/navigation-toolbar-android/4706c15209dff67e3f5ce191211fbb87dedfd13d/navigation-toolbar-example/src/main/res/drawable/aaron_bradley.webp -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/drawable/barry_allen.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/navigation-toolbar-android/4706c15209dff67e3f5ce191211fbb87dedfd13d/navigation-toolbar-example/src/main/res/drawable/barry_allen.webp -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/drawable/bella_holmes.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/navigation-toolbar-android/4706c15209dff67e3f5ce191211fbb87dedfd13d/navigation-toolbar-example/src/main/res/drawable/bella_holmes.webp -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/drawable/card_1_background.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/navigation-toolbar-android/4706c15209dff67e3f5ce191211fbb87dedfd13d/navigation-toolbar-example/src/main/res/drawable/card_1_background.webp -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/drawable/card_1_gradient.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/drawable/card_2_background.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/navigation-toolbar-android/4706c15209dff67e3f5ce191211fbb87dedfd13d/navigation-toolbar-example/src/main/res/drawable/card_2_background.webp -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/drawable/card_2_gradient.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/drawable/card_3_background.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/navigation-toolbar-android/4706c15209dff67e3f5ce191211fbb87dedfd13d/navigation-toolbar-example/src/main/res/drawable/card_3_background.webp -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/drawable/card_3_gradient.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/drawable/card_4_background.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/navigation-toolbar-android/4706c15209dff67e3f5ce191211fbb87dedfd13d/navigation-toolbar-example/src/main/res/drawable/card_4_background.webp -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/drawable/card_4_gradient.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/drawable/caroline_shaw.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/navigation-toolbar-android/4706c15209dff67e3f5ce191211fbb87dedfd13d/navigation-toolbar-example/src/main/res/drawable/caroline_shaw.webp -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/drawable/connor_graham.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/navigation-toolbar-android/4706c15209dff67e3f5ce191211fbb87dedfd13d/navigation-toolbar-example/src/main/res/drawable/connor_graham.webp -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/drawable/deann_hunt.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/navigation-toolbar-android/4706c15209dff67e3f5ce191211fbb87dedfd13d/navigation-toolbar-example/src/main/res/drawable/deann_hunt.webp -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/drawable/ella_cole.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/navigation-toolbar-android/4706c15209dff67e3f5ce191211fbb87dedfd13d/navigation-toolbar-example/src/main/res/drawable/ella_cole.webp -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/drawable/header_background.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/navigation-toolbar-android/4706c15209dff67e3f5ce191211fbb87dedfd13d/navigation-toolbar-example/src/main/res/drawable/header_background.webp -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/drawable/header_background_gradient.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | -------------------------------------------------------------------------------- /navigation-toolbar-example/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 | -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/drawable/jayden_shaw.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/navigation-toolbar-android/4706c15209dff67e3f5ce191211fbb87dedfd13d/navigation-toolbar-example/src/main/res/drawable/jayden_shaw.webp -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/drawable/jerry_carrol.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/navigation-toolbar-android/4706c15209dff67e3f5ce191211fbb87dedfd13d/navigation-toolbar-example/src/main/res/drawable/jerry_carrol.webp -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/drawable/lena_lucas.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/navigation-toolbar-android/4706c15209dff67e3f5ce191211fbb87dedfd13d/navigation-toolbar-example/src/main/res/drawable/lena_lucas.webp -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/drawable/leonrd_kim.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/navigation-toolbar-android/4706c15209dff67e3f5ce191211fbb87dedfd13d/navigation-toolbar-example/src/main/res/drawable/leonrd_kim.webp -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/drawable/marc_baker.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/navigation-toolbar-android/4706c15209dff67e3f5ce191211fbb87dedfd13d/navigation-toolbar-example/src/main/res/drawable/marc_baker.webp -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/drawable/marjorie_ellis.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/navigation-toolbar-android/4706c15209dff67e3f5ce191211fbb87dedfd13d/navigation-toolbar-example/src/main/res/drawable/marjorie_ellis.webp -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/drawable/mattew_jordan.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/navigation-toolbar-android/4706c15209dff67e3f5ce191211fbb87dedfd13d/navigation-toolbar-example/src/main/res/drawable/mattew_jordan.webp -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/drawable/ross_rodriguez.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/navigation-toolbar-android/4706c15209dff67e3f5ce191211fbb87dedfd13d/navigation-toolbar-example/src/main/res/drawable/ross_rodriguez.webp -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/drawable/tina_caldwell.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/navigation-toolbar-android/4706c15209dff67e3f5ce191211fbb87dedfd13d/navigation-toolbar-example/src/main/res/drawable/tina_caldwell.webp -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/drawable/wallace_sutton.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/navigation-toolbar-android/4706c15209dff67e3f5ce191211fbb87dedfd13d/navigation-toolbar-example/src/main/res/drawable/wallace_sutton.webp -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | 14 | 15 | 16 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/layout/content_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | 18 | 27 | 28 | 40 | 41 | 51 | 52 | 62 | 63 | -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/layout/header_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 13 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/layout/header_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 13 | 17 | 18 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/layout/header_overlay.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/layout/list_item_image.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 20 | 21 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/layout/list_item_user.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 20 | 21 | 35 | 36 | 48 | 49 | -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/layout/pager_item.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 | 5 | 10 | 11 | -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/navigation-toolbar-android/4706c15209dff67e3f5ce191211fbb87dedfd13d/navigation-toolbar-example/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/navigation-toolbar-android/4706c15209dff67e3f5ce191211fbb87dedfd13d/navigation-toolbar-example/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/navigation-toolbar-android/4706c15209dff67e3f5ce191211fbb87dedfd13d/navigation-toolbar-example/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/navigation-toolbar-android/4706c15209dff67e3f5ce191211fbb87dedfd13d/navigation-toolbar-example/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/navigation-toolbar-android/4706c15209dff67e3f5ce191211fbb87dedfd13d/navigation-toolbar-example/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/navigation-toolbar-android/4706c15209dff67e3f5ce191211fbb87dedfd13d/navigation-toolbar-example/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/navigation-toolbar-android/4706c15209dff67e3f5ce191211fbb87dedfd13d/navigation-toolbar-example/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/navigation-toolbar-android/4706c15209dff67e3f5ce191211fbb87dedfd13d/navigation-toolbar-example/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/navigation-toolbar-android/4706c15209dff67e3f5ce191211fbb87dedfd13d/navigation-toolbar-example/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ramotion/navigation-toolbar-android/4706c15209dff67e3f5ce191211fbb87dedfd13d/navigation-toolbar-example/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #a2303f9f 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 16dp 3 | 5dp 4 | -20dp 5 | 20dp 6 | 20dp 7 | 10dp 8 | 9 | -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/values/spinners_data.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Popular 5 | New 6 | 7 | 8 | This week 9 | This month 10 | 11 | -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | navigation-toolbar-example 3 | Settings 4 | 5 | second item image 6 | avatar 7 | header background 8 | header item background 9 | 10 | -------------------------------------------------------------------------------- /navigation-toolbar-example/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 13 | 24 | 25 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /navigation-toolbar/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /navigation-toolbar/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'signing' 4 | apply plugin: 'com.bmuschko.nexus' 5 | 6 | group = 'com.ramotion.navigationtoolbar' 7 | version = '0.1.3' 8 | 9 | android { 10 | compileSdkVersion 28 11 | defaultConfig { 12 | minSdkVersion 21 13 | targetSdkVersion 28 14 | versionCode 4 15 | versionName version 16 | 17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 18 | } 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | sourceSets { 26 | main.java.srcDirs += 'src/main/kotlin' 27 | } 28 | buildToolsVersion '28.0.3' 29 | } 30 | 31 | dependencies { 32 | implementation fileTree(include: ['*.jar'], dir: 'libs') 33 | implementation 'androidx.appcompat:appcompat:1.0.2' 34 | implementation 'com.google.android.material:material:1.0.0' 35 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 36 | testImplementation 'junit:junit:4.12' 37 | androidTestImplementation 'androidx.test:runner:1.1.1' 38 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' 39 | } 40 | 41 | 42 | modifyPom { 43 | project { 44 | name 'Navigation Toolbar for Android' 45 | description 'Navigation toolbar is a slide-modeled UI navigation controller. http://ramotion.com' 46 | url 'https://github.com/Ramotion/navigation-toolbar-android' 47 | inceptionYear '2018' 48 | 49 | scm { 50 | url 'https://github.com/Ramotion/navigation-toolbar-android' 51 | connection 'scm:git@github.com:Ramotion/navigation-toolbar-android.git' 52 | developerConnection 'scm:git@github.com:Ramotion/navigation-toolbar-android.git' 53 | } 54 | 55 | licenses { 56 | license { 57 | name 'The MIT License (MIT)' 58 | url 'https://opensource.org/licenses/mit-license.php' 59 | distribution 'repo' 60 | } 61 | } 62 | 63 | developers { 64 | developer { 65 | id 'dvg4000' 66 | name 'Dmitry Grishechkin' 67 | email 'dvgrishechkin@yandex.ru' 68 | } 69 | } 70 | } 71 | } 72 | 73 | nexus { 74 | sign = true 75 | repositoryUrl = 'https://oss.sonatype.org/service/local/staging/deploy/maven2/' 76 | snapshotRepositoryUrl = 'https://oss.sonatype.org/content/repositories/snapshots/' 77 | } 78 | repositories { 79 | mavenCentral() 80 | } 81 | 82 | -------------------------------------------------------------------------------- /navigation-toolbar/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 | -------------------------------------------------------------------------------- /navigation-toolbar/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /navigation-toolbar/src/main/kotlin/com/ramotion/navigationtoolbar/DefaultItemTransformer.kt: -------------------------------------------------------------------------------- 1 | package com.ramotion.navigationtoolbar 2 | 3 | import com.ramotion.navigationtoolbar.HeaderLayoutManager.Point 4 | import kotlin.math.max 5 | import kotlin.math.min 6 | 7 | /** 8 | * DefaultItemTransformer - default implementation if ItemTransformer interface. 9 | * @see NavigationToolBarLayout.ItemTransformer 10 | * @see NavigationToolBarLayout.setItemTransformer 11 | */ 12 | open class DefaultItemTransformer 13 | : NavigationToolBarLayout.ItemTransformer(), HeaderLayoutManager.ItemClickListener { 14 | 15 | private val hPoints: MutableList = mutableListOf() 16 | private val vPoints: MutableList = mutableListOf() 17 | 18 | private var ratioWork = 0f 19 | private var ratioTopHalf = 0f 20 | private var ratioBottomHalf = 0f 21 | 22 | private var clickedItemIndex: Int? = null 23 | private var prevItemCount: Int? = null 24 | 25 | /** 26 | * Current HeaderLayout bottom position form 1f (bottom) to 0.11f (top). -1f if not computed yet. 27 | */ 28 | protected var currentRatio = -1f; private set 29 | /** 30 | * Current HeaderLayout bottom position from 1f (bottom) to 0f (top, bellow ToolBar). -1f if not computed yet. 31 | */ 32 | protected var currentRatioWork = -1f; private set 33 | /** 34 | * Current HeaderLayout bottom position from 1f (middle) to 0f (top, bellow ToolBar). -1f if not computed yet. 35 | */ 36 | protected var currentRatioTopHalf = -1f; private set 37 | /** 38 | * Current HeaderLayout bottom position from 1f (bottom) to 0f (middle). -1f if not computed yet. 39 | */ 40 | protected var currentRatioBottomHalf = -1f; private set 41 | 42 | override fun onAttach(ntl: NavigationToolBarLayout) { 43 | ntl.layoutManager.also { lm -> 44 | ratioWork = lm.workHeight / lm.workBottom.toFloat() 45 | ratioTopHalf = lm.workTop / lm.workBottom.toFloat() 46 | ratioBottomHalf = lm.workMiddle / lm.workBottom.toFloat() 47 | } 48 | 49 | ntl.addItemClickListener(this) 50 | } 51 | 52 | override fun onDetach() { 53 | navigationToolBarLayout?.removeItemClickListener(this) 54 | } 55 | 56 | override fun transform(lm: HeaderLayoutManager, header: HeaderLayout, headerBottom: Int) { 57 | val prevRatio = currentRatio 58 | val prevRatioTopHalf = currentRatioTopHalf 59 | val prevRatioBottomHalf = currentRatioBottomHalf 60 | 61 | val prevItemCount = prevItemCount ?: 0 62 | val curItemCount = header.childCount 63 | this.prevItemCount = curItemCount 64 | 65 | updateRatios(lm, headerBottom) 66 | 67 | val nothingChanged = prevRatio == currentRatio && prevItemCount == curItemCount 68 | if (nothingChanged) { 69 | return 70 | } 71 | 72 | var transformed = false 73 | 74 | // On scroll from top (top half) to bottom (bottom half) 75 | val expandedToTopOfBottomHalf = currentRatioTopHalf == 1f 76 | && prevRatioTopHalf < currentRatioTopHalf && prevRatioTopHalf != -1f 77 | if (expandedToTopOfBottomHalf) { 78 | transformTopHalf(lm, header, headerBottom) 79 | clearPoints() 80 | transformBottomHalf(lm, header) 81 | transformed = true 82 | } else { 83 | // On scroll from top to bottom 84 | val expandedToBottomOfBottomHalf = currentRatioBottomHalf == 1f 85 | && prevRatioBottomHalf <= currentRatioBottomHalf 86 | if (expandedToBottomOfBottomHalf) { 87 | transformBottomHalf(lm, header) 88 | clearPoints() 89 | transformed = true 90 | } else { 91 | // On scroll from bottom to top 92 | val collapsedToTopOfBottomHalf = currentRatioBottomHalf == 0f 93 | && prevRatioBottomHalf > currentRatioBottomHalf 94 | if (collapsedToTopOfBottomHalf) { 95 | transformBottomHalf(lm, header) 96 | transformTopHalf(lm, header, headerBottom) 97 | lm.fill(header) 98 | clearPoints() 99 | transformed = true 100 | } else { 101 | val collapsedToTopOfTopHalf = currentRatioTopHalf == 0f 102 | && prevRatioTopHalf > currentRatioTopHalf && prevRatioTopHalf != -1f 103 | if (collapsedToTopOfTopHalf) { 104 | transformTopHalf(lm, header, headerBottom) 105 | clearPoints() 106 | transformed = true 107 | } 108 | } 109 | } 110 | } 111 | 112 | if (!transformed) { 113 | val isAtBottomHalf = currentRatioBottomHalf > 0f && currentRatioBottomHalf < 1f 114 | if (isAtBottomHalf) { 115 | val arePointsEmpty = hPoints.isEmpty() || vPoints.isEmpty() 116 | if (arePointsEmpty) { 117 | updatePoints(lm, header, false) 118 | } 119 | transformBottomHalf(lm, header) 120 | } else { 121 | transformTopHalf(lm, header, headerBottom) 122 | } 123 | } 124 | } 125 | 126 | override fun onItemClicked(viewHolder: HeaderLayout.ViewHolder) { 127 | navigationToolBarLayout 128 | ?.takeIf { currentRatioBottomHalf == 1f } 129 | ?.also { it -> 130 | clickedItemIndex = it.headerLayout.indexOfChild(viewHolder.view) 131 | updatePoints(it.layoutManager, it.headerLayout, true) 132 | } 133 | } 134 | 135 | private fun updateRatios(lm: HeaderLayoutManager, headerBottom: Int) { 136 | val oldWorkBottom = lm.workBottom 137 | if (headerBottom > oldWorkBottom) 138 | lm.initBaseSizeValues(headerBottom) 139 | 140 | currentRatio = max(0f, headerBottom / lm.workBottom.toFloat()) 141 | currentRatioWork = max(0f, (headerBottom - lm.workTop) / lm.workHeight.toFloat()) 142 | currentRatioTopHalf = max(0f, 1 - (ratioBottomHalf - min(max(currentRatio, ratioTopHalf), ratioBottomHalf)) / (ratioBottomHalf - ratioTopHalf)) 143 | currentRatioBottomHalf = max(0f, (currentRatio - ratioBottomHalf) / ratioBottomHalf) 144 | } 145 | 146 | private fun updatePoints(lm: HeaderLayoutManager, header: HeaderLayout, up: Boolean) { 147 | val index = if (up) { 148 | clickedItemIndex ?: throw RuntimeException("No vertical (clicked) item index") 149 | } else { 150 | lm.getHorizontalAnchorView(header) 151 | ?.let { header.indexOfChild(it) } 152 | ?: throw RuntimeException("No horizontal item index") 153 | } 154 | 155 | clearPoints() 156 | 157 | if (up) { 158 | val left = -index * lm.horizontalTabWidth 159 | val (x, y) = lm.getHorizontalPoint() 160 | 161 | for (i in 0 until header.childCount) { 162 | vPoints.add(header.getChildAt(i).let { Point(lm.getDecoratedLeft(it), lm.getDecoratedTop(it)) }) 163 | hPoints.add(Point(x + left + i * lm.horizontalTabWidth, y)) 164 | } 165 | } else { 166 | val totalHeight = (header.adapter?.getItemCount() ?: 0) * lm.verticalTabHeight 167 | if (totalHeight > lm.workHeight) { 168 | val top = -index * lm.verticalTabHeight 169 | val (x, y) = lm.getVerticalPoint() 170 | 171 | for (i in 0 until header.childCount) { 172 | hPoints.add(header.getChildAt(i).let { Point(lm.getDecoratedLeft(it), lm.getDecoratedTop(it)) }) 173 | vPoints.add(Point(x, y + top + i * lm.verticalTabHeight)) 174 | } 175 | } else { 176 | val x = lm.getVerticalPoint().x 177 | val y = (header.height - totalHeight) / 2 178 | 179 | for (i in 0 until header.childCount) { 180 | hPoints.add(header.getChildAt(i).let { Point(lm.getDecoratedLeft(it), lm.getDecoratedTop(it)) }) 181 | vPoints.add(Point(x, y + i * lm.verticalTabHeight)) 182 | } 183 | } 184 | } 185 | } 186 | 187 | private fun clearPoints() { 188 | hPoints.clear() 189 | vPoints.clear() 190 | } 191 | 192 | private fun transformTopHalf(lm: HeaderLayoutManager, header: HeaderLayout, headerBottom: Int) { 193 | val top = header.height - headerBottom 194 | (0 until header.childCount) 195 | .map { header.getChildAt(it) } 196 | .forEach { lm.layoutChild(it, lm.getDecoratedLeft(it), top, lm.getDecoratedWidth(it), headerBottom) } 197 | } 198 | 199 | private fun transformBottomHalf(lm: HeaderLayoutManager, header: HeaderLayout) { 200 | val hw = lm.horizontalTabWidth 201 | val hh = lm.horizontalTabHeight 202 | val vw = lm.verticalTabWidth 203 | val vh = lm.verticalTabHeight 204 | 205 | val newWidth = hw - (hw - vw) * currentRatioBottomHalf 206 | val newHeight = hh - (hh - vh) * currentRatioBottomHalf 207 | 208 | val count = min(header.childCount, hPoints.size) 209 | for (i in 0 until count) { 210 | val hp = hPoints[i] 211 | val vp = vPoints[i] 212 | val hDiff = (vp.x - hp.x) * currentRatioBottomHalf 213 | val vDiff = (vp.y - hp.y) * currentRatioBottomHalf 214 | 215 | val x = (hp.x + hDiff).toInt() 216 | val y = (hp.y + vDiff).toInt() 217 | lm.layoutChild(header.getChildAt(i), x, y, newWidth.toInt(), newHeight.toInt()) 218 | } 219 | } 220 | 221 | } -------------------------------------------------------------------------------- /navigation-toolbar/src/main/kotlin/com/ramotion/navigationtoolbar/HeaderLayout.kt: -------------------------------------------------------------------------------- 1 | package com.ramotion.navigationtoolbar 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.graphics.Rect 6 | import android.util.AttributeSet 7 | import android.view.* 8 | import android.widget.FrameLayout 9 | import androidx.core.view.GestureDetectorCompat 10 | 11 | /** 12 | * Header views container and producer with cache (recycler). 13 | */ 14 | class HeaderLayout : FrameLayout { 15 | 16 | companion object { 17 | /** 18 | * Defines invalid adapter position constant. 19 | * @see Adapter 20 | */ 21 | const val INVALID_POSITION = -1 22 | 23 | /** 24 | * Returns header's child HeaderLayout.LayoutParams 25 | * @param child Header's view 26 | * @return HeaderLayout.LayoutParams or null. 27 | * @see LayoutParams 28 | */ 29 | fun getChildLayoutParams(child: View) = child.layoutParams as? LayoutParams 30 | 31 | /** 32 | * Returns header's child ViewHolder. 33 | * @param child Header's view. 34 | * @return Header's child ViewHolder or null. 35 | * @see ViewHolder 36 | */ 37 | fun getChildViewHolder(child: View) = getChildLayoutParams(child)?.viewHolder 38 | 39 | /** 40 | * Returns header's child adapter position. 41 | * @param child Header's view. 42 | * @return child adapter position or INVALID_POSITION. 43 | * @see INVALID_POSITION 44 | * @see Adapter 45 | */ 46 | fun getChildPosition(child: View) = getChildViewHolder(child)?.position ?: INVALID_POSITION 47 | } 48 | 49 | internal interface ScrollListener { 50 | fun onItemClick(header: HeaderLayout, viewHolder: ViewHolder): Boolean 51 | fun onHeaderDown(header: HeaderLayout): Boolean 52 | fun onHeaderUp(header: HeaderLayout) 53 | fun onHeaderHorizontalScroll(header: HeaderLayout, distance: Float): Boolean 54 | fun onHeaderVerticalScroll(header: HeaderLayout, distance: Float): Boolean 55 | fun onHeaderHorizontalFling(header: HeaderLayout, velocity: Float): Boolean 56 | fun onHeaderVerticalFling(header: HeaderLayout, velocity: Float): Boolean 57 | } 58 | 59 | internal interface AdapterChangeListener { 60 | fun onAdapterChanged(header: HeaderLayout) 61 | } 62 | 63 | private val gestureDetector: GestureDetectorCompat 64 | 65 | internal val recycler = Recycler() 66 | 67 | internal var isHorizontalScrollEnabled = false 68 | internal var isVerticalScrollEnabled = false 69 | 70 | internal var scrollListener: ScrollListener? = null 71 | internal var adapterChangeListener: AdapterChangeListener? = null 72 | 73 | /** 74 | * HeaderLayout's adapter. 75 | * @see Adapter 76 | * @see ViewHolder 77 | */ 78 | var adapter: Adapter? = null; private set 79 | 80 | private inner class TouchGestureListener : GestureDetector.SimpleOnGestureListener() { 81 | override fun onSingleTapUp(e: MotionEvent): Boolean { 82 | val listener = scrollListener ?: return false 83 | 84 | val rect = Rect() 85 | val location = IntArray(2) 86 | 87 | for (i in 0 until childCount) { 88 | val child = getChildAt(i) 89 | child.getDrawingRect(rect) 90 | child.getLocationOnScreen(location) 91 | rect.offset(location[0], location[1]) 92 | val contains = rect.contains(e.rawX.toInt(), e.rawY.toInt()) 93 | if (contains) { 94 | return getChildViewHolder(child) 95 | ?.let { listener.onItemClick(this@HeaderLayout, it) } 96 | ?: throw RuntimeException("View holder not found") 97 | } 98 | } 99 | return false 100 | } 101 | 102 | override fun onDown(e: MotionEvent?): Boolean { 103 | return scrollListener 104 | ?.onHeaderDown(this@HeaderLayout) 105 | ?: false 106 | } 107 | 108 | override fun onScroll(e1: MotionEvent?, e2: MotionEvent?, distanceX: Float, distanceY: Float): Boolean { 109 | return scrollListener?.run { 110 | when { 111 | isHorizontalScrollEnabled -> onHeaderHorizontalScroll(this@HeaderLayout, distanceX) 112 | isVerticalScrollEnabled -> onHeaderVerticalScroll(this@HeaderLayout, distanceY) 113 | else -> false 114 | } 115 | } ?: false 116 | } 117 | 118 | override fun onFling(e1: MotionEvent?, e2: MotionEvent?, velocityX: Float, velocityY: Float): Boolean { 119 | return scrollListener?.run { 120 | when { 121 | isHorizontalScrollEnabled -> onHeaderHorizontalFling(this@HeaderLayout, velocityX) 122 | isVerticalScrollEnabled -> onHeaderVerticalFling(this@HeaderLayout, velocityY) 123 | else -> false 124 | } 125 | } ?: false 126 | } 127 | } 128 | 129 | /** 130 | * ViewHolder of HeaderLayout's child. 131 | * @param view ViewHolder's view, 132 | */ 133 | open class ViewHolder(val view: View) { 134 | /** 135 | * ViewHolder's current adapter position. 136 | * @return Current adapter position or INVALID_POSITION if ViewHolder not bound. 137 | * @see INVALID_POSITION 138 | * @see Adapter 139 | */ 140 | var position: Int = INVALID_POSITION 141 | internal set 142 | } 143 | 144 | /** 145 | * LayoutParams of HeaderLayout's child. 146 | */ 147 | open class LayoutParams : FrameLayout.LayoutParams { 148 | internal val decorRect = Rect() 149 | 150 | internal var decorRectValid = false 151 | internal var viewHolder: ViewHolder? = null 152 | 153 | constructor(c: Context, attrs: AttributeSet) : super(c, attrs) 154 | 155 | constructor(width: Int, height: Int) : super(width, height) 156 | 157 | constructor(source: ViewGroup.MarginLayoutParams) : super(source) 158 | 159 | constructor(source: ViewGroup.LayoutParams) : super(source) 160 | 161 | constructor(source: LayoutParams) : super(source as ViewGroup.LayoutParams) 162 | } 163 | 164 | /** 165 | * Adapter that produce ViewHolders for HeaderLayout. 166 | * @see ViewHolder 167 | */ 168 | abstract class Adapter { 169 | /** 170 | * Must return max item count. 171 | */ 172 | abstract fun getItemCount(): Int 173 | 174 | /** 175 | * Called on creating new ViewHolder. Must return new ViewHolder. 176 | * @param parent Parent of ViewHolder's view. 177 | * @see ViewHolder 178 | */ 179 | abstract fun onCreateViewHolder(parent: ViewGroup): VH 180 | 181 | /** 182 | * Called on binding ViewHolder to specified position. 183 | * @param holder ViewHolder to bind. 184 | * @param position Position to which ViewHolder will be bound. 185 | */ 186 | abstract fun onBindViewHolder(holder: VH, position: Int) 187 | 188 | /** 189 | * Called before ViewHolder recycling. 190 | * @param holder ViewHolder that will be recycled. 191 | */ 192 | open fun onViewRecycled(holder: VH) {} 193 | 194 | fun createViewHolder(parent: ViewGroup): VH { 195 | val holder = onCreateViewHolder(parent) 196 | holder.view.outlineProvider = ViewOutlineProvider.BOUNDS 197 | return holder 198 | } 199 | 200 | fun bindViewHolder(holder: VH, position: Int) { 201 | holder.position = position 202 | onBindViewHolder(holder, position) 203 | 204 | val lp = holder.view.layoutParams 205 | val hlp = when (lp) { 206 | null -> LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) 207 | !is LayoutParams -> LayoutParams(lp) 208 | else -> lp 209 | } 210 | 211 | hlp.viewHolder = holder 212 | hlp.decorRectValid = false 213 | holder.view.layoutParams = hlp 214 | } 215 | 216 | fun recycleView(holder: VH) = onViewRecycled(holder) 217 | } 218 | 219 | internal inner class Recycler { 220 | private val viewCache = mutableListOf() 221 | 222 | fun getViewForPosition(position: Int): View { 223 | val adapter = adapter ?: throw RuntimeException("No adapter set") 224 | val holder = viewCache.firstOrNull() 225 | ?.let { viewCache.remove(it); getChildViewHolder(it) } 226 | ?: adapter.createViewHolder(this@HeaderLayout) 227 | bindViewToPosition(holder, position) 228 | return holder.view 229 | } 230 | 231 | fun recycleView(view: View, cache: Boolean = true) { 232 | val adapter = adapter ?: throw RuntimeException("No adapter set") 233 | val lp = getChildLayoutParams(view) ?: throw RuntimeException("Invalid layout params") 234 | val holder = lp.viewHolder ?: throw RuntimeException("No view holder") 235 | adapter.recycleView(holder) 236 | this@HeaderLayout.removeView(view) 237 | if (cache) { 238 | lp.decorRectValid = false 239 | holder.position = INVALID_POSITION 240 | viewCache.add(holder.view) 241 | } 242 | } 243 | 244 | internal fun markItemDecorInsetsDirty() { 245 | viewCache.forEach { view -> 246 | HeaderLayout.getChildLayoutParams(view) 247 | ?.let { it.decorRectValid = false } 248 | } 249 | } 250 | 251 | internal fun recycleAll() { 252 | while (childCount > 0) { 253 | recycler.recycleView(getChildAt(0), false) 254 | } 255 | clearCache() 256 | } 257 | 258 | internal fun onAdapterChanged() { 259 | recycleAll() 260 | } 261 | 262 | private fun bindViewToPosition(holder: ViewHolder, position: Int) { 263 | val adapter = adapter ?: throw RuntimeException("No adapter set") 264 | adapter.bindViewHolder(holder, position) 265 | } 266 | 267 | private fun clearCache() { 268 | viewCache.forEach { recycleView(it, false) } 269 | viewCache.clear() 270 | } 271 | } 272 | 273 | constructor(context: Context, attrs: AttributeSet) : this(context, attrs, 0) 274 | 275 | constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { 276 | gestureDetector = GestureDetectorCompat(context, TouchGestureListener()) 277 | } 278 | 279 | @SuppressLint("ClickableViewAccessibility") 280 | override fun onTouchEvent(event: MotionEvent): Boolean { 281 | val res = gestureDetector.onTouchEvent(event) 282 | if (event.action == MotionEvent.ACTION_UP) { 283 | scrollListener?.onHeaderUp(this) 284 | } 285 | return res 286 | } 287 | 288 | override fun onDetachedFromWindow() { 289 | recycler.recycleAll() 290 | super.onDetachedFromWindow() 291 | } 292 | 293 | internal fun detachView(child: View) = detachViewFromParent(child) 294 | 295 | internal fun attachView(child: View) = attachViewToParent(child, -1, child.layoutParams) 296 | 297 | internal fun setAdapter(newAdapter: Adapter) { 298 | adapter = newAdapter as Adapter 299 | recycler.onAdapterChanged() 300 | adapterChangeListener?.onAdapterChanged(this@HeaderLayout) 301 | } 302 | } -------------------------------------------------------------------------------- /navigation-toolbar/src/main/kotlin/com/ramotion/navigationtoolbar/HeaderLayoutManager.kt: -------------------------------------------------------------------------------- 1 | package com.ramotion.navigationtoolbar 2 | 3 | import android.animation.Animator 4 | import android.animation.AnimatorListenerAdapter 5 | import android.animation.ValueAnimator 6 | import android.content.Context 7 | import android.graphics.Rect 8 | import android.os.Build 9 | import android.os.Looper 10 | import android.os.Parcel 11 | import android.os.Parcelable 12 | import android.util.AttributeSet 13 | import android.util.SparseArray 14 | import android.util.TypedValue 15 | import android.view.View 16 | import android.view.animation.LinearInterpolator 17 | import android.widget.OverScroller 18 | import androidx.coordinatorlayout.widget.CoordinatorLayout 19 | import androidx.core.view.ViewCompat 20 | import com.google.android.material.appbar.AppBarLayout 21 | import kotlin.math.abs 22 | import kotlin.math.min 23 | 24 | 25 | /** 26 | * Responsible from moving and placing HeaderLayout's cards. 27 | */ 28 | class HeaderLayoutManager(context: Context, attrs: AttributeSet?) 29 | : CoordinatorLayout.Behavior(context, attrs), AppBarLayout.OnOffsetChangedListener { 30 | 31 | /** 32 | * Defines header orientation states. 33 | * HORIZONTAL - horizontal orientation, when cards placed horizontally. 34 | * VERTICAL - vertical orientation, when cards placed vertically. 35 | * TRANSITIONAL - state when when header is changing from vertical to horizontal and vice versa. 36 | */ 37 | enum class Orientation { 38 | /** 39 | * Horizontal orientation, when cards placed horizontally. 40 | */ 41 | HORIZONTAL, 42 | /** 43 | * Vertical orientation, when cards placed vertically. 44 | */ 45 | VERTICAL, 46 | /** 47 | * state when when header is changing from vertical to horizontal and vice versa. 48 | */ 49 | TRANSITIONAL 50 | } 51 | 52 | /** 53 | * Defines header scroll states. 54 | */ 55 | enum class ScrollState { 56 | IDLE, DRAGGING, FLING 57 | } 58 | 59 | enum class VerticalGravity(val value: Int) { 60 | LEFT(-1), 61 | CENTER(-2), 62 | RIGHT(-3); 63 | 64 | companion object { 65 | private val map = VerticalGravity.values().associateBy(VerticalGravity::value) 66 | fun fromInt(type: Int, defaultValue: VerticalGravity = RIGHT) = map.getOrElse(type) { defaultValue } 67 | } 68 | } 69 | 70 | data class Point(val x: Int, val y: Int) 71 | 72 | class State : View.BaseSavedState { 73 | companion object { 74 | @JvmField 75 | @Suppress("unused") 76 | val CREATOR = object : Parcelable.Creator { 77 | override fun createFromParcel(parcel: Parcel): State = State(parcel) 78 | override fun newArray(size: Int): Array = arrayOfNulls(size) 79 | } 80 | } 81 | 82 | val anchorPos: Int 83 | 84 | constructor(superState: Parcelable, anchorPos: Int) : super(superState) { 85 | this.anchorPos = anchorPos 86 | } 87 | 88 | private constructor(parcel: Parcel) : super(parcel) { 89 | this.anchorPos = parcel.readInt() 90 | } 91 | 92 | override fun writeToParcel(parcel: Parcel, i: Int) { 93 | parcel.writeInt(anchorPos) 94 | } 95 | 96 | override fun describeContents(): Int = 0 97 | } 98 | 99 | internal companion object { 100 | const val TAB_ON_SCREEN_COUNT = 5 101 | const val TAB_OFF_SCREEN_COUNT = 1 102 | const val VERTICAL_TAB_WIDTH_RATIO = 4f / 5f 103 | const val SCROLL_STOP_CHECK_DELAY = 100L 104 | const val COLLAPSING_BY_SELECT_DURATION = 500 105 | const val SNAP_ANIMATION_DURATION = 300L 106 | const val MAX_SCROLL_DURATION = 600L 107 | } 108 | 109 | /** 110 | * Observes header's change (expand / collapse). 111 | */ 112 | interface HeaderChangeListener { 113 | /** 114 | * Invoked whenever header is changed (expanded / collapsed). 115 | * @param lm HeaderLayoutManager. 116 | * @param header HeaderLayout. 117 | * @param headerBottom HeaderLayout bottom position. 118 | * @see HeaderLayoutManager 119 | * @see HeaderLayout 120 | */ 121 | fun onHeaderChanged(lm: HeaderLayoutManager, header: HeaderLayout, headerBottom: Int) 122 | } 123 | 124 | /** 125 | * Observes header's update (redraw). 126 | */ 127 | interface HeaderUpdateListener { 128 | /** 129 | * Invoked whenever header is updated (filled / redrawn). 130 | * @param lm HeaderLayoutManager. 131 | * @param header HeaderLayout. 132 | * @param headerBottom HeaderLayout bottom position. 133 | * @see HeaderLayoutManager 134 | * @see HeaderLayout 135 | */ 136 | fun onHeaderUpdated(lm: HeaderLayoutManager, header: HeaderLayout, headerBottom: Int) 137 | } 138 | 139 | /** 140 | * Enhanced HeaderChangeListener which observes header's position states: collapsed, middle, expanded. 141 | */ 142 | abstract class HeaderChangeStateListener : HeaderChangeListener { 143 | /** 144 | * Invoked on header's position state change to: collapsed, middle, expanded. 145 | * @param lm HeaderLayoutManager. 146 | * @param header HeaderLayout. 147 | * @param headerBottom HeaderLayout bottom position. 148 | * @see HeaderLayoutManager 149 | * @see HeaderLayout 150 | */ 151 | final override fun onHeaderChanged(lm: HeaderLayoutManager, header: HeaderLayout, headerBottom: Int) { 152 | when (headerBottom) { 153 | lm.workTop -> onCollapsed() 154 | lm.workMiddle -> onMiddle() 155 | lm.workBottom -> onExpanded() 156 | } 157 | } 158 | 159 | /** 160 | * Invoked when header is collapsed. 161 | */ 162 | open fun onCollapsed() {} 163 | 164 | /** 165 | * Invoked when header passed or stopped in the middle. 166 | */ 167 | open fun onMiddle() {} 168 | 169 | /** 170 | * Invoked when header is expanded. 171 | */ 172 | open fun onExpanded() {} 173 | } 174 | 175 | /** 176 | * Observes card's click events. 177 | */ 178 | interface ItemClickListener { 179 | /** 180 | * Invoked when header card is clicked. 181 | * @param viewHolder ViewHolder of clicked card. 182 | * @see HeaderLayout.ViewHolder 183 | */ 184 | fun onItemClicked(viewHolder: HeaderLayout.ViewHolder) 185 | } 186 | 187 | /** 188 | * Observes header scrolling. 189 | */ 190 | interface ItemChangeListener { 191 | /** 192 | * Invoked when header scroll started 193 | * @param position Current anchor position. 194 | * @see getAnchorPos 195 | */ 196 | fun onItemChangeStarted(position: Int) {} 197 | 198 | /** 199 | * Invoked when header item (card) position is changed. 200 | * @param position new anchor position. 201 | * @see getAnchorPos 202 | */ 203 | fun onItemChanged(position: Int) 204 | } 205 | 206 | /** 207 | * Observes header scroll states 208 | */ 209 | interface ScrollStateListener { 210 | /** 211 | * Invoked when scroll state is changed. 212 | * @param state Current scroll state. 213 | * @see HeaderLayoutManager.ScrollState 214 | */ 215 | fun onScrollStateChanged(state: HeaderLayoutManager.ScrollState) 216 | } 217 | 218 | /** 219 | * Used for cards decoration. 220 | */ 221 | interface ItemDecoration { 222 | /** 223 | * Retrieve any offsets for the given item. Each field of outRect specifies the number of pixels that the item view should be inset by, similar to padding or margin. 224 | * @param outRect Rect to receive the output. 225 | * @param viewHolder Card ViewHolder. 226 | */ 227 | fun getItemOffsets(outRect: Rect, viewHolder: HeaderLayout.ViewHolder) 228 | } 229 | 230 | private val tabOffsetCount = TAB_OFF_SCREEN_COUNT 231 | private val screenWidth = context.resources.displayMetrics.widthPixels 232 | private val viewCache = SparseArray() 233 | private val viewFlinger = ViewFlinger(context) 234 | private val verticalGravity: VerticalGravity 235 | private val tempDecorRect = Rect() 236 | private val itemDecorations = mutableListOf() 237 | private val scrollListener = HeaderLayoutScrollListener() 238 | private val adapterChangeListener = AdapterChangeListener() 239 | 240 | private var verticalScrollTopBorder: Int = Int.MIN_VALUE 241 | private val collapsingBySelectDuration: Int 242 | private val tabOnScreenCount: Int 243 | private val centerIndex: Int 244 | private var topSnapDistance: Int = Int.MIN_VALUE 245 | private var bottomSnapDistance: Int = Int.MIN_VALUE 246 | 247 | var workBottom = context.resources.displayMetrics.heightPixels 248 | var workMiddle = workBottom / 2 249 | var workTop: Int = Int.MIN_VALUE 250 | var workHeight: Int = Int.MIN_VALUE 251 | 252 | private var statusBarHeight:Int = Int.MIN_VALUE 253 | private var actionBarHeight:Int = Int.MIN_VALUE 254 | 255 | /** 256 | * Width of card in horizontal orientation. 257 | */ 258 | var horizontalTabWidth = screenWidth 259 | /** 260 | * Height of card in horizontal orientation. 261 | */ 262 | var horizontalTabHeight = workMiddle 263 | /** 264 | * Width of card in vertical orientation. 265 | */ 266 | var verticalTabWidth: Int = Int.MIN_VALUE 267 | /** 268 | * Height of card in vertical orientation. 269 | */ 270 | var verticalTabHeight: Int = Int.MIN_VALUE 271 | 272 | internal val appBarBehavior = AppBarBehavior() 273 | internal val changeListener = mutableListOf() 274 | internal val updateListener = mutableListOf() 275 | internal val itemClickListeners = mutableListOf() 276 | internal val itemChangeListeners = mutableListOf() 277 | internal val scrollStateListeners = mutableListOf() 278 | 279 | private var offsetAnimator: ValueAnimator? = null 280 | private var appBar: AppBarLayout? = null 281 | private var headerLayout: HeaderLayout? = null 282 | 283 | private var isCanDrag = true 284 | private var isOffsetChanged = false 285 | private var isCheckingScrollStop = false 286 | private var isHeaderChanging = false 287 | 288 | private var scrollState = ScrollState.IDLE 289 | private var curOrientation: Orientation? = null 290 | private var prevOffset: Int = Int.MAX_VALUE 291 | private var restoredAnchorPosition: Int? = null 292 | 293 | private var vScrollTopBorder:Boolean = false 294 | 295 | private var hPoint: Point? = null 296 | private var vPoint: Point? = null 297 | 298 | internal inner class AppBarBehavior : AppBarLayout.Behavior() { 299 | init { 300 | setDragCallback(object : AppBarLayout.Behavior.DragCallback() { 301 | override fun canDrag(appBarLayout: AppBarLayout) = isCanDrag 302 | }) 303 | } 304 | } 305 | 306 | private inner class HeaderLayoutScrollListener : HeaderLayout.ScrollListener { 307 | override fun onItemClick(header: HeaderLayout, viewHolder: HeaderLayout.ViewHolder) = 308 | this@HeaderLayoutManager.onHeaderItemClick(header, viewHolder) 309 | 310 | override fun onHeaderDown(header: HeaderLayout) = 311 | this@HeaderLayoutManager.onHeaderDown(header) 312 | 313 | override fun onHeaderUp(header: HeaderLayout) = 314 | this@HeaderLayoutManager.onHeaderUp(header) 315 | 316 | override fun onHeaderHorizontalScroll(header: HeaderLayout, distance: Float) = 317 | this@HeaderLayoutManager.onHeaderHorizontalScroll(header, distance) 318 | 319 | override fun onHeaderVerticalScroll(header: HeaderLayout, distance: Float) = 320 | this@HeaderLayoutManager.onHeaderVerticalScroll(header, distance) 321 | 322 | override fun onHeaderHorizontalFling(header: HeaderLayout, velocity: Float) = 323 | this@HeaderLayoutManager.onHeaderHorizontalFling(header, velocity) 324 | 325 | override fun onHeaderVerticalFling(header: HeaderLayout, velocity: Float) = 326 | this@HeaderLayoutManager.onHeaderVerticalFling(header, velocity) 327 | } 328 | 329 | private inner class AdapterChangeListener : HeaderLayout.AdapterChangeListener { 330 | override fun onAdapterChanged(header: HeaderLayout) { 331 | updatePoints(header) 332 | } 333 | } 334 | 335 | private inner class ViewFlinger(context: Context) : Runnable { 336 | private val scroller = OverScroller(context, LinearInterpolator()) 337 | 338 | override fun run() { 339 | val header = headerLayout ?: return 340 | 341 | val x = scroller.currX 342 | val y = scroller.currY 343 | 344 | if (!scroller.computeScrollOffset()) { 345 | setScrollState(ScrollState.IDLE) 346 | return 347 | } 348 | 349 | val diffX = scroller.currX - x 350 | val diffY = scroller.currY - y 351 | 352 | if (diffX == 0 && diffY == 0) { 353 | ViewCompat.postOnAnimation(header, this) 354 | return 355 | } 356 | 357 | for (i in 0 until header.childCount) { 358 | val child = header.getChildAt(i) 359 | child.offsetLeftAndRight(diffX) 360 | child.offsetTopAndBottom(diffY) 361 | } 362 | 363 | fill(header) 364 | 365 | ViewCompat.postOnAnimation(header, this) 366 | } 367 | 368 | fun fling(startX: Int, startY: Int, velocityX: Int, velocityY: Int, minX: Int, maxX: Int, minY: Int, maxY: Int) { 369 | setScrollState(ScrollState.FLING) 370 | scroller.forceFinished(true) 371 | scroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY) 372 | ViewCompat.postOnAnimation(headerLayout!!, this) 373 | } 374 | 375 | fun startScroll(startX: Int, startY: Int, dx: Int, dy: Int, duration: Int) { 376 | setScrollState(ScrollState.FLING) 377 | scroller.forceFinished(true) 378 | scroller.startScroll(startX, startY, dx, dy, duration) 379 | if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1) { 380 | scroller.computeScrollOffset() 381 | } 382 | ViewCompat.postOnAnimation(headerLayout!!, this) 383 | } 384 | 385 | fun stop() { 386 | if (!scroller.isFinished) { 387 | setScrollState(ScrollState.IDLE) 388 | scroller.abortAnimation() 389 | } 390 | } 391 | } 392 | 393 | init { 394 | Looper.myQueue().addIdleHandler { 395 | if (isOffsetChanged && !isCheckingScrollStop) { 396 | checkIfOffsetChangingStopped() 397 | } 398 | true 399 | } 400 | 401 | var itemCount = TAB_ON_SCREEN_COUNT 402 | var verticalItemWidth = screenWidth * VERTICAL_TAB_WIDTH_RATIO 403 | var gravity = VerticalGravity.RIGHT 404 | var collapsingDuration = COLLAPSING_BY_SELECT_DURATION 405 | 406 | attrs?.also { 407 | val a = context.theme.obtainStyledAttributes(attrs, R.styleable.NavigationToolBarr, 0, 0) 408 | try { 409 | itemCount = a.getInteger(R.styleable.NavigationToolBarr_headerOnScreenItemCount, -1) 410 | .let { if (it <= 0) TAB_ON_SCREEN_COUNT else it } 411 | gravity = VerticalGravity.fromInt(a.getInteger(R.styleable.NavigationToolBarr_headerVerticalGravity, VerticalGravity.RIGHT.value)) 412 | collapsingDuration = a.getInteger(R.styleable.NavigationToolBarr_headerCollapsingBySelectDuration, COLLAPSING_BY_SELECT_DURATION) 413 | vScrollTopBorder = a.getBoolean(R.styleable.NavigationToolBarr_headerTopBorderAtSystemBar, false) 414 | 415 | if (a.hasValue(R.styleable.NavigationToolBarr_headerVerticalItemWidth)) { 416 | verticalItemWidth = if (a.getType(R.styleable.NavigationToolBarr_headerVerticalItemWidth) == TypedValue.TYPE_DIMENSION) { 417 | a.getDimension(R.styleable.NavigationToolBarr_headerVerticalItemWidth, verticalItemWidth) 418 | } else { 419 | screenWidth.toFloat() 420 | } 421 | } 422 | } finally { 423 | a.recycle() 424 | } 425 | } 426 | 427 | 428 | 429 | tabOnScreenCount = itemCount 430 | centerIndex = tabOnScreenCount / 2 431 | verticalGravity = gravity 432 | collapsingBySelectDuration = collapsingDuration 433 | 434 | val resourceId = context.resources.getIdentifier("status_bar_height", "dimen", "android") 435 | statusBarHeight = if (resourceId > 0) { 436 | context.resources.getDimensionPixelSize(resourceId) 437 | } else 0 438 | 439 | val styledAttributes = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.actionBarSize)) 440 | try { 441 | actionBarHeight = styledAttributes.getDimension(0, 0f).toInt() 442 | } finally { 443 | styledAttributes.recycle() 444 | } 445 | 446 | initBaseSizeValues(workBottom) 447 | } 448 | 449 | fun initBaseSizeValues(newWorkBottom: Int) { 450 | verticalTabHeight = (workBottom * (1f / tabOnScreenCount)).toInt() 451 | verticalTabWidth = (screenWidth * VERTICAL_TAB_WIDTH_RATIO).toInt() 452 | 453 | workBottom = newWorkBottom 454 | workMiddle = workBottom / 2 455 | 456 | workTop = actionBarHeight + statusBarHeight 457 | workHeight = workBottom - workTop 458 | 459 | topSnapDistance = (workTop + (workMiddle - workTop) / 2) 460 | bottomSnapDistance = (workMiddle + workMiddle / 2) 461 | 462 | verticalScrollTopBorder = if (vScrollTopBorder) statusBarHeight else 0 463 | 464 | horizontalTabHeight = workMiddle 465 | } 466 | 467 | override fun layoutDependsOn(parent: CoordinatorLayout, child: HeaderLayout, dependency: View): Boolean { 468 | return dependency is AppBarLayout 469 | } 470 | 471 | override fun onLayoutChild(parent: CoordinatorLayout, header: HeaderLayout, layoutDirection: Int): Boolean { 472 | if (!parent.isLaidOut) { 473 | parent.onLayoutChild(header, layoutDirection) 474 | 475 | appBar = parent.findViewById(R.id.com_ramotion_app_bar) 476 | 477 | headerLayout = header 478 | header.scrollListener = scrollListener 479 | header.adapterChangeListener = adapterChangeListener 480 | 481 | updatePoints(header) 482 | fill(header) 483 | } 484 | 485 | return true 486 | } 487 | 488 | override fun onDependentViewChanged(parent: CoordinatorLayout, header: HeaderLayout, dependency: View): Boolean { 489 | val headerBottom = dependency.bottom 490 | header.y = (headerBottom - header.height).toFloat() // Offset header on collapsing 491 | curOrientation = null 492 | isHeaderChanging = true 493 | viewFlinger.stop() 494 | changeListener.forEach { it.onHeaderChanged(this, header, headerBottom) } 495 | return true 496 | } 497 | 498 | override fun onOffsetChanged(appBarLayout: AppBarLayout?, verticalOffset: Int) { 499 | isOffsetChanged = true 500 | } 501 | 502 | override fun onSaveInstanceState(parent: CoordinatorLayout, header: HeaderLayout): Parcelable { 503 | val anchorPos = getAnchorPos(header) 504 | val superParcel = super.onSaveInstanceState(parent, header) 505 | return State(superParcel!!, anchorPos) 506 | } 507 | 508 | override fun onRestoreInstanceState(parent: CoordinatorLayout, child: HeaderLayout, state: Parcelable) { 509 | super.onRestoreInstanceState(parent, child, state) 510 | if (state is State) { 511 | restoredAnchorPosition = state.anchorPos 512 | } 513 | } 514 | 515 | // TODO: fun scroll(distance) 516 | 517 | /** 518 | * Scroll header to specified position. 519 | * @param pos Position where to scroll. 520 | */ 521 | fun scrollToPosition(pos: Int) { 522 | scrollToPosition(pos) { header, _, offset, horizontal -> 523 | when (horizontal) { 524 | true -> onHeaderHorizontalScroll(header, offset.toFloat()) 525 | else -> onHeaderVerticalScroll(header, offset.toFloat()) 526 | } 527 | } 528 | } 529 | 530 | /** 531 | * Smooth scroll header to specified position. 532 | * @param pos Position where to scroll. 533 | */ 534 | fun smoothScrollToPosition(pos: Int) { 535 | scrollToPosition(pos) { _, anchorView, offset, horizontal -> 536 | if (horizontal) { 537 | val startX = getStartX(anchorView) 538 | val delta = abs(offset) / horizontalTabWidth 539 | val duration = min((delta + 1) * 100, MAX_SCROLL_DURATION.toInt()) 540 | viewFlinger.startScroll(startX, 0, -offset, 0, duration) 541 | } else { 542 | val startY = getStartY(anchorView) 543 | val delta = abs(offset) / verticalTabHeight 544 | val duration = min((delta + 1) * 100, MAX_SCROLL_DURATION.toInt()) 545 | viewFlinger.startScroll(0, startY, 0, -offset, duration) 546 | } 547 | } 548 | } 549 | 550 | /** 551 | * Returns point of left top corner where clicked card get placed when header change orientation 552 | * from vertical to horizontal. 553 | * @return Point(x, y) of left top corner where card get placed when header change orientation 554 | * from vertical to horizontal. 555 | */ 556 | fun getHorizontalPoint(): Point { 557 | return hPoint ?: throw RuntimeException("Layout manager not initialized yet") 558 | } 559 | 560 | /** 561 | * Returns point of left top corner where anchor card get placed when header change orientation 562 | * from horizontal to vertical. 563 | * @return Point(x, y) of left top corner where anchor card get placed when header change orientation 564 | * from horizontal to vertical. 565 | */ 566 | fun getVerticalPoint(): Point { 567 | return vPoint ?: throw RuntimeException("Layout manager not initialized yet") 568 | } 569 | 570 | /** 571 | * Returns horizontal center card. 572 | * @return View or null if card not found. 573 | */ 574 | fun getHorizontalAnchorView(header: HeaderLayout): View? { 575 | val centerLeft = hPoint?.x ?: return null 576 | 577 | var result: View? = null 578 | var lastDiff = Int.MAX_VALUE 579 | 580 | for (i in 0 until header.childCount) { 581 | val child = header.getChildAt(i) 582 | val diff = Math.abs(getDecoratedLeft(child) - centerLeft) 583 | if (diff < lastDiff) { 584 | lastDiff = diff 585 | result = child 586 | } 587 | } 588 | 589 | return result 590 | } 591 | 592 | /** 593 | * Returns vertical center card. 594 | * @return View or null if card not found. 595 | */ 596 | fun getVerticalAnchorView(header: HeaderLayout): View? { 597 | val centerTop = vPoint?.y ?: return null 598 | 599 | var result: View? = null 600 | var lastDiff = Int.MAX_VALUE 601 | 602 | for (i in 0 until header.childCount) { 603 | val child = header.getChildAt(i) 604 | val diff = Math.abs(getDecoratedTop(child) - centerTop) 605 | if (diff < lastDiff) { 606 | lastDiff = diff 607 | result = child 608 | } 609 | } 610 | 611 | return result 612 | } 613 | 614 | /** 615 | * Lays out view at specified position. 616 | * @param child View to lay out. 617 | * @param x Left position, relative to parent. 618 | * @param y Top position, relative to parent. 619 | * @param w View width. 620 | * @param h View height. 621 | */ 622 | fun layoutChild(child: View, x: Int, y: Int, w: Int, h: Int) { 623 | val inset = getDecorInsetsForChild(child) 624 | 625 | val wPadding = inset.left + inset.right 626 | val hPadding = inset.top + inset.bottom 627 | val pw = w - wPadding 628 | val ph = h - hPadding 629 | 630 | val ws = View.MeasureSpec.makeMeasureSpec(pw, View.MeasureSpec.EXACTLY) 631 | val hs = View.MeasureSpec.makeMeasureSpec(ph, View.MeasureSpec.EXACTLY) 632 | child.measure(ws, hs) 633 | 634 | val l = x + inset.left 635 | val t = y + inset.top 636 | child.layout(l, t, l + pw, t + ph) 637 | } 638 | 639 | /** 640 | * Fill and redraws all cards in header. 641 | * @param header HeaderLayout. 642 | */ 643 | fun fill(header: HeaderLayout) { 644 | val orientation = getOrientation(::getPositionRatio) 645 | if (orientation == Orientation.TRANSITIONAL) { 646 | return 647 | } 648 | 649 | val anchorPos = getAnchorPos(header) 650 | 651 | viewCache.clear() 652 | 653 | for (i in 0 until header.childCount) { 654 | val view = header.getChildAt(i) 655 | viewCache.put(HeaderLayout.getChildPosition(view), view) 656 | } 657 | 658 | for (i in 0 until viewCache.size()) { 659 | viewCache.valueAt(i)?.also { header.detachView(it) } 660 | } 661 | 662 | if (orientation == Orientation.HORIZONTAL) { 663 | fillLeft(header, anchorPos) 664 | fillRight(header, anchorPos) 665 | } else { 666 | fillTop(header, anchorPos) 667 | fillBottom(header, anchorPos) 668 | } 669 | 670 | for (i in 0 until viewCache.size()) { 671 | viewCache.valueAt(i)?.also { header.recycler.recycleView(it) } 672 | } 673 | 674 | val headerBottom = (header.y + header.height).toInt() 675 | updateListener.forEach { it.onHeaderUpdated(this, header, headerBottom) } 676 | } 677 | 678 | /** 679 | * Returns current center card, of current orientation. 680 | * @param header HeaderLayout. 681 | * @return View or null if card not found. 682 | */ 683 | fun getAnchorView(header: HeaderLayout): View? { 684 | val orientation = getOrientation(::getPositionRatio) 685 | return when (orientation) { 686 | Orientation.HORIZONTAL -> getHorizontalAnchorView(header) 687 | Orientation.VERTICAL -> getVerticalAnchorView(header) 688 | Orientation.TRANSITIONAL -> null 689 | } 690 | } 691 | 692 | /** 693 | * Returns current center card, adapter position. 694 | * @param header HeaderLayout. 695 | * @return Current center card, adapter position or HeaderLayout.INVALID_POSITION if card not found. 696 | * @see HeaderLayout.INVALID_POSITION 697 | */ 698 | fun getAnchorPos(header: HeaderLayout): Int { 699 | return restoredAnchorPosition 700 | ?.also { restoredAnchorPosition = null } 701 | ?: getAnchorView(header) 702 | ?.let { HeaderLayout.getChildPosition(it) } 703 | ?: HeaderLayout.INVALID_POSITION 704 | } 705 | 706 | /** 707 | * Returns child (card) decorated width. 708 | * @param child Header's card which decorated width needed. 709 | * @return child width + decorators width offsets. 710 | * @link ItemDecoration 711 | */ 712 | fun getDecoratedWidth(child: View): Int { 713 | val width = child.width 714 | val inset = getDecorInsetsForChild(child) 715 | return width + inset.left + inset.right 716 | } 717 | 718 | /** 719 | * Returns child (card) decorated height. 720 | * @param child Header's card which decorated width needed. 721 | * @return child height + decorators height offsets. 722 | * @link ItemDecoration 723 | */ 724 | fun getDecoratedHeight(child: View): Int { 725 | val height = child.height 726 | val inset = getDecorInsetsForChild(child) 727 | return height + inset.top + inset.bottom 728 | } 729 | 730 | /** 731 | * Returns child (card) decorated left position. 732 | * @param child Header's card which decorated left position needed. 733 | * @return child left - decorators left offsets. 734 | * @link ItemDecoration 735 | */ 736 | fun getDecoratedLeft(child: View): Int { 737 | return child.left - getDecorInsetsForChild(child).left 738 | } 739 | 740 | /** 741 | * Returns child (card) decorated right position. 742 | * @param child Header's card which decorated right position needed. 743 | * @return child right + decorators right offsets. 744 | * @link ItemDecoration 745 | */ 746 | fun getDecoratedRight(child: View): Int { 747 | return child.right + getDecorInsetsForChild(child).right 748 | } 749 | 750 | /** 751 | * Returns child (card) decorated top position. 752 | * @param child Header's card which decorated top position needed. 753 | * @return child top - decorators top offsets. 754 | * @link ItemDecoration 755 | */ 756 | fun getDecoratedTop(child: View): Int { 757 | return child.top - getDecorInsetsForChild(child).top 758 | } 759 | 760 | /** 761 | * Returns child (card) decorated bottom position. 762 | * @param child Header's card which decorated bottom position needed. 763 | * @return child bottom + decorators bottom offsets. 764 | * @link ItemDecoration 765 | */ 766 | fun getDecoratedBottom(child: View): Int { 767 | return child.bottom + getDecorInsetsForChild(child).bottom 768 | } 769 | 770 | internal fun addItemDecoration(decoration: ItemDecoration) { 771 | itemDecorations += decoration 772 | markItemDecorInsetsDirty() 773 | } 774 | 775 | internal fun removeItemDecoration(decoration: ItemDecoration) { 776 | itemDecorations -= decoration 777 | markItemDecorInsetsDirty() 778 | } 779 | 780 | internal fun onHeaderItemClick(header: HeaderLayout, viewHolder: HeaderLayout.ViewHolder): Boolean { 781 | return when { 782 | header.isHorizontalScrollEnabled -> { 783 | smoothScrollToPosition(viewHolder.position) 784 | itemClickListeners.forEach { it.onItemClicked(viewHolder) } 785 | true 786 | } 787 | header.isVerticalScrollEnabled -> { 788 | smoothOffset(workMiddle) 789 | itemClickListeners.forEach { it.onItemClicked(viewHolder) } 790 | true 791 | } 792 | else -> false 793 | } 794 | } 795 | 796 | private fun scrollToPosition(pos: Int, 797 | task: (header: HeaderLayout, 798 | anchorView: View, 799 | offset: Int, 800 | horizontal: Boolean) -> Unit) { 801 | if (offsetAnimator?.isRunning == true) { 802 | return 803 | } 804 | 805 | val hx = hPoint?.x ?: return 806 | val vy = vPoint?.y ?: return 807 | 808 | val header = headerLayout ?: return 809 | if (header.childCount == 0) { 810 | return 811 | } 812 | 813 | val itemCount = header.adapter?.getItemCount() ?: -1 814 | if (pos < 0 || pos > itemCount) { 815 | return 816 | } 817 | 818 | val anchorView = getAnchorView(header) ?: return 819 | val anchorPos = HeaderLayout.getChildPosition(anchorView) 820 | if (anchorPos == HeaderLayout.INVALID_POSITION) { 821 | return 822 | } 823 | 824 | itemChangeListeners.forEach { it.onItemChanged(pos) } 825 | 826 | val offset = when { 827 | header.isHorizontalScrollEnabled -> { 828 | val childWidth = getDecoratedWidth(anchorView) 829 | (pos - anchorPos) * childWidth + (getDecoratedLeft(anchorView) - hx) 830 | } 831 | header.isVerticalScrollEnabled -> { 832 | val childHeight = getDecoratedHeight(anchorView) 833 | (pos - anchorPos) * childHeight + (getDecoratedTop(anchorView) - vy) 834 | } 835 | else -> 0 836 | } 837 | 838 | if (offset == 0) { 839 | return 840 | } 841 | 842 | if (pos == anchorPos && header.isVerticalScrollEnabled) { 843 | val lastChild = header.getChildAt(header.childCount - 1) 844 | val lastChildPos = HeaderLayout.getChildPosition(lastChild) 845 | val lastChildIsLastItem = lastChildPos == itemCount - 1 846 | if (lastChildIsLastItem && lastChild.bottom <= header.height) { 847 | return 848 | } 849 | } 850 | 851 | task(header, anchorView, offset, header.isHorizontalScrollEnabled) 852 | } 853 | 854 | private fun markItemDecorInsetsDirty() { 855 | headerLayout?.also { header -> 856 | val childCount = header.childCount 857 | for (i in 0 until childCount) { 858 | HeaderLayout.getChildLayoutParams(header.getChildAt(i)) 859 | ?.let { it.decorRectValid = false } 860 | } 861 | header.recycler.markItemDecorInsetsDirty() 862 | } 863 | } 864 | 865 | private fun onHeaderDown(header: HeaderLayout): Boolean { 866 | if (header.childCount == 0) { 867 | return false 868 | } 869 | 870 | viewFlinger.stop() 871 | return true 872 | } 873 | 874 | private fun onHeaderUp(header: HeaderLayout) { 875 | if (scrollState != ScrollState.FLING) { 876 | setScrollState(ScrollState.IDLE) 877 | } 878 | } 879 | 880 | private fun onHeaderHorizontalScroll(header: HeaderLayout, distance: Float): Boolean { 881 | val childCount = header.childCount 882 | if (childCount == 0) { 883 | return false 884 | } 885 | 886 | setScrollState(ScrollState.DRAGGING) 887 | 888 | val scrollLeft = distance >= 0 889 | val offset = if (scrollLeft) { 890 | val lastRight = getDecoratedRight(header.getChildAt(childCount - 1)) 891 | val newRight = lastRight - distance 892 | if (newRight > header.width) distance.toInt() else lastRight - header.width 893 | } else { 894 | val firstLeft = getDecoratedLeft(header.getChildAt(0)) 895 | if (firstLeft > 0) { // TODO: firstTop > border, border - center or systemBar height 896 | 0 897 | } else { 898 | val newLeft = firstLeft - distance 899 | if (newLeft < 0) distance.toInt() else firstLeft 900 | } 901 | } 902 | 903 | for (i in 0 until childCount) { 904 | header.getChildAt(i).offsetLeftAndRight(-offset) 905 | } 906 | 907 | fill(header) 908 | return true 909 | } 910 | 911 | private fun onHeaderVerticalScroll(header: HeaderLayout, distance: Float): Boolean { 912 | val childCount = header.childCount 913 | if (childCount == 0) { 914 | return false 915 | } 916 | 917 | setScrollState(ScrollState.DRAGGING) 918 | 919 | val scrollUp = distance >= 0 920 | val offset = if (scrollUp) { 921 | val lastBottom = getDecoratedBottom(header.getChildAt(childCount - 1)) 922 | if (lastBottom < header.height) { 923 | 0 924 | } else { 925 | val newBottom = lastBottom - distance 926 | if (newBottom > header.height) distance.toInt() else lastBottom - header.height 927 | } 928 | } else { 929 | val firstTop = getDecoratedTop(header.getChildAt(0)) 930 | if (firstTop > verticalScrollTopBorder) { 931 | 0 932 | } else { 933 | val newTop = firstTop - distance 934 | if (newTop < verticalScrollTopBorder) distance.toInt() else firstTop - verticalScrollTopBorder 935 | } 936 | } 937 | 938 | for (i in 0 until childCount) { 939 | header.getChildAt(i).offsetTopAndBottom(-offset) 940 | } 941 | 942 | fill(header) 943 | return true 944 | } 945 | 946 | private fun onHeaderHorizontalFling(header: HeaderLayout, velocity: Float): Boolean { 947 | val startX = getAnchorView(header)?.left?.takeIf { it != 0 } ?: return false 948 | val (min, max) = if (startX < 0) (-horizontalTabWidth to 0) else (0 to horizontalTabWidth) 949 | viewFlinger.fling(startX, 0, velocity.toInt(), 0, min, max, 0, 0) 950 | return true 951 | } 952 | 953 | private fun onHeaderVerticalFling(header: HeaderLayout, velocity: Float): Boolean { 954 | val childCount = header.childCount 955 | if (childCount == 0) { 956 | return false 957 | } 958 | 959 | val flingUp = velocity < 0 960 | val itemCount = header.adapter?.getItemCount() ?: return false 961 | val startY = getStartY(header.getChildAt(0)) 962 | val min = -itemCount * verticalTabHeight + header.height 963 | val max = if (flingUp) header.height else 0 964 | 965 | viewFlinger.fling(0, startY, 0, velocity.toInt(), 0, 0, min, max) 966 | 967 | return true 968 | } 969 | 970 | private fun getStartX(view: View): Int { 971 | return HeaderLayout.getChildViewHolder(view) 972 | ?.let { getDecoratedLeft(view) - it.position * horizontalTabWidth } 973 | ?: throw RuntimeException("View holder not found") 974 | } 975 | 976 | private fun getStartY(view: View): Int { 977 | return HeaderLayout.getChildViewHolder(view) 978 | ?.let { getDecoratedTop(view) - it.position * verticalTabHeight } 979 | ?: throw RuntimeException("View holder not found") 980 | } 981 | 982 | private fun updatePoints(header: HeaderLayout) { 983 | val vx = when (verticalGravity) { 984 | HeaderLayoutManager.VerticalGravity.LEFT -> 0 985 | HeaderLayoutManager.VerticalGravity.CENTER -> header.width - verticalTabWidth / 2 986 | HeaderLayoutManager.VerticalGravity.RIGHT -> header.width - verticalTabWidth 987 | } 988 | 989 | val totalHeight = (header.adapter?.getItemCount() ?: 0) * verticalTabHeight 990 | val vy = if (totalHeight > workHeight) { 991 | (workBottom / tabOnScreenCount) * centerIndex + verticalScrollTopBorder 992 | } else { 993 | (header.height - totalHeight) / 2 994 | } 995 | 996 | hPoint = Point(0, workMiddle) 997 | vPoint = Point(vx, vy) 998 | } 999 | 1000 | private fun getPositionRatio() = appBar?.let { Math.max(0f, it.bottom / workBottom.toFloat()) } ?: 0f 1001 | 1002 | private tailrec fun getOrientation(getRatio: () -> Float, force: Boolean = false): Orientation { 1003 | return if (force) { 1004 | val ratio = getRatio() 1005 | when { 1006 | ratio <= 0.5f -> Orientation.HORIZONTAL 1007 | ratio < 1 -> Orientation.TRANSITIONAL 1008 | else -> Orientation.VERTICAL 1009 | }.also { 1010 | curOrientation = it 1011 | } 1012 | } else { 1013 | curOrientation ?: getOrientation(getRatio, true) 1014 | } 1015 | } 1016 | 1017 | private fun fillLeft(header: HeaderLayout, anchorPos: Int) { 1018 | val (hx, hy) = hPoint ?: return 1019 | 1020 | if (anchorPos == HeaderLayout.INVALID_POSITION) { 1021 | return 1022 | } 1023 | 1024 | val top = appBar?.let { header.height - it.bottom } ?: hy 1025 | val bottom = appBar?.bottom ?: horizontalTabHeight 1026 | val leftDiff = hx - (viewCache.get(anchorPos)?.let { getDecoratedLeft(it) } ?: 0) 1027 | 1028 | var pos = Math.max(0, anchorPos - centerIndex - tabOffsetCount) 1029 | var left = (hx - (anchorPos - pos) * horizontalTabWidth) - leftDiff 1030 | 1031 | while (pos < anchorPos) { 1032 | val view = getPlacedChildForPosition(header, pos, left, top, horizontalTabWidth, bottom) 1033 | left = getDecoratedRight(view) 1034 | pos++ 1035 | } 1036 | } 1037 | 1038 | private fun fillRight(header: HeaderLayout, anchorPos: Int) { 1039 | val (hx, hy) = hPoint ?: return 1040 | 1041 | val count = header.adapter?.getItemCount() ?: 0 1042 | if (count == 0) { 1043 | return 1044 | } 1045 | 1046 | val startPos = when (anchorPos) { 1047 | HeaderLayout.INVALID_POSITION -> 0 1048 | else -> anchorPos 1049 | } 1050 | 1051 | val top = appBar?.let { header.height - it.bottom } ?: hy 1052 | val bottom = appBar?.bottom ?: horizontalTabHeight 1053 | val maxPos = Math.min(count, startPos + centerIndex + 1 + tabOffsetCount) 1054 | 1055 | var pos = startPos 1056 | var left = if (header.childCount > 0) { 1057 | getDecoratedRight(header.getChildAt(header.childCount - 1)) 1058 | } else { 1059 | hx 1060 | } 1061 | 1062 | while (pos < maxPos) { 1063 | val view = getPlacedChildForPosition(header, pos, left, top, horizontalTabWidth, bottom) 1064 | left = getDecoratedRight(view) 1065 | pos++ 1066 | } 1067 | } 1068 | 1069 | private fun fillTop(header: HeaderLayout, anchorPos: Int) { 1070 | val (left, vy) = vPoint ?: return 1071 | 1072 | if (anchorPos == HeaderLayout.INVALID_POSITION) { 1073 | return 1074 | } 1075 | 1076 | var pos = Math.max(0, anchorPos - centerIndex - tabOffsetCount) 1077 | val topDiff = viewCache.get(anchorPos)?.let { vy - getDecoratedTop(it) } ?: 0 1078 | var top = (vy - (anchorPos - pos) * verticalTabHeight) - topDiff 1079 | 1080 | while (pos < anchorPos) { 1081 | val view = getPlacedChildForPosition(header, pos, left, top, verticalTabWidth, verticalTabHeight) 1082 | top = getDecoratedBottom(view) 1083 | pos++ 1084 | } 1085 | } 1086 | 1087 | private fun fillBottom(header: HeaderLayout, anchorPos: Int) { 1088 | val (left, vy) = vPoint ?: return 1089 | 1090 | val count = header.adapter?.getItemCount() ?: 0 1091 | if (count == 0) { 1092 | return 1093 | } 1094 | 1095 | val startPos = when (anchorPos) { 1096 | HeaderLayout.INVALID_POSITION -> 0 1097 | else -> anchorPos 1098 | } 1099 | 1100 | val maxPos = Math.min(count, startPos + centerIndex + 1 + tabOffsetCount) 1101 | var pos = startPos 1102 | 1103 | var top = if (header.childCount > 0) { 1104 | getDecoratedBottom(header.getChildAt(header.childCount - 1)) 1105 | } else { 1106 | vy 1107 | } 1108 | 1109 | while (pos < maxPos) { 1110 | val view = getPlacedChildForPosition(header, pos, left, top, verticalTabWidth, verticalTabHeight) 1111 | top = getDecoratedBottom(view) 1112 | pos++ 1113 | } 1114 | } 1115 | 1116 | private fun getPlacedChildForPosition(header: HeaderLayout, pos: Int, x: Int, y: Int, w: Int, h: Int): View { 1117 | val cacheView = viewCache.get(pos) 1118 | if (cacheView != null) { 1119 | header.attachView(cacheView) 1120 | viewCache.remove(pos) 1121 | return cacheView 1122 | } 1123 | 1124 | val view = header.recycler.getViewForPosition(pos) 1125 | header.addView(view) 1126 | layoutChild(view, x, y, w, h) 1127 | return view 1128 | } 1129 | 1130 | private fun checkIfOffsetChangingStopped() { 1131 | val header = headerLayout ?: return 1132 | 1133 | isOffsetChanged = false 1134 | isCheckingScrollStop = true 1135 | 1136 | val startOffset = appBarBehavior.topAndBottomOffset 1137 | header.postOnAnimationDelayed({ 1138 | isCheckingScrollStop = false 1139 | val currentOffset = appBarBehavior.topAndBottomOffset 1140 | val scrollStopped = currentOffset == startOffset 1141 | if (scrollStopped) { 1142 | onOffsetChangingStopped(currentOffset) 1143 | } 1144 | }, SCROLL_STOP_CHECK_DELAY) 1145 | } 1146 | 1147 | private fun onOffsetChangingStopped(offset: Int) { 1148 | isHeaderChanging = false 1149 | 1150 | if (offset == prevOffset) { 1151 | return 1152 | } 1153 | prevOffset = offset 1154 | 1155 | val header = headerLayout ?: return 1156 | val appBar = appBar ?: return 1157 | 1158 | var hScrollEnable = false 1159 | var vScrollEnable = false 1160 | 1161 | val invertedOffset = workBottom + offset 1162 | when (invertedOffset) { 1163 | workBottom -> { 1164 | vScrollEnable = true 1165 | isCanDrag = false 1166 | } 1167 | workMiddle -> { 1168 | hScrollEnable = true 1169 | isCanDrag = false 1170 | header.postOnAnimation { smoothScrollToPosition(getAnchorPos(header)) } 1171 | } 1172 | workTop -> { 1173 | hScrollEnable = true 1174 | isCanDrag = true 1175 | header.postOnAnimation { smoothScrollToPosition(getAnchorPos(header)) } 1176 | } 1177 | in workTop..(topSnapDistance - 1) -> { 1178 | appBar.setExpanded(false, true) 1179 | } 1180 | in topSnapDistance..(bottomSnapDistance - 1) -> { 1181 | smoothOffset(workMiddle, SNAP_ANIMATION_DURATION) 1182 | } 1183 | else -> { 1184 | appBar.setExpanded(true, true) 1185 | } 1186 | } 1187 | 1188 | header.isHorizontalScrollEnabled = hScrollEnable 1189 | header.isVerticalScrollEnabled = vScrollEnable 1190 | } 1191 | 1192 | private fun smoothOffset(offset: Int, duration: Long = collapsingBySelectDuration.toLong()) { 1193 | val header = headerLayout ?: return 1194 | 1195 | offsetAnimator?.cancel() 1196 | 1197 | offsetAnimator = ValueAnimator().also { animator -> 1198 | animator.duration = duration 1199 | animator.setIntValues(appBarBehavior.topAndBottomOffset, -offset) 1200 | animator.addUpdateListener { 1201 | val value = it.animatedValue as Int 1202 | appBarBehavior.topAndBottomOffset = value 1203 | appBar?.also { it.postOnAnimation { it.requestLayout() } } 1204 | } 1205 | animator.addListener(object : AnimatorListenerAdapter() { 1206 | override fun onAnimationStart(animation: Animator?, isReverse: Boolean) { 1207 | header.isHorizontalScrollEnabled = false 1208 | header.isVerticalScrollEnabled = false 1209 | } 1210 | 1211 | override fun onAnimationEnd(animation: Animator?) { 1212 | this@HeaderLayoutManager.onOffsetChangingStopped(-offset) 1213 | } 1214 | }) 1215 | animator.start() 1216 | } 1217 | } 1218 | 1219 | private fun setScrollState(state: ScrollState) { 1220 | if (scrollState == state) { 1221 | return 1222 | } 1223 | 1224 | scrollStateListeners.forEach { it.onScrollStateChanged(state) } 1225 | scrollState = state 1226 | } 1227 | 1228 | private fun getDecorInsetsForChild(child: View): Rect { 1229 | val lp = HeaderLayout.getChildLayoutParams(child) 1230 | ?: throw RuntimeException("Invalid layout params") 1231 | 1232 | val vh = lp.viewHolder 1233 | ?: throw RuntimeException("No view holder") 1234 | 1235 | val decorRect = lp.decorRect 1236 | if (!isHeaderChanging && lp.decorRectValid) { 1237 | return decorRect 1238 | } 1239 | 1240 | decorRect.set(0, 0, 0, 0) 1241 | for (decoration in itemDecorations) { 1242 | tempDecorRect.set(0, 0, 0, 0) 1243 | decoration.getItemOffsets(tempDecorRect, vh) 1244 | decorRect.left += tempDecorRect.left 1245 | decorRect.top += tempDecorRect.top 1246 | decorRect.right += tempDecorRect.right 1247 | decorRect.bottom += tempDecorRect.bottom 1248 | } 1249 | lp.decorRectValid = true 1250 | 1251 | return decorRect 1252 | } 1253 | } -------------------------------------------------------------------------------- /navigation-toolbar/src/main/kotlin/com/ramotion/navigationtoolbar/NavigationToolBarLayout.kt: -------------------------------------------------------------------------------- 1 | package com.ramotion.navigationtoolbar 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.view.LayoutInflater 6 | import androidx.annotation.AttrRes 7 | import androidx.appcompat.widget.Toolbar 8 | import androidx.coordinatorlayout.widget.CoordinatorLayout 9 | import com.google.android.material.appbar.AppBarLayout 10 | import com.google.android.material.appbar.CollapsingToolbarLayout 11 | import com.ramotion.navigationtoolbar.HeaderLayoutManager.* 12 | 13 | /** 14 | * The main class that combines two other main classes: HeaderLayoutManager and HeaderLayout. 15 | * @see HeaderLayoutManager 16 | * @see HeaderLayout 17 | */ 18 | class NavigationToolBarLayout : CoordinatorLayout { 19 | 20 | private companion object { 21 | const val HEADER_HIDE_START = 0.5f 22 | } 23 | 24 | /** 25 | * ItemTransformer abstract class can be used as parent for class that will be responsible 26 | * for transformation of HeaderLayout child, during HeaderLayout change (expand / collapse) 27 | * or update (redraw / fill). 28 | * @see DefaultItemTransformer 29 | * @see setItemTransformer 30 | */ 31 | abstract class ItemTransformer : HeaderChangeListener, HeaderUpdateListener { 32 | protected var navigationToolBarLayout: NavigationToolBarLayout? = null 33 | private set 34 | 35 | /** 36 | * Called on HeaderLayout change (expand / collapse) or update (redraw / fill). 37 | * @param lm HeaderLayoutManager 38 | * @param header HeaderLayout 39 | * @param headerBottom HeaderLayout bottom position. 40 | */ 41 | abstract fun transform(lm: HeaderLayoutManager, header: HeaderLayout, headerBottom: Int) 42 | 43 | /** 44 | * Called on attach ot NavigationToolBarLayout. 45 | * @see setItemTransformer 46 | */ 47 | abstract fun onAttach(ntl: NavigationToolBarLayout) 48 | 49 | /** 50 | * Called on detach from NavigationToolBarLayout. 51 | * @see setItemTransformer 52 | */ 53 | abstract fun onDetach() 54 | 55 | internal fun attach(ntl: NavigationToolBarLayout) { 56 | navigationToolBarLayout = ntl 57 | ntl.addHeaderChangeListener(this) 58 | ntl.addHeaderUpdateListener(this) 59 | onAttach(ntl) 60 | } 61 | 62 | internal fun detach() { 63 | onDetach() 64 | navigationToolBarLayout?.also { 65 | it.removeHeaderChangeListener(this) 66 | it.removeHeaderUpdateListener(this) 67 | } 68 | navigationToolBarLayout = null 69 | } 70 | 71 | final override fun onHeaderChanged(lm: HeaderLayoutManager, header: HeaderLayout, headerBottom: Int) = 72 | transform(lm, header, headerBottom) 73 | 74 | final override fun onHeaderUpdated(lm: HeaderLayoutManager, header: HeaderLayout, headerBottom: Int) = 75 | transform(lm, header, headerBottom) 76 | } 77 | 78 | /** 79 | * Toolbar layout with id `@id/com_ramotion_toolbar`. 80 | */ 81 | val toolBar: Toolbar 82 | /** 83 | * AppBarLayout with id `@id/com_ramotion_app_bar`. 84 | */ 85 | val appBarLayout: AppBarLayout 86 | /** 87 | * HeaderLayout. 88 | * @see HeaderLayout 89 | */ 90 | val headerLayout: HeaderLayout 91 | /** 92 | * HeaderLayoutManager. 93 | * @see HeaderLayoutManager 94 | */ 95 | val layoutManager: HeaderLayoutManager 96 | 97 | private var itemTransformer: ItemTransformer? = null 98 | 99 | constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) 100 | 101 | constructor(context: Context, attrs: AttributeSet?, @AttrRes defStyleAttr: Int) : super(context, attrs, defStyleAttr) { 102 | LayoutInflater.from(context).inflate(R.layout.navigation_layout, this, true) 103 | 104 | toolBar = findViewById(R.id.com_ramotion_toolbar) 105 | headerLayout = findViewById(R.id.com_ramotion_header_layout) 106 | layoutManager = HeaderLayoutManager(context, attrs) 107 | (headerLayout.layoutParams as CoordinatorLayout.LayoutParams).behavior = layoutManager 108 | 109 | appBarLayout = findViewById(R.id.com_ramotion_app_bar) 110 | appBarLayout.outlineProvider = null 111 | appBarLayout.addOnOffsetChangedListener(layoutManager) 112 | (appBarLayout.layoutParams as CoordinatorLayout.LayoutParams).behavior = layoutManager.appBarBehavior 113 | 114 | attrs?.also { 115 | val a = context.theme.obtainStyledAttributes(attrs, R.styleable.NavigationToolBarr, defStyleAttr, 0) 116 | try { 117 | if (a.hasValue(R.styleable.NavigationToolBarr_headerBackgroundLayout)) { 118 | val backgroundId = a.getResourceId(R.styleable.NavigationToolBarr_headerBackgroundLayout, -1) 119 | initBackgroundLayout(context, backgroundId) 120 | } 121 | } finally { 122 | a.recycle() 123 | } 124 | } 125 | 126 | setItemTransformer(null) 127 | } 128 | 129 | /** 130 | * Sets HeaderLayout Adapter. 131 | * @see HeaderLayout 132 | * @see HeaderLayout.Adapter 133 | */ 134 | fun setAdapter(adapter: HeaderLayout.Adapter) = headerLayout.setAdapter(adapter) 135 | 136 | /** 137 | * Scroll header to specified position. 138 | * @param pos Position where to scroll 139 | * @see HeaderLayoutManager.scrollToPosition 140 | */ 141 | fun scrollToPosition(pos: Int) = layoutManager.scrollToPosition(pos) 142 | 143 | /** 144 | * Smooth scroll header to specified position. 145 | * @param pos Position where to scroll. 146 | * @see HeaderLayoutManager.smoothScrollToPosition 147 | */ 148 | fun smoothScrollToPosition(pos: Int) = layoutManager.smoothScrollToPosition(pos) 149 | 150 | /** 151 | * Returns current center card, adapter position. 152 | * @return Current center card, adapter position or HeaderLayout.INVALID_POSITION if card not found. 153 | * @see HeaderLayout.INVALID_POSITION 154 | * @see HeaderLayoutManager.getAnchorPos 155 | */ 156 | fun getAnchorPos(): Int = layoutManager.getAnchorPos(headerLayout) 157 | 158 | /** 159 | * Adds ItemChangeListener. 160 | * @param listener ItemChangeListener to add. 161 | * @see ItemChangeListener 162 | */ 163 | fun addItemChangeListener(listener: ItemChangeListener) { 164 | layoutManager.itemChangeListeners += listener 165 | } 166 | 167 | /** 168 | * Removes ItemChangeListener. 169 | * @param listener ItemChangeListener to remove. 170 | * @see ItemChangeListener 171 | */ 172 | fun removeItemChangeListener(listener: ItemChangeListener) { 173 | layoutManager.itemChangeListeners -= listener 174 | } 175 | 176 | /** 177 | * Adds ScrollStateListener. 178 | * @param listener ScrollStateListener to add. 179 | * @see ScrollStateListener 180 | */ 181 | fun addScrollStateListener(listener: ScrollStateListener) { 182 | layoutManager.scrollStateListeners += listener 183 | } 184 | 185 | /** 186 | * Removes ScrollStateListener. 187 | * @param listener ScrollStateListener to remove. 188 | * @see ScrollStateListener 189 | */ 190 | fun removeScrollStateListener(listener: ScrollStateListener) { 191 | layoutManager.scrollStateListeners -= listener 192 | } 193 | 194 | /** 195 | * Adds ItemClickListener. 196 | * @param listener ItemClickListener to add. 197 | * @see ItemChangeListener 198 | */ 199 | fun addItemClickListener(listener: ItemClickListener) { 200 | layoutManager.itemClickListeners += listener 201 | } 202 | 203 | /** 204 | * Removes ItemClickListener. 205 | * @param listener ItemClickListener to remove. 206 | * @see ItemClickListener 207 | */ 208 | fun removeItemClickListener(listener: ItemClickListener) { 209 | layoutManager.itemClickListeners -= listener 210 | } 211 | 212 | /** 213 | * Adds HeaderChangeListener 214 | * @param listener HeaderChangeListener to add. 215 | * @see HeaderChangeListener 216 | */ 217 | fun addHeaderChangeListener(listener: HeaderChangeListener) { 218 | layoutManager.changeListener += listener 219 | } 220 | 221 | /** 222 | * Removes HeaderChangeListener 223 | * @param listener HeaderChangeListener to remove. 224 | * @see HeaderChangeListener 225 | */ 226 | fun removeHeaderChangeListener(listener: HeaderChangeListener) { 227 | layoutManager.changeListener -= listener 228 | } 229 | 230 | /** 231 | * Adds HeaderUpdateListener. 232 | * @param listener HeaderUpdateListener to add. 233 | * @see HeaderUpdateListener 234 | */ 235 | fun addHeaderUpdateListener(listener: HeaderUpdateListener) { 236 | layoutManager.updateListener += listener 237 | } 238 | 239 | /** 240 | * Removes HeaderUpdateListener. 241 | * @param listener HeaderUpdateListener to remove. 242 | * @see HeaderUpdateListener 243 | */ 244 | fun removeHeaderUpdateListener(listener: HeaderUpdateListener) { 245 | layoutManager.updateListener -= listener 246 | } 247 | 248 | /** 249 | * Adds ItemDecoration. 250 | * @param decoration ItemDecoration to add. 251 | * @see ItemDecoration 252 | */ 253 | fun addItemDecoration(decoration: ItemDecoration) { 254 | layoutManager.addItemDecoration(decoration) 255 | } 256 | 257 | /** 258 | * Removes ItemDecoration. 259 | * @param decoration ItemDecoration to remove. 260 | * @see ItemDecoration 261 | */ 262 | fun removeItemDecoration(decoration: ItemDecoration) { 263 | layoutManager.removeItemDecoration(decoration) 264 | } 265 | 266 | /** 267 | * Adds HeaderChangeStateListener. 268 | * @param listener HeaderChangeStateListener to add. 269 | * @see HeaderChangeStateListener 270 | */ 271 | fun addHeaderChangeStateListener(listener: HeaderChangeStateListener) { 272 | layoutManager.changeListener += listener 273 | } 274 | 275 | /** 276 | * Removes HeaderChangeStateListener. 277 | * @param listener HeaderChangeStateListener to remove. 278 | * @see HeaderChangeStateListener 279 | */ 280 | fun removeHeaderChangeStateListener(listener: HeaderChangeStateListener) { 281 | layoutManager.changeListener -= listener 282 | } 283 | 284 | /** 285 | * Sets ItemTransformer. 286 | * @param newTransformer New transformer. Can be null. If null, then DefaultItemTransformer will be used. 287 | * @see ItemTransformer 288 | */ 289 | fun setItemTransformer(newTransformer: ItemTransformer?) { 290 | itemTransformer?.also { it.detach() } 291 | 292 | (newTransformer ?: DefaultItemTransformer()).also { 293 | it.attach(this) 294 | itemTransformer = it 295 | } 296 | } 297 | 298 | fun collapse() { 299 | layoutManager.getAnchorView(headerLayout) 300 | ?.let { HeaderLayout.getChildViewHolder(it) } 301 | ?.also { layoutManager.onHeaderItemClick(headerLayout, it) } 302 | } 303 | 304 | fun expand(animate: Boolean) = appBarLayout.setExpanded(true, animate) 305 | 306 | private fun initBackgroundLayout(context: Context, layoutId: Int) { 307 | val ctl = findViewById(R.id.com_ramotion_toolbar_layout) 308 | val background = LayoutInflater.from(context).inflate(layoutId, ctl, true) 309 | addHeaderChangeListener(object : HeaderChangeListener { 310 | override fun onHeaderChanged(lm: HeaderLayoutManager, header: HeaderLayout, headerBottom: Int) { 311 | val ratio = 1f - headerBottom / (headerLayout.height + 1f) 312 | val headerAlpha = if (ratio >= HEADER_HIDE_START) 0f else 1f 313 | background.alpha = headerAlpha 314 | } 315 | }) 316 | } 317 | } -------------------------------------------------------------------------------- /navigation-toolbar/src/main/kotlin/com/ramotion/navigationtoolbar/SimpleSnapHelper.kt: -------------------------------------------------------------------------------- 1 | package com.ramotion.navigationtoolbar 2 | 3 | import com.ramotion.navigationtoolbar.HeaderLayoutManager.ScrollState 4 | import com.ramotion.navigationtoolbar.HeaderLayoutManager.ScrollState.IDLE 5 | import java.lang.ref.WeakReference 6 | 7 | class SimpleSnapHelper : HeaderLayoutManager.ScrollStateListener { 8 | 9 | private var toolBarRef: WeakReference? = null 10 | 11 | override fun onScrollStateChanged(state: ScrollState) { 12 | if (state != IDLE) { 13 | return 14 | } 15 | 16 | toolBarRef?.get()?.also { toolbar -> 17 | toolbar.smoothScrollToPosition(toolbar.getAnchorPos()) 18 | } 19 | } 20 | 21 | fun attach(toolbar: NavigationToolBarLayout) { 22 | toolBarRef = WeakReference(toolbar) 23 | toolbar.addScrollStateListener(this) 24 | } 25 | 26 | fun detach(toolbar: NavigationToolBarLayout) { 27 | toolBarRef = null 28 | toolbar.removeScrollStateListener(this) 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /navigation-toolbar/src/main/res/layout/navigation_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 15 | 16 | 24 | 25 | 26 | 27 | 34 | 35 | 40 | 41 | -------------------------------------------------------------------------------- /navigation-toolbar/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /navigation-toolbar/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | navigation-toolbar 3 | 4 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':navigation-toolbar', ':navigation-toolbar-example' 2 | --------------------------------------------------------------------------------