├── .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