├── .gitignore ├── CHANGELOG.md ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── library ├── build.gradle ├── gradle.properties └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── etsy │ │ └── android │ │ └── grid │ │ ├── ClassLoaderSavedState.java │ │ ├── ExtendableListView.java │ │ ├── HeaderViewListAdapter.java │ │ ├── StaggeredGridView.java │ │ └── util │ │ ├── DynamicHeightImageView.java │ │ └── DynamicHeightTextView.java │ └── res │ └── values │ └── attrs.xml ├── sample ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── etsy │ │ └── android │ │ └── sample │ │ ├── ListViewActivity.java │ │ ├── MainActivity.java │ │ ├── SampleAdapter.java │ │ ├── SampleData.java │ │ ├── StaggeredGridActivity.java │ │ ├── StaggeredGridActivityFragment.java │ │ └── StaggeredGridEmptyViewActivity.java │ └── res │ ├── drawable-hdpi │ └── ic_launcher.png │ ├── drawable-mdpi │ └── ic_launcher.png │ ├── drawable-xhdpi │ └── ic_launcher.png │ ├── drawable │ └── list_item_selector.xml │ ├── layout │ ├── activity_list_view.xml │ ├── activity_main.xml │ ├── activity_sgv.xml │ ├── activity_sgv_empy_view.xml │ ├── list_item_header_footer.xml │ └── list_item_sample.xml │ ├── menu │ ├── activity_sgv_empty_view.xml │ └── menu_sgv_dynamic.xml │ ├── values-land │ └── integers.xml │ ├── values-v11 │ └── styles.xml │ ├── values-v14 │ └── styles.xml │ └── values │ ├── colors.xml │ ├── integers.xml │ ├── strings.xml │ └── styles.xml └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | .metadata/ 2 | 3 | # built application files 4 | *.apk 5 | *.ap_ 6 | 7 | # files for the dex VM 8 | *.dex 9 | 10 | # Java class files 11 | *.class 12 | 13 | #DStore 14 | .DS_Store 15 | 16 | # generated files 17 | bin 18 | gen 19 | tmp/ 20 | junit/ 21 | 22 | # Local configuration file (sdk path, etc) 23 | local.properties 24 | 25 | # Local Eclipse settings files 26 | *.settings 27 | 28 | # Android Gradle build folders 29 | build/ 30 | .gradle 31 | 32 | #IntelliJ IDEA 33 | .idea 34 | *.ipr 35 | *.iws 36 | *.iml 37 | out 38 | 39 | #Plugins and errata 40 | mirror/ 41 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Change Log 2 | =============================================================================== 3 | 4 | Version 1.0.5 (2014-04-24) 5 | ---------------------------- 6 | 7 | * Fixed OnScrollListener issue after updating contents #100 8 | * Fixed LayoutParams casting issue when recycling state #102 9 | * Fixed the onClick item id #86 10 | * Fixed bug that the default case on Touch event #67 11 | * Fixed for "NPE onSizeChanged" #41 PR #91 12 | * Added guard against issue #45 13 | * Fixed setAdapter bug #86 #85 14 | * Using newest build tools and libraries - #78 #96 15 | 16 | Version 1.0.4 (2014-02-22) 17 | ---------------------------- 18 | 19 | * Added column_count attribute #32 20 | * Added setColumnCount() methods to support dynamically changing columns #26 21 | * Added empty view support to ExtendableListView #33 22 | * Fixed grid_paddingTop and paddingTop being ignored #20 23 | * Fixed ArrayIndexOutOfBoundsException fix #52 24 | * Fixed Unintentional onClick event on scroll down #42 25 | * Fixed onScrollStateChanged never get called #18 26 | * Fixed notifyDataSetChanged sync bug when mFirstPosition = 0 #22 27 | 28 | Version 1.0.3 (2014-01-06) 29 | ---------------------------- 30 | 31 | * Bug fixes #19 & #27 32 | 33 | Version 1.0.2 (2014-01-02) 34 | ---------------------------- 35 | 36 | * Reverted targetSdkVersion for backwards compatibility 37 | 38 | Version 1.0.1 (2013-12-31) 39 | ---------------------------- 40 | 41 | * Added - initial support for OnItemClickListener #14 42 | * Fix - measurement of root with wrap content height #16 43 | * Fix - high velocity fling bug #11 44 | 45 | Version 1.0.0 (2013-12-28) 46 | ---------------------------- 47 | 48 | * Initial version available via Maven Central -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | AndroidStaggeredGrid 3 | ===================== 4 | 5 | 6 | ##Notice - Deprecated - 09-2015 7 | 8 | This library has been deprecated. We will no longer be shipping any updates or approving community pull requests for this project. 9 | 10 | While the code will remain for anyone who wishes to use it, we suggest you prefer using Google's own [`RecyclerView `](https://developer.android.com/reference/android/support/v7/widget/RecyclerView.html) with their [`StaggeredGridLayoutManager`](https://developer.android.com/reference/android/support/v7/widget/StaggeredGridLayoutManager.html). We are doing the same internally at Etsy. 11 | 12 | Thanks to everyone who used the library and submitted code or issues to improve it. 13 | 14 | ##About 15 | 16 | An Android staggered grid view which supports multiple columns with rows of varying sizes. 17 | 18 | The `StaggeredGridView` was developed due to requirements for the Etsy app not met by any existing Android libraries. 19 | Namely a stable implementation with the ability to have a different number of columns in landscape & portrait, 20 | to sync grid position across orientation changes and support for headers & footers. 21 | 22 | ![Staggered Grid Sample Image][1] 23 | 24 | ##Features 25 | 26 | * Configurable column count for portrait and landscape orientations. 27 | * Sync'd row position across orientation changes. 28 | * Configurable item margin. 29 | * Support for headers & footers. 30 | * Internal padding that does not affect the header & footer. 31 | * Extends [`AbsListView`](http://developer.android.com/reference/android/widget/AbsListView.html) - "mostly" 32 | * Supports [`AbsListView.OnScrollListener`](http://developer.android.com/reference/android/widget/AbsListView.OnScrollListener.html) 33 | 34 | ##Setup 35 | 36 | The library was built for and tested on Android version 2.3.3(SDK 10) and above. It could be modified to support older versions if required. 37 | 38 | The simplest way to use AndroidStaggeredGrid is to add the library as a gradle aar dependency to your build. See the [CHANGELOG.md](https://github.com/etsy/AndroidStaggeredGrid/blob/master/CHANGELOG.md) for the latest version number. 39 | 40 | ``` 41 | repositories { 42 | mavenCentral() 43 | } 44 | 45 | dependencies { 46 | compile 'com.etsy.android.grid:library:x.x.x' // see changelog 47 | } 48 | ``` 49 | 50 | Alternatively import the `/library` project into your Android Studio project and add it as a dependency in your `build.gradle`. 51 | 52 | The library is currently configured to be built via Gradle only. It has the following dependencies: 53 | 54 | * Android Gradle plugin v0.9.2 - `com.android.tools.build:gradle:0.9.2` 55 | * Android Support Library v19.1 - `com.android.support:support-v4:19.1.+` 56 | 57 | Still use Eclipse/building with Ant? You can still use AndroidStaggeredGrid, it's just a few extra steps (left up to the reader). 58 | 59 | ##Usage 60 | 61 | *Please see the `/sample` app for a more detailed code example of how to use the library.* 62 | 63 | 1. Add the `StaggeredGridView` to the layout you want to show. 64 | ```xml 65 | 66 | 74 | ``` 75 | 2. Configure attributes. 76 | * `item_margin` - The margin around each grid item (default 0dp). 77 | * `column_count` - The number of columns displayed. Will override column_count_portrait and column_count_landscape if present (default 0) 78 | * `column_count_portrait` - The number of columns displayed when the grid is in portrait (default 2). 79 | * `column_count_landscape` - The number of columns displayed when the grid is in landscape (default 3). 80 | * `grid_paddingLeft` - Padding to the left of the grid. Does not apply to headers and footers (default 0). 81 | * `grid_paddingRight` - Padding to the right of the grid. Does not apply to headers and footers (default 0). 82 | * `grid_paddingTop` - Padding to the top of the grid. Does not apply to headers and footers (default 0). 83 | * `grid_paddingBottom` - Padding to the bottom of the grid. Does not apply to headers and footers (default 0). 84 | 3. Setup an adapter just like you would with a `GridView`/`ListView`. 85 | ```java 86 | ListAdapter adapter = ...; 87 | 88 | StaggeredGridView gridView = (StaggeredGridView) findViewById(R.id.grid_view); 89 | 90 | gridView.setAdapter(adapter); 91 | ``` 92 | **NOTE:** 93 | As column widths change on orientation change, the grid view expects that all children 94 | maintain their own width to height ratio. To assist with this the project includes the 95 | `DynamicHeightImageView` as an example of a view that measures its height based on its width. 96 | 97 | ##TODO 98 | 99 | The `StaggeredGridView` does not support the following: 100 | 101 | * Item selector drawables 102 | * Item long press event 103 | * Scroll bars 104 | * Row dividers 105 | * Edge effect 106 | * Fading edge 107 | * Overscroll 108 | 109 | ##License 110 | 111 | Copyright (c) 2013 Etsy 112 | 113 | Licensed under the Apache License, Version 2.0 (the "License"); 114 | you may not use this file except in compliance with the License. 115 | You may obtain a copy of the License at 116 | 117 | http://www.apache.org/licenses/LICENSE-2.0 118 | 119 | Unless required by applicable law or agreed to in writing, software 120 | distributed under the License is distributed on an "AS IS" BASIS, 121 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 122 | See the License for the specific language governing permissions and 123 | limitations under the License. 124 | 125 | [1]: http://f.cl.ly/items/2z1B0Y0M0G0O2k1l3J03/Trending.png 126 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | } 5 | dependencies { 6 | classpath 'com.android.tools.build:gradle:0.12.+' 7 | } 8 | } 9 | 10 | ext { 11 | compileSdkVersion = 19 12 | buildToolsVersion = "20.0.0" 13 | } 14 | 15 | def isReleaseBuild() { 16 | return version.contains("SNAPSHOT") == false 17 | } 18 | 19 | allprojects { 20 | repositories { 21 | mavenCentral() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | VERSION_NAME=1.0.5 2 | VERSION_CODE=1 3 | GROUP=com.etsy.android.grid 4 | 5 | POM_DESCRIPTION=An Android staggered grid view which supports multiple columns with rows of varying sizes. 6 | POM_URL=https://github.com/etsy/AndroidStaggeredGrid 7 | POM_SCM_URL=https://github.com/etsy/AndroidStaggeredGrid 8 | POM_SCM_CONNECTION=scm:git@github.com:etsy/AndroidStaggeredGrid.git 9 | POM_SCM_DEV_CONNECTION=scm:git@github.com:etsy/AndroidStaggeredGrid.git 10 | POM_LICENCE_NAME=The Apache Software License, Version 2.0 11 | POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt 12 | POM_LICENCE_DIST=repo 13 | POM_DEVELOPER_ID=etsy 14 | POM_DEVELOPER_NAME=Etsy -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etsy/AndroidStaggeredGrid/97739f6690a7676b62f0eb1246ae195ac98df4e2/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Jun 30 23:54:08 EDT 2014 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-1.12-bin.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 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /library/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | dependencies { 4 | compile 'com.android.support:support-v4:19.1.+' 5 | } 6 | 7 | version = VERSION_NAME 8 | group = GROUP 9 | 10 | android { 11 | compileSdkVersion rootProject.ext.compileSdkVersion 12 | buildToolsVersion rootProject.ext.buildToolsVersion 13 | 14 | defaultConfig { 15 | minSdkVersion 10 16 | } 17 | 18 | lintOptions { 19 | abortOnError false 20 | } 21 | } 22 | 23 | apply from: 'https://raw.githubusercontent.com/denizmveli/gradle-mvn-push/master/gradle-mvn-push.gradle' 24 | -------------------------------------------------------------------------------- /library/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=AndroidStaggeredGrid-Library 2 | POM_ARTIFACT_ID=library 3 | POM_PACKAGING=aar -------------------------------------------------------------------------------- /library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /library/src/main/java/com/etsy/android/grid/ClassLoaderSavedState.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 Etsy 3 | * Copyright (C) 2006 The Android Open Source Project 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package com.etsy.android.grid; 19 | 20 | import android.os.Parcel; 21 | import android.os.Parcelable; 22 | 23 | 24 | /** 25 | * A {@link android.os.Parcelable} implementation that should be used by inheritance 26 | * hierarchies to ensure the state of all classes along the chain is saved. 27 | */ 28 | public abstract class ClassLoaderSavedState implements Parcelable { 29 | public static final ClassLoaderSavedState EMPTY_STATE = new ClassLoaderSavedState() {}; 30 | 31 | private Parcelable mSuperState = EMPTY_STATE; 32 | private ClassLoader mClassLoader; 33 | 34 | /** 35 | * Constructor used to make the EMPTY_STATE singleton 36 | */ 37 | private ClassLoaderSavedState() { 38 | mSuperState = null; 39 | mClassLoader = null; 40 | } 41 | 42 | /** 43 | * Constructor called by derived classes when creating their ListSavedState objects 44 | * 45 | * @param superState The state of the superclass of this view 46 | */ 47 | protected ClassLoaderSavedState(Parcelable superState, ClassLoader classLoader) { 48 | mClassLoader = classLoader; 49 | if (superState == null) { 50 | throw new IllegalArgumentException("superState must not be null"); 51 | } 52 | else { 53 | mSuperState = superState != EMPTY_STATE ? superState : null; 54 | } 55 | } 56 | 57 | /** 58 | * Constructor used when reading from a parcel. Reads the state of the superclass. 59 | * 60 | * @param source 61 | */ 62 | protected ClassLoaderSavedState(Parcel source) { 63 | // ETSY : we're using the passed super class loader unlike AbsSavedState 64 | Parcelable superState = source.readParcelable(mClassLoader); 65 | mSuperState = superState != null ? superState : EMPTY_STATE; 66 | } 67 | 68 | final public Parcelable getSuperState() { 69 | return mSuperState; 70 | } 71 | 72 | public int describeContents() { 73 | return 0; 74 | } 75 | 76 | public void writeToParcel(Parcel dest, int flags) { 77 | dest.writeParcelable(mSuperState, flags); 78 | } 79 | 80 | public static final Parcelable.Creator CREATOR 81 | = new Parcelable.Creator() { 82 | 83 | public ClassLoaderSavedState createFromParcel(Parcel in) { 84 | Parcelable superState = in.readParcelable(null); 85 | if (superState != null) { 86 | throw new IllegalStateException("superState must be null"); 87 | } 88 | return EMPTY_STATE; 89 | } 90 | 91 | public ClassLoaderSavedState[] newArray(int size) { 92 | return new ClassLoaderSavedState[size]; 93 | } 94 | }; 95 | } 96 | -------------------------------------------------------------------------------- /library/src/main/java/com/etsy/android/grid/HeaderViewListAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2006 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.etsy.android.grid; 18 | 19 | import android.database.DataSetObserver; 20 | import android.view.View; 21 | import android.view.ViewGroup; 22 | import android.widget.AdapterView; 23 | import android.widget.Filter; 24 | import android.widget.Filterable; 25 | import android.widget.ListAdapter; 26 | import android.widget.WrapperListAdapter; 27 | 28 | import java.util.ArrayList; 29 | 30 | /** 31 | * ListAdapter used when a ListView has header views. This ListAdapter 32 | * wraps another one and also keeps track of the header views and their 33 | * associated data objects. 34 | *

