├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── README.md ├── bintray.gradle ├── build.gradle ├── dependencies.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images └── sample.gif ├── install.gradle ├── lint.xml ├── sample ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── codewaves │ │ └── stickyheadergrid │ │ └── sample │ │ ├── SampleActivity.java │ │ └── SampleAdapter.java │ └── res │ ├── drawable │ └── header_shadow.xml │ ├── layout │ ├── activity_sample.xml │ ├── cell_header.xml │ └── cell_item.xml │ ├── menu │ └── main.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 │ ├── strings.xml │ └── styles.xml ├── settings.gradle └── stickyheadergrid ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src └── main ├── AndroidManifest.xml ├── java └── com │ └── codewaves │ └── stickyheadergrid │ ├── StickyHeaderGridAdapter.java │ └── StickyHeaderGridLayoutManager.java └── res └── values └── strings.xml /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io 2 | 3 | ### Android ### 4 | 5 | # Files for the Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | 15 | # Gradle files 16 | .gradle/ 17 | build/ 18 | 19 | # Local configuration file (sdk path, etc) 20 | local.properties 21 | 22 | # Proguard folder generated by Eclipse 23 | proguard/ 24 | 25 | # Log Files 26 | *.log 27 | 28 | 29 | ### Intellij ### 30 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm 31 | 32 | *.iml 33 | 34 | ## Directory-based project format: 35 | .idea/ 36 | # if you remove the above rule, at least ignore the following: 37 | 38 | # User-specific stuff: 39 | # .idea/workspace.xml 40 | # .idea/tasks.xml 41 | # .idea/dictionaries 42 | 43 | # Sensitive or high-churn files: 44 | # .idea/dataSources.ids 45 | # .idea/dataSources.xml 46 | # .idea/sqlDataSources.xml 47 | # .idea/dynamic.xml 48 | # .idea/uiDesigner.xml 49 | 50 | # Gradle: 51 | # .idea/gradle.xml 52 | # .idea/libraries 53 | 54 | # Mongo Explorer plugin: 55 | # .idea/mongoSettings.xml 56 | 57 | ## File-based project format: 58 | *.ipr 59 | *.iws 60 | 61 | ## Plugin-specific files: 62 | 63 | # IntelliJ 64 | out/ 65 | 66 | # mpeltonen/sbt-idea plugin 67 | .idea_modules/ 68 | 69 | # JIRA plugin 70 | atlassian-ide-plugin.xml 71 | 72 | # Crashlytics plugin (for Android Studio and IntelliJ) 73 | com_crashlytics_export_strings.xml 74 | 75 | # Ignore Gradle GUI config 76 | gradle-app.setting 77 | 78 | # Mobile Tools for Java (J2ME) 79 | .mtj.tmp/ 80 | 81 | # Package Files # 82 | *.war 83 | *.ear 84 | 85 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 86 | hs_err_pid* 87 | 88 | *.DS_Store 89 | 90 | *.jks -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | jdk: oraclejdk8 3 | android: 4 | components: 5 | - tools 6 | - platform-tools 7 | - build-tools-28.0.3 8 | - android-28 9 | - extra-android-support 10 | - extra-android-m2repository 11 | - extra-google-m2repository 12 | 13 | licenses: 14 | - '.+' 15 | 16 | before_install: 17 | - chmod +x gradlew 18 | 19 | after_failure: "cat $TRAVIS_BUILD_DIR/sample/build/reports/lint-results.xml" -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Sergej Kravcenko 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sticky Header Grid Layout Manager 2 | [![Download](https://api.bintray.com/packages/codewaves/maven/sticky-header-grid/images/download.svg) ](https://bintray.com/codewaves/maven/sticky-header-grid/_latestVersion) 3 | [![Build Status](https://travis-ci.org/Codewaves/Sticky-Header-Grid.svg?branch=master)](https://travis-ci.org/Codewaves/Sticky-Header-Grid) 4 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/6ef37130881446ef94953775e9598e40)](https://www.codacy.com/app/Codewaves/Sticky-Header-Grid?utm_source=github.com&utm_medium=referral&utm_content=Codewaves/Sticky-Header-Grid&utm_campaign=Badge_Grade) 5 | [![GitHub license](https://img.shields.io/github/license/mashape/apistatus.svg)](https://github.com/Codewaves/Sticky-Header-Grid/blob/master/LICENSE.txt) 6 | 7 | Android RecyclerView sticky header list/grid layout. 8 | 9 | ![](images/sample.gif) 10 | 11 | ## Download 12 | 13 | Download [the latest AAR][1] or grab via Gradle: 14 | ```groovy 15 | compile 'com.codewaves.stickyheadergrid:stickyheadergrid:0.9.7' 16 | ``` 17 | 18 | ## Features 19 | 20 | * Sticky section headers 21 | * Individually control header stickiness 22 | * Span support like in GridLayoutManager 23 | * Header bottom shadows 24 | * Header state listener 25 | * Smooth scrolling 26 | * Scrollbars 27 | 28 | ## Usage 29 | 30 | To use library: 31 | 32 | 1. Implement an adapter by subclassing StickyHeaderGridAdapter 33 | 2. Create holder class for each header and item type. Use HeaderViewHolder and ItemViewHolder as base classes. 34 | 3. Override and implement getSectionCount(), getSectionItemCount(int section), onCreateHeaderViewHolder(ViewGroup parent, int headerType), 35 | onCreateItemViewHolder(ViewGroup parent, int itemType), onBindHeaderViewHolder(HeaderViewHolder viewHolder, int section), 36 | onBindItemViewHolder(ItemViewHolder viewHolder, int section, int offset). 37 | 4. Create a StickyHeaderGridLayoutManager with required column count and assign it to your RecyclerView. 38 | 5. Use only StickyHeaderGridAdapter::notify* methods 39 | 40 | If you need the position of an item in a click listener, always use holder .getAdapterPosition() which will have 41 | the correct adapter position. 42 | 43 | ```java 44 | holder..setOnClickListener(new View.OnClickListener() { 45 | @Override 46 | public void onClick(View v) { 47 | final int section = getAdapterPositionSection(holder.getAdapterPosition()); 48 | final int offset = getItemSectionOffset(section, holder.getAdapterPosition()); 49 | 50 | // Do click action here using setction and offset 51 | } 52 | }); 53 | ``` 54 | 55 | ### Span support 56 | 57 | Like in GridLayoutManager, use SpanSizeLookup to provide span information. 58 | 59 | ### Individual stickiness 60 | 61 | Override adapter isSectionHeaderSticky method and return `true` to make section header 62 | sticky, `false` otherwise. 63 | 64 | ### Header bottom shadow 65 | 66 | Because of the limitation of old Android platforms we cannot use an elevation. Little workaround 67 | was made to support bottom header shadows. Use layout manager .setHeaderBottomOverlapMargin 68 | method to set size of the header overlapping part and insert shadow into header layout. Overlapping 69 | part will be drawn over the first section item. 70 | 71 | ### Header state listener 72 | 73 | Use HeaderStateChangeListener to receive information about headers state. 74 | 75 | ## Other features 76 | 77 | If you missing some feature, feel free to create an issue or pull request. 78 | 79 | ## Author 80 | 81 | Sergej Kravcenko - [Codewaves][2] 82 | 83 | 84 | ## License 85 | 86 | The MIT License (MIT) 87 | 88 | Copyright (c) 2017 Sergej Kravcenko 89 | 90 | Permission is hereby granted, free of charge, to any person obtaining a copy 91 | of this software and associated documentation files (the "Software"), to deal 92 | in the Software without restriction, including without limitation the rights 93 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 94 | copies of the Software, and to permit persons to whom the Software is 95 | furnished to do so, subject to the following conditions: 96 | 97 | The above copyright notice and this permission notice shall be included in all 98 | copies or substantial portions of the Software. 99 | 100 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 101 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 102 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 103 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 104 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 105 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 106 | SOFTWARE. 107 | 108 | [1]: https://bintray.com/codewaves/maven/sticky-header-grid/_latestVersion 109 | [2]: http://www.codewaves.com 110 | -------------------------------------------------------------------------------- /bintray.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.jfrog.bintray' 2 | 3 | version = libraryVersion 4 | 5 | task sourcesJar(type: Jar) { 6 | from android.sourceSets.main.java.srcDirs 7 | classifier = 'sources' 8 | } 9 | 10 | task javadoc(type: Javadoc) { 11 | source = android.sourceSets.main.java.srcDirs 12 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) + configurations.compile 13 | failOnError false 14 | } 15 | 16 | task javadocJar(type: Jar, dependsOn: javadoc) { 17 | classifier = 'javadoc' 18 | from javadoc.destinationDir 19 | } 20 | artifacts { 21 | archives javadocJar 22 | archives sourcesJar 23 | } 24 | 25 | // Bintray 26 | Properties properties = new Properties() 27 | try { 28 | properties.load(project.rootProject.file('local.properties').newDataInputStream()) 29 | } catch (Exception ignored) {} 30 | 31 | bintray { 32 | user = properties.getProperty("bintray.user") 33 | key = properties.getProperty("bintray.apikey") 34 | 35 | configurations = ['archives'] 36 | pkg { 37 | repo = bintrayRepo 38 | name = bintrayName 39 | desc = libraryDescription 40 | websiteUrl = siteUrl 41 | vcsUrl = gitUrl 42 | licenses = allLicenses 43 | publish = true 44 | publicDownloadNumbers = true 45 | version { 46 | desc = libraryDescription 47 | gpg { 48 | sign = false 49 | passphrase = properties.getProperty("bintray.gpg.password") 50 | } 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | apply from: './dependencies.gradle' 2 | 3 | buildscript { 4 | apply from: './dependencies.gradle' 5 | 6 | repositories { 7 | google() 8 | jcenter() 9 | } 10 | dependencies { 11 | classpath 'com.android.tools.build:gradle:' + versions.gradlePlugin 12 | classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7.3' 13 | classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5' 14 | } 15 | } 16 | 17 | allprojects { 18 | repositories { 19 | google() 20 | jcenter() 21 | } 22 | } -------------------------------------------------------------------------------- /dependencies.gradle: -------------------------------------------------------------------------------- 1 | ext.versions = [ 2 | minSdk: 14, 3 | compileSdk: 28, 4 | buildTools: '28.0.3', 5 | publishVersion: '0.9.7', 6 | publishVersionCode: 8, 7 | gradlePlugin: '3.2.1', 8 | 9 | supportLib: '28.0.0', 10 | ] -------------------------------------------------------------------------------- /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 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | # org.gradle.parallel=true 18 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codewaves/Sticky-Header-Grid/5a0e9270a4458ef826f247802f3f1e79a32cef11/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Apr 13 20:41:28 EEST 2017 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.6-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 | -------------------------------------------------------------------------------- /images/sample.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codewaves/Sticky-Header-Grid/5a0e9270a4458ef826f247802f3f1e79a32cef11/images/sample.gif -------------------------------------------------------------------------------- /install.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.github.dcendents.android-maven' 2 | 3 | group = publishedGroupId 4 | 5 | install { 6 | repositories.mavenInstaller { 7 | // This generates POM.xml with proper parameters 8 | pom { 9 | project { 10 | packaging 'aar' 11 | groupId publishedGroupId 12 | artifactId artifact 13 | 14 | // Add your description here 15 | name libraryName 16 | description libraryDescription 17 | url siteUrl 18 | 19 | // Set your license 20 | licenses { 21 | license { 22 | name licenseName 23 | url licenseUrl 24 | } 25 | } 26 | developers { 27 | developer { 28 | id developerId 29 | name developerName 30 | email developerEmail 31 | } 32 | } 33 | scm { 34 | connection gitUrl 35 | developerConnection gitUrl 36 | url siteUrl 37 | 38 | } 39 | } 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /lint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /sample/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply from: '../dependencies.gradle' 3 | 4 | android { 5 | compileSdkVersion versions.compileSdk 6 | buildToolsVersion versions.buildTools 7 | 8 | defaultConfig { 9 | applicationId "com.codewaves.stickyheadergrid.sample" 10 | minSdkVersion versions.minSdk 11 | targetSdkVersion versions.compileSdk 12 | versionCode versions.publishVersionCode 13 | versionName versions.publishVersion 14 | } 15 | buildTypes { 16 | release { 17 | minifyEnabled false 18 | shrinkResources false 19 | } 20 | } 21 | lintOptions { 22 | lintConfig file("../lint.xml") 23 | } 24 | } 25 | 26 | dependencies { 27 | implementation 'com.android.support:appcompat-v7:' + versions.supportLib 28 | implementation 'com.android.support:design:' + versions.supportLib 29 | implementation project(':stickyheadergrid') 30 | } 31 | -------------------------------------------------------------------------------- /sample/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in E:\Android\android-sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /sample/src/main/java/com/codewaves/stickyheadergrid/sample/SampleActivity.java: -------------------------------------------------------------------------------- 1 | package com.codewaves.stickyheadergrid.sample; 2 | 3 | import android.support.v7.app.AppCompatActivity; 4 | import android.os.Bundle; 5 | import android.support.v7.widget.DefaultItemAnimator; 6 | import android.support.v7.widget.RecyclerView; 7 | import android.view.Menu; 8 | import android.view.MenuItem; 9 | 10 | import com.codewaves.sample.R; 11 | import com.codewaves.stickyheadergrid.StickyHeaderGridLayoutManager; 12 | 13 | public class SampleActivity extends AppCompatActivity { 14 | private static final int SPAN_SIZE = 3; 15 | private static final int SECTIONS = 10; 16 | private static final int SECTION_ITEMS = 5; 17 | 18 | private RecyclerView mRecycler; 19 | private StickyHeaderGridLayoutManager mLayoutManager; 20 | 21 | @Override 22 | protected void onCreate(Bundle savedInstanceState) { 23 | super.onCreate(savedInstanceState); 24 | setContentView(R.layout.activity_sample); 25 | 26 | // Setup recycler 27 | mRecycler = (RecyclerView)findViewById(R.id.recycler); 28 | mLayoutManager = new StickyHeaderGridLayoutManager(SPAN_SIZE); 29 | mLayoutManager.setHeaderBottomOverlapMargin(getResources().getDimensionPixelSize(R.dimen.header_shadow_size)); 30 | mLayoutManager.setSpanSizeLookup(new StickyHeaderGridLayoutManager.SpanSizeLookup() { 31 | @Override 32 | public int getSpanSize(int section, int position) { 33 | switch (section) { 34 | case 0: 35 | return 3; 36 | case 1: 37 | return 1; 38 | case 2: 39 | return 3 - position % 3; 40 | case 3: 41 | return position % 2 + 1; 42 | default: 43 | return 1; 44 | } 45 | } 46 | }); 47 | 48 | // Workaround RecyclerView limitation when playing remove animations. RecyclerView always 49 | // puts the removed item on the top of other views and it will be drawn above sticky header. 50 | // The only way to fix this, abandon remove animations :( 51 | mRecycler.setItemAnimator(new DefaultItemAnimator() { 52 | @Override 53 | public boolean animateRemove(RecyclerView.ViewHolder holder) { 54 | dispatchRemoveFinished(holder); 55 | return false; 56 | } 57 | }); 58 | mRecycler.setLayoutManager(mLayoutManager); 59 | mRecycler.setAdapter(new SampleAdapter(SECTIONS, SECTION_ITEMS)); 60 | } 61 | 62 | @Override 63 | public boolean onCreateOptionsMenu(Menu menu) { 64 | getMenuInflater().inflate(R.menu.main, menu); 65 | return true; 66 | } 67 | 68 | @Override 69 | public boolean onOptionsItemSelected(MenuItem item) { 70 | int id = item.getItemId(); 71 | switch (id) { 72 | case R.id.action_top: 73 | mRecycler.scrollToPosition(0); 74 | break; 75 | case R.id.action_center: 76 | mRecycler.scrollToPosition(mRecycler.getAdapter().getItemCount() / 2); 77 | break; 78 | case R.id.action_bottom: 79 | mRecycler.scrollToPosition(mRecycler.getAdapter().getItemCount() - 1); 80 | break; 81 | case R.id.action_top_smooth: 82 | mRecycler.smoothScrollToPosition(0); 83 | break; 84 | case R.id.action_center_smooth: 85 | mRecycler.smoothScrollToPosition(mRecycler.getAdapter().getItemCount() / 2); 86 | break; 87 | case R.id.action_bottom_smooth: 88 | mRecycler.smoothScrollToPosition(mRecycler.getAdapter().getItemCount() - 1); 89 | break; 90 | } 91 | return super.onOptionsItemSelected(item); 92 | 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /sample/src/main/java/com/codewaves/stickyheadergrid/sample/SampleAdapter.java: -------------------------------------------------------------------------------- 1 | package com.codewaves.stickyheadergrid.sample; 2 | 3 | import android.view.LayoutInflater; 4 | import android.view.View; 5 | import android.view.ViewGroup; 6 | import android.widget.TextView; 7 | import android.widget.Toast; 8 | 9 | import com.codewaves.sample.R; 10 | import com.codewaves.stickyheadergrid.StickyHeaderGridAdapter; 11 | 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | 15 | /** 16 | * Created by Sergej Kravcenko on 4/24/2017. 17 | * Copyright (c) 2017 Sergej Kravcenko 18 | */ 19 | 20 | public class SampleAdapter extends StickyHeaderGridAdapter { 21 | private List> labels; 22 | 23 | SampleAdapter(int sections, int count) { 24 | labels = new ArrayList<>(sections); 25 | for (int s = 0; s < sections; ++s) { 26 | List labels = new ArrayList<>(count); 27 | for (int i = 0; i < count; ++i) { 28 | String label = "Item " + String.valueOf(i); 29 | /*for (int p = 0; p < s - i; ++p) { 30 | label += "*\n"; 31 | }*/ 32 | labels.add(label); 33 | } 34 | this.labels.add(labels); 35 | } 36 | } 37 | 38 | @Override 39 | public int getSectionCount() { 40 | return labels.size(); 41 | } 42 | 43 | @Override 44 | public int getSectionItemCount(int section) { 45 | return labels.get(section).size(); 46 | } 47 | 48 | @Override 49 | public HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent, int headerType) { 50 | final View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.cell_header, parent, false); 51 | return new MyHeaderViewHolder(view); 52 | } 53 | 54 | @Override 55 | public ItemViewHolder onCreateItemViewHolder(ViewGroup parent, int itemType) { 56 | final View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.cell_item, parent, false); 57 | return new MyItemViewHolder(view); 58 | } 59 | 60 | @Override 61 | public void onBindHeaderViewHolder(HeaderViewHolder viewHolder, int section) { 62 | final MyHeaderViewHolder holder = (MyHeaderViewHolder)viewHolder; 63 | final String label = "Header " + section; 64 | holder.labelView.setText(label); 65 | } 66 | 67 | @Override 68 | public void onBindItemViewHolder(ItemViewHolder viewHolder, final int section, final int position) { 69 | final MyItemViewHolder holder = (MyItemViewHolder)viewHolder; 70 | final String label = labels.get(section).get(position); 71 | holder.labelView.setText(label); 72 | holder.labelView.setOnClickListener(new View.OnClickListener() { 73 | @Override 74 | public void onClick(View v) { 75 | final int section = getAdapterPositionSection(holder.getAdapterPosition()); 76 | final int offset = getItemSectionOffset(section, holder.getAdapterPosition()); 77 | 78 | labels.get(section).remove(offset); 79 | notifySectionItemRemoved(section, offset); 80 | Toast.makeText(holder.labelView.getContext(), label, Toast.LENGTH_SHORT).show(); 81 | } 82 | }); 83 | } 84 | 85 | public static class MyHeaderViewHolder extends HeaderViewHolder { 86 | TextView labelView; 87 | 88 | MyHeaderViewHolder(View itemView) { 89 | super(itemView); 90 | labelView = (TextView) itemView.findViewById(R.id.label); 91 | } 92 | } 93 | 94 | public static class MyItemViewHolder extends ItemViewHolder { 95 | TextView labelView; 96 | 97 | MyItemViewHolder(View itemView) { 98 | super(itemView); 99 | labelView = (TextView) itemView.findViewById(R.id.label); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/header_shadow.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_sample.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/cell_header.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 13 | 14 | 17 | 18 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/cell_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 16 | 17 | -------------------------------------------------------------------------------- /sample/src/main/res/menu/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 7 | 9 | 11 | 13 | 15 | 17 | -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codewaves/Sticky-Header-Grid/5a0e9270a4458ef826f247802f3f1e79a32cef11/sample/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codewaves/Sticky-Header-Grid/5a0e9270a4458ef826f247802f3f1e79a32cef11/sample/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codewaves/Sticky-Header-Grid/5a0e9270a4458ef826f247802f3f1e79a32cef11/sample/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codewaves/Sticky-Header-Grid/5a0e9270a4458ef826f247802f3f1e79a32cef11/sample/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codewaves/Sticky-Header-Grid/5a0e9270a4458ef826f247802f3f1e79a32cef11/sample/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codewaves/Sticky-Header-Grid/5a0e9270a4458ef826f247802f3f1e79a32cef11/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codewaves/Sticky-Header-Grid/5a0e9270a4458ef826f247802f3f1e79a32cef11/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codewaves/Sticky-Header-Grid/5a0e9270a4458ef826f247802f3f1e79a32cef11/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codewaves/Sticky-Header-Grid/5a0e9270a4458ef826f247802f3f1e79a32cef11/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codewaves/Sticky-Header-Grid/5a0e9270a4458ef826f247802f3f1e79a32cef11/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /sample/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5dp 4 | 5dp 5 | 5dp 6 | -------------------------------------------------------------------------------- /sample/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | StickyHeaderGrid 3 | 4 | Top 5 | Bottom 6 | Top Smooth 7 | Bottom Smooth 8 | Center 9 | Center Smooth 10 | 11 | -------------------------------------------------------------------------------- /sample/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':sample', ':stickyheadergrid' 2 | -------------------------------------------------------------------------------- /stickyheadergrid/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /stickyheadergrid/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply from: '../dependencies.gradle' 3 | 4 | ext { 5 | bintrayRepo = 'maven' 6 | bintrayName = 'sticky-header-grid' 7 | 8 | publishedGroupId = 'com.codewaves.stickyheadergrid' 9 | libraryName = 'Sticky-Header-Grid' 10 | artifact = 'stickyheadergrid' 11 | 12 | libraryDescription = 'Sticky header grid layout manager for RecycleView' 13 | 14 | siteUrl = 'https://github.com/Codewaves/Sticky-Header-Grid' 15 | gitUrl = 'https://github.com/Codewaves/Sticky-Header-Grid.git' 16 | 17 | libraryVersion = versions.publishVersion 18 | 19 | developerId = 'codewaves' 20 | developerName = 'Sergej Kravcenko' 21 | developerEmail = 'skravcenko@codewaves.com' 22 | 23 | licenseName = 'The MIT License' 24 | licenseUrl = 'https://opensource.org/licenses/mit-license.php' 25 | allLicenses = ["MIT"] 26 | } 27 | 28 | android { 29 | compileSdkVersion versions.compileSdk 30 | buildToolsVersion versions.buildTools 31 | 32 | defaultConfig { 33 | minSdkVersion versions.minSdk 34 | targetSdkVersion versions.compileSdk 35 | versionCode versions.publishVersionCode 36 | versionName versions.publishVersion 37 | } 38 | buildTypes { 39 | release { 40 | minifyEnabled false 41 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 42 | } 43 | } 44 | lintOptions { 45 | lintConfig file("../lint.xml") 46 | } 47 | } 48 | 49 | dependencies { 50 | implementation 'com.android.support:support-v13:' + versions.supportLib 51 | implementation 'com.android.support:appcompat-v7:' + versions.supportLib 52 | implementation 'com.android.support:recyclerview-v7:' + versions.supportLib 53 | } 54 | 55 | apply from: '../install.gradle' 56 | apply from: '../bintray.gradle' -------------------------------------------------------------------------------- /stickyheadergrid/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in E:\Android\android-sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | 27 | -injars in.jar 28 | -outjars out.jar 29 | -libraryjars /lib/rt.jar 30 | -printmapping out.map 31 | 32 | -keepparameternames 33 | -renamesourcefileattribute SourceFile 34 | -keepattributes Exceptions,InnerClasses,Signature,Deprecated,SourceFile,LineNumberTable,*Annotation*,EnclosingMethod,LocalVariableTable,LocalVariableTypeTable 35 | 36 | -keep public class * { 37 | public protected *; 38 | } 39 | 40 | -keepclassmembernames class * { 41 | java.lang.Class class$(java.lang.String); 42 | java.lang.Class class$(java.lang.String, boolean); 43 | } 44 | 45 | -keepclasseswithmembernames,includedescriptorclasses class * { 46 | native ; 47 | } 48 | 49 | -keepclassmembers,allowoptimization enum * { 50 | public static **[] values(); public static ** valueOf(java.lang.String); 51 | } 52 | 53 | -keepclassmembers class * implements java.io.Serializable { 54 | static final long serialVersionUID; 55 | private static final java.io.ObjectStreamField[] serialPersistentFields; 56 | private void writeObject(java.io.ObjectOutputStream); 57 | private void readObject(java.io.ObjectInputStream); 58 | java.lang.Object writeReplace(); 59 | java.lang.Object readResolve(); 60 | } -------------------------------------------------------------------------------- /stickyheadergrid/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /stickyheadergrid/src/main/java/com/codewaves/stickyheadergrid/StickyHeaderGridAdapter.java: -------------------------------------------------------------------------------- 1 | package com.codewaves.stickyheadergrid; 2 | 3 | import android.support.annotation.NonNull; 4 | import android.support.v7.widget.RecyclerView; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | 8 | import java.security.InvalidParameterException; 9 | import java.util.ArrayList; 10 | 11 | import static android.support.v7.widget.RecyclerView.NO_POSITION; 12 | 13 | /** 14 | * Created by Sergej Kravcenko on 4/24/2017. 15 | * Copyright (c) 2017 Sergej Kravcenko 16 | */ 17 | 18 | @SuppressWarnings({"unused", "WeakerAccess"}) 19 | public abstract class StickyHeaderGridAdapter extends RecyclerView.Adapter { 20 | public static final String TAG = "StickyHeaderGridAdapter"; 21 | 22 | public static final int TYPE_HEADER = 0; 23 | public static final int TYPE_ITEM = 1; 24 | 25 | private ArrayList
mSections; 26 | private int[] mSectionIndices; 27 | private int mTotalItemNumber; 28 | 29 | @SuppressWarnings("WeakerAccess") 30 | public static class ViewHolder extends RecyclerView.ViewHolder { 31 | public ViewHolder(View itemView) { 32 | super(itemView); 33 | } 34 | 35 | public boolean isHeader() { 36 | return false; 37 | } 38 | 39 | 40 | public int getSectionItemViewType() { 41 | return StickyHeaderGridAdapter.externalViewType(getItemViewType()); 42 | } 43 | } 44 | 45 | public static class ItemViewHolder extends ViewHolder { 46 | public ItemViewHolder(View itemView) { 47 | super(itemView); 48 | } 49 | } 50 | 51 | public static class HeaderViewHolder extends ViewHolder { 52 | public HeaderViewHolder(View itemView) { 53 | super(itemView); 54 | } 55 | 56 | @Override 57 | public boolean isHeader() { 58 | return true; 59 | } 60 | } 61 | 62 | private static class Section { 63 | private int position; 64 | private int itemNumber; 65 | private int length; 66 | } 67 | 68 | private void calculateSections() { 69 | mSections = new ArrayList<>(); 70 | 71 | int total = 0; 72 | int sectionCount = getSectionCount(); 73 | for (int s = 0; s < sectionCount; s++) { 74 | final Section section = new Section(); 75 | section.position = total; 76 | section.itemNumber = getSectionItemCount(s); 77 | section.length = section.itemNumber + 1; 78 | mSections.add(section); 79 | 80 | total += section.length; 81 | } 82 | mTotalItemNumber = total; 83 | 84 | total = 0; 85 | mSectionIndices = new int[mTotalItemNumber]; 86 | for (int s = 0; s < sectionCount; s++) { 87 | final Section section = mSections.get(s); 88 | for (int i = 0; i < section.length; i++) { 89 | mSectionIndices[total + i] = s; 90 | } 91 | total += section.length; 92 | } 93 | } 94 | 95 | protected int getItemViewInternalType(int position) { 96 | final int section = getAdapterPositionSection(position); 97 | final Section sectionObject = mSections.get(section); 98 | final int sectionPosition = position - sectionObject.position; 99 | 100 | return getItemViewInternalType(section, sectionPosition); 101 | } 102 | 103 | private int getItemViewInternalType(int section, int position) { 104 | return position == 0 ? TYPE_HEADER : TYPE_ITEM; 105 | } 106 | 107 | static private int internalViewType(int type) { 108 | return type & 0xFF; 109 | } 110 | 111 | static private int externalViewType(int type) { 112 | return type >> 8; 113 | } 114 | 115 | @Override 116 | final public int getItemCount() { 117 | if (mSections == null) { 118 | calculateSections(); 119 | } 120 | return mTotalItemNumber; 121 | } 122 | 123 | @NonNull 124 | @Override 125 | final public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 126 | final int internalType = internalViewType(viewType); 127 | final int externalType = externalViewType(viewType); 128 | 129 | switch (internalType) { 130 | case TYPE_HEADER: 131 | return onCreateHeaderViewHolder(parent, externalType); 132 | case TYPE_ITEM: 133 | return onCreateItemViewHolder(parent, externalType); 134 | default: 135 | throw new InvalidParameterException("Invalid viewType: " + viewType); 136 | } 137 | } 138 | 139 | @Override 140 | final public void onBindViewHolder(@NonNull ViewHolder holder, int position) { 141 | if (mSections == null) { 142 | calculateSections(); 143 | } 144 | 145 | final int section = mSectionIndices[position]; 146 | final int internalType = internalViewType(holder.getItemViewType()); 147 | final int externalType = externalViewType(holder.getItemViewType()); 148 | 149 | switch (internalType) { 150 | case TYPE_HEADER: 151 | onBindHeaderViewHolder((HeaderViewHolder)holder, section); 152 | break; 153 | case TYPE_ITEM: 154 | final ItemViewHolder itemHolder = (ItemViewHolder)holder; 155 | final int offset = getItemSectionOffset(section, position); 156 | onBindItemViewHolder((ItemViewHolder)holder, section, offset); 157 | break; 158 | default: 159 | throw new InvalidParameterException("invalid viewType: " + internalType); 160 | } 161 | } 162 | 163 | @Override 164 | final public int getItemViewType(int position) { 165 | final int section = getAdapterPositionSection(position); 166 | final Section sectionObject = mSections.get(section); 167 | final int sectionPosition = position - sectionObject.position; 168 | final int internalType = getItemViewInternalType(section, sectionPosition); 169 | int externalType = 0; 170 | 171 | switch (internalType) { 172 | case TYPE_HEADER: 173 | externalType = getSectionHeaderViewType(section); 174 | break; 175 | case TYPE_ITEM: 176 | externalType = getSectionItemViewType(section, sectionPosition - 1); 177 | break; 178 | } 179 | 180 | return ((externalType & 0xFF) << 8) | (internalType & 0xFF); 181 | } 182 | 183 | // Helpers 184 | private int getItemSectionHeaderPosition(int position) { 185 | return getSectionHeaderPosition(getAdapterPositionSection(position)); 186 | } 187 | 188 | private int getAdapterPosition(int section, int offset) { 189 | if (mSections == null) { 190 | calculateSections(); 191 | } 192 | 193 | if (section < 0) { 194 | throw new IndexOutOfBoundsException("section " + section + " < 0"); 195 | } 196 | 197 | if (section >= mSections.size()) { 198 | throw new IndexOutOfBoundsException("section " + section + " >=" + mSections.size()); 199 | } 200 | 201 | final Section sectionObject = mSections.get(section); 202 | return sectionObject.position + offset; 203 | } 204 | 205 | /** 206 | * Given a section and an adapter position get the offset of an item 207 | * inside section. 208 | * 209 | * @param section section to query 210 | * @param position adapter position 211 | * @return The item offset inside the section. 212 | */ 213 | public int getItemSectionOffset(int section, int position) { 214 | if (mSections == null) { 215 | calculateSections(); 216 | } 217 | 218 | if (section < 0) { 219 | throw new IndexOutOfBoundsException("section " + section + " < 0"); 220 | } 221 | 222 | if (section >= mSections.size()) { 223 | throw new IndexOutOfBoundsException("section " + section + " >=" + mSections.size()); 224 | } 225 | 226 | final Section sectionObject = mSections.get(section); 227 | final int localPosition = position - sectionObject.position; 228 | if (localPosition >= sectionObject.length) { 229 | throw new IndexOutOfBoundsException("localPosition: " + localPosition + " >=" + sectionObject.length); 230 | } 231 | 232 | return localPosition - 1; 233 | } 234 | 235 | /** 236 | * Returns the section index having item or header with provided 237 | * provider position. 238 | * 239 | * @param position adapter position 240 | * @return The section containing provided adapter position. 241 | */ 242 | public int getAdapterPositionSection(int position) { 243 | if (mSections == null) { 244 | calculateSections(); 245 | } 246 | 247 | if (getItemCount() == 0) { 248 | return NO_POSITION; 249 | } 250 | 251 | if (position < 0) { 252 | throw new IndexOutOfBoundsException("position " + position + " < 0"); 253 | } 254 | 255 | if (position >= getItemCount()) { 256 | throw new IndexOutOfBoundsException("position " + position + " >=" + getItemCount()); 257 | } 258 | 259 | return mSectionIndices[position]; 260 | } 261 | 262 | /** 263 | * Returns the adapter position for given section header. Use 264 | * this only for {@link RecyclerView#scrollToPosition(int)} or similar functions. 265 | * Never directly manipulate adapter items using this position. 266 | * 267 | * @param section section to query 268 | * @return The adapter position. 269 | */ 270 | public int getSectionHeaderPosition(int section) { 271 | return getAdapterPosition(section, 0); 272 | } 273 | 274 | /** 275 | * Returns the adapter position for given section and 276 | * offset. Use this only for {@link RecyclerView#scrollToPosition(int)} 277 | * or similar functions. Never directly manipulate adapter items using this position. 278 | * 279 | * @param section section to query 280 | * @param position item position inside the section 281 | * @return The adapter position. 282 | */ 283 | public int getSectionItemPosition(int section, int position) { 284 | return getAdapterPosition(section, position + 1); 285 | } 286 | 287 | // Overrides 288 | /** 289 | * Returns the total number of sections in the data set held by the adapter. 290 | * 291 | * @return The total number of section in this adapter. 292 | */ 293 | public int getSectionCount() { 294 | return 0; 295 | } 296 | 297 | /** 298 | * Returns the number of items in the section. 299 | * 300 | * @param section section to query 301 | * @return The total number of items in the section. 302 | */ 303 | public int getSectionItemCount(int section) { 304 | return 0; 305 | } 306 | 307 | /** 308 | * Return the view type of the section header for the purposes 309 | * of view recycling. 310 | * 311 | *

