├── .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 | [](http://twitter.com/Ramotion)
27 | [](https://app.codacy.com/app/dvg4000/navigation-toolbar-android/dashboard)
28 | [](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 |
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 |
--------------------------------------------------------------------------------