This is intended as a base class; you will probably not need to 35 | * use this class directly in your own code. 36 | */ 37 | public class HeaderViewListAdapter implements WrapperListAdapter, Filterable { 38 | 39 | private final ListAdapter mAdapter; 40 | 41 | // These two ArrayList are assumed to NOT be null. 42 | // They are indeed created when declared in ListView and then shared. 43 | ArrayList mHeaderViewInfos; 44 | ArrayList mFooterViewInfos; 45 | 46 | // Used as a placeholder in case the provided info views are indeed null. 47 | // Currently only used by some CTS tests, which may be removed. 48 | static final ArrayList EMPTY_INFO_LIST = 49 | new ArrayList(); 50 | 51 | boolean mAreAllFixedViewsSelectable; 52 | 53 | private final boolean mIsFilterable; 54 | 55 | public HeaderViewListAdapter(ArrayList headerViewInfos, 56 | ArrayList footerViewInfos, 57 | ListAdapter adapter) { 58 | mAdapter = adapter; 59 | mIsFilterable = adapter instanceof Filterable; 60 | 61 | if (headerViewInfos == null) { 62 | mHeaderViewInfos = EMPTY_INFO_LIST; 63 | } else { 64 | mHeaderViewInfos = headerViewInfos; 65 | } 66 | 67 | if (footerViewInfos == null) { 68 | mFooterViewInfos = EMPTY_INFO_LIST; 69 | } else { 70 | mFooterViewInfos = footerViewInfos; 71 | } 72 | 73 | mAreAllFixedViewsSelectable = 74 | areAllListInfosSelectable(mHeaderViewInfos) 75 | && areAllListInfosSelectable(mFooterViewInfos); 76 | } 77 | 78 | public int getHeadersCount() { 79 | return mHeaderViewInfos.size(); 80 | } 81 | 82 | public int getFootersCount() { 83 | return mFooterViewInfos.size(); 84 | } 85 | 86 | public boolean isEmpty() { 87 | return mAdapter == null || mAdapter.isEmpty(); 88 | } 89 | 90 | private boolean areAllListInfosSelectable(ArrayList infos) { 91 | if (infos != null) { 92 | for (StaggeredGridView.FixedViewInfo info : infos) { 93 | if (!info.isSelectable) { 94 | return false; 95 | } 96 | } 97 | } 98 | return true; 99 | } 100 | 101 | public boolean removeHeader(View v) { 102 | for (int i = 0; i < mHeaderViewInfos.size(); i++) { 103 | StaggeredGridView.FixedViewInfo info = mHeaderViewInfos.get(i); 104 | if (info.view == v) { 105 | mHeaderViewInfos.remove(i); 106 | 107 | mAreAllFixedViewsSelectable = 108 | areAllListInfosSelectable(mHeaderViewInfos) 109 | && areAllListInfosSelectable(mFooterViewInfos); 110 | 111 | return true; 112 | } 113 | } 114 | 115 | return false; 116 | } 117 | 118 | public boolean removeFooter(View v) { 119 | for (int i = 0; i < mFooterViewInfos.size(); i++) { 120 | StaggeredGridView.FixedViewInfo info = mFooterViewInfos.get(i); 121 | if (info.view == v) { 122 | mFooterViewInfos.remove(i); 123 | 124 | mAreAllFixedViewsSelectable = 125 | areAllListInfosSelectable(mHeaderViewInfos) 126 | && areAllListInfosSelectable(mFooterViewInfos); 127 | 128 | return true; 129 | } 130 | } 131 | 132 | return false; 133 | } 134 | 135 | public int getCount() { 136 | if (mAdapter != null) { 137 | return getFootersCount() + getHeadersCount() + mAdapter.getCount(); 138 | } else { 139 | return getFootersCount() + getHeadersCount(); 140 | } 141 | } 142 | 143 | public boolean areAllItemsEnabled() { 144 | if (mAdapter != null) { 145 | return mAreAllFixedViewsSelectable && mAdapter.areAllItemsEnabled(); 146 | } else { 147 | return true; 148 | } 149 | } 150 | 151 | public boolean isEnabled(int position) { 152 | // Header (negative positions will throw an ArrayIndexOutOfBoundsException) 153 | int numHeaders = getHeadersCount(); 154 | if (position < numHeaders) { 155 | return mHeaderViewInfos.get(position).isSelectable; 156 | } 157 | 158 | // Adapter 159 | final int adjPosition = position - numHeaders; 160 | int adapterCount = 0; 161 | if (mAdapter != null) { 162 | adapterCount = mAdapter.getCount(); 163 | if (adjPosition < adapterCount) { 164 | return mAdapter.isEnabled(adjPosition); 165 | } 166 | } 167 | 168 | // Footer (off-limits positions will throw an ArrayIndexOutOfBoundsException) 169 | return mFooterViewInfos.get(adjPosition - adapterCount).isSelectable; 170 | } 171 | 172 | public Object getItem(int position) { 173 | // Header (negative positions will throw an ArrayIndexOutOfBoundsException) 174 | int numHeaders = getHeadersCount(); 175 | if (position < numHeaders) { 176 | return mHeaderViewInfos.get(position).data; 177 | } 178 | 179 | // Adapter 180 | final int adjPosition = position - numHeaders; 181 | int adapterCount = 0; 182 | if (mAdapter != null) { 183 | adapterCount = mAdapter.getCount(); 184 | if (adjPosition < adapterCount) { 185 | return mAdapter.getItem(adjPosition); 186 | } 187 | } 188 | 189 | // Footer (off-limits positions will throw an ArrayIndexOutOfBoundsException) 190 | return mFooterViewInfos.get(adjPosition - adapterCount).data; 191 | } 192 | 193 | public long getItemId(int position) { 194 | int numHeaders = getHeadersCount(); 195 | if (mAdapter != null && position >= numHeaders) { 196 | int adjPosition = position - numHeaders; 197 | int adapterCount = mAdapter.getCount(); 198 | if (adjPosition < adapterCount) { 199 | return mAdapter.getItemId(adjPosition); 200 | } 201 | } 202 | return -1; 203 | } 204 | 205 | public boolean hasStableIds() { 206 | if (mAdapter != null) { 207 | return mAdapter.hasStableIds(); 208 | } 209 | return false; 210 | } 211 | 212 | public View getView(int position, View convertView, ViewGroup parent) { 213 | // Header (negative positions will throw an ArrayIndexOutOfBoundsException) 214 | int numHeaders = getHeadersCount(); 215 | if (position < numHeaders) { 216 | return mHeaderViewInfos.get(position).view; 217 | } 218 | 219 | // Adapter 220 | final int adjPosition = position - numHeaders; 221 | int adapterCount = 0; 222 | if (mAdapter != null) { 223 | adapterCount = mAdapter.getCount(); 224 | if (adjPosition < adapterCount) { 225 | return mAdapter.getView(adjPosition, convertView, parent); 226 | } 227 | } 228 | 229 | // Footer (off-limits positions will throw an ArrayIndexOutOfBoundsException) 230 | return mFooterViewInfos.get(adjPosition - adapterCount).view; 231 | } 232 | 233 | public int getItemViewType(int position) { 234 | int numHeaders = getHeadersCount(); 235 | if (mAdapter != null && position >= numHeaders) { 236 | int adjPosition = position - numHeaders; 237 | int adapterCount = mAdapter.getCount(); 238 | if (adjPosition < adapterCount) { 239 | return mAdapter.getItemViewType(adjPosition); 240 | } 241 | } 242 | 243 | return AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER; 244 | } 245 | 246 | public int getViewTypeCount() { 247 | if (mAdapter != null) { 248 | return mAdapter.getViewTypeCount(); 249 | } 250 | return 1; 251 | } 252 | 253 | public void registerDataSetObserver(DataSetObserver observer) { 254 | if (mAdapter != null) { 255 | mAdapter.registerDataSetObserver(observer); 256 | } 257 | } 258 | 259 | public void unregisterDataSetObserver(DataSetObserver observer) { 260 | if (mAdapter != null) { 261 | mAdapter.unregisterDataSetObserver(observer); 262 | } 263 | } 264 | 265 | public Filter getFilter() { 266 | if (mIsFilterable) { 267 | return ((Filterable) mAdapter).getFilter(); 268 | } 269 | return null; 270 | } 271 | 272 | public ListAdapter getWrappedAdapter() { 273 | return mAdapter; 274 | } 275 | } -------------------------------------------------------------------------------- /library/src/main/java/com/etsy/android/grid/StaggeredGridView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013 Etsy 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.etsy.android.grid; 18 | 19 | import android.content.Context; 20 | import android.content.res.Configuration; 21 | import android.content.res.TypedArray; 22 | import android.os.Parcel; 23 | import android.os.Parcelable; 24 | import android.util.AttributeSet; 25 | import android.util.Log; 26 | import android.util.SparseArray; 27 | import android.view.View; 28 | import android.view.ViewGroup; 29 | 30 | import java.util.Arrays; 31 | 32 | /** 33 | * A staggered grid view which supports multiple columns with rows of varying sizes. 34 | *

35 | * Builds multiple columns on top of {@link ExtendableListView} 36 | *

37 | * Partly inspired by - https://github.com/huewu/PinterestLikeAdapterView 38 | */ 39 | public class StaggeredGridView extends ExtendableListView { 40 | 41 | private static final String TAG = "StaggeredGridView"; 42 | private static final boolean DBG = false; 43 | 44 | private static final int DEFAULT_COLUMNS_PORTRAIT = 2; 45 | private static final int DEFAULT_COLUMNS_LANDSCAPE = 3; 46 | 47 | private int mColumnCount; 48 | private int mItemMargin; 49 | private int mColumnWidth; 50 | private boolean mNeedSync; 51 | 52 | private int mColumnCountPortrait = DEFAULT_COLUMNS_PORTRAIT; 53 | private int mColumnCountLandscape = DEFAULT_COLUMNS_LANDSCAPE; 54 | 55 | /** 56 | * A key-value collection where the key is the position and the 57 | * {@link GridItemRecord} with some info about that position 58 | * so we can maintain it's position - and reorg on orientation change. 59 | */ 60 | private SparseArray mPositionData; 61 | private int mGridPaddingLeft; 62 | private int mGridPaddingRight; 63 | private int mGridPaddingTop; 64 | private int mGridPaddingBottom; 65 | 66 | /*** 67 | * Our grid item state record with {@link Parcelable} implementation 68 | * so we can persist them across the SGV lifecycle. 69 | */ 70 | static class GridItemRecord implements Parcelable { 71 | int column; 72 | double heightRatio; 73 | boolean isHeaderFooter; 74 | 75 | GridItemRecord() { } 76 | 77 | /** 78 | * Constructor called from {@link #CREATOR} 79 | */ 80 | private GridItemRecord(Parcel in) { 81 | column = in.readInt(); 82 | heightRatio = in.readDouble(); 83 | isHeaderFooter = in.readByte() == 1; 84 | } 85 | 86 | @Override 87 | public int describeContents() { 88 | return 0; 89 | } 90 | 91 | @Override 92 | public void writeToParcel(Parcel out, int flags) { 93 | out.writeInt(column); 94 | out.writeDouble(heightRatio); 95 | out.writeByte((byte) (isHeaderFooter ? 1 : 0)); 96 | } 97 | 98 | @Override 99 | public String toString() { 100 | return "GridItemRecord.ListSavedState{" 101 | + Integer.toHexString(System.identityHashCode(this)) 102 | + " column:" + column 103 | + " heightRatio:" + heightRatio 104 | + " isHeaderFooter:" + isHeaderFooter 105 | + "}"; 106 | } 107 | 108 | public static final Parcelable.Creator CREATOR 109 | = new Parcelable.Creator() { 110 | public GridItemRecord createFromParcel(Parcel in) { 111 | return new GridItemRecord(in); 112 | } 113 | 114 | public GridItemRecord[] newArray(int size) { 115 | return new GridItemRecord[size]; 116 | } 117 | }; 118 | } 119 | 120 | /** 121 | * The location of the top of each top item added in each column. 122 | */ 123 | private int[] mColumnTops; 124 | 125 | /** 126 | * The location of the bottom of each bottom item added in each column. 127 | */ 128 | private int[] mColumnBottoms; 129 | 130 | /** 131 | * The left location to put items for each column 132 | */ 133 | private int[] mColumnLefts; 134 | 135 | /*** 136 | * Tells us the distance we've offset from the top. 137 | * Can be slightly off on orientation change - TESTING 138 | */ 139 | private int mDistanceToTop; 140 | 141 | public StaggeredGridView(final Context context) { 142 | this(context, null); 143 | } 144 | 145 | public StaggeredGridView(final Context context, final AttributeSet attrs) { 146 | this(context, attrs, 0); 147 | } 148 | 149 | public StaggeredGridView(final Context context, final AttributeSet attrs, final int defStyle) { 150 | super(context, attrs, defStyle); 151 | 152 | if (attrs != null) { 153 | // get the number of columns in portrait and landscape 154 | TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.StaggeredGridView, defStyle, 0); 155 | 156 | mColumnCount = typedArray.getInteger( 157 | R.styleable.StaggeredGridView_column_count, 0); 158 | 159 | if (mColumnCount > 0) { 160 | mColumnCountPortrait = mColumnCount; 161 | mColumnCountLandscape = mColumnCount; 162 | } 163 | else { 164 | mColumnCountPortrait = typedArray.getInteger( 165 | R.styleable.StaggeredGridView_column_count_portrait, 166 | DEFAULT_COLUMNS_PORTRAIT); 167 | mColumnCountLandscape = typedArray.getInteger( 168 | R.styleable.StaggeredGridView_column_count_landscape, 169 | DEFAULT_COLUMNS_LANDSCAPE); 170 | } 171 | 172 | mItemMargin = typedArray.getDimensionPixelSize( 173 | R.styleable.StaggeredGridView_item_margin, 0); 174 | mGridPaddingLeft = typedArray.getDimensionPixelSize( 175 | R.styleable.StaggeredGridView_grid_paddingLeft, 0); 176 | mGridPaddingRight = typedArray.getDimensionPixelSize( 177 | R.styleable.StaggeredGridView_grid_paddingRight, 0); 178 | mGridPaddingTop = typedArray.getDimensionPixelSize( 179 | R.styleable.StaggeredGridView_grid_paddingTop, 0); 180 | mGridPaddingBottom = typedArray.getDimensionPixelSize( 181 | R.styleable.StaggeredGridView_grid_paddingBottom, 0); 182 | 183 | typedArray.recycle(); 184 | } 185 | 186 | mColumnCount = 0; // determined onMeasure 187 | // Creating these empty arrays to avoid saving null states 188 | mColumnTops = new int[0]; 189 | mColumnBottoms = new int[0]; 190 | mColumnLefts = new int[0]; 191 | mPositionData = new SparseArray(); 192 | } 193 | 194 | // ////////////////////////////////////////////////////////////////////////////////////////// 195 | // PROPERTIES 196 | // 197 | 198 | // Grid padding is applied to the list item rows but not the header and footer 199 | public int getRowPaddingLeft() { 200 | return getListPaddingLeft() + mGridPaddingLeft; 201 | } 202 | 203 | public int getRowPaddingRight() { 204 | return getListPaddingRight() + mGridPaddingRight; 205 | } 206 | 207 | public int getRowPaddingTop() { 208 | return getListPaddingTop() + mGridPaddingTop; 209 | } 210 | 211 | public int getRowPaddingBottom() { 212 | return getListPaddingBottom() + mGridPaddingBottom; 213 | } 214 | 215 | public void setGridPadding(int left, int top, int right, int bottom) { 216 | mGridPaddingLeft = left; 217 | mGridPaddingTop = top; 218 | mGridPaddingRight = right; 219 | mGridPaddingBottom = bottom; 220 | } 221 | 222 | public void setColumnCountPortrait(int columnCountPortrait) { 223 | mColumnCountPortrait = columnCountPortrait; 224 | onSizeChanged(getWidth(), getHeight()); 225 | requestLayoutChildren(); 226 | } 227 | 228 | public void setColumnCountLandscape(int columnCountLandscape) { 229 | mColumnCountLandscape = columnCountLandscape; 230 | onSizeChanged(getWidth(), getHeight()); 231 | requestLayoutChildren(); 232 | } 233 | 234 | public void setColumnCount(int columnCount) { 235 | mColumnCountPortrait = columnCount; 236 | mColumnCountLandscape = columnCount; 237 | // mColumnCount set onSizeChanged(); 238 | onSizeChanged(getWidth(), getHeight()); 239 | requestLayoutChildren(); 240 | } 241 | 242 | // ////////////////////////////////////////////////////////////////////////////////////////// 243 | // MEASUREMENT 244 | // 245 | private boolean isLandscape() { 246 | return getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; 247 | } 248 | 249 | @Override 250 | protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { 251 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 252 | 253 | if (mColumnCount <= 0) { 254 | boolean isLandscape = isLandscape(); 255 | mColumnCount = isLandscape ? mColumnCountLandscape : mColumnCountPortrait; 256 | } 257 | 258 | // our column width is the width of the listview 259 | // minus it's padding 260 | // minus the total items margin 261 | // divided by the number of columns 262 | mColumnWidth = calculateColumnWidth(getMeasuredWidth()); 263 | 264 | if (mColumnTops == null || mColumnTops.length != mColumnCount) { 265 | mColumnTops = new int[mColumnCount]; 266 | initColumnTops(); 267 | } 268 | if (mColumnBottoms == null || mColumnBottoms.length != mColumnCount) { 269 | mColumnBottoms = new int[mColumnCount]; 270 | initColumnBottoms(); 271 | } 272 | if (mColumnLefts == null || mColumnLefts.length != mColumnCount) { 273 | mColumnLefts = new int[mColumnCount]; 274 | initColumnLefts(); 275 | } 276 | } 277 | 278 | @Override 279 | protected void onMeasureChild(final View child, final LayoutParams layoutParams) { 280 | final int viewType = layoutParams.viewType; 281 | final int position = layoutParams.position; 282 | 283 | if (viewType == ITEM_VIEW_TYPE_HEADER_OR_FOOTER || 284 | viewType == ITEM_VIEW_TYPE_IGNORE) { 285 | // for headers and weird ignored views 286 | super.onMeasureChild(child, layoutParams); 287 | } 288 | else { 289 | if (DBG) Log.d(TAG, "onMeasureChild BEFORE position:" + position + 290 | " h:" + getMeasuredHeight()); 291 | // measure it to the width of our column. 292 | int childWidthSpec = MeasureSpec.makeMeasureSpec(mColumnWidth, MeasureSpec.EXACTLY); 293 | int childHeightSpec; 294 | if (layoutParams.height > 0) { 295 | childHeightSpec = MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY); 296 | } 297 | else { 298 | childHeightSpec = MeasureSpec.makeMeasureSpec(LayoutParams.WRAP_CONTENT, MeasureSpec.UNSPECIFIED); 299 | } 300 | child.measure(childWidthSpec, childHeightSpec); 301 | } 302 | 303 | final int childHeight = getChildHeight(child); 304 | setPositionHeightRatio(position, childHeight); 305 | 306 | if (DBG) Log.d(TAG, "onMeasureChild AFTER position:" + position + 307 | " h:" + childHeight); 308 | } 309 | 310 | public int getColumnWidth() { 311 | return mColumnWidth; 312 | } 313 | 314 | public void resetToTop() { 315 | if (mColumnCount > 0) { 316 | 317 | if (mColumnTops == null) { 318 | mColumnTops = new int[mColumnCount]; 319 | } 320 | if (mColumnBottoms == null) { 321 | mColumnBottoms = new int[mColumnCount]; 322 | } 323 | initColumnTopsAndBottoms(); 324 | 325 | mPositionData.clear(); 326 | mNeedSync = false; 327 | mDistanceToTop = 0; 328 | setSelection(0); 329 | } 330 | } 331 | 332 | // ////////////////////////////////////////////////////////////////////////////////////////// 333 | // POSITIONING 334 | // 335 | 336 | @Override 337 | protected void onChildCreated(final int position, final boolean flowDown) { 338 | super.onChildCreated(position, flowDown); 339 | if (!isHeaderOrFooter(position)) { 340 | // do we already have a column for this position? 341 | final int column = getChildColumn(position, flowDown); 342 | setPositionColumn(position, column); 343 | if (DBG) Log.d(TAG, "onChildCreated position:" + position + 344 | " is in column:" + column); 345 | } 346 | else { 347 | setPositionIsHeaderFooter(position); 348 | } 349 | } 350 | 351 | private void requestLayoutChildren() { 352 | final int count = getChildCount(); 353 | for (int i = 0; i < count; i++) { 354 | final View v = getChildAt(i); 355 | if (v != null) v.requestLayout(); 356 | } 357 | } 358 | 359 | @Override 360 | protected void layoutChildren() { 361 | preLayoutChildren(); 362 | super.layoutChildren(); 363 | } 364 | 365 | private void preLayoutChildren() { 366 | // on a major re-layout reset for our next layout pass 367 | if (!mNeedSync) { 368 | Arrays.fill(mColumnBottoms, 0); 369 | } 370 | else { 371 | mNeedSync = false; 372 | } 373 | // copy the tops into the bottom 374 | // since we're going to redo a layout pass that will draw down from 375 | // the top 376 | System.arraycopy(mColumnTops, 0, mColumnBottoms, 0, mColumnCount); 377 | } 378 | 379 | // NOTE : Views will either be layout out via onLayoutChild 380 | // OR 381 | // Views will be offset if they are active but offscreen so that we can recycle! 382 | // Both onLayoutChild() and onOffsetChild are called after we measure our view 383 | // see ExtensibleListView.setupChild(); 384 | 385 | @Override 386 | protected void onLayoutChild(final View child, 387 | final int position, 388 | final boolean flowDown, 389 | final int childrenLeft, final int childTop, 390 | final int childRight, final int childBottom) { 391 | if (isHeaderOrFooter(position)) { 392 | layoutGridHeaderFooter(child, position, flowDown, childrenLeft, childTop, childRight, childBottom); 393 | } 394 | else { 395 | layoutGridChild(child, position, flowDown, childrenLeft, childRight); 396 | } 397 | } 398 | 399 | private void layoutGridHeaderFooter(final View child, final int position, final boolean flowDown, final int childrenLeft, final int childTop, final int childRight, final int childBottom) { 400 | // offset the top and bottom of all our columns 401 | // if it's the footer we want it below the lowest child bottom 402 | int gridChildTop; 403 | int gridChildBottom; 404 | 405 | if (flowDown) { 406 | gridChildTop = getLowestPositionedBottom(); 407 | gridChildBottom = gridChildTop + getChildHeight(child); 408 | } 409 | else { 410 | gridChildBottom = getHighestPositionedTop(); 411 | gridChildTop = gridChildBottom - getChildHeight(child); 412 | } 413 | 414 | for (int i = 0; i < mColumnCount; i++) { 415 | updateColumnTopIfNeeded(i, gridChildTop); 416 | updateColumnBottomIfNeeded(i, gridChildBottom); 417 | } 418 | 419 | super.onLayoutChild(child, position, flowDown, 420 | childrenLeft, gridChildTop, childRight, gridChildBottom); 421 | } 422 | 423 | private void layoutGridChild(final View child, final int position, 424 | final boolean flowDown, 425 | final int childrenLeft, final int childRight) { 426 | // stash the bottom and the top if it's higher positioned 427 | int column = getPositionColumn(position); 428 | 429 | int gridChildTop; 430 | int gridChildBottom; 431 | 432 | int childTopMargin = getChildTopMargin(position); 433 | int childBottomMargin = getChildBottomMargin(); 434 | int verticalMargins = childTopMargin + childBottomMargin; 435 | 436 | if (flowDown) { 437 | gridChildTop = mColumnBottoms[column]; // the next items top is the last items bottom 438 | gridChildBottom = gridChildTop + (getChildHeight(child) + verticalMargins); 439 | } 440 | else { 441 | gridChildBottom = mColumnTops[column]; // the bottom of the next column up is our top 442 | gridChildTop = gridChildBottom - (getChildHeight(child) + verticalMargins); 443 | } 444 | 445 | if (DBG) Log.d(TAG, "onLayoutChild position:" + position + 446 | " column:" + column + 447 | " gridChildTop:" + gridChildTop + 448 | " gridChildBottom:" + gridChildBottom); 449 | 450 | // we also know the column of this view so let's stash it in the 451 | // view's layout params 452 | GridLayoutParams layoutParams = (GridLayoutParams) child.getLayoutParams(); 453 | layoutParams.column = column; 454 | 455 | updateColumnBottomIfNeeded(column, gridChildBottom); 456 | updateColumnTopIfNeeded(column, gridChildTop); 457 | 458 | // subtract the margins before layout 459 | gridChildTop += childTopMargin; 460 | gridChildBottom -= childBottomMargin; 461 | 462 | child.layout(childrenLeft, gridChildTop, childRight, gridChildBottom); 463 | } 464 | 465 | @Override 466 | protected void onOffsetChild(final View child, final int position, 467 | final boolean flowDown, final int childrenLeft, final int childTop) { 468 | // if the child is recycled and is just offset 469 | // we still want to add its deets into our store 470 | if (isHeaderOrFooter(position)) { 471 | 472 | offsetGridHeaderFooter(child, position, flowDown, childrenLeft, childTop); 473 | } 474 | else { 475 | offsetGridChild(child, position, flowDown, childrenLeft, childTop); 476 | } 477 | } 478 | 479 | private void offsetGridHeaderFooter(final View child, final int position, final boolean flowDown, final int childrenLeft, final int childTop) { 480 | // offset the top and bottom of all our columns 481 | // if it's the footer we want it below the lowest child bottom 482 | int gridChildTop; 483 | int gridChildBottom; 484 | 485 | if (flowDown) { 486 | gridChildTop = getLowestPositionedBottom(); 487 | gridChildBottom = gridChildTop + getChildHeight(child); 488 | } 489 | else { 490 | gridChildBottom = getHighestPositionedTop(); 491 | gridChildTop = gridChildBottom - getChildHeight(child); 492 | } 493 | 494 | for (int i = 0; i < mColumnCount; i++) { 495 | updateColumnTopIfNeeded(i, gridChildTop); 496 | updateColumnBottomIfNeeded(i, gridChildBottom); 497 | } 498 | 499 | super.onOffsetChild(child, position, flowDown, childrenLeft, gridChildTop); 500 | } 501 | 502 | private void offsetGridChild(final View child, final int position, final boolean flowDown, final int childrenLeft, final int childTop) { 503 | // stash the bottom and the top if it's higher positioned 504 | int column = getPositionColumn(position); 505 | 506 | int gridChildTop; 507 | int gridChildBottom; 508 | 509 | int childTopMargin = getChildTopMargin(position); 510 | int childBottomMargin = getChildBottomMargin(); 511 | int verticalMargins = childTopMargin + childBottomMargin; 512 | 513 | if (flowDown) { 514 | gridChildTop = mColumnBottoms[column]; // the next items top is the last items bottom 515 | gridChildBottom = gridChildTop + (getChildHeight(child) + verticalMargins); 516 | } 517 | else { 518 | gridChildBottom = mColumnTops[column]; // the bottom of the next column up is our top 519 | gridChildTop = gridChildBottom - (getChildHeight(child) + verticalMargins); 520 | } 521 | 522 | if (DBG) Log.d(TAG, "onOffsetChild position:" + position + 523 | " column:" + column + 524 | " childTop:" + childTop + 525 | " gridChildTop:" + gridChildTop + 526 | " gridChildBottom:" + gridChildBottom); 527 | 528 | // we also know the column of this view so let's stash it in the 529 | // view's layout params 530 | GridLayoutParams layoutParams = (GridLayoutParams) child.getLayoutParams(); 531 | layoutParams.column = column; 532 | 533 | updateColumnBottomIfNeeded(column, gridChildBottom); 534 | updateColumnTopIfNeeded(column, gridChildTop); 535 | 536 | super.onOffsetChild(child, position, flowDown, childrenLeft, gridChildTop + childTopMargin); 537 | } 538 | 539 | private int getChildHeight(final View child) { 540 | return child.getMeasuredHeight(); 541 | } 542 | 543 | private int getChildTopMargin(final int position) { 544 | boolean isFirstRow = position < (getHeaderViewsCount() + mColumnCount); 545 | return isFirstRow ? mItemMargin : 0; 546 | } 547 | 548 | private int getChildBottomMargin() { 549 | return mItemMargin; 550 | } 551 | 552 | @Override 553 | protected LayoutParams generateChildLayoutParams(final View child) { 554 | GridLayoutParams layoutParams = null; 555 | 556 | final ViewGroup.LayoutParams childParams = child.getLayoutParams(); 557 | if (childParams != null) { 558 | if (childParams instanceof GridLayoutParams) { 559 | layoutParams = (GridLayoutParams) childParams; 560 | } 561 | else { 562 | layoutParams = new GridLayoutParams(childParams); 563 | } 564 | } 565 | if (layoutParams == null) { 566 | layoutParams = new GridLayoutParams( 567 | mColumnWidth, ViewGroup.LayoutParams.WRAP_CONTENT); 568 | } 569 | 570 | return layoutParams; 571 | } 572 | 573 | private void updateColumnTopIfNeeded(int column, int childTop) { 574 | if (childTop < mColumnTops[column]) { 575 | mColumnTops[column] = childTop; 576 | } 577 | } 578 | 579 | private void updateColumnBottomIfNeeded(int column, int childBottom) { 580 | if (childBottom > mColumnBottoms[column]) { 581 | mColumnBottoms[column] = childBottom; 582 | } 583 | } 584 | 585 | @Override 586 | protected int getChildLeft(final int position) { 587 | if (isHeaderOrFooter(position)) { 588 | return super.getChildLeft(position); 589 | } 590 | else { 591 | final int column = getPositionColumn(position); 592 | return mColumnLefts[column]; 593 | } 594 | } 595 | 596 | @Override 597 | protected int getChildTop(final int position) { 598 | if (isHeaderOrFooter(position)) { 599 | return super.getChildTop(position); 600 | } 601 | else { 602 | final int column = getPositionColumn(position); 603 | if (column == -1) { 604 | return getHighestPositionedBottom(); 605 | } 606 | return mColumnBottoms[column]; 607 | } 608 | } 609 | 610 | /** 611 | * Get the top for the next child down in our view 612 | * (maybe a column across) so we can fill down. 613 | */ 614 | @Override 615 | protected int getNextChildDownsTop(final int position) { 616 | if (isHeaderOrFooter(position)) { 617 | return super.getNextChildDownsTop(position); 618 | } 619 | else { 620 | return getHighestPositionedBottom(); 621 | } 622 | } 623 | 624 | @Override 625 | protected int getChildBottom(final int position) { 626 | if (isHeaderOrFooter(position)) { 627 | return super.getChildBottom(position); 628 | } 629 | else { 630 | final int column = getPositionColumn(position); 631 | if (column == -1) { 632 | return getLowestPositionedTop(); 633 | } 634 | return mColumnTops[column]; 635 | } 636 | } 637 | 638 | /** 639 | * Get the bottom for the next child up in our view 640 | * (maybe a column across) so we can fill up. 641 | */ 642 | @Override 643 | protected int getNextChildUpsBottom(final int position) { 644 | if (isHeaderOrFooter(position)) { 645 | return super.getNextChildUpsBottom(position); 646 | } 647 | else { 648 | return getLowestPositionedTop(); 649 | } 650 | } 651 | 652 | @Override 653 | protected int getLastChildBottom() { 654 | final int lastPosition = mFirstPosition + (getChildCount() - 1); 655 | if (isHeaderOrFooter(lastPosition)) { 656 | return super.getLastChildBottom(); 657 | } 658 | return getHighestPositionedBottom(); 659 | } 660 | 661 | @Override 662 | protected int getFirstChildTop() { 663 | if (isHeaderOrFooter(mFirstPosition)) { 664 | return super.getFirstChildTop(); 665 | } 666 | return getLowestPositionedTop(); 667 | } 668 | 669 | @Override 670 | protected int getHighestChildTop() { 671 | if (isHeaderOrFooter(mFirstPosition)) { 672 | return super.getHighestChildTop(); 673 | } 674 | return getHighestPositionedTop(); 675 | } 676 | 677 | @Override 678 | protected int getLowestChildBottom() { 679 | final int lastPosition = mFirstPosition + (getChildCount() - 1); 680 | if (isHeaderOrFooter(lastPosition)) { 681 | return super.getLowestChildBottom(); 682 | } 683 | return getLowestPositionedBottom(); 684 | } 685 | 686 | @Override 687 | protected void offsetChildrenTopAndBottom(final int offset) { 688 | super.offsetChildrenTopAndBottom(offset); 689 | offsetAllColumnsTopAndBottom(offset); 690 | offsetDistanceToTop(offset); 691 | } 692 | 693 | protected void offsetChildrenTopAndBottom(final int offset, final int column) { 694 | if (DBG) Log.d(TAG, "offsetChildrenTopAndBottom: " + offset + " column:" + column); 695 | final int count = getChildCount(); 696 | for (int i = 0; i < count; i++) { 697 | final View v = getChildAt(i); 698 | if (v != null && 699 | v.getLayoutParams() != null && 700 | v.getLayoutParams() instanceof GridLayoutParams) { 701 | GridLayoutParams lp = (GridLayoutParams) v.getLayoutParams(); 702 | if (lp.column == column) { 703 | v.offsetTopAndBottom(offset); 704 | } 705 | } 706 | } 707 | offsetColumnTopAndBottom(offset, column); 708 | } 709 | 710 | private void offsetDistanceToTop(final int offset) { 711 | mDistanceToTop += offset; 712 | if (DBG) Log.d(TAG, "offset mDistanceToTop:" + mDistanceToTop); 713 | } 714 | 715 | public int getDistanceToTop() { 716 | return mDistanceToTop; 717 | } 718 | 719 | private void offsetAllColumnsTopAndBottom(final int offset) { 720 | if (offset != 0) { 721 | for (int i = 0; i < mColumnCount; i++) { 722 | offsetColumnTopAndBottom(offset, i); 723 | } 724 | } 725 | } 726 | 727 | private void offsetColumnTopAndBottom(final int offset, final int column) { 728 | if (offset != 0) { 729 | mColumnTops[column] += offset; 730 | mColumnBottoms[column] += offset; 731 | } 732 | } 733 | 734 | @Override 735 | protected void adjustViewsAfterFillGap(final boolean down) { 736 | super.adjustViewsAfterFillGap(down); 737 | // fix vertical gaps when hitting the top after a rotate 738 | // only when scrolling back up! 739 | if (!down) { 740 | alignTops(); 741 | } 742 | } 743 | 744 | private void alignTops() { 745 | if (mFirstPosition == getHeaderViewsCount()) { 746 | // we're showing all the views before the header views 747 | int[] nonHeaderTops = getHighestNonHeaderTops(); 748 | // we should now have our non header tops 749 | // align them 750 | boolean isAligned = true; 751 | int highestColumn = -1; 752 | int highestTop = Integer.MAX_VALUE; 753 | for (int i = 0; i < nonHeaderTops.length; i++) { 754 | // are they all aligned 755 | if (isAligned && i > 0 && nonHeaderTops[i] != highestTop) { 756 | isAligned = false; // not all the tops are aligned 757 | } 758 | // what's the highest 759 | if (nonHeaderTops[i] < highestTop) { 760 | highestTop = nonHeaderTops[i]; 761 | highestColumn = i; 762 | } 763 | } 764 | 765 | // skip the rest. 766 | if (isAligned) return; 767 | 768 | // we've got the highest column - lets align the others 769 | for (int i = 0; i < nonHeaderTops.length; i++) { 770 | if (i != highestColumn) { 771 | // there's a gap in this column 772 | int offset = highestTop - nonHeaderTops[i]; 773 | offsetChildrenTopAndBottom(offset, i); 774 | } 775 | } 776 | invalidate(); 777 | } 778 | } 779 | 780 | private int[] getHighestNonHeaderTops() { 781 | int[] nonHeaderTops = new int[mColumnCount]; 782 | int childCount = getChildCount(); 783 | if (childCount > 0) { 784 | for (int i = 0; i < childCount; i++) { 785 | View child = getChildAt(i); 786 | if (child != null && 787 | child.getLayoutParams() != null && 788 | child.getLayoutParams() instanceof GridLayoutParams) { 789 | // is this child's top the highest non 790 | GridLayoutParams lp = (GridLayoutParams) child.getLayoutParams(); 791 | // is it a child that isn't a header 792 | if (lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER && 793 | child.getTop() < nonHeaderTops[lp.column]) { 794 | nonHeaderTops[lp.column] = child.getTop(); 795 | } 796 | } 797 | } 798 | } 799 | return nonHeaderTops; 800 | } 801 | 802 | @Override 803 | protected void onChildrenDetached(final int start, final int count) { 804 | super.onChildrenDetached(start, count); 805 | // go through our remaining views and sync the top and bottom stash. 806 | 807 | // Repair the top and bottom column boundaries from the views we still have 808 | Arrays.fill(mColumnTops, Integer.MAX_VALUE); 809 | Arrays.fill(mColumnBottoms, 0); 810 | 811 | for (int i = 0; i < getChildCount(); i++) { 812 | final View child = getChildAt(i); 813 | if (child != null) { 814 | final LayoutParams childParams = (LayoutParams) child.getLayoutParams(); 815 | if (childParams.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER && 816 | childParams instanceof GridLayoutParams) { 817 | GridLayoutParams layoutParams = (GridLayoutParams) childParams; 818 | int column = layoutParams.column; 819 | int position = layoutParams.position; 820 | final int childTop = child.getTop(); 821 | if (childTop < mColumnTops[column]) { 822 | mColumnTops[column] = childTop - getChildTopMargin(position); 823 | } 824 | final int childBottom = child.getBottom(); 825 | if (childBottom > mColumnBottoms[column]) { 826 | mColumnBottoms[column] = childBottom + getChildBottomMargin(); 827 | } 828 | } 829 | else { 830 | // the header and footer here 831 | final int childTop = child.getTop(); 832 | final int childBottom = child.getBottom(); 833 | 834 | for (int col = 0; col < mColumnCount; col++) { 835 | if (childTop < mColumnTops[col]) { 836 | mColumnTops[col] = childTop; 837 | } 838 | if (childBottom > mColumnBottoms[col]) { 839 | mColumnBottoms[col] = childBottom; 840 | } 841 | } 842 | 843 | } 844 | } 845 | } 846 | } 847 | 848 | @Override 849 | protected boolean hasSpaceUp() { 850 | int end = mClipToPadding ? getRowPaddingTop() : 0; 851 | return getLowestPositionedTop() > end; 852 | } 853 | 854 | // ////////////////////////////////////////////////////////////////////////////////////////// 855 | // SYNCING ACROSS ROTATION 856 | // 857 | 858 | @Override 859 | protected void onSizeChanged(final int w, final int h, final int oldw, final int oldh) { 860 | super.onSizeChanged(w, h, oldw, oldh); 861 | onSizeChanged(w, h); 862 | } 863 | 864 | @Override 865 | protected void onSizeChanged(int w, int h) { 866 | super.onSizeChanged(w, h); 867 | boolean isLandscape = isLandscape(); 868 | int newColumnCount = isLandscape ? mColumnCountLandscape : mColumnCountPortrait; 869 | if (mColumnCount != newColumnCount) { 870 | mColumnCount = newColumnCount; 871 | 872 | mColumnWidth = calculateColumnWidth(w); 873 | 874 | mColumnTops = new int[mColumnCount]; 875 | mColumnBottoms = new int[mColumnCount]; 876 | mColumnLefts = new int[mColumnCount]; 877 | 878 | mDistanceToTop = 0; 879 | 880 | // rebuild the columns 881 | initColumnTopsAndBottoms(); 882 | initColumnLefts(); 883 | 884 | // if we have data 885 | if (getCount() > 0 && mPositionData.size() > 0) { 886 | onColumnSync(); 887 | } 888 | 889 | requestLayout(); 890 | } 891 | } 892 | 893 | private int calculateColumnWidth(final int gridWidth) { 894 | final int listPadding = getRowPaddingLeft() + getRowPaddingRight(); 895 | return (gridWidth - listPadding - mItemMargin * (mColumnCount + 1)) / mColumnCount; 896 | } 897 | 898 | private int calculateColumnLeft(final int colIndex) { 899 | return getRowPaddingLeft() + mItemMargin + ((mItemMargin + mColumnWidth) * colIndex); 900 | } 901 | 902 | /*** 903 | * Our mColumnTops and mColumnBottoms need to be re-built up to the 904 | * mSyncPosition - the following layout request will then 905 | * layout the that position and then fillUp and fillDown appropriately. 906 | */ 907 | private void onColumnSync() { 908 | // re-calc tops for new column count! 909 | int syncPosition = Math.min(mSyncPosition, getCount() - 1); 910 | 911 | SparseArray positionHeightRatios = new SparseArray(syncPosition); 912 | for (int pos = 0; pos < syncPosition; pos++) { 913 | // check for weirdness 914 | final GridItemRecord rec = mPositionData.get(pos); 915 | if (rec == null) break; 916 | 917 | Log.d(TAG, "onColumnSync:" + pos + " ratio:" + rec.heightRatio); 918 | positionHeightRatios.append(pos, rec.heightRatio); 919 | } 920 | 921 | mPositionData.clear(); 922 | 923 | // re-calc our relative position while at the same time 924 | // rebuilding our GridItemRecord collection 925 | 926 | if (DBG) Log.d(TAG, "onColumnSync column width:" + mColumnWidth); 927 | 928 | for (int pos = 0; pos < syncPosition; pos++) { 929 | //Check for weirdness again 930 | final Double heightRatio = positionHeightRatios.get(pos); 931 | if(heightRatio == null){ 932 | break; 933 | } 934 | 935 | final GridItemRecord rec = getOrCreateRecord(pos); 936 | final int height = (int) (mColumnWidth * heightRatio); 937 | rec.heightRatio = heightRatio; 938 | 939 | int top; 940 | int bottom; 941 | // check for headers 942 | if (isHeaderOrFooter(pos)) { 943 | // the next top is the bottom for that column 944 | top = getLowestPositionedBottom(); 945 | bottom = top + height; 946 | 947 | for (int i = 0; i < mColumnCount; i++) { 948 | mColumnTops[i] = top; 949 | mColumnBottoms[i] = bottom; 950 | } 951 | } 952 | else { 953 | // what's the next column down ? 954 | final int column = getHighestPositionedBottomColumn(); 955 | // the next top is the bottom for that column 956 | top = mColumnBottoms[column]; 957 | bottom = top + height + getChildTopMargin(pos) + getChildBottomMargin(); 958 | 959 | mColumnTops[column] = top; 960 | mColumnBottoms[column] = bottom; 961 | 962 | rec.column = column; 963 | } 964 | 965 | 966 | if (DBG) Log.d(TAG, "onColumnSync position:" + pos + 967 | " top:" + top + 968 | " bottom:" + bottom + 969 | " height:" + height + 970 | " heightRatio:" + heightRatio); 971 | } 972 | 973 | // our sync position will be displayed in this column 974 | final int syncColumn = getHighestPositionedBottomColumn(); 975 | setPositionColumn(syncPosition, syncColumn); 976 | 977 | // we want to offset from height of the sync position 978 | // minus the offset 979 | int syncToBottom = mColumnBottoms[syncColumn]; 980 | int offset = -syncToBottom + mSpecificTop; 981 | // offset all columns by 982 | offsetAllColumnsTopAndBottom(offset); 983 | 984 | // sync the distance to top 985 | mDistanceToTop = -syncToBottom; 986 | 987 | // stash our bottoms in our tops - though these will be copied back to the bottoms 988 | System.arraycopy(mColumnBottoms, 0, mColumnTops, 0, mColumnCount); 989 | } 990 | 991 | 992 | // ////////////////////////////////////////////////////////////////////////////////////////// 993 | // GridItemRecord UTILS 994 | // 995 | 996 | private void setPositionColumn(final int position, final int column) { 997 | GridItemRecord rec = getOrCreateRecord(position); 998 | rec.column = column; 999 | } 1000 | 1001 | private void setPositionHeightRatio(final int position, final int height) { 1002 | GridItemRecord rec = getOrCreateRecord(position); 1003 | rec.heightRatio = (double) height / (double) mColumnWidth; 1004 | if (DBG) Log.d(TAG, "position:" + position + 1005 | " width:" + mColumnWidth + 1006 | " height:" + height + 1007 | " heightRatio:" + rec.heightRatio); 1008 | } 1009 | 1010 | private void setPositionIsHeaderFooter(final int position) { 1011 | GridItemRecord rec = getOrCreateRecord(position); 1012 | rec.isHeaderFooter = true; 1013 | } 1014 | 1015 | private GridItemRecord getOrCreateRecord(final int position) { 1016 | GridItemRecord rec = mPositionData.get(position, null); 1017 | if (rec == null) { 1018 | rec = new GridItemRecord(); 1019 | mPositionData.append(position, rec); 1020 | } 1021 | return rec; 1022 | } 1023 | 1024 | private int getPositionColumn(final int position) { 1025 | GridItemRecord rec = mPositionData.get(position, null); 1026 | return rec != null ? rec.column : -1; 1027 | } 1028 | 1029 | 1030 | // ////////////////////////////////////////////////////////////////////////////////////////// 1031 | // HELPERS 1032 | // 1033 | 1034 | private boolean isHeaderOrFooter(final int position) { 1035 | final int viewType = mAdapter.getItemViewType(position); 1036 | return viewType == ITEM_VIEW_TYPE_HEADER_OR_FOOTER; 1037 | } 1038 | 1039 | private int getChildColumn(final int position, final boolean flowDown) { 1040 | 1041 | // do we already have a column for this child position? 1042 | int column = getPositionColumn(position); 1043 | // we don't have the column or it no longer fits in our grid 1044 | final int columnCount = mColumnCount; 1045 | if (column < 0 || column >= columnCount) { 1046 | // if we're going down - 1047 | // get the highest positioned (lowest value) 1048 | // column bottom 1049 | if (flowDown) { 1050 | column = getHighestPositionedBottomColumn(); 1051 | } 1052 | else { 1053 | column = getLowestPositionedTopColumn(); 1054 | 1055 | } 1056 | } 1057 | return column; 1058 | } 1059 | 1060 | private void initColumnTopsAndBottoms() { 1061 | initColumnTops(); 1062 | initColumnBottoms(); 1063 | } 1064 | 1065 | private void initColumnTops() { 1066 | Arrays.fill(mColumnTops, getPaddingTop() + mGridPaddingTop); 1067 | } 1068 | 1069 | private void initColumnBottoms() { 1070 | Arrays.fill(mColumnBottoms, getPaddingTop() + mGridPaddingTop); 1071 | } 1072 | 1073 | private void initColumnLefts() { 1074 | for (int i = 0; i < mColumnCount; i++) { 1075 | mColumnLefts[i] = calculateColumnLeft(i); 1076 | } 1077 | } 1078 | 1079 | 1080 | // ////////////////////////////////////////////////////////////////////////////////////////// 1081 | // BOTTOM 1082 | // 1083 | 1084 | private int getHighestPositionedBottom() { 1085 | final int column = getHighestPositionedBottomColumn(); 1086 | return mColumnBottoms[column]; 1087 | } 1088 | 1089 | private int getHighestPositionedBottomColumn() { 1090 | int columnFound = 0; 1091 | int highestPositionedBottom = Integer.MAX_VALUE; 1092 | // the highest positioned bottom is the one with the lowest value :D 1093 | for (int i = 0; i < mColumnCount; i++) { 1094 | int bottom = mColumnBottoms[i]; 1095 | if (bottom < highestPositionedBottom) { 1096 | highestPositionedBottom = bottom; 1097 | columnFound = i; 1098 | } 1099 | } 1100 | return columnFound; 1101 | } 1102 | 1103 | private int getLowestPositionedBottom() { 1104 | final int column = getLowestPositionedBottomColumn(); 1105 | return mColumnBottoms[column]; 1106 | } 1107 | 1108 | private int getLowestPositionedBottomColumn() { 1109 | int columnFound = 0; 1110 | int lowestPositionedBottom = Integer.MIN_VALUE; 1111 | // the lowest positioned bottom is the one with the highest value :D 1112 | for (int i = 0; i < mColumnCount; i++) { 1113 | int bottom = mColumnBottoms[i]; 1114 | if (bottom > lowestPositionedBottom) { 1115 | lowestPositionedBottom = bottom; 1116 | columnFound = i; 1117 | } 1118 | } 1119 | return columnFound; 1120 | } 1121 | 1122 | // ////////////////////////////////////////////////////////////////////////////////////////// 1123 | // TOP 1124 | // 1125 | 1126 | private int getLowestPositionedTop() { 1127 | final int column = getLowestPositionedTopColumn(); 1128 | return mColumnTops[column]; 1129 | } 1130 | 1131 | private int getLowestPositionedTopColumn() { 1132 | int columnFound = 0; 1133 | // we'll go backwards through since the right most 1134 | // will likely be the lowest positioned Top 1135 | int lowestPositionedTop = Integer.MIN_VALUE; 1136 | // the lowest positioned top is the one with the highest value :D 1137 | for (int i = 0; i < mColumnCount; i++) { 1138 | int top = mColumnTops[i]; 1139 | if (top > lowestPositionedTop) { 1140 | lowestPositionedTop = top; 1141 | columnFound = i; 1142 | } 1143 | } 1144 | return columnFound; 1145 | } 1146 | 1147 | private int getHighestPositionedTop() { 1148 | final int column = getHighestPositionedTopColumn(); 1149 | return mColumnTops[column]; 1150 | } 1151 | 1152 | private int getHighestPositionedTopColumn() { 1153 | int columnFound = 0; 1154 | int highestPositionedTop = Integer.MAX_VALUE; 1155 | // the highest positioned top is the one with the lowest value :D 1156 | for (int i = 0; i < mColumnCount; i++) { 1157 | int top = mColumnTops[i]; 1158 | if (top < highestPositionedTop) { 1159 | highestPositionedTop = top; 1160 | columnFound = i; 1161 | } 1162 | } 1163 | return columnFound; 1164 | } 1165 | 1166 | // ////////////////////////////////////////////////////////////////////////////////////////// 1167 | // LAYOUT PARAMS 1168 | // 1169 | 1170 | /** 1171 | * Extended LayoutParams to column position and anything else we may been for the grid 1172 | */ 1173 | public static class GridLayoutParams extends LayoutParams { 1174 | 1175 | // The column the view is displayed in 1176 | int column; 1177 | 1178 | public GridLayoutParams(Context c, AttributeSet attrs) { 1179 | super(c, attrs); 1180 | enforceStaggeredLayout(); 1181 | } 1182 | 1183 | public GridLayoutParams(int w, int h) { 1184 | super(w, h); 1185 | enforceStaggeredLayout(); 1186 | } 1187 | 1188 | public GridLayoutParams(int w, int h, int viewType) { 1189 | super(w, h); 1190 | enforceStaggeredLayout(); 1191 | } 1192 | 1193 | public GridLayoutParams(ViewGroup.LayoutParams source) { 1194 | super(source); 1195 | enforceStaggeredLayout(); 1196 | } 1197 | 1198 | /** 1199 | * Here we're making sure that all grid view items 1200 | * are width MATCH_PARENT and height WRAP_CONTENT. 1201 | * That's what this grid is designed for 1202 | */ 1203 | private void enforceStaggeredLayout() { 1204 | if (width != MATCH_PARENT) { 1205 | width = MATCH_PARENT; 1206 | } 1207 | if (height == MATCH_PARENT) { 1208 | height = WRAP_CONTENT; 1209 | } 1210 | } 1211 | } 1212 | 1213 | // ////////////////////////////////////////////////////////////////////////////////////////// 1214 | // SAVED STATE 1215 | 1216 | 1217 | public static class GridListSavedState extends ListSavedState { 1218 | int columnCount; 1219 | int[] columnTops; 1220 | SparseArray positionData; 1221 | 1222 | public GridListSavedState(Parcelable superState) { 1223 | super(superState); 1224 | } 1225 | 1226 | /** 1227 | * Constructor called from {@link #CREATOR} 1228 | */ 1229 | public GridListSavedState(Parcel in) { 1230 | super(in); 1231 | columnCount = in.readInt(); 1232 | columnTops = new int[columnCount >= 0 ? columnCount : 0]; 1233 | in.readIntArray(columnTops); 1234 | positionData = in.readSparseArray(GridItemRecord.class.getClassLoader()); 1235 | } 1236 | 1237 | @Override 1238 | public void writeToParcel(Parcel out, int flags) { 1239 | super.writeToParcel(out, flags); 1240 | out.writeInt(columnCount); 1241 | out.writeIntArray(columnTops); 1242 | out.writeSparseArray(positionData); 1243 | } 1244 | 1245 | @Override 1246 | public String toString() { 1247 | return "StaggeredGridView.GridListSavedState{" 1248 | + Integer.toHexString(System.identityHashCode(this)) + "}"; 1249 | } 1250 | 1251 | public static final Creator CREATOR 1252 | = new Creator() { 1253 | public GridListSavedState createFromParcel(Parcel in) { 1254 | return new GridListSavedState(in); 1255 | } 1256 | 1257 | public GridListSavedState[] newArray(int size) { 1258 | return new GridListSavedState[size]; 1259 | } 1260 | }; 1261 | } 1262 | 1263 | 1264 | @Override 1265 | public Parcelable onSaveInstanceState() { 1266 | ListSavedState listState = (ListSavedState) super.onSaveInstanceState(); 1267 | GridListSavedState ss = new GridListSavedState(listState.getSuperState()); 1268 | 1269 | // from the list state 1270 | ss.selectedId = listState.selectedId; 1271 | ss.firstId = listState.firstId; 1272 | ss.viewTop = listState.viewTop; 1273 | ss.position = listState.position; 1274 | ss.height = listState.height; 1275 | 1276 | // our state 1277 | 1278 | boolean haveChildren = getChildCount() > 0 && getCount() > 0; 1279 | 1280 | if (haveChildren && mFirstPosition > 0) { 1281 | ss.columnCount = mColumnCount; 1282 | ss.columnTops = mColumnTops; 1283 | ss.positionData = mPositionData; 1284 | } 1285 | else { 1286 | ss.columnCount = mColumnCount >= 0 ? mColumnCount : 0; 1287 | ss.columnTops = new int[ss.columnCount]; 1288 | ss.positionData = new SparseArray(); 1289 | } 1290 | 1291 | return ss; 1292 | } 1293 | 1294 | @Override 1295 | public void onRestoreInstanceState(Parcelable state) { 1296 | GridListSavedState ss = (GridListSavedState) state; 1297 | mColumnCount = ss.columnCount; 1298 | mColumnTops = ss.columnTops; 1299 | mColumnBottoms = new int[mColumnCount]; 1300 | mPositionData = ss.positionData; 1301 | mNeedSync = true; 1302 | super.onRestoreInstanceState(ss); 1303 | } 1304 | } 1305 | -------------------------------------------------------------------------------- /library/src/main/java/com/etsy/android/grid/util/DynamicHeightImageView.java: -------------------------------------------------------------------------------- 1 | package com.etsy.android.grid.util; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.widget.ImageView; 6 | 7 | /** 8 | * An {@link android.widget.ImageView} layout that maintains a consistent width to height aspect ratio. 9 | */ 10 | public class DynamicHeightImageView extends ImageView { 11 | 12 | private double mHeightRatio; 13 | 14 | public DynamicHeightImageView(Context context, AttributeSet attrs) { 15 | super(context, attrs); 16 | } 17 | 18 | public DynamicHeightImageView(Context context) { 19 | super(context); 20 | } 21 | 22 | public void setHeightRatio(double ratio) { 23 | if (ratio != mHeightRatio) { 24 | mHeightRatio = ratio; 25 | requestLayout(); 26 | } 27 | } 28 | 29 | public double getHeightRatio() { 30 | return mHeightRatio; 31 | } 32 | 33 | @Override 34 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 35 | if (mHeightRatio > 0.0) { 36 | // set the image views size 37 | int width = MeasureSpec.getSize(widthMeasureSpec); 38 | int height = (int) (width * mHeightRatio); 39 | setMeasuredDimension(width, height); 40 | } 41 | else { 42 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /library/src/main/java/com/etsy/android/grid/util/DynamicHeightTextView.java: -------------------------------------------------------------------------------- 1 | package com.etsy.android.grid.util; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.widget.TextView; 6 | 7 | /** 8 | * 9 | * A {@link android.widget.TextView} that maintains a consistent width to height aspect ratio. 10 | * In the real world this would likely extend ImageView. 11 | */ 12 | public class DynamicHeightTextView extends TextView { 13 | 14 | private double mHeightRatio; 15 | 16 | public DynamicHeightTextView(Context context, AttributeSet attrs) { 17 | super(context, attrs); 18 | } 19 | 20 | public DynamicHeightTextView(Context context) { 21 | super(context); 22 | } 23 | 24 | public void setHeightRatio(double ratio) { 25 | if (ratio != mHeightRatio) { 26 | mHeightRatio = ratio; 27 | requestLayout(); 28 | } 29 | } 30 | 31 | public double getHeightRatio() { 32 | return mHeightRatio; 33 | } 34 | 35 | @Override 36 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 37 | if (mHeightRatio > 0.0) { 38 | // set the image views size 39 | int width = MeasureSpec.getSize(widthMeasureSpec); 40 | int height = (int) (width * mHeightRatio); 41 | setMeasuredDimension(width, height); 42 | } 43 | else { 44 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /library/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | version = VERSION_NAME 4 | group = GROUP 5 | 6 | android { 7 | compileSdkVersion rootProject.ext.compileSdkVersion 8 | buildToolsVersion rootProject.ext.buildToolsVersion 9 | 10 | defaultConfig { 11 | minSdkVersion 10 12 | targetSdkVersion 19 13 | } 14 | 15 | lintOptions { 16 | abortOnError false 17 | } 18 | } 19 | 20 | dependencies { 21 | compile 'com.android.support:support-v4:19.+' 22 | compile project(':library') 23 | } 24 | -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 10 | 11 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /sample/src/main/java/com/etsy/android/sample/ListViewActivity.java: -------------------------------------------------------------------------------- 1 | package com.etsy.android.sample; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.widget.*; 8 | 9 | import java.util.List; 10 | 11 | public class ListViewActivity extends Activity implements AdapterView.OnItemClickListener { 12 | 13 | @Override 14 | protected void onCreate(Bundle savedInstanceState) { 15 | super.onCreate(savedInstanceState); 16 | setContentView(R.layout.activity_list_view); 17 | 18 | setTitle("ListView"); 19 | 20 | final ListView listView = (ListView) findViewById(R.id.list_view); 21 | 22 | LayoutInflater layoutInflater = getLayoutInflater(); 23 | 24 | View header = layoutInflater.inflate(R.layout.list_item_header_footer, null); 25 | View footer = layoutInflater.inflate(R.layout.list_item_header_footer, null); 26 | TextView txtHeaderTitle = (TextView) header.findViewById(R.id.txt_title); 27 | TextView txtFooterTitle = (TextView) footer.findViewById(R.id.txt_title); 28 | txtHeaderTitle.setText("THE HEADER!"); 29 | txtFooterTitle.setText("THE FOOTER!"); 30 | 31 | listView.addHeaderView(header); 32 | listView.addFooterView(footer); 33 | 34 | final SampleAdapter adapter = new SampleAdapter(this, R.id.txt_line1); 35 | listView.setAdapter(adapter); 36 | listView.setOnItemClickListener(this); 37 | 38 | final List sampleData = SampleData.generateSampleData(); 39 | for (String data : sampleData) { 40 | adapter.add(data); 41 | } 42 | } 43 | 44 | @Override 45 | public void onItemClick(AdapterView adapterView, View view, int position, long id) { 46 | Toast.makeText(this, "Item Clicked: " + position, Toast.LENGTH_SHORT).show(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /sample/src/main/java/com/etsy/android/sample/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.etsy.android.sample; 2 | 3 | import android.app.Activity; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.view.View; 7 | 8 | public class MainActivity extends Activity implements View.OnClickListener { 9 | 10 | @Override 11 | protected void onCreate(Bundle savedInstanceState) { 12 | super.onCreate(savedInstanceState); 13 | setTitle("SGV Sample"); 14 | setContentView(R.layout.activity_main); 15 | 16 | findViewById(R.id.btn_sgv).setOnClickListener(this); 17 | findViewById(R.id.btn_sgv_fragment).setOnClickListener(this); 18 | findViewById(R.id.btn_sgv_empty_view).setOnClickListener(this); 19 | findViewById(R.id.btn_listview).setOnClickListener(this); 20 | } 21 | 22 | 23 | @Override 24 | public void onClick(final View v) { 25 | if (v.getId() == R.id.btn_sgv) { 26 | startActivity(new Intent(this, StaggeredGridActivity.class)); 27 | } 28 | else if (v.getId() == R.id.btn_sgv_fragment) { 29 | startActivity(new Intent(this, StaggeredGridActivityFragment.class)); 30 | } 31 | else if (v.getId() == R.id.btn_sgv_empty_view) { 32 | startActivity(new Intent(this, StaggeredGridEmptyViewActivity.class)); 33 | } 34 | else if (v.getId() == R.id.btn_listview) { 35 | startActivity(new Intent(this, ListViewActivity.class)); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /sample/src/main/java/com/etsy/android/sample/SampleAdapter.java: -------------------------------------------------------------------------------- 1 | package com.etsy.android.sample; 2 | 3 | 4 | import java.util.ArrayList; 5 | import java.util.Random; 6 | 7 | import android.content.Context; 8 | import android.util.Log; 9 | import android.util.SparseArray; 10 | import android.view.LayoutInflater; 11 | import android.view.View; 12 | import android.view.ViewGroup; 13 | import android.widget.ArrayAdapter; 14 | import android.widget.Button; 15 | import android.widget.Toast; 16 | 17 | import com.etsy.android.grid.util.DynamicHeightTextView; 18 | 19 | /*** 20 | * ADAPTER 21 | */ 22 | 23 | public class SampleAdapter extends ArrayAdapter { 24 | 25 | private static final String TAG = "SampleAdapter"; 26 | 27 | static class ViewHolder { 28 | DynamicHeightTextView txtLineOne; 29 | Button btnGo; 30 | } 31 | 32 | private final LayoutInflater mLayoutInflater; 33 | private final Random mRandom; 34 | private final ArrayList mBackgroundColors; 35 | 36 | private static final SparseArray sPositionHeightRatios = new SparseArray(); 37 | 38 | public SampleAdapter(final Context context, final int textViewResourceId) { 39 | super(context, textViewResourceId); 40 | mLayoutInflater = LayoutInflater.from(context); 41 | mRandom = new Random(); 42 | mBackgroundColors = new ArrayList(); 43 | mBackgroundColors.add(R.color.orange); 44 | mBackgroundColors.add(R.color.green); 45 | mBackgroundColors.add(R.color.blue); 46 | mBackgroundColors.add(R.color.yellow); 47 | mBackgroundColors.add(R.color.grey); 48 | } 49 | 50 | @Override 51 | public View getView(final int position, View convertView, final ViewGroup parent) { 52 | 53 | ViewHolder vh; 54 | if (convertView == null) { 55 | convertView = mLayoutInflater.inflate(R.layout.list_item_sample, parent, false); 56 | vh = new ViewHolder(); 57 | vh.txtLineOne = (DynamicHeightTextView) convertView.findViewById(R.id.txt_line1); 58 | vh.btnGo = (Button) convertView.findViewById(R.id.btn_go); 59 | 60 | convertView.setTag(vh); 61 | } 62 | else { 63 | vh = (ViewHolder) convertView.getTag(); 64 | } 65 | 66 | double positionHeight = getPositionRatio(position); 67 | int backgroundIndex = position >= mBackgroundColors.size() ? 68 | position % mBackgroundColors.size() : position; 69 | 70 | convertView.setBackgroundResource(mBackgroundColors.get(backgroundIndex)); 71 | 72 | Log.d(TAG, "getView position:" + position + " h:" + positionHeight); 73 | 74 | vh.txtLineOne.setHeightRatio(positionHeight); 75 | vh.txtLineOne.setText(getItem(position) + position); 76 | 77 | vh.btnGo.setOnClickListener(new View.OnClickListener() { 78 | @Override 79 | public void onClick(final View v) { 80 | Toast.makeText(getContext(), "Button Clicked Position " + 81 | position, Toast.LENGTH_SHORT).show(); 82 | } 83 | }); 84 | 85 | return convertView; 86 | } 87 | 88 | private double getPositionRatio(final int position) { 89 | double ratio = sPositionHeightRatios.get(position, 0.0); 90 | // if not yet done generate and stash the columns height 91 | // in our real world scenario this will be determined by 92 | // some match based on the known height and width of the image 93 | // and maybe a helpful way to get the column height! 94 | if (ratio == 0) { 95 | ratio = getRandomHeightRatio(); 96 | sPositionHeightRatios.append(position, ratio); 97 | Log.d(TAG, "getPositionRatio:" + position + " ratio:" + ratio); 98 | } 99 | return ratio; 100 | } 101 | 102 | private double getRandomHeightRatio() { 103 | return (mRandom.nextDouble() / 2.0) + 1.0; // height will be 1.0 - 1.5 the width 104 | } 105 | } -------------------------------------------------------------------------------- /sample/src/main/java/com/etsy/android/sample/SampleData.java: -------------------------------------------------------------------------------- 1 | package com.etsy.android.sample; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | public class SampleData { 7 | 8 | public static final int SAMPLE_DATA_ITEM_COUNT = 30; 9 | 10 | public static ArrayList generateSampleData() { 11 | final ArrayList data = new ArrayList(SAMPLE_DATA_ITEM_COUNT); 12 | 13 | for (int i = 0; i < SAMPLE_DATA_ITEM_COUNT; i++) { 14 | data.add("SAMPLE #"); 15 | } 16 | 17 | return data; 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /sample/src/main/java/com/etsy/android/sample/StaggeredGridActivity.java: -------------------------------------------------------------------------------- 1 | package com.etsy.android.sample; 2 | 3 | import java.util.ArrayList; 4 | 5 | import android.app.Activity; 6 | import android.os.Bundle; 7 | import android.util.Log; 8 | import android.view.LayoutInflater; 9 | import android.view.Menu; 10 | import android.view.MenuItem; 11 | import android.view.View; 12 | import android.widget.AbsListView; 13 | import android.widget.AdapterView; 14 | import android.widget.TextView; 15 | import android.widget.Toast; 16 | 17 | import com.etsy.android.grid.StaggeredGridView; 18 | 19 | public class StaggeredGridActivity extends Activity implements AbsListView.OnScrollListener, AbsListView.OnItemClickListener, AdapterView.OnItemLongClickListener { 20 | 21 | private static final String TAG = "StaggeredGridActivity"; 22 | public static final String SAVED_DATA_KEY = "SAVED_DATA"; 23 | 24 | private StaggeredGridView mGridView; 25 | private boolean mHasRequestedMore; 26 | private SampleAdapter mAdapter; 27 | 28 | private ArrayList mData; 29 | 30 | @Override 31 | protected void onCreate(Bundle savedInstanceState) { 32 | super.onCreate(savedInstanceState); 33 | setContentView(R.layout.activity_sgv); 34 | 35 | setTitle("SGV"); 36 | mGridView = (StaggeredGridView) findViewById(R.id.grid_view); 37 | 38 | LayoutInflater layoutInflater = getLayoutInflater(); 39 | 40 | View header = layoutInflater.inflate(R.layout.list_item_header_footer, null); 41 | View footer = layoutInflater.inflate(R.layout.list_item_header_footer, null); 42 | TextView txtHeaderTitle = (TextView) header.findViewById(R.id.txt_title); 43 | TextView txtFooterTitle = (TextView) footer.findViewById(R.id.txt_title); 44 | txtHeaderTitle.setText("THE HEADER!"); 45 | txtFooterTitle.setText("THE FOOTER!"); 46 | 47 | mGridView.addHeaderView(header); 48 | mGridView.addFooterView(footer); 49 | mAdapter = new SampleAdapter(this, R.id.txt_line1); 50 | 51 | // do we have saved data? 52 | if (savedInstanceState != null) { 53 | mData = savedInstanceState.getStringArrayList(SAVED_DATA_KEY); 54 | } 55 | 56 | if (mData == null) { 57 | mData = SampleData.generateSampleData(); 58 | } 59 | 60 | for (String data : mData) { 61 | mAdapter.add(data); 62 | } 63 | 64 | mGridView.setAdapter(mAdapter); 65 | mGridView.setOnScrollListener(this); 66 | mGridView.setOnItemClickListener(this); 67 | mGridView.setOnItemLongClickListener(this); 68 | } 69 | 70 | @Override 71 | public boolean onCreateOptionsMenu(Menu menu) { 72 | getMenuInflater().inflate(R.menu.menu_sgv_dynamic, menu); 73 | return true; 74 | } 75 | 76 | @Override 77 | public boolean onOptionsItemSelected(MenuItem item) { 78 | switch (item.getItemId()) { 79 | case R.id.col1: 80 | mGridView.setColumnCount(1); 81 | break; 82 | case R.id.col2: 83 | mGridView.setColumnCount(2); 84 | break; 85 | case R.id.col3: 86 | mGridView.setColumnCount(3); 87 | break; 88 | } 89 | return true; 90 | } 91 | 92 | @Override 93 | protected void onSaveInstanceState(final Bundle outState) { 94 | super.onSaveInstanceState(outState); 95 | outState.putStringArrayList(SAVED_DATA_KEY, mData); 96 | } 97 | 98 | @Override 99 | public void onScrollStateChanged(final AbsListView view, final int scrollState) { 100 | Log.d(TAG, "onScrollStateChanged:" + scrollState); 101 | } 102 | 103 | @Override 104 | public void onScroll(final AbsListView view, final int firstVisibleItem, final int visibleItemCount, final int totalItemCount) { 105 | Log.d(TAG, "onScroll firstVisibleItem:" + firstVisibleItem + 106 | " visibleItemCount:" + visibleItemCount + 107 | " totalItemCount:" + totalItemCount); 108 | // our handling 109 | if (!mHasRequestedMore) { 110 | int lastInScreen = firstVisibleItem + visibleItemCount; 111 | if (lastInScreen >= totalItemCount) { 112 | Log.d(TAG, "onScroll lastInScreen - so load more"); 113 | mHasRequestedMore = true; 114 | onLoadMoreItems(); 115 | } 116 | } 117 | } 118 | 119 | private void onLoadMoreItems() { 120 | final ArrayList sampleData = SampleData.generateSampleData(); 121 | for (String data : sampleData) { 122 | mAdapter.add(data); 123 | } 124 | // stash all the data in our backing store 125 | mData.addAll(sampleData); 126 | // notify the adapter that we can update now 127 | mAdapter.notifyDataSetChanged(); 128 | mHasRequestedMore = false; 129 | } 130 | 131 | @Override 132 | public void onItemClick(AdapterView adapterView, View view, int position, long id) { 133 | Toast.makeText(this, "Item Clicked: " + position, Toast.LENGTH_SHORT).show(); 134 | } 135 | 136 | @Override 137 | public boolean onItemLongClick(AdapterView parent, View view, int position, long id) 138 | { 139 | Toast.makeText(this, "Item Long Clicked: " + position, Toast.LENGTH_SHORT).show(); 140 | return true; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /sample/src/main/java/com/etsy/android/sample/StaggeredGridActivityFragment.java: -------------------------------------------------------------------------------- 1 | package com.etsy.android.sample; 2 | 3 | import android.os.Bundle; 4 | import android.support.v4.app.Fragment; 5 | import android.support.v4.app.FragmentActivity; 6 | import android.support.v4.app.FragmentManager; 7 | import android.util.Log; 8 | import android.view.*; 9 | import android.widget.AbsListView; 10 | import android.widget.AdapterView; 11 | import android.widget.TextView; 12 | import android.widget.Toast; 13 | import com.etsy.android.grid.StaggeredGridView; 14 | 15 | import java.util.ArrayList; 16 | 17 | public class StaggeredGridActivityFragment extends FragmentActivity { 18 | 19 | private static final String TAG = "StaggeredGridActivityFragment"; 20 | 21 | @Override 22 | protected void onCreate(Bundle savedInstanceState) { 23 | super.onCreate(savedInstanceState); 24 | 25 | setTitle("SGV"); 26 | 27 | final FragmentManager fm = getSupportFragmentManager(); 28 | 29 | // Create the list fragment and add it as our sole content. 30 | if (fm.findFragmentById(android.R.id.content) == null) { 31 | final StaggeredGridFragment fragment = new StaggeredGridFragment(); 32 | fm.beginTransaction().add(android.R.id.content, fragment).commit(); 33 | } 34 | } 35 | 36 | 37 | private class StaggeredGridFragment extends Fragment implements 38 | AbsListView.OnScrollListener, AbsListView.OnItemClickListener { 39 | 40 | 41 | private StaggeredGridView mGridView; 42 | private boolean mHasRequestedMore; 43 | private SampleAdapter mAdapter; 44 | 45 | private ArrayList mData; 46 | 47 | @Override 48 | public void onCreate(final Bundle savedInstanceState) { 49 | super.onCreate(savedInstanceState); 50 | setRetainInstance(true); 51 | } 52 | 53 | @Override 54 | public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) { 55 | return inflater.inflate(R.layout.activity_sgv, container, false); 56 | } 57 | 58 | @Override 59 | public void onActivityCreated(final Bundle savedInstanceState) { 60 | super.onActivityCreated(savedInstanceState); 61 | 62 | mGridView = (StaggeredGridView) getView().findViewById(R.id.grid_view); 63 | 64 | if (savedInstanceState == null) { 65 | final LayoutInflater layoutInflater = getActivity().getLayoutInflater(); 66 | 67 | View header = layoutInflater.inflate(R.layout.list_item_header_footer, null); 68 | View footer = layoutInflater.inflate(R.layout.list_item_header_footer, null); 69 | TextView txtHeaderTitle = (TextView) header.findViewById(R.id.txt_title); 70 | TextView txtFooterTitle = (TextView) footer.findViewById(R.id.txt_title); 71 | txtHeaderTitle.setText("THE HEADER!"); 72 | txtFooterTitle.setText("THE FOOTER!"); 73 | 74 | mGridView.addHeaderView(header); 75 | mGridView.addFooterView(footer); 76 | } 77 | 78 | if (mAdapter == null) { 79 | mAdapter = new SampleAdapter(getActivity(), R.id.txt_line1); 80 | } 81 | 82 | if (mData == null) { 83 | mData = SampleData.generateSampleData(); 84 | } 85 | 86 | for (String data : mData) { 87 | mAdapter.add(data); 88 | } 89 | 90 | mGridView.setAdapter(mAdapter); 91 | mGridView.setOnScrollListener(this); 92 | mGridView.setOnItemClickListener(this); 93 | } 94 | 95 | @Override 96 | public void onScrollStateChanged(final AbsListView view, final int scrollState) { 97 | Log.d(TAG, "onScrollStateChanged:" + scrollState); 98 | } 99 | 100 | @Override 101 | public void onScroll(final AbsListView view, final int firstVisibleItem, final int visibleItemCount, final int totalItemCount) { 102 | Log.d(TAG, "onScroll firstVisibleItem:" + firstVisibleItem + 103 | " visibleItemCount:" + visibleItemCount + 104 | " totalItemCount:" + totalItemCount); 105 | // our handling 106 | if (!mHasRequestedMore) { 107 | int lastInScreen = firstVisibleItem + visibleItemCount; 108 | if (lastInScreen >= totalItemCount) { 109 | Log.d(TAG, "onScroll lastInScreen - so load more"); 110 | mHasRequestedMore = true; 111 | onLoadMoreItems(); 112 | } 113 | } 114 | } 115 | 116 | private void onLoadMoreItems() { 117 | final ArrayList sampleData = SampleData.generateSampleData(); 118 | for (String data : sampleData) { 119 | mAdapter.add(data); 120 | } 121 | // stash all the data in our backing store 122 | mData.addAll(sampleData); 123 | // notify the adapter that we can update now 124 | mAdapter.notifyDataSetChanged(); 125 | mHasRequestedMore = false; 126 | } 127 | 128 | @Override 129 | public void onItemClick(AdapterView adapterView, View view, int position, long id) { 130 | Toast.makeText(getActivity(), "Item Clicked: " + position, Toast.LENGTH_SHORT).show(); 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /sample/src/main/java/com/etsy/android/sample/StaggeredGridEmptyViewActivity.java: -------------------------------------------------------------------------------- 1 | package com.etsy.android.sample; 2 | 3 | import android.app.Activity; 4 | import android.os.AsyncTask; 5 | import android.os.Bundle; 6 | import android.os.SystemClock; 7 | import android.view.LayoutInflater; 8 | import android.view.Menu; 9 | import android.view.MenuItem; 10 | import android.view.View; 11 | import android.widget.AbsListView; 12 | import android.widget.AdapterView; 13 | import android.widget.TextView; 14 | import android.widget.Toast; 15 | 16 | import com.etsy.android.grid.StaggeredGridView; 17 | 18 | import java.util.ArrayList; 19 | 20 | public class StaggeredGridEmptyViewActivity extends Activity implements AbsListView.OnItemClickListener { 21 | 22 | public static final String SAVED_DATA_KEY = "SAVED_DATA"; 23 | private static final int FETCH_DATA_TASK_DURATION = 2000; 24 | 25 | private StaggeredGridView mGridView; 26 | private SampleAdapter mAdapter; 27 | 28 | private ArrayList mData; 29 | 30 | @Override 31 | protected void onCreate(Bundle savedInstanceState) { 32 | super.onCreate(savedInstanceState); 33 | setContentView(R.layout.activity_sgv_empy_view); 34 | 35 | setTitle("SGV"); 36 | mGridView = (StaggeredGridView) findViewById(R.id.grid_view); 37 | 38 | LayoutInflater layoutInflater = getLayoutInflater(); 39 | 40 | View header = layoutInflater.inflate(R.layout.list_item_header_footer, null); 41 | View footer = layoutInflater.inflate(R.layout.list_item_header_footer, null); 42 | TextView txtHeaderTitle = (TextView) header.findViewById(R.id.txt_title); 43 | TextView txtFooterTitle = (TextView) footer.findViewById(R.id.txt_title); 44 | txtHeaderTitle.setText("THE HEADER!"); 45 | txtFooterTitle.setText("THE FOOTER!"); 46 | 47 | mGridView.addHeaderView(header); 48 | mGridView.addFooterView(footer); 49 | mGridView.setEmptyView(findViewById(android.R.id.empty)); 50 | mAdapter = new SampleAdapter(this, R.id.txt_line1); 51 | 52 | // do we have saved data? 53 | if (savedInstanceState != null) { 54 | mData = savedInstanceState.getStringArrayList(SAVED_DATA_KEY); 55 | fillAdapter(); 56 | } 57 | 58 | if (mData == null) { 59 | mData = SampleData.generateSampleData(); 60 | } 61 | 62 | 63 | mGridView.setAdapter(mAdapter); 64 | 65 | mGridView.setOnItemClickListener(this); 66 | 67 | fetchData(); 68 | } 69 | 70 | private void fillAdapter() { 71 | for (String data : mData) { 72 | mAdapter.add(data); 73 | } 74 | } 75 | 76 | private void fetchData() { 77 | new AsyncTask() { 78 | @Override 79 | protected Void doInBackground(Void... params) { 80 | SystemClock.sleep(FETCH_DATA_TASK_DURATION); 81 | return null; 82 | } 83 | 84 | @Override 85 | protected void onPostExecute(Void aVoid) { 86 | fillAdapter(); 87 | } 88 | }.execute(); 89 | } 90 | 91 | @Override 92 | public boolean onCreateOptionsMenu(Menu menu) { 93 | getMenuInflater().inflate(R.menu.activity_sgv_empty_view, menu); 94 | return true; 95 | } 96 | 97 | @Override 98 | public boolean onOptionsItemSelected(MenuItem item) { 99 | mAdapter.clear(); 100 | fetchData(); 101 | return true; 102 | } 103 | 104 | @Override 105 | public void onItemClick(AdapterView adapterView, View view, int position, long id) { 106 | Toast.makeText(this, "Item Clicked: " + position, Toast.LENGTH_SHORT).show(); 107 | } 108 | 109 | @Override 110 | protected void onSaveInstanceState(final Bundle outState) { 111 | super.onSaveInstanceState(outState); 112 | outState.putStringArrayList(SAVED_DATA_KEY, mData); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etsy/AndroidStaggeredGrid/97739f6690a7676b62f0eb1246ae195ac98df4e2/sample/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etsy/AndroidStaggeredGrid/97739f6690a7676b62f0eb1246ae195ac98df4e2/sample/src/main/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etsy/AndroidStaggeredGrid/97739f6690a7676b62f0eb1246ae195ac98df4e2/sample/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable/list_item_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |   9 | 10 | 11 | 12 | 13 | 14 | 15 |   16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_list_view.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 |