The default implementation of this method returns 0, making the assumption of 312 | * a single view type for the headers. Unlike ListView adapters, types need not 313 | * be contiguous. Consider using id resources to uniquely identify item view types. 314 | * 315 | * @param section section to query 316 | * @return integer value identifying the type of the view needed to represent the header in 317 | * section. Type codes need not be contiguous. 318 | */ 319 | public int getSectionHeaderViewType(int section) { 320 | return 0; 321 | } 322 | 323 | /** 324 | * Return the view type of the item at position in section for 325 | * the purposes of view recycling. 326 | * 327 | *

The default implementation of this method returns 0, making the assumption of 328 | * a single view type for the adapter. Unlike ListView adapters, types need not 329 | * be contiguous. Consider using id resources to uniquely identify item view types. 330 | * 331 | * @param section section to query 332 | * @param offset section position to query 333 | * @return integer value identifying the type of the view needed to represent the item at 334 | * position in section. Type codes need not be 335 | * contiguous. 336 | */ 337 | public int getSectionItemViewType(int section, int offset) { 338 | return 0; 339 | } 340 | 341 | /** 342 | * Returns true if header in section is sticky. 343 | * 344 | * @param section section to query 345 | * @return true if section header is sticky. 346 | */ 347 | public boolean isSectionHeaderSticky(int section) { 348 | return true; 349 | } 350 | 351 | /** 352 | * Called when RecyclerView needs a new {@link HeaderViewHolder} of the given type to represent 353 | * a header. 354 | *

355 | * This new HeaderViewHolder should be constructed with a new View that can represent the headers 356 | * of the given type. You can either create a new View manually or inflate it from an XML 357 | * layout file. 358 | *

359 | * The new HeaderViewHolder will be used to display items of the adapter using 360 | * {@link #onBindHeaderViewHolder(HeaderViewHolder, int)}. Since it will be re-used to display 361 | * different items in the data set, it is a good idea to cache references to sub views of 362 | * the View to avoid unnecessary {@link View#findViewById(int)} calls. 363 | * 364 | * @param parent The ViewGroup into which the new View will be added after it is bound to 365 | * an adapter position. 366 | * @param headerType The view type of the new View. 367 | * 368 | * @return A new ViewHolder that holds a View of the given view type. 369 | * @see #getSectionHeaderViewType(int) 370 | * @see #onBindHeaderViewHolder(HeaderViewHolder, int) 371 | */ 372 | public abstract HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent, int headerType); 373 | 374 | /** 375 | * Called when RecyclerView needs a new {@link ItemViewHolder} of the given type to represent 376 | * an item. 377 | *

378 | * This new ViewHolder should be constructed with a new View that can represent the items 379 | * of the given type. You can either create a new View manually or inflate it from an XML 380 | * layout file. 381 | *

382 | * The new ViewHolder will be used to display items of the adapter using 383 | * {@link #onBindItemViewHolder(ItemViewHolder, int, int)}. Since it will be re-used to display 384 | * different items in the data set, it is a good idea to cache references to sub views of 385 | * the View to avoid unnecessary {@link View#findViewById(int)} calls. 386 | * 387 | * @param parent The ViewGroup into which the new View will be added after it is bound to 388 | * an adapter position. 389 | * @param itemType The view type of the new View. 390 | * 391 | * @return A new ViewHolder that holds a View of the given view type. 392 | * @see #getSectionItemViewType(int, int) 393 | * @see #onBindItemViewHolder(ItemViewHolder, int, int) 394 | */ 395 | public abstract ItemViewHolder onCreateItemViewHolder(ViewGroup parent, int itemType); 396 | 397 | /** 398 | * Called by RecyclerView to display the data at the specified position. This method should 399 | * update the contents of the {@link HeaderViewHolder#itemView} to reflect the header at the given 400 | * position. 401 | *

402 | * Note that unlike {@link android.widget.ListView}, RecyclerView will not call this method 403 | * again if the position of the header changes in the data set unless the header itself is 404 | * invalidated or the new position cannot be determined. For this reason, you should only 405 | * use the section parameter while acquiring the 406 | * related header data inside this method and should not keep a copy of it. If you need the 407 | * position of a header later on (e.g. in a click listener), use 408 | * {@link HeaderViewHolder#getAdapterPosition()} which will have the updated adapter 409 | * position. Then you can use {@link #getAdapterPositionSection(int)} to get section index. 410 | * 411 | * 412 | * @param viewHolder The ViewHolder which should be updated to represent the contents of the 413 | * header at the given position in the data set. 414 | * @param section The index of the section. 415 | */ 416 | public abstract void onBindHeaderViewHolder(HeaderViewHolder viewHolder, int section); 417 | 418 | /** 419 | * Called by RecyclerView to display the data at the specified position. This method should 420 | * update the contents of the {@link ItemViewHolder#itemView} to reflect the item at the given 421 | * position. 422 | *

423 | * Note that unlike {@link android.widget.ListView}, RecyclerView will not call this method 424 | * again if the position of the item changes in the data set unless the item itself is 425 | * invalidated or the new position cannot be determined. For this reason, you should only 426 | * use the offset and section parameters while acquiring the 427 | * related data item inside this method and should not keep a copy of it. If you need the 428 | * position of an item later on (e.g. in a click listener), use 429 | * {@link ItemViewHolder#getAdapterPosition()} which will have the updated adapter 430 | * position. Then you can use {@link #getAdapterPositionSection(int)} and 431 | * {@link #getItemSectionOffset(int, int)} 432 | * 433 | * 434 | * @param viewHolder The ViewHolder which should be updated to represent the contents of the 435 | * item at the given position in the data set. 436 | * @param section The index of the section. 437 | * @param offset The position of the item within the section. 438 | */ 439 | public abstract void onBindItemViewHolder(ItemViewHolder viewHolder, int section, int offset); 440 | 441 | // Notify 442 | /** 443 | * Notify any registered observers that the data set has changed. 444 | * 445 | *

There are two different classes of data change events, item changes and structural 446 | * changes. Item changes are when a single item has its data updated but no positional 447 | * changes have occurred. Structural changes are when items are inserted, removed or moved 448 | * within the data set.

449 | * 450 | *

This event does not specify what about the data set has changed, forcing 451 | * any observers to assume that all existing items and structure may no longer be valid. 452 | * LayoutManagers will be forced to fully rebind and relayout all visible views.

453 | * 454 | *

RecyclerView will attempt to synthesize visible structural change events 455 | * for adapters that report that they have {@link #hasStableIds() stable IDs} when 456 | * this method is used. This can help for the purposes of animation and visual 457 | * object persistence but individual item views will still need to be rebound 458 | * and relaid out.

459 | * 460 | *

If you are writing an adapter it will always be more efficient to use the more 461 | * specific change events if you can. Rely on notifyDataSetChanged() 462 | * as a last resort.

463 | * 464 | * @see #notifySectionDataSetChanged(int) 465 | * @see #notifySectionHeaderChanged(int) 466 | * @see #notifySectionItemChanged(int, int) 467 | * @see #notifySectionInserted(int) 468 | * @see #notifySectionItemInserted(int, int) 469 | * @see #notifySectionItemRangeInserted(int, int, int) 470 | * @see #notifySectionRemoved(int) 471 | * @see #notifySectionItemRemoved(int, int) 472 | * @see #notifySectionItemRangeRemoved(int, int, int) 473 | */ 474 | public void notifyAllSectionsDataSetChanged() { 475 | calculateSections(); 476 | notifyDataSetChanged(); 477 | } 478 | 479 | public void notifySectionDataSetChanged(int section) { 480 | calculateSections(); 481 | if (mSections == null) { 482 | notifyAllSectionsDataSetChanged(); 483 | } 484 | else { 485 | final Section sectionObject = mSections.get(section); 486 | notifyItemRangeChanged(sectionObject.position, sectionObject.length); 487 | } 488 | } 489 | 490 | public void notifySectionHeaderChanged(int section) { 491 | calculateSections(); 492 | if (mSections == null) { 493 | notifyAllSectionsDataSetChanged(); 494 | } 495 | else { 496 | final Section sectionObject = mSections.get(section); 497 | notifyItemRangeChanged(sectionObject.position, 1); 498 | } 499 | } 500 | 501 | public void notifySectionItemChanged(int section, int position) { 502 | calculateSections(); 503 | if (mSections == null) { 504 | notifyAllSectionsDataSetChanged(); 505 | } 506 | else { 507 | final Section sectionObject = mSections.get(section); 508 | 509 | if (position >= sectionObject.itemNumber) { 510 | throw new IndexOutOfBoundsException("Invalid index " + position + ", size is " + sectionObject.itemNumber); 511 | } 512 | 513 | notifyItemChanged(sectionObject.position + position + 1); 514 | } 515 | } 516 | 517 | public void notifySectionInserted(int section) { 518 | calculateSections(); 519 | if (mSections == null) { 520 | notifyAllSectionsDataSetChanged(); 521 | } 522 | else { 523 | final Section sectionObject = mSections.get(section); 524 | notifyItemRangeInserted(sectionObject.position, sectionObject.length); 525 | } 526 | } 527 | 528 | public void notifySectionItemInserted(int section, int position) { 529 | calculateSections(); 530 | if (mSections == null) { 531 | notifyAllSectionsDataSetChanged(); 532 | } 533 | else { 534 | final Section sectionObject = mSections.get(section); 535 | 536 | if (position < 0 || position >= sectionObject.itemNumber) { 537 | throw new IndexOutOfBoundsException("Invalid index " + position + ", size is " + sectionObject.itemNumber); 538 | } 539 | 540 | notifyItemInserted(sectionObject.position + position + 1); 541 | } 542 | } 543 | 544 | public void notifySectionItemRangeInserted(int section, int position, int count) { 545 | calculateSections(); 546 | if (mSections == null) { 547 | notifyAllSectionsDataSetChanged(); 548 | } 549 | else { 550 | final Section sectionObject = mSections.get(section); 551 | 552 | if (position < 0 || position >= sectionObject.itemNumber) { 553 | throw new IndexOutOfBoundsException("Invalid index " + position + ", size is " + sectionObject.itemNumber); 554 | } 555 | if (position + count > sectionObject.itemNumber) { 556 | throw new IndexOutOfBoundsException("Invalid index " + (position + count) + ", size is " + sectionObject.itemNumber); 557 | } 558 | 559 | notifyItemRangeInserted(sectionObject.position + position + 1, count); 560 | } 561 | } 562 | 563 | public void notifySectionRemoved(int section) { 564 | if (mSections == null) { 565 | calculateSections(); 566 | notifyAllSectionsDataSetChanged(); 567 | } 568 | else { 569 | final Section sectionObject = mSections.get(section); 570 | calculateSections(); 571 | notifyItemRangeRemoved(sectionObject.position, sectionObject.length); 572 | } 573 | } 574 | 575 | public void notifySectionItemRemoved(int section, int position) { 576 | if (mSections == null) { 577 | calculateSections(); 578 | notifyAllSectionsDataSetChanged(); 579 | } 580 | else { 581 | final Section sectionObject = mSections.get(section); 582 | 583 | if (position < 0 || position >= sectionObject.itemNumber) { 584 | throw new IndexOutOfBoundsException("Invalid index " + position + ", size is " + sectionObject.itemNumber); 585 | } 586 | 587 | calculateSections(); 588 | notifyItemRemoved(sectionObject.position + position + 1); 589 | } 590 | } 591 | 592 | public void notifySectionItemRangeRemoved(int section, int position, int count) { 593 | if (mSections == null) { 594 | calculateSections(); 595 | notifyAllSectionsDataSetChanged(); 596 | } 597 | else { 598 | final Section sectionObject = mSections.get(section); 599 | 600 | if (position < 0 || position >= sectionObject.itemNumber) { 601 | throw new IndexOutOfBoundsException("Invalid index " + position + ", size is " + sectionObject.itemNumber); 602 | } 603 | if (position + count > sectionObject.itemNumber) { 604 | throw new IndexOutOfBoundsException("Invalid index " + (position + count) + ", size is " + sectionObject.itemNumber); 605 | } 606 | 607 | calculateSections(); 608 | notifyItemRangeRemoved(sectionObject.position + position + 1, count); 609 | } 610 | } 611 | } 612 | -------------------------------------------------------------------------------- /stickyheadergrid/src/main/java/com/codewaves/stickyheadergrid/StickyHeaderGridLayoutManager.java: -------------------------------------------------------------------------------- 1 | package com.codewaves.stickyheadergrid; 2 | 3 | import android.content.Context; 4 | import android.graphics.PointF; 5 | import android.os.Parcel; 6 | import android.os.Parcelable; 7 | import android.support.v7.widget.LinearSmoothScroller; 8 | import android.support.v7.widget.RecyclerView; 9 | import android.util.AttributeSet; 10 | import android.util.Log; 11 | import android.view.View; 12 | import android.view.ViewGroup; 13 | 14 | import java.util.ArrayList; 15 | import java.util.Arrays; 16 | 17 | import static android.support.v7.widget.RecyclerView.NO_POSITION; 18 | import static com.codewaves.stickyheadergrid.StickyHeaderGridAdapter.TYPE_HEADER; 19 | import static com.codewaves.stickyheadergrid.StickyHeaderGridAdapter.TYPE_ITEM; 20 | 21 | /** 22 | * Created by Sergej Kravcenko on 4/24/2017. 23 | * Copyright (c) 2017 Sergej Kravcenko 24 | */ 25 | 26 | @SuppressWarnings({"unused", "WeakerAccess"}) 27 | public class StickyHeaderGridLayoutManager extends RecyclerView.LayoutManager implements RecyclerView.SmoothScroller.ScrollVectorProvider { 28 | public static final String TAG = "StickyLayoutManager"; 29 | 30 | private static final int DEFAULT_ROW_COUNT = 16; 31 | 32 | private int mSpanCount; 33 | private SpanSizeLookup mSpanSizeLookup = new DefaultSpanSizeLookup(); 34 | 35 | private StickyHeaderGridAdapter mAdapter; 36 | 37 | private int mHeadersStartPosition; 38 | 39 | private View mFloatingHeaderView; 40 | private int mFloatingHeaderPosition; 41 | private int mStickOffset; 42 | private int mAverageHeaderHeight; 43 | private int mHeaderOverlapMargin; 44 | 45 | private HeaderStateChangeListener mHeaderStateListener; 46 | private int mStickyHeaderSection = NO_POSITION; 47 | private View mStickyHeaderView; 48 | private HeaderState mStickyHeadeState; 49 | 50 | private View mFillViewSet[]; 51 | 52 | private SavedState mPendingSavedState; 53 | private int mPendingScrollPosition = NO_POSITION; 54 | private int mPendingScrollPositionOffset; 55 | private AnchorPosition mAnchor = new AnchorPosition(); 56 | 57 | private final FillResult mFillResult = new FillResult(); 58 | private ArrayList mLayoutRows = new ArrayList<>(DEFAULT_ROW_COUNT); 59 | 60 | public enum HeaderState { 61 | NORMAL, 62 | STICKY, 63 | PUSHED 64 | } 65 | 66 | /** 67 | * The interface to be implemented by listeners to header events from this 68 | * LayoutManager. 69 | */ 70 | public interface HeaderStateChangeListener { 71 | /** 72 | * Called when a section header state changes. The position can be HeaderState.NORMAL, 73 | * HeaderState.STICKY, HeaderState.PUSHED. 74 | * 75 | *

76 | *

    77 | *
  • NORMAL - the section header is invisible or has normal position
  • 78 | *
  • STICKY - the section header is sticky at the top of RecyclerView
  • 79 | *
  • PUSHED - the section header is sticky and pushed up by next header
  • 80 | *
0) { 211 | state.mAnchorSection = mAnchor.section; 212 | state.mAnchorItem = mAnchor.item; 213 | state.mAnchorOffset = mAnchor.offset; 214 | } 215 | else { 216 | state.invalidateAnchor(); 217 | } 218 | 219 | return state; 220 | } 221 | 222 | @Override 223 | public void onRestoreInstanceState(Parcelable state) { 224 | if (state instanceof SavedState) { 225 | mPendingSavedState = (SavedState) state; 226 | requestLayout(); 227 | } 228 | else { 229 | Log.d(TAG, "invalid saved state class"); 230 | } 231 | } 232 | 233 | @Override 234 | public boolean checkLayoutParams(RecyclerView.LayoutParams lp) { 235 | return lp instanceof LayoutParams; 236 | } 237 | 238 | @Override 239 | public boolean canScrollVertically() { 240 | return true; 241 | } 242 | 243 | /** 244 | *

Scroll the RecyclerView to make the position visible.

245 | * 246 | *

RecyclerView will scroll the minimum amount that is necessary to make the 247 | * target position visible. 248 | * 249 | *

Note that scroll position change will not be reflected until the next layout call.

250 | * 251 | * @param position Scroll to this adapter position 252 | */ 253 | @Override 254 | public void scrollToPosition(int position) { 255 | if (position < 0 || position > getItemCount()) { 256 | throw new IndexOutOfBoundsException("adapter position out of range"); 257 | } 258 | 259 | mPendingScrollPosition = position; 260 | mPendingScrollPositionOffset = 0; 261 | if (mPendingSavedState != null) { 262 | mPendingSavedState.invalidateAnchor(); 263 | } 264 | requestLayout(); 265 | } 266 | 267 | private int getExtraLayoutSpace(RecyclerView.State state) { 268 | if (state.hasTargetScrollPosition()) { 269 | return getHeight(); 270 | } 271 | else { 272 | return 0; 273 | } 274 | } 275 | 276 | @Override 277 | public void smoothScrollToPosition(final RecyclerView recyclerView, RecyclerView.State state, int position) { 278 | final LinearSmoothScroller linearSmoothScroller = new LinearSmoothScroller(recyclerView.getContext()) { 279 | @Override 280 | public int calculateDyToMakeVisible(View view, int snapPreference) { 281 | final RecyclerView.LayoutManager layoutManager = getLayoutManager(); 282 | if (layoutManager == null || !layoutManager.canScrollVertically()) { 283 | return 0; 284 | } 285 | 286 | final int adapterPosition = getPosition(view); 287 | final int topOffset = getPositionSectionHeaderHeight(adapterPosition); 288 | final int top = layoutManager.getDecoratedTop(view); 289 | final int bottom = layoutManager.getDecoratedBottom(view); 290 | final int start = layoutManager.getPaddingTop() + topOffset; 291 | final int end = layoutManager.getHeight() - layoutManager.getPaddingBottom(); 292 | return calculateDtToFit(top, bottom, start, end, snapPreference); 293 | } 294 | }; 295 | linearSmoothScroller.setTargetPosition(position); 296 | startSmoothScroll(linearSmoothScroller); 297 | } 298 | 299 | @Override 300 | public PointF computeScrollVectorForPosition(int targetPosition) { 301 | if (getChildCount() == 0) { 302 | return null; 303 | } 304 | 305 | final LayoutRow firstRow = getFirstVisibleRow(); 306 | if (firstRow == null) { 307 | return null; 308 | } 309 | 310 | return new PointF(0, targetPosition - firstRow.adapterPosition); 311 | } 312 | 313 | private int getAdapterPositionFromAnchor(AnchorPosition anchor) { 314 | if (anchor.section < 0 || anchor.section >= mAdapter.getSectionCount()) { 315 | anchor.reset(); 316 | return NO_POSITION; 317 | } 318 | else if (anchor.item < 0 || anchor.item >= mAdapter.getSectionItemCount(anchor.section)) { 319 | anchor.offset = 0; 320 | return mAdapter.getSectionHeaderPosition(anchor.section); 321 | } 322 | return mAdapter.getSectionItemPosition(anchor.section, anchor.item); 323 | } 324 | 325 | private int getAdapterPositionChecked(int section, int offset) { 326 | if (section < 0 || section >= mAdapter.getSectionCount()) { 327 | return NO_POSITION; 328 | } 329 | else if (offset < 0 || offset >= mAdapter.getSectionItemCount(section)) { 330 | return mAdapter.getSectionHeaderPosition(section); 331 | } 332 | return mAdapter.getSectionItemPosition(section, offset); 333 | } 334 | 335 | @Override 336 | public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 337 | if (mAdapter == null || state.getItemCount() == 0) { 338 | removeAndRecycleAllViews(recycler); 339 | clearState(); 340 | return; 341 | } 342 | 343 | int pendingAdapterPosition; 344 | int pendingAdapterOffset; 345 | if (mPendingScrollPosition >= 0) { 346 | pendingAdapterPosition = mPendingScrollPosition; 347 | pendingAdapterOffset = mPendingScrollPositionOffset; 348 | } 349 | else if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) { 350 | pendingAdapterPosition = getAdapterPositionChecked(mPendingSavedState.mAnchorSection, mPendingSavedState.mAnchorItem); 351 | pendingAdapterOffset = mPendingSavedState.mAnchorOffset; 352 | mPendingSavedState = null; 353 | } 354 | else { 355 | pendingAdapterPosition = getAdapterPositionFromAnchor(mAnchor); 356 | pendingAdapterOffset = mAnchor.offset; 357 | } 358 | 359 | if (pendingAdapterPosition < 0 || pendingAdapterPosition >= state.getItemCount()) { 360 | pendingAdapterPosition = 0; 361 | pendingAdapterOffset = 0; 362 | mPendingScrollPosition = NO_POSITION; 363 | } 364 | 365 | if (pendingAdapterOffset > 0) { 366 | pendingAdapterOffset = 0; 367 | } 368 | 369 | detachAndScrapAttachedViews(recycler); 370 | clearState(); 371 | 372 | // Make sure mFirstViewPosition is the start of the row 373 | pendingAdapterPosition = findFirstRowItem(pendingAdapterPosition); 374 | 375 | int left = getPaddingLeft(); 376 | int right = getWidth() - getPaddingRight(); 377 | final int recyclerBottom = getHeight() - getPaddingBottom(); 378 | int totalHeight = 0; 379 | 380 | int adapterPosition = pendingAdapterPosition; 381 | int top = getPaddingTop() + pendingAdapterOffset; 382 | while (true) { 383 | if (adapterPosition >= state.getItemCount()) { 384 | break; 385 | } 386 | 387 | int bottom; 388 | final int viewType = mAdapter.getItemViewInternalType(adapterPosition); 389 | if (viewType == TYPE_HEADER) { 390 | final View v = recycler.getViewForPosition(adapterPosition); 391 | addView(v); 392 | measureChildWithMargins(v, 0, 0); 393 | 394 | int height = getDecoratedMeasuredHeight(v); 395 | final int margin = height >= mHeaderOverlapMargin ? mHeaderOverlapMargin : height; 396 | bottom = top + height; 397 | layoutDecorated(v, left, top, right, bottom); 398 | 399 | bottom -= margin; 400 | height -= margin; 401 | mLayoutRows.add(new LayoutRow(v, adapterPosition, 1, top, bottom)); 402 | adapterPosition++; 403 | mAverageHeaderHeight = height; 404 | } 405 | else { 406 | final FillResult result = fillBottomRow(recycler, state, adapterPosition, top); 407 | bottom = top + result.height; 408 | mLayoutRows.add(new LayoutRow(result.adapterPosition, result.length, top, bottom)); 409 | adapterPosition += result.length; 410 | } 411 | top = bottom; 412 | 413 | if (bottom >= recyclerBottom + getExtraLayoutSpace(state)) { 414 | break; 415 | } 416 | } 417 | 418 | if (getBottomRow().bottom < recyclerBottom) { 419 | scrollVerticallyBy(getBottomRow().bottom - recyclerBottom, recycler, state); 420 | } 421 | else { 422 | clearViewsAndStickHeaders(recycler, state, false); 423 | } 424 | 425 | // If layout was caused by the pending scroll, adjust top item position and move it under sticky header 426 | if (mPendingScrollPosition >= 0) { 427 | mPendingScrollPosition = NO_POSITION; 428 | 429 | final int topOffset = getPositionSectionHeaderHeight(pendingAdapterPosition); 430 | if (topOffset != 0) { 431 | scrollVerticallyBy(-topOffset, recycler, state); 432 | } 433 | } 434 | } 435 | 436 | @Override 437 | public void onLayoutCompleted(RecyclerView.State state) { 438 | super.onLayoutCompleted(state); 439 | mPendingSavedState = null; 440 | } 441 | 442 | private int getPositionSectionHeaderHeight(int adapterPosition) { 443 | final int section = mAdapter.getAdapterPositionSection(adapterPosition); 444 | if (section >= 0 && mAdapter.isSectionHeaderSticky(section)) { 445 | final int offset = mAdapter.getItemSectionOffset(section, adapterPosition); 446 | if (offset >= 0) { 447 | final int headerAdapterPosition = mAdapter.getSectionHeaderPosition(section); 448 | if (mFloatingHeaderView != null && headerAdapterPosition == mFloatingHeaderPosition) { 449 | return Math.max(0, getDecoratedMeasuredHeight(mFloatingHeaderView) - mHeaderOverlapMargin); 450 | } 451 | else { 452 | final LayoutRow header = getHeaderRow(headerAdapterPosition); 453 | if (header != null) { 454 | return header.getHeight(); 455 | } 456 | else { 457 | // Fall back to cached header size, can be incorrect 458 | return mAverageHeaderHeight; 459 | } 460 | } 461 | } 462 | } 463 | 464 | return 0; 465 | } 466 | 467 | private int findFirstRowItem(int adapterPosition) { 468 | final int section = mAdapter.getAdapterPositionSection(adapterPosition); 469 | int sectionPosition = mAdapter.getItemSectionOffset(section, adapterPosition); 470 | while (sectionPosition > 0 && mSpanSizeLookup.getSpanIndex(section, sectionPosition, mSpanCount) != 0) { 471 | sectionPosition--; 472 | adapterPosition--; 473 | } 474 | 475 | return adapterPosition; 476 | } 477 | 478 | private int getSpanWidth(int recyclerWidth, int spanIndex, int spanSize) { 479 | final int spanWidth = recyclerWidth / mSpanCount; 480 | final int spanWidthReminder = recyclerWidth - spanWidth * mSpanCount; 481 | final int widthCorrection = Math.min(Math.max(0, spanWidthReminder - spanIndex), spanSize); 482 | 483 | return spanWidth * spanSize + widthCorrection; 484 | } 485 | 486 | private int getSpanLeft(int recyclerWidth, int spanIndex) { 487 | final int spanWidth = recyclerWidth / mSpanCount; 488 | final int spanWidthReminder = recyclerWidth - spanWidth * mSpanCount; 489 | final int widthCorrection = Math.min(spanWidthReminder, spanIndex); 490 | 491 | return spanWidth * spanIndex + widthCorrection; 492 | } 493 | 494 | private FillResult fillBottomRow(RecyclerView.Recycler recycler, RecyclerView.State state, int position, int top) { 495 | final int recyclerWidth = getWidth() - getPaddingLeft() - getPaddingRight(); 496 | final int section = mAdapter.getAdapterPositionSection(position); 497 | int adapterPosition = position; 498 | int sectionPosition = mAdapter.getItemSectionOffset(section, adapterPosition); 499 | int spanSize = mSpanSizeLookup.getSpanSize(section, sectionPosition); 500 | int spanIndex = mSpanSizeLookup.getSpanIndex(section, sectionPosition, mSpanCount); 501 | int count = 0; 502 | int maxHeight = 0; 503 | 504 | // Create phase 505 | Arrays.fill(mFillViewSet, null); 506 | while (spanIndex + spanSize <= mSpanCount) { 507 | // Create view and fill layout params 508 | final int spanWidth = getSpanWidth(recyclerWidth, spanIndex, spanSize); 509 | final View v = recycler.getViewForPosition(adapterPosition); 510 | final LayoutParams params = (LayoutParams)v.getLayoutParams(); 511 | params.mSpanIndex = spanIndex; 512 | params.mSpanSize = spanSize; 513 | 514 | addView(v, mHeadersStartPosition); 515 | mHeadersStartPosition++; 516 | measureChildWithMargins(v, recyclerWidth - spanWidth, 0); 517 | mFillViewSet[count] = v; 518 | count++; 519 | 520 | final int height = getDecoratedMeasuredHeight(v); 521 | if (maxHeight < height) { 522 | maxHeight = height; 523 | } 524 | 525 | // Check next 526 | adapterPosition++; 527 | sectionPosition++; 528 | if (sectionPosition >= mAdapter.getSectionItemCount(section)) { 529 | break; 530 | } 531 | 532 | spanIndex += spanSize; 533 | spanSize = mSpanSizeLookup.getSpanSize(section, sectionPosition); 534 | } 535 | 536 | // Layout phase 537 | int left = getPaddingLeft(); 538 | for (int i = 0; i < count; ++i) { 539 | final View v = mFillViewSet[i]; 540 | final int height = getDecoratedMeasuredHeight(v); 541 | final int width = getDecoratedMeasuredWidth(v); 542 | layoutDecorated(v, left, top, left + width, top + height); 543 | left += width; 544 | } 545 | 546 | mFillResult.edgeView = mFillViewSet[count - 1]; 547 | mFillResult.adapterPosition = position; 548 | mFillResult.length = count; 549 | mFillResult.height = maxHeight; 550 | 551 | return mFillResult; 552 | } 553 | 554 | private FillResult fillTopRow(RecyclerView.Recycler recycler, RecyclerView.State state, int position, int top) { 555 | final int recyclerWidth = getWidth() - getPaddingLeft() - getPaddingRight(); 556 | final int section = mAdapter.getAdapterPositionSection(position); 557 | int adapterPosition = position; 558 | int sectionPosition = mAdapter.getItemSectionOffset(section, adapterPosition); 559 | int spanSize = mSpanSizeLookup.getSpanSize(section, sectionPosition); 560 | int spanIndex = mSpanSizeLookup.getSpanIndex(section, sectionPosition, mSpanCount); 561 | int count = 0; 562 | int maxHeight = 0; 563 | 564 | Arrays.fill(mFillViewSet, null); 565 | while (spanIndex >= 0) { 566 | // Create view and fill layout params 567 | final int spanWidth = getSpanWidth(recyclerWidth, spanIndex, spanSize); 568 | final View v = recycler.getViewForPosition(adapterPosition); 569 | final LayoutParams params = (LayoutParams)v.getLayoutParams(); 570 | params.mSpanIndex = spanIndex; 571 | params.mSpanSize = spanSize; 572 | 573 | addView(v, 0); 574 | mHeadersStartPosition++; 575 | measureChildWithMargins(v, recyclerWidth - spanWidth, 0); 576 | mFillViewSet[count] = v; 577 | count++; 578 | 579 | final int height = getDecoratedMeasuredHeight(v); 580 | if (maxHeight < height) { 581 | maxHeight = height; 582 | } 583 | 584 | // Check next 585 | adapterPosition--; 586 | sectionPosition--; 587 | if (sectionPosition < 0) { 588 | break; 589 | } 590 | 591 | spanSize = mSpanSizeLookup.getSpanSize(section, sectionPosition); 592 | spanIndex -= spanSize; 593 | } 594 | 595 | // Layout phase 596 | int left = getPaddingLeft(); 597 | for (int i = count - 1; i >= 0; --i) { 598 | final View v = mFillViewSet[i]; 599 | final int height = getDecoratedMeasuredHeight(v); 600 | final int width = getDecoratedMeasuredWidth(v); 601 | layoutDecorated(v, left, top - maxHeight, left + width, top - (maxHeight - height)); 602 | left += width; 603 | } 604 | 605 | mFillResult.edgeView = mFillViewSet[count - 1]; 606 | mFillResult.adapterPosition = adapterPosition + 1; 607 | mFillResult.length = count; 608 | mFillResult.height = maxHeight; 609 | 610 | return mFillResult; 611 | } 612 | 613 | private void clearHiddenRows(RecyclerView.Recycler recycler, RecyclerView.State state, boolean top) { 614 | if (mLayoutRows.size() <= 0) { 615 | return; 616 | } 617 | 618 | final int recyclerTop = getPaddingTop(); 619 | final int recyclerBottom = getHeight() - getPaddingBottom(); 620 | 621 | if (top) { 622 | LayoutRow row = getTopRow(); 623 | while (row.bottom < recyclerTop - getExtraLayoutSpace(state) || row.top > recyclerBottom) { 624 | if (row.header) { 625 | removeAndRecycleViewAt(mHeadersStartPosition + (mFloatingHeaderView != null ? 1 : 0), recycler); 626 | } 627 | else { 628 | for (int i = 0; i < row.length; ++i) { 629 | removeAndRecycleViewAt(0, recycler); 630 | mHeadersStartPosition--; 631 | } 632 | } 633 | mLayoutRows.remove(0); 634 | row = getTopRow(); 635 | } 636 | } 637 | else { 638 | LayoutRow row = getBottomRow(); 639 | while (row.bottom < recyclerTop || row.top > recyclerBottom + getExtraLayoutSpace(state)) { 640 | if (row.header) { 641 | removeAndRecycleViewAt(getChildCount() - 1, recycler); 642 | } 643 | else { 644 | for (int i = 0; i < row.length; ++i) { 645 | removeAndRecycleViewAt(mHeadersStartPosition - 1, recycler); 646 | mHeadersStartPosition--; 647 | } 648 | } 649 | mLayoutRows.remove(mLayoutRows.size() - 1); 650 | row = getBottomRow(); 651 | } 652 | } 653 | } 654 | 655 | private void clearViewsAndStickHeaders(RecyclerView.Recycler recycler, RecyclerView.State state, boolean top) { 656 | clearHiddenRows(recycler, state, top); 657 | if (getChildCount() > 0) { 658 | stickTopHeader(recycler); 659 | } 660 | updateTopPosition(); 661 | } 662 | 663 | private LayoutRow getBottomRow() { 664 | return mLayoutRows.get(mLayoutRows.size() - 1); 665 | } 666 | 667 | private LayoutRow getTopRow() { 668 | return mLayoutRows.get(0); 669 | } 670 | 671 | private void offsetRowsVertical(int offset) { 672 | for (LayoutRow row : mLayoutRows) { 673 | row.top += offset; 674 | row.bottom += offset; 675 | } 676 | offsetChildrenVertical(offset); 677 | } 678 | 679 | private void addRow(RecyclerView.Recycler recycler, RecyclerView.State state, boolean isTop, int adapterPosition, int top) { 680 | final int left = getPaddingLeft(); 681 | final int right = getWidth() - getPaddingRight(); 682 | 683 | // Reattach floating header if needed 684 | if (isTop && mFloatingHeaderView != null && adapterPosition == mFloatingHeaderPosition) { 685 | removeFloatingHeader(recycler); 686 | } 687 | 688 | final int viewType = mAdapter.getItemViewInternalType(adapterPosition); 689 | if (viewType == TYPE_HEADER) { 690 | final View v = recycler.getViewForPosition(adapterPosition); 691 | if (isTop) { 692 | addView(v, mHeadersStartPosition); 693 | } 694 | else { 695 | addView(v); 696 | } 697 | measureChildWithMargins(v, 0, 0); 698 | final int height = getDecoratedMeasuredHeight(v); 699 | final int margin = height >= mHeaderOverlapMargin ? mHeaderOverlapMargin : height; 700 | if (isTop) { 701 | layoutDecorated(v, left, top - height + margin, right, top + margin); 702 | mLayoutRows.add(0, new LayoutRow(v, adapterPosition, 1, top - height + margin, top)); 703 | } 704 | else { 705 | layoutDecorated(v, left, top, right, top + height); 706 | mLayoutRows.add(new LayoutRow(v, adapterPosition, 1, top, top + height - margin)); 707 | } 708 | mAverageHeaderHeight = height - margin; 709 | } 710 | else { 711 | if (isTop) { 712 | final FillResult result = fillTopRow(recycler, state, adapterPosition, top); 713 | mLayoutRows.add(0, new LayoutRow(result.adapterPosition, result.length, top - result.height, top)); 714 | } 715 | else { 716 | final FillResult result = fillBottomRow(recycler, state, adapterPosition, top); 717 | mLayoutRows.add(new LayoutRow(result.adapterPosition, result.length, top, top + result.height)); 718 | } 719 | } 720 | } 721 | 722 | private void addOffScreenRows(RecyclerView.Recycler recycler, RecyclerView.State state, int recyclerTop, int recyclerBottom, boolean bottom) { 723 | if (bottom) { 724 | // Bottom 725 | while (true) { 726 | final LayoutRow bottomRow = getBottomRow(); 727 | final int adapterPosition = bottomRow.adapterPosition + bottomRow.length; 728 | if (bottomRow.bottom >= recyclerBottom + getExtraLayoutSpace(state) || adapterPosition >= state.getItemCount()) { 729 | break; 730 | } 731 | addRow(recycler, state, false, adapterPosition, bottomRow.bottom); 732 | } 733 | } 734 | else { 735 | // Top 736 | while (true) { 737 | final LayoutRow topRow = getTopRow(); 738 | final int adapterPosition = topRow.adapterPosition - 1; 739 | if (topRow.top < recyclerTop - getExtraLayoutSpace(state) || adapterPosition < 0) { 740 | break; 741 | } 742 | addRow(recycler, state, true, adapterPosition, topRow.top); 743 | } 744 | } 745 | } 746 | 747 | @Override 748 | public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { 749 | if (getChildCount() == 0) { 750 | return 0; 751 | } 752 | 753 | int scrolled = 0; 754 | int left = getPaddingLeft(); 755 | int right = getWidth() - getPaddingRight(); 756 | final int recyclerTop = getPaddingTop(); 757 | final int recyclerBottom = getHeight() - getPaddingBottom(); 758 | 759 | // If we have simple header stick, offset it back 760 | final int firstHeader = getFirstVisibleSectionHeader(); 761 | if (firstHeader != NO_POSITION) { 762 | mLayoutRows.get(firstHeader).headerView.offsetTopAndBottom(-mStickOffset); 763 | } 764 | 765 | if (dy >= 0) { 766 | // Up 767 | while (scrolled < dy) { 768 | final LayoutRow bottomRow = getBottomRow(); 769 | final int scrollChunk = -Math.min(Math.max(bottomRow.bottom - recyclerBottom, 0), dy - scrolled); 770 | 771 | offsetRowsVertical(scrollChunk); 772 | scrolled -= scrollChunk; 773 | 774 | final int adapterPosition = bottomRow.adapterPosition + bottomRow.length; 775 | if (scrolled >= dy || adapterPosition >= state.getItemCount()) { 776 | break; 777 | } 778 | 779 | addRow(recycler, state, false, adapterPosition, bottomRow.bottom); 780 | } 781 | } 782 | else { 783 | // Down 784 | while (scrolled > dy) { 785 | final LayoutRow topRow = getTopRow(); 786 | final int scrollChunk = Math.min(Math.max(-topRow.top + recyclerTop, 0), scrolled - dy); 787 | 788 | offsetRowsVertical(scrollChunk); 789 | scrolled -= scrollChunk; 790 | 791 | final int adapterPosition = topRow.adapterPosition - 1; 792 | if (scrolled <= dy || adapterPosition >= state.getItemCount() || adapterPosition < 0) { 793 | break; 794 | } 795 | 796 | addRow(recycler, state, true, adapterPosition, topRow.top); 797 | } 798 | } 799 | 800 | // Fill extra offscreen rows for smooth scroll 801 | if (scrolled == dy) { 802 | addOffScreenRows(recycler, state, recyclerTop, recyclerBottom, dy >= 0); 803 | } 804 | 805 | clearViewsAndStickHeaders(recycler, state, dy >= 0); 806 | return scrolled; 807 | } 808 | 809 | /** 810 | * Returns first visible item excluding headers. 811 | * 812 | * @param visibleTop Whether item top edge should be visible or not 813 | * @return The first visible item adapter position closest to top of the layout. 814 | */ 815 | public int getFirstVisibleItemPosition(boolean visibleTop) { 816 | return getFirstVisiblePosition(TYPE_ITEM, visibleTop); 817 | } 818 | 819 | /** 820 | * Returns last visible item excluding headers. 821 | * 822 | * @return The last visible item adapter position closest to bottom of the layout. 823 | */ 824 | public int getLastVisibleItemPosition() { 825 | return getLastVisiblePosition(TYPE_ITEM); 826 | } 827 | 828 | /** 829 | * Returns first visible header. 830 | * 831 | * @param visibleTop Whether header top edge should be visible or not 832 | * @return The first visible header adapter position closest to top of the layout. 833 | */ 834 | public int getFirstVisibleHeaderPosition(boolean visibleTop) { 835 | return getFirstVisiblePosition(TYPE_HEADER, visibleTop); 836 | } 837 | 838 | /** 839 | * Returns last visible header. 840 | * 841 | * @return The last visible header adapter position closest to bottom of the layout. 842 | */ 843 | public int getLastVisibleHeaderPosition() { 844 | return getLastVisiblePosition(TYPE_HEADER); 845 | } 846 | 847 | private int getFirstVisiblePosition(int type, boolean visibleTop) { 848 | if (type == TYPE_ITEM && mHeadersStartPosition <= 0) { 849 | return NO_POSITION; 850 | } 851 | else if (type == TYPE_HEADER && mHeadersStartPosition >= getChildCount()) { 852 | return NO_POSITION; 853 | } 854 | 855 | int viewFrom = type == TYPE_ITEM ? 0 : mHeadersStartPosition; 856 | int viewTo = type == TYPE_ITEM ? mHeadersStartPosition : getChildCount(); 857 | final int recyclerTop = getPaddingTop(); 858 | for (int i = viewFrom; i < viewTo; ++i) { 859 | final View v = getChildAt(i); 860 | final int adapterPosition = getPosition(v); 861 | final int headerHeight = getPositionSectionHeaderHeight(adapterPosition); 862 | final int top = getDecoratedTop(v); 863 | final int bottom = getDecoratedBottom(v); 864 | 865 | if (visibleTop) { 866 | if (top >= recyclerTop + headerHeight) { 867 | return adapterPosition; 868 | } 869 | } 870 | else { 871 | if (bottom >= recyclerTop + headerHeight) { 872 | return adapterPosition; 873 | } 874 | } 875 | } 876 | 877 | return NO_POSITION; 878 | } 879 | 880 | private int getLastVisiblePosition(int type) { 881 | if (type == TYPE_ITEM && mHeadersStartPosition <= 0) { 882 | return NO_POSITION; 883 | } 884 | else if (type == TYPE_HEADER && mHeadersStartPosition >= getChildCount()) { 885 | return NO_POSITION; 886 | } 887 | 888 | int viewFrom = type == TYPE_ITEM ? mHeadersStartPosition - 1 : getChildCount() - 1; 889 | int viewTo = type == TYPE_ITEM ? 0 : mHeadersStartPosition; 890 | final int recyclerBottom = getHeight() - getPaddingBottom(); 891 | for (int i = viewFrom; i >= viewTo; --i) { 892 | final View v = getChildAt(i); 893 | final int top = getDecoratedTop(v); 894 | 895 | if (top < recyclerBottom) { 896 | return getPosition(v); 897 | } 898 | } 899 | 900 | return NO_POSITION; 901 | } 902 | 903 | private LayoutRow getFirstVisibleRow() { 904 | final int recyclerTop = getPaddingTop(); 905 | for (LayoutRow row : mLayoutRows) { 906 | if (row.bottom > recyclerTop) { 907 | return row; 908 | } 909 | } 910 | return null; 911 | } 912 | 913 | private int getFirstVisibleSectionHeader() { 914 | final int recyclerTop = getPaddingTop(); 915 | 916 | int header = NO_POSITION; 917 | for (int i = 0, n = mLayoutRows.size(); i < n; ++i) { 918 | final LayoutRow row = mLayoutRows.get(i); 919 | if (row.header) { 920 | header = i; 921 | } 922 | if (row.bottom > recyclerTop) { 923 | return header; 924 | } 925 | } 926 | return NO_POSITION; 927 | } 928 | 929 | private LayoutRow getNextVisibleSectionHeader(int headerFrom) { 930 | for (int i = headerFrom + 1, n = mLayoutRows.size(); i < n; ++i) { 931 | final LayoutRow row = mLayoutRows.get(i); 932 | if (row.header) { 933 | return row; 934 | } 935 | } 936 | return null; 937 | } 938 | 939 | private LayoutRow getHeaderRow(int adapterPosition) { 940 | for (int i = 0, n = mLayoutRows.size(); i < n; ++i) { 941 | final LayoutRow row = mLayoutRows.get(i); 942 | if (row.header && row.adapterPosition == adapterPosition) { 943 | return row; 944 | } 945 | } 946 | return null; 947 | } 948 | 949 | private void removeFloatingHeader(RecyclerView.Recycler recycler) { 950 | if (mFloatingHeaderView == null) { 951 | return; 952 | } 953 | 954 | final View view = mFloatingHeaderView; 955 | mFloatingHeaderView = null; 956 | mFloatingHeaderPosition = NO_POSITION; 957 | removeAndRecycleView(view, recycler); 958 | } 959 | 960 | private void onHeaderChanged(int section, View view, HeaderState state, int pushOffset) { 961 | if (mStickyHeaderSection != NO_POSITION && section != mStickyHeaderSection) { 962 | onHeaderUnstick(); 963 | } 964 | 965 | final boolean headerStateChanged = mStickyHeaderSection != section || !mStickyHeadeState.equals(state) || state.equals(HeaderState.PUSHED); 966 | 967 | mStickyHeaderSection = section; 968 | mStickyHeaderView = view; 969 | mStickyHeadeState = state; 970 | 971 | if (headerStateChanged && mHeaderStateListener != null) { 972 | mHeaderStateListener.onHeaderStateChanged(section, view, state, pushOffset); 973 | } 974 | } 975 | 976 | private void onHeaderUnstick() { 977 | if (mStickyHeaderSection != NO_POSITION) { 978 | if (mHeaderStateListener != null) { 979 | mHeaderStateListener.onHeaderStateChanged(mStickyHeaderSection, mStickyHeaderView, HeaderState.NORMAL, 0); 980 | } 981 | mStickyHeaderSection = NO_POSITION; 982 | mStickyHeaderView = null; 983 | mStickyHeadeState = HeaderState.NORMAL; 984 | } 985 | } 986 | 987 | private void stickTopHeader(RecyclerView.Recycler recycler) { 988 | final int firstHeader = getFirstVisibleSectionHeader(); 989 | final int top = getPaddingTop(); 990 | final int left = getPaddingLeft(); 991 | final int right = getWidth() - getPaddingRight(); 992 | 993 | int notifySection = NO_POSITION; 994 | View notifyView = null; 995 | HeaderState notifyState = HeaderState.NORMAL; 996 | int notifyOffset = 0; 997 | 998 | if (firstHeader != NO_POSITION) { 999 | // Top row is header, floating header is not visible, remove 1000 | removeFloatingHeader(recycler); 1001 | 1002 | final LayoutRow firstHeaderRow = mLayoutRows.get(firstHeader); 1003 | final int section = mAdapter.getAdapterPositionSection(firstHeaderRow.adapterPosition); 1004 | if (mAdapter.isSectionHeaderSticky(section)) { 1005 | final LayoutRow nextHeaderRow = getNextVisibleSectionHeader(firstHeader); 1006 | int offset = 0; 1007 | if (nextHeaderRow != null) { 1008 | final int height = firstHeaderRow.getHeight(); 1009 | offset = Math.min(Math.max(top - nextHeaderRow.top, -height) + height, height); 1010 | } 1011 | 1012 | mStickOffset = top - firstHeaderRow.top - offset; 1013 | firstHeaderRow.headerView.offsetTopAndBottom(mStickOffset); 1014 | 1015 | onHeaderChanged(section, firstHeaderRow.headerView, offset == 0 ? HeaderState.STICKY : HeaderState.PUSHED, offset); 1016 | } 1017 | else { 1018 | onHeaderUnstick(); 1019 | mStickOffset = 0; 1020 | } 1021 | } 1022 | else { 1023 | // We don't have first visible sector header in layout, create floating 1024 | final LayoutRow firstVisibleRow = getFirstVisibleRow(); 1025 | if (firstVisibleRow != null) { 1026 | final int section = mAdapter.getAdapterPositionSection(firstVisibleRow.adapterPosition); 1027 | if (mAdapter.isSectionHeaderSticky(section)) { 1028 | final int headerPosition = mAdapter.getSectionHeaderPosition(section); 1029 | if (mFloatingHeaderView == null || mFloatingHeaderPosition != headerPosition) { 1030 | removeFloatingHeader(recycler); 1031 | 1032 | // Create floating header 1033 | final View v = recycler.getViewForPosition(headerPosition); 1034 | addView(v, mHeadersStartPosition); 1035 | measureChildWithMargins(v, 0, 0); 1036 | mFloatingHeaderView = v; 1037 | mFloatingHeaderPosition = headerPosition; 1038 | } 1039 | 1040 | // Push floating header up, if needed 1041 | final int height = getDecoratedMeasuredHeight(mFloatingHeaderView); 1042 | int offset = 0; 1043 | if (getChildCount() - mHeadersStartPosition > 1) { 1044 | final View nextHeader = getChildAt(mHeadersStartPosition + 1); 1045 | final int contentHeight = Math.max(0, height - mHeaderOverlapMargin); 1046 | offset = Math.max(top - getDecoratedTop(nextHeader), -contentHeight) + contentHeight; 1047 | } 1048 | 1049 | layoutDecorated(mFloatingHeaderView, left, top - offset, right, top + height - offset); 1050 | onHeaderChanged(section, mFloatingHeaderView, offset == 0 ? HeaderState.STICKY : HeaderState.PUSHED, offset); 1051 | } 1052 | else { 1053 | onHeaderUnstick(); 1054 | } 1055 | } 1056 | else { 1057 | onHeaderUnstick(); 1058 | } 1059 | } 1060 | } 1061 | 1062 | private void updateTopPosition() { 1063 | if (getChildCount() == 0) { 1064 | mAnchor.reset(); 1065 | } 1066 | 1067 | final LayoutRow firstVisibleRow = getFirstVisibleRow(); 1068 | if (firstVisibleRow != null) { 1069 | mAnchor.section = mAdapter.getAdapterPositionSection(firstVisibleRow.adapterPosition); 1070 | mAnchor.item = mAdapter.getItemSectionOffset(mAnchor.section, firstVisibleRow.adapterPosition); 1071 | mAnchor.offset = Math.min(firstVisibleRow.top - getPaddingTop(), 0); 1072 | } 1073 | } 1074 | 1075 | private int getViewType(View view) { 1076 | return getItemViewType(view) & 0xFF; 1077 | } 1078 | 1079 | private int getViewType(int position) { 1080 | return mAdapter.getItemViewType(position) & 0xFF; 1081 | } 1082 | 1083 | private void clearState() { 1084 | mHeadersStartPosition = 0; 1085 | mStickOffset = 0; 1086 | mFloatingHeaderView = null; 1087 | mFloatingHeaderPosition = -1; 1088 | mAverageHeaderHeight = 0; 1089 | mLayoutRows.clear(); 1090 | 1091 | if (mStickyHeaderSection != NO_POSITION) { 1092 | if (mHeaderStateListener != null) { 1093 | mHeaderStateListener.onHeaderStateChanged(mStickyHeaderSection, mStickyHeaderView, HeaderState.NORMAL, 0); 1094 | } 1095 | mStickyHeaderSection = NO_POSITION; 1096 | mStickyHeaderView = null; 1097 | mStickyHeadeState = HeaderState.NORMAL; 1098 | } 1099 | } 1100 | 1101 | @Override 1102 | public int computeVerticalScrollExtent(RecyclerView.State state) { 1103 | if (mHeadersStartPosition == 0 || state.getItemCount() == 0) { 1104 | return 0; 1105 | } 1106 | 1107 | final View startChild = getChildAt(0); 1108 | final View endChild = getChildAt(mHeadersStartPosition - 1); 1109 | if (startChild == null || endChild == null) { 1110 | return 0; 1111 | } 1112 | 1113 | return Math.abs(getPosition(startChild) - getPosition(endChild)) + 1; 1114 | } 1115 | 1116 | @Override 1117 | public int computeVerticalScrollOffset(RecyclerView.State state) { 1118 | if (mHeadersStartPosition == 0 || state.getItemCount() == 0) { 1119 | return 0; 1120 | } 1121 | 1122 | final View startChild = getChildAt(0); 1123 | final View endChild = getChildAt(mHeadersStartPosition - 1); 1124 | if (startChild == null || endChild == null) { 1125 | return 0; 1126 | } 1127 | 1128 | final int recyclerTop = getPaddingTop(); 1129 | final LayoutRow topRow = getTopRow(); 1130 | final int scrollChunk = Math.max(-topRow.top + recyclerTop, 0); 1131 | if (scrollChunk == 0) { 1132 | return 0; 1133 | } 1134 | 1135 | final int minPosition = Math.min(getPosition(startChild), getPosition(endChild)); 1136 | final int maxPosition = Math.max(getPosition(startChild), getPosition(endChild)); 1137 | return Math.max(0, minPosition); 1138 | } 1139 | 1140 | @Override 1141 | public int computeVerticalScrollRange(RecyclerView.State state) { 1142 | if (mHeadersStartPosition == 0 || state.getItemCount() == 0) { 1143 | return 0; 1144 | } 1145 | 1146 | final View startChild = getChildAt(0); 1147 | final View endChild = getChildAt(mHeadersStartPosition - 1); 1148 | if (startChild == null || endChild == null) { 1149 | return 0; 1150 | } 1151 | 1152 | return state.getItemCount(); 1153 | } 1154 | 1155 | public static class LayoutParams extends RecyclerView.LayoutParams { 1156 | public static final int INVALID_SPAN_ID = -1; 1157 | 1158 | private int mSpanIndex = INVALID_SPAN_ID; 1159 | private int mSpanSize = 0; 1160 | 1161 | public LayoutParams(Context c, AttributeSet attrs) { 1162 | super(c, attrs); 1163 | } 1164 | 1165 | public LayoutParams(int width, int height) { 1166 | super(width, height); 1167 | } 1168 | 1169 | public LayoutParams(ViewGroup.MarginLayoutParams source) { 1170 | super(source); 1171 | } 1172 | 1173 | public LayoutParams(ViewGroup.LayoutParams source) { 1174 | super(source); 1175 | } 1176 | 1177 | public LayoutParams(RecyclerView.LayoutParams source) { 1178 | super(source); 1179 | } 1180 | 1181 | public int getSpanIndex() { 1182 | return mSpanIndex; 1183 | } 1184 | 1185 | public int getSpanSize() { 1186 | return mSpanSize; 1187 | } 1188 | } 1189 | 1190 | public static final class DefaultSpanSizeLookup extends SpanSizeLookup { 1191 | @Override 1192 | public int getSpanSize(int section, int position) { 1193 | return 1; 1194 | } 1195 | 1196 | @Override 1197 | public int getSpanIndex(int section, int position, int spanCount) { 1198 | return position % spanCount; 1199 | } 1200 | } 1201 | 1202 | /** 1203 | * An interface to provide the number of spans each item occupies. 1204 | *

1205 | * Default implementation sets each item to occupy exactly 1 span. 1206 | * 1207 | * @see StickyHeaderGridLayoutManager#setSpanSizeLookup(StickyHeaderGridLayoutManager.SpanSizeLookup) 1208 | */ 1209 | public static abstract class SpanSizeLookup { 1210 | /** 1211 | * Returns the number of span occupied by the item in section at position. 1212 | * 1213 | * @param section The adapter section of the item 1214 | * @param position The adapter position of the item in section 1215 | * @return The number of spans occupied by the item at the provided section and position 1216 | */ 1217 | abstract public int getSpanSize(int section, int position); 1218 | 1219 | /** 1220 | * Returns the final span index of the provided position. 1221 | * 1222 | *

1223 | * If you override this method, you need to make sure it is consistent with 1224 | * {@link #getSpanSize(int, int)}. StickyHeaderGridLayoutManager does not call this method for 1225 | * each item. It is called only for the reference item and rest of the items 1226 | * are assigned to spans based on the reference item. For example, you cannot assign a 1227 | * position to span 2 while span 1 is empty. 1228 | *

1229 | * 1230 | * @param section The adapter section of the item 1231 | * @param position The adapter position of the item in section 1232 | * @param spanCount The total number of spans in the grid 1233 | * @return The final span position of the item. Should be between 0 (inclusive) and 1234 | * spanCount(exclusive) 1235 | */ 1236 | public int getSpanIndex(int section, int position, int spanCount) { 1237 | // TODO: cache them? 1238 | final int positionSpanSize = getSpanSize(section, position); 1239 | if (positionSpanSize >= spanCount) { 1240 | return 0; 1241 | } 1242 | 1243 | int spanIndex = 0; 1244 | for (int i = 0; i < position; ++i) { 1245 | final int spanSize = getSpanSize(section, i); 1246 | spanIndex += spanSize; 1247 | 1248 | if (spanIndex == spanCount) { 1249 | spanIndex = 0; 1250 | } 1251 | else if (spanIndex > spanCount) { 1252 | spanIndex = spanSize; 1253 | } 1254 | } 1255 | 1256 | if (spanIndex + positionSpanSize <= spanCount) { 1257 | return spanIndex; 1258 | } 1259 | 1260 | return 0; 1261 | } 1262 | } 1263 | 1264 | public static class SavedState implements Parcelable { 1265 | private int mAnchorSection; 1266 | private int mAnchorItem; 1267 | private int mAnchorOffset; 1268 | 1269 | public SavedState() { 1270 | 1271 | } 1272 | 1273 | SavedState(Parcel in) { 1274 | mAnchorSection = in.readInt(); 1275 | mAnchorItem = in.readInt(); 1276 | mAnchorOffset = in.readInt(); 1277 | } 1278 | 1279 | public SavedState(SavedState other) { 1280 | mAnchorSection = other.mAnchorSection; 1281 | mAnchorItem = other.mAnchorItem; 1282 | mAnchorOffset = other.mAnchorOffset; 1283 | } 1284 | 1285 | boolean hasValidAnchor() { 1286 | return mAnchorSection >= 0; 1287 | } 1288 | 1289 | void invalidateAnchor() { 1290 | mAnchorSection = NO_POSITION; 1291 | } 1292 | 1293 | @Override 1294 | public int describeContents() { 1295 | return 0; 1296 | } 1297 | 1298 | @Override 1299 | public void writeToParcel(Parcel dest, int flags) { 1300 | dest.writeInt(mAnchorSection); 1301 | dest.writeInt(mAnchorItem); 1302 | dest.writeInt(mAnchorOffset); 1303 | } 1304 | 1305 | public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { 1306 | @Override 1307 | public SavedState createFromParcel(Parcel in) { 1308 | return new SavedState(in); 1309 | } 1310 | 1311 | @Override 1312 | public SavedState[] newArray(int size) { 1313 | return new SavedState[size]; 1314 | } 1315 | }; 1316 | } 1317 | 1318 | private static class LayoutRow { 1319 | private boolean header; 1320 | private View headerView; 1321 | private int adapterPosition; 1322 | private int length; 1323 | private int top; 1324 | private int bottom; 1325 | 1326 | public LayoutRow(int adapterPosition, int length, int top, int bottom) { 1327 | this.header = false; 1328 | this.headerView = null; 1329 | this.adapterPosition = adapterPosition; 1330 | this.length = length; 1331 | this.top = top; 1332 | this.bottom = bottom; 1333 | } 1334 | 1335 | public LayoutRow(View headerView, int adapterPosition, int length, int top, int bottom) { 1336 | this.header = true; 1337 | this.headerView = headerView; 1338 | this.adapterPosition = adapterPosition; 1339 | this.length = length; 1340 | this.top = top; 1341 | this.bottom = bottom; 1342 | } 1343 | 1344 | int getHeight() { 1345 | return bottom - top; 1346 | } 1347 | } 1348 | 1349 | private static class FillResult { 1350 | private View edgeView; 1351 | private int adapterPosition; 1352 | private int length; 1353 | private int height; 1354 | } 1355 | 1356 | private static class AnchorPosition { 1357 | private int section; 1358 | private int item; 1359 | private int offset; 1360 | 1361 | public AnchorPosition() { 1362 | reset(); 1363 | } 1364 | 1365 | public void reset() { 1366 | section = NO_POSITION; 1367 | item = 0; 1368 | offset = 0; 1369 | } 1370 | } 1371 | } 1372 | -------------------------------------------------------------------------------- /stickyheadergrid/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | stickyheadergrid 3 | 4 | --------------------------------------------------------------------------------