diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..5a07af7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,17 @@
+# Local configuration file (sdk path, etc)
+local.properties
+
+# Android Studio
+.idea/
+*.iml
+
+# Gradle
+.gradle
+build/
+
+# MacOS X DS_Store file
+.DS_Store
+
+# Google Maps API key files
+/sample/src/debug/res/values/google_maps_api.xml
+/sample/src/release/res/values/google_maps_api.xml
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..626c634
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,38 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+ repositories {
+ jcenter()
+ google()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.0.0'
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ jcenter()
+ maven {
+ url "https://maven.google.com"
+ }
+ google()
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
+
+ext {
+ minSdkVersion = 16
+ targetSdkVersion = 27
+ compileSdkVersion = 27
+ buildToolsVersion = '25.0.3'
+
+ supportVersion = '27.0.1'
+ playServicesVersion = '11.6.0'
+}
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..aac7c9b
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,17 @@
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..13372ae
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..c0d9564
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Fri Nov 10 21:28:24 CET 2017
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..9d82f78
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,160 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..8a0b282
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/library/build.gradle b/library/build.gradle
new file mode 100644
index 0000000..6885d29
--- /dev/null
+++ b/library/build.gradle
@@ -0,0 +1,30 @@
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion rootProject.compileSdkVersion
+ buildToolsVersion rootProject.buildToolsVersion
+
+ sourceSets {
+ main.res.srcDirs 'res', 'res-public'
+ }
+
+ defaultConfig {
+ minSdkVersion rootProject.minSdkVersion
+ targetSdkVersion rootProject.targetSdkVersion
+ versionCode 1
+ versionName "1.0"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+ buildToolsVersion '26.0.2'
+}
+
+dependencies {
+ implementation "com.android.support:support-annotations:$supportVersion"
+ implementation "com.google.android.gms:play-services-maps:$playServicesVersion"
+}
diff --git a/library/proguard-rules.pro b/library/proguard-rules.pro
new file mode 100644
index 0000000..725fef5
--- /dev/null
+++ b/library/proguard-rules.pro
@@ -0,0 +1,25 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /Users/makovkastar/Library/Android/sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/library/src/main/AndroidManifest.xml b/library/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..0d386b8
--- /dev/null
+++ b/library/src/main/AndroidManifest.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/library/src/main/java/net/sharewire/googlemapsclustering/Cluster.java b/library/src/main/java/net/sharewire/googlemapsclustering/Cluster.java
new file mode 100644
index 0000000..0462c9a
--- /dev/null
+++ b/library/src/main/java/net/sharewire/googlemapsclustering/Cluster.java
@@ -0,0 +1,65 @@
+package net.sharewire.googlemapsclustering;
+
+import android.support.annotation.NonNull;
+
+import java.util.List;
+
+public class Cluster {
+
+ private final double latitude;
+ private final double longitude;
+ private final List items;
+ private final double north;
+ private final double west;
+ private final double south;
+ private final double east;
+
+ Cluster(double latitude, double longitude, @NonNull List items,
+ double north, double west, double south, double east) {
+ this.latitude = latitude;
+ this.longitude = longitude;
+ this.items = items;
+ this.north = north;
+ this.west = west;
+ this.south = south;
+ this.east = east;
+ }
+
+ public double getLatitude() {
+ return latitude;
+ }
+
+ public double getLongitude() {
+ return longitude;
+ }
+
+ @NonNull
+ public List getItems() {
+ return items;
+ }
+
+ boolean contains(double latitude, double longitude) {
+ return longitude >= west && longitude <= east
+ && latitude <= north && latitude >= south;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Cluster cluster = (Cluster) o;
+ return Double.compare(cluster.latitude, latitude) == 0 &&
+ Double.compare(cluster.longitude, longitude) == 0;
+ }
+
+ @Override
+ public int hashCode() {
+ int result;
+ long temp;
+ temp = Double.doubleToLongBits(latitude);
+ result = (int) (temp ^ (temp >>> 32));
+ temp = Double.doubleToLongBits(longitude);
+ result = 31 * result + (int) (temp ^ (temp >>> 32));
+ return result;
+ }
+}
diff --git a/library/src/main/java/net/sharewire/googlemapsclustering/ClusterItem.java b/library/src/main/java/net/sharewire/googlemapsclustering/ClusterItem.java
new file mode 100644
index 0000000..16e6c49
--- /dev/null
+++ b/library/src/main/java/net/sharewire/googlemapsclustering/ClusterItem.java
@@ -0,0 +1,11 @@
+package net.sharewire.googlemapsclustering;
+
+import android.support.annotation.Nullable;
+
+public interface ClusterItem extends QuadTreePoint {
+ @Nullable
+ String getTitle();
+
+ @Nullable
+ String getSnippet();
+}
diff --git a/library/src/main/java/net/sharewire/googlemapsclustering/ClusterManager.java b/library/src/main/java/net/sharewire/googlemapsclustering/ClusterManager.java
new file mode 100644
index 0000000..96b8aed
--- /dev/null
+++ b/library/src/main/java/net/sharewire/googlemapsclustering/ClusterManager.java
@@ -0,0 +1,169 @@
+package net.sharewire.googlemapsclustering;
+
+import android.content.Context;
+import android.os.AsyncTask;
+import android.support.annotation.NonNull;
+
+import com.google.android.gms.maps.GoogleMap;
+import com.google.android.gms.maps.model.LatLngBounds;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ClusterManager implements GoogleMap.OnCameraIdleListener {
+
+ private static final int QUAD_TREE_BUCKET_CAPACITY = 4;
+
+ private final GoogleMap mGoogleMap;
+
+ private final QuadTree mQuadTree;
+
+ private final ClusterRenderer mRenderer;
+
+ private AsyncTask mQuadTreeTask;
+
+ private AsyncTask mClusterTask;
+
+ public interface Callbacks {
+ boolean onClusterClick(@NonNull Cluster cluster);
+
+ boolean onClusterItemClick(@NonNull T clusterItem);
+ }
+
+ public ClusterManager(@NonNull Context context, @NonNull GoogleMap googleMap) {
+ mGoogleMap = googleMap;
+ mRenderer = new ClusterRenderer<>(context, googleMap);
+ mQuadTree = new QuadTree<>(QUAD_TREE_BUCKET_CAPACITY);
+ }
+
+ public void setIconGenerator(@NonNull IconGenerator iconGenerator) {
+ mRenderer.setIconGenerator(iconGenerator);
+ }
+
+ public void setCallbacks(@NonNull Callbacks callbacks) {
+ mRenderer.setCallbacks(callbacks);
+ }
+
+ public void setItems(@NonNull List clusterItems) {
+ buildQuadTree(clusterItems);
+ }
+
+ @Override
+ public void onCameraIdle() {
+ cluster();
+ }
+
+ private void buildQuadTree(@NonNull List clusterItems) {
+ if (mQuadTreeTask != null) {
+ mQuadTreeTask.cancel(true);
+ }
+
+ mQuadTreeTask = new QuadTreeTask(clusterItems).execute();
+ }
+
+ private void cluster() {
+ if (mClusterTask != null) {
+ mClusterTask.cancel(true);
+ }
+
+ mClusterTask = new ClusterTask(mGoogleMap.getProjection().getVisibleRegion().latLngBounds,
+ mGoogleMap.getCameraPosition().zoom).execute();
+ }
+
+ @NonNull
+ private List> getClusters(@NonNull LatLngBounds latLngBounds, float zoomLevel) {
+ List> clusters = new ArrayList<>();
+
+ long tileCount = (long) (Math.pow(2, zoomLevel) * 2);
+
+ double startLatitude = latLngBounds.northeast.latitude;
+ double endLatitude = latLngBounds.southwest.latitude;
+
+ double startLongitude = latLngBounds.southwest.longitude;
+ double endLongitude = latLngBounds.northeast.longitude;
+
+ double stepLatitude = 180.0 / tileCount;
+ double stepLongitude = 360.0 / tileCount;
+
+ long startX = (long) ((startLongitude + 180.0) / stepLongitude);
+ long startY = (long) ((90.0 - startLatitude) / stepLatitude);
+
+ long endX = (long) ((endLongitude + 180.0) / stepLongitude) + 1;
+ long endY = (long) ((90.0 - endLatitude) / stepLatitude) + 1;
+
+ for (long tileX = startX; tileX <= endX; tileX++) {
+ for (long tileY = startY; tileY <= endY; tileY++) {
+ double north = 90.0 - tileY * stepLatitude;
+ double west = tileX * stepLongitude - 180.0;
+ double south = north - stepLatitude;
+ double east = west + stepLongitude;
+
+ List points = mQuadTree.queryRange(north, west, south, east);
+
+ if (points.size() > 0) {
+ double totalLatitude = 0;
+ double totalLongitude = 0;
+
+ for (QuadTreePoint point : points) {
+ totalLatitude += point.getLatitude();
+ totalLongitude += point.getLongitude();
+ }
+
+ double latitude = totalLatitude / points.size();
+ double longitude = totalLongitude / points.size();
+
+ clusters.add(new Cluster<>(latitude, longitude, points,
+ north, west, south, east));
+ }
+ }
+ }
+
+ return clusters;
+ }
+
+ private class QuadTreeTask extends AsyncTask {
+
+ private final List mClusterItems;
+
+ private QuadTreeTask(@NonNull List clusterItems) {
+ mClusterItems = clusterItems;
+ }
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ mQuadTree.clear();
+ for (T clusterItem : mClusterItems) {
+ mQuadTree.insert(clusterItem);
+ }
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void aVoid) {
+ cluster();
+ mQuadTreeTask = null;
+ }
+ }
+
+ private class ClusterTask extends AsyncTask>> {
+
+ private final LatLngBounds mLatLngBounds;
+ private final float mZoomLevel;
+
+ private ClusterTask(@NonNull LatLngBounds latLngBounds, float zoomLevel) {
+ mLatLngBounds = latLngBounds;
+ mZoomLevel = zoomLevel;
+ }
+
+ @Override
+ protected List> doInBackground(Void... params) {
+ return getClusters(mLatLngBounds, mZoomLevel);
+ }
+
+ @Override
+ protected void onPostExecute(@NonNull List> clusters) {
+ mRenderer.render(clusters);
+ mClusterTask = null;
+ }
+ }
+}
diff --git a/library/src/main/java/net/sharewire/googlemapsclustering/ClusterRenderer.java b/library/src/main/java/net/sharewire/googlemapsclustering/ClusterRenderer.java
new file mode 100644
index 0000000..e4aa571
--- /dev/null
+++ b/library/src/main/java/net/sharewire/googlemapsclustering/ClusterRenderer.java
@@ -0,0 +1,210 @@
+package net.sharewire.googlemapsclustering;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.animation.TypeEvaluator;
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import com.google.android.gms.maps.GoogleMap;
+import com.google.android.gms.maps.model.BitmapDescriptor;
+import com.google.android.gms.maps.model.LatLng;
+import com.google.android.gms.maps.model.Marker;
+import com.google.android.gms.maps.model.MarkerOptions;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+class ClusterRenderer implements GoogleMap.OnMarkerClickListener {
+
+ private static final int BACKGROUND_MARKER_Z_INDEX = 0;
+
+ private static final int FOREGROUND_MARKER_Z_INDEX = 1;
+
+ private final GoogleMap mGoogleMap;
+
+ private final List> mClusters = new ArrayList<>();
+
+ private final Map, Marker> mMarkers = new HashMap<>();
+
+ private IconGenerator mIconGenerator;
+
+ private ClusterManager.Callbacks mCallbacks;
+
+ ClusterRenderer(@NonNull Context context, @NonNull GoogleMap googleMap) {
+ mGoogleMap = googleMap;
+ mGoogleMap.setOnMarkerClickListener(this);
+ mIconGenerator = new DefaultIconGenerator<>(context);
+ }
+
+ @Override
+ public boolean onMarkerClick(Marker marker) {
+ Object markerTag = marker.getTag();
+ if (markerTag instanceof Cluster) {
+ //noinspection unchecked
+ Cluster cluster = (Cluster) marker.getTag();
+ //noinspection ConstantConditions
+ List clusterItems = cluster.getItems();
+
+ if (clusterItems.size() > 1) {
+ return mCallbacks.onClusterClick(cluster);
+ } else {
+ return mCallbacks.onClusterItemClick(clusterItems.get(0));
+ }
+ } else {
+ return false;
+ }
+ }
+
+ void setCallbacks(@NonNull ClusterManager.Callbacks listener) {
+ mCallbacks = listener;
+ }
+
+ void setIconGenerator(@NonNull IconGenerator iconGenerator) {
+ mIconGenerator = iconGenerator;
+ }
+
+ void render(@NonNull List> clusters) {
+ List> clustersToAdd = new ArrayList<>();
+ List> clustersToRemove = new ArrayList<>();
+
+ for (Cluster cluster : clusters) {
+ if (!mMarkers.containsKey(cluster)) {
+ clustersToAdd.add(cluster);
+ }
+ }
+
+ for (Cluster cluster : mMarkers.keySet()) {
+ if (!clusters.contains(cluster)) {
+ clustersToRemove.add(cluster);
+ }
+ }
+
+ mClusters.addAll(clustersToAdd);
+ mClusters.removeAll(clustersToRemove);
+
+ // Remove the old clusters.
+ for (Cluster clusterToRemove : clustersToRemove) {
+ Marker markerToRemove = mMarkers.get(clusterToRemove);
+ markerToRemove.setZIndex(BACKGROUND_MARKER_Z_INDEX);
+
+ Cluster parentCluster = findParentCluster(mClusters, clusterToRemove.getLatitude(),
+ clusterToRemove.getLongitude());
+ if (parentCluster != null) {
+ animateMarkerToLocation(markerToRemove, new LatLng(parentCluster.getLatitude(),
+ parentCluster.getLongitude()), true);
+ } else {
+ markerToRemove.remove();
+ }
+
+ mMarkers.remove(clusterToRemove);
+ }
+
+ // Add the new clusters.
+ for (Cluster clusterToAdd : clustersToAdd) {
+ Marker markerToAdd;
+
+ BitmapDescriptor markerIcon = getMarkerIcon(clusterToAdd);
+ String markerTitle = getMarkerTitle(clusterToAdd);
+ String markerSnippet = getMarkerSnippet(clusterToAdd);
+
+ Cluster parentCluster = findParentCluster(clustersToRemove, clusterToAdd.getLatitude(),
+ clusterToAdd.getLongitude());
+ if (parentCluster != null) {
+ markerToAdd = mGoogleMap.addMarker(new MarkerOptions()
+ .position(new LatLng(parentCluster.getLatitude(), parentCluster.getLongitude()))
+ .icon(markerIcon)
+ .title(markerTitle)
+ .snippet(markerSnippet)
+ .zIndex(FOREGROUND_MARKER_Z_INDEX));
+ animateMarkerToLocation(markerToAdd,
+ new LatLng(clusterToAdd.getLatitude(), clusterToAdd.getLongitude()), false);
+ } else {
+ markerToAdd = mGoogleMap.addMarker(new MarkerOptions()
+ .position(new LatLng(clusterToAdd.getLatitude(), clusterToAdd.getLongitude()))
+ .icon(markerIcon)
+ .title(markerTitle)
+ .snippet(markerSnippet)
+ .zIndex(FOREGROUND_MARKER_Z_INDEX));
+ }
+ markerToAdd.setTag(clusterToAdd);
+
+ mMarkers.put(clusterToAdd, markerToAdd);
+ }
+ }
+
+ @NonNull
+ private BitmapDescriptor getMarkerIcon(@NonNull Cluster cluster) {
+ BitmapDescriptor clusterIcon;
+
+ List clusterItems = cluster.getItems();
+ if (clusterItems.size() > 1) {
+ clusterIcon = mIconGenerator.getClusterIcon(cluster);
+ } else {
+ clusterIcon = mIconGenerator.getClusterItemIcon(clusterItems.get(0));
+ }
+
+ return clusterIcon;
+ }
+
+ @Nullable
+ private String getMarkerTitle(@NonNull Cluster cluster) {
+ List clusterItems = cluster.getItems();
+ if (clusterItems.size() > 1) {
+ return null;
+ } else {
+ return clusterItems.get(0).getTitle();
+ }
+ }
+
+ @Nullable
+ private String getMarkerSnippet(@NonNull Cluster cluster) {
+ List clusterItems = cluster.getItems();
+ if (clusterItems.size() > 1) {
+ return null;
+ } else {
+ return clusterItems.get(0).getSnippet();
+ }
+ }
+
+ @Nullable
+ private Cluster findParentCluster(@NonNull List> clusters,
+ double latitude, double longitude) {
+ for (Cluster cluster : clusters) {
+ if (cluster.contains(latitude, longitude)) {
+ return cluster;
+ }
+ }
+
+ return null;
+ }
+
+ private void animateMarkerToLocation(@NonNull final Marker marker, @NonNull LatLng targetLocation,
+ final boolean removeAfter) {
+ ObjectAnimator objectAnimator = ObjectAnimator.ofObject(marker, "position",
+ new LatLngTypeEvaluator(), targetLocation);
+ objectAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (removeAfter) {
+ marker.remove();
+ }
+ }
+ });
+ objectAnimator.start();
+ }
+
+ private static class LatLngTypeEvaluator implements TypeEvaluator {
+
+ @Override
+ public LatLng evaluate(float fraction, LatLng startValue, LatLng endValue) {
+ double latitude = (endValue.latitude - startValue.latitude) * fraction + startValue.latitude;
+ double longitude = (endValue.longitude - startValue.longitude) * fraction + startValue.longitude;
+ return new LatLng(latitude, longitude);
+ }
+ }
+}
diff --git a/library/src/main/java/net/sharewire/googlemapsclustering/DefaultIconGenerator.java b/library/src/main/java/net/sharewire/googlemapsclustering/DefaultIconGenerator.java
new file mode 100644
index 0000000..40012f6
--- /dev/null
+++ b/library/src/main/java/net/sharewire/googlemapsclustering/DefaultIconGenerator.java
@@ -0,0 +1,127 @@
+package net.sharewire.googlemapsclustering;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
+import android.support.annotation.NonNull;
+import android.util.SparseArray;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.TextView;
+
+import com.google.android.gms.maps.model.BitmapDescriptor;
+import com.google.android.gms.maps.model.BitmapDescriptorFactory;
+
+public class DefaultIconGenerator implements IconGenerator {
+
+ private static final int[] CLUSTER_ICON_BUCKETS = {10, 20, 50, 100, 500, 1000, 5000, 10000, 20000};
+
+ private final Context mContext;
+
+ private IconStyle mIconStyle;
+
+ private final SparseArray mClusterIcons = new SparseArray<>();
+
+ private BitmapDescriptor mClusterItemIcon;
+
+ public DefaultIconGenerator(@NonNull Context context) {
+ mContext = context;
+ setIconStyle(createDefaultIconStyle());
+ }
+
+ public void setIconStyle(@NonNull IconStyle iconStyle) {
+ mIconStyle = iconStyle;
+ }
+
+ @NonNull
+ public BitmapDescriptor getClusterIcon(@NonNull Cluster cluster) {
+ int clusterBucket = getClusterIconBucket(cluster);
+ BitmapDescriptor clusterIcon = mClusterIcons.get(clusterBucket);
+
+ if (clusterIcon == null) {
+ clusterIcon = createClusterIcon(clusterBucket);
+ mClusterIcons.put(clusterBucket, clusterIcon);
+ }
+
+ return clusterIcon;
+ }
+
+ @NonNull
+ @Override
+ public BitmapDescriptor getClusterItemIcon(@NonNull T clusterItem) {
+ if (mClusterItemIcon == null) {
+ mClusterItemIcon = createClusterItemIcon();
+ }
+ return mClusterItemIcon;
+ }
+
+ @NonNull
+ private IconStyle createDefaultIconStyle() {
+ return new IconStyle.Builder(mContext).build();
+ }
+
+ @NonNull
+ private BitmapDescriptor createClusterIcon(int clusterBucket) {
+ @SuppressLint("InflateParams")
+ TextView clusterIconView = (TextView) LayoutInflater.from(mContext)
+ .inflate(R.layout.map_cluster_icon, null);
+ clusterIconView.setBackground(createClusterBackground());
+ clusterIconView.setTextColor(mIconStyle.getClusterTextColor());
+ clusterIconView.setTextSize(TypedValue.COMPLEX_UNIT_PX,
+ mIconStyle.getClusterTextSize());
+
+ clusterIconView.setText(getClusterIconText(clusterBucket));
+
+ clusterIconView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
+ clusterIconView.layout(0, 0, clusterIconView.getMeasuredWidth(),
+ clusterIconView.getMeasuredHeight());
+
+ Bitmap iconBitmap = Bitmap.createBitmap(clusterIconView.getMeasuredWidth(),
+ clusterIconView.getMeasuredHeight(), Bitmap.Config.ARGB_8888);
+
+ Canvas canvas = new Canvas(iconBitmap);
+ clusterIconView.draw(canvas);
+
+ return BitmapDescriptorFactory.fromBitmap(iconBitmap);
+ }
+
+ @NonNull
+ private Drawable createClusterBackground() {
+ GradientDrawable gradientDrawable = new GradientDrawable();
+ gradientDrawable.setShape(GradientDrawable.OVAL);
+ gradientDrawable.setColor(mIconStyle.getClusterBackgroundColor());
+ gradientDrawable.setStroke(mIconStyle.getClusterStrokeWidth(),
+ mIconStyle.getClusterStrokeColor());
+ return gradientDrawable;
+ }
+
+ @NonNull
+ private BitmapDescriptor createClusterItemIcon() {
+ return BitmapDescriptorFactory.fromResource(mIconStyle.getClusterIconResId());
+ }
+
+ private int getClusterIconBucket(@NonNull Cluster cluster) {
+ int itemCount = cluster.getItems().size();
+ if (itemCount <= CLUSTER_ICON_BUCKETS[0]) {
+ return itemCount;
+ }
+
+ for (int i = 0; i < CLUSTER_ICON_BUCKETS.length - 1; i++) {
+ if (itemCount < CLUSTER_ICON_BUCKETS[i + 1]) {
+ return CLUSTER_ICON_BUCKETS[i];
+ }
+ }
+
+ return CLUSTER_ICON_BUCKETS[CLUSTER_ICON_BUCKETS.length - 1];
+ }
+
+ @NonNull
+ private String getClusterIconText(int clusterIconBucket) {
+ return (clusterIconBucket < CLUSTER_ICON_BUCKETS[0]) ?
+ String.valueOf(clusterIconBucket) : String.valueOf(clusterIconBucket) + "+";
+ }
+}
diff --git a/library/src/main/java/net/sharewire/googlemapsclustering/IconGenerator.java b/library/src/main/java/net/sharewire/googlemapsclustering/IconGenerator.java
new file mode 100644
index 0000000..97c489a
--- /dev/null
+++ b/library/src/main/java/net/sharewire/googlemapsclustering/IconGenerator.java
@@ -0,0 +1,13 @@
+package net.sharewire.googlemapsclustering;
+
+import android.support.annotation.NonNull;
+
+import com.google.android.gms.maps.model.BitmapDescriptor;
+
+public interface IconGenerator {
+ @NonNull
+ BitmapDescriptor getClusterIcon(@NonNull Cluster cluster);
+
+ @NonNull
+ BitmapDescriptor getClusterItemIcon(@NonNull T clusterItem);
+}
diff --git a/library/src/main/java/net/sharewire/googlemapsclustering/IconStyle.java b/library/src/main/java/net/sharewire/googlemapsclustering/IconStyle.java
new file mode 100644
index 0000000..02fa526
--- /dev/null
+++ b/library/src/main/java/net/sharewire/googlemapsclustering/IconStyle.java
@@ -0,0 +1,113 @@
+package net.sharewire.googlemapsclustering;
+
+import android.content.Context;
+import android.support.annotation.ColorInt;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.NonNull;
+import android.support.v4.content.ContextCompat;
+
+public class IconStyle {
+
+ private final int clusterBackgroundColor;
+ private final int clusterTextColor;
+ private final int clusterStrokeColor;
+ private final int clusterStrokeWidth;
+ private final int clusterTextSize;
+ private final int clusterIconResId;
+
+ private IconStyle(@NonNull Builder builder) {
+ clusterBackgroundColor = builder.clusterBackgroundColor;
+ clusterTextColor = builder.clusterTextColor;
+ clusterStrokeColor = builder.clusterStrokeColor;
+ clusterStrokeWidth = builder.clusterStrokeWidth;
+ clusterTextSize = builder.clusterTextSize;
+ clusterIconResId = builder.clusterIconResId;
+ }
+
+ @ColorInt
+ public int getClusterBackgroundColor() {
+ return clusterBackgroundColor;
+ }
+
+ @ColorInt
+ public int getClusterTextColor() {
+ return clusterTextColor;
+ }
+
+ @ColorInt
+ public int getClusterStrokeColor() {
+ return clusterStrokeColor;
+ }
+
+ public int getClusterStrokeWidth() {
+ return clusterStrokeWidth;
+ }
+
+ public int getClusterTextSize() {
+ return clusterTextSize;
+ }
+
+ @DrawableRes
+ public int getClusterIconResId() {
+ return clusterIconResId;
+ }
+
+ public static class Builder {
+
+ private int clusterBackgroundColor;
+ private int clusterTextColor;
+ private int clusterStrokeColor;
+ private int clusterStrokeWidth;
+ private int clusterTextSize;
+ private int clusterIconResId;
+
+ public Builder(@NonNull Context context) {
+ clusterBackgroundColor = ContextCompat.getColor(
+ context, R.color.cluster_background);
+ clusterTextColor = ContextCompat.getColor(
+ context, R.color.cluster_text);
+ clusterStrokeColor = ContextCompat.getColor(
+ context, R.color.cluster_stroke);
+ clusterStrokeWidth = context.getResources()
+ .getDimensionPixelSize(R.dimen.cluster_stroke_width);
+ clusterTextSize = context.getResources()
+ .getDimensionPixelSize(R.dimen.cluster_text_size);
+ clusterIconResId = R.drawable.ic_map_marker;
+ }
+
+ public Builder setClusterBackgroundColor(@ColorInt int color) {
+ clusterBackgroundColor = color;
+ return this;
+ }
+
+ public Builder setClusterTextColor(@ColorInt int color) {
+ clusterTextColor = color;
+ return this;
+ }
+
+ public Builder setClusterStrokeColor(@ColorInt int color) {
+ clusterStrokeColor = color;
+ return this;
+ }
+
+ public Builder setClusterStrokeWidth(int width) {
+ clusterStrokeWidth = width;
+ return this;
+ }
+
+ public Builder setClusterTextSize(int size) {
+ clusterTextSize = size;
+ return this;
+ }
+
+ public Builder setClusterIconResId(@DrawableRes int resId) {
+ clusterIconResId = resId;
+ return this;
+ }
+
+ @NonNull
+ public IconStyle build() {
+ return new IconStyle(this);
+ }
+ }
+}
diff --git a/library/src/main/java/net/sharewire/googlemapsclustering/QuadTree.java b/library/src/main/java/net/sharewire/googlemapsclustering/QuadTree.java
new file mode 100644
index 0000000..ff89bd5
--- /dev/null
+++ b/library/src/main/java/net/sharewire/googlemapsclustering/QuadTree.java
@@ -0,0 +1,38 @@
+package net.sharewire.googlemapsclustering;
+
+import android.support.annotation.NonNull;
+
+import java.util.ArrayList;
+import java.util.List;
+
+class QuadTree {
+
+ private final int bucketSize;
+
+ private QuadTreeNode root;
+
+ QuadTree(int bucketSize) {
+ this.bucketSize = bucketSize;
+ this.root = createRootNode(bucketSize);
+ }
+
+ void insert(@NonNull T point) {
+ root.insert(point);
+ }
+
+ @NonNull
+ List queryRange(double north, double west, double south, double east) {
+ List points = new ArrayList<>();
+ root.queryRange(new QuadTreeRect(north, west, south, east), points);
+ return points;
+ }
+
+ void clear() {
+ root = createRootNode(bucketSize);
+ }
+
+ @NonNull
+ private QuadTreeNode createRootNode(int bucketSize) {
+ return new QuadTreeNode<>(90.0, -180.0, -90.0, 180.0, bucketSize);
+ }
+}
diff --git a/library/src/main/java/net/sharewire/googlemapsclustering/QuadTreeNode.java b/library/src/main/java/net/sharewire/googlemapsclustering/QuadTreeNode.java
new file mode 100644
index 0000000..3933bf1
--- /dev/null
+++ b/library/src/main/java/net/sharewire/googlemapsclustering/QuadTreeNode.java
@@ -0,0 +1,92 @@
+package net.sharewire.googlemapsclustering;
+
+import android.support.annotation.NonNull;
+
+import java.util.ArrayList;
+import java.util.List;
+
+class QuadTreeNode {
+
+ private final QuadTreeRect bounds;
+ private final List points;
+ private QuadTreeNode northWest;
+ private QuadTreeNode northEast;
+ private QuadTreeNode southWest;
+ private QuadTreeNode southEast;
+ private int bucketSize;
+
+ QuadTreeNode(double north, double west, double south, double east, int bucketSize) {
+ this.bounds = new QuadTreeRect(north, west, south, east);
+ this.points = new ArrayList<>(bucketSize);
+ this.bucketSize = bucketSize;
+ }
+
+ boolean insert(@NonNull T point) {
+ // Ignore objects that do not belong in this quad tree.
+ if (!bounds.contains(point.getLatitude(), point.getLongitude())) {
+ return false;
+ }
+
+ // If there is space in this quad tree, add the object here.
+ if (points.size() < bucketSize) {
+ points.add(point);
+ return true;
+ }
+
+ // Otherwise, subdivide and then add the point to whichever node will accept it.
+ if (northWest == null) {
+ subdivide();
+ }
+
+ if (northWest.insert(point)) {
+ return true;
+ }
+ if (northEast.insert(point)) {
+ return true;
+ }
+ if (southWest.insert(point)) {
+ return true;
+ }
+ if (southEast.insert(point)) {
+ return true;
+ }
+
+ // Otherwise, the point cannot be inserted for some unknown reason (this should never happen).
+ return false;
+ }
+
+ void queryRange(@NonNull QuadTreeRect range, @NonNull List pointsInRange) {
+ // Automatically abort if the range does not intersect this quad.
+ if (!bounds.intersects(range)) {
+ return;
+ }
+
+ // Check objects at this quad level.
+ for (T point : points) {
+ if (range.contains(point.getLatitude(), point.getLongitude())) {
+ pointsInRange.add(point);
+ }
+ }
+
+ // Terminate here, if there are no children.
+ if (northWest == null) {
+ return;
+ }
+
+ // Otherwise, add the points from the children.
+ northWest.queryRange(range, pointsInRange);
+ northEast.queryRange(range, pointsInRange);
+ southWest.queryRange(range, pointsInRange);
+ southEast.queryRange(range, pointsInRange);
+ }
+
+ private void subdivide() {
+ double northSouthHalf = bounds.north - (bounds.north - bounds.south) / 2.0;
+ double eastWestHalf = bounds.east - (bounds.east - bounds.west) / 2.0;
+
+ northWest = new QuadTreeNode<>(bounds.north, bounds.west, northSouthHalf, eastWestHalf, bucketSize);
+ northEast = new QuadTreeNode<>(bounds.north, eastWestHalf, northSouthHalf, bounds.east, bucketSize);
+ southWest = new QuadTreeNode<>(northSouthHalf, bounds.west, bounds.south, eastWestHalf, bucketSize);
+ southEast = new QuadTreeNode<>(northSouthHalf, eastWestHalf, bounds.south, bounds.east, bucketSize);
+ }
+}
diff --git a/library/src/main/java/net/sharewire/googlemapsclustering/QuadTreePoint.java b/library/src/main/java/net/sharewire/googlemapsclustering/QuadTreePoint.java
new file mode 100644
index 0000000..db9d766
--- /dev/null
+++ b/library/src/main/java/net/sharewire/googlemapsclustering/QuadTreePoint.java
@@ -0,0 +1,7 @@
+package net.sharewire.googlemapsclustering;
+
+public interface QuadTreePoint {
+ double getLatitude();
+
+ double getLongitude();
+}
diff --git a/library/src/main/java/net/sharewire/googlemapsclustering/QuadTreeRect.java b/library/src/main/java/net/sharewire/googlemapsclustering/QuadTreeRect.java
new file mode 100644
index 0000000..2fb4321
--- /dev/null
+++ b/library/src/main/java/net/sharewire/googlemapsclustering/QuadTreeRect.java
@@ -0,0 +1,26 @@
+package net.sharewire.googlemapsclustering;
+
+import android.support.annotation.NonNull;
+
+class QuadTreeRect {
+
+ final double north;
+ final double west;
+ final double south;
+ final double east;
+
+ QuadTreeRect(double north, double west, double south, double east) {
+ this.north = north;
+ this.west = west;
+ this.south = south;
+ this.east = east;
+ }
+
+ boolean contains(double latitude, double longitude) {
+ return longitude >= west && longitude <= east && latitude <= north && latitude >= south;
+ }
+
+ boolean intersects(@NonNull QuadTreeRect bounds) {
+ return west <= bounds.east && east >= bounds.west && north >= bounds.south && south <= bounds.north;
+ }
+}
diff --git a/library/src/main/java/net/sharewire/googlemapsclustering/SquareTextView.java b/library/src/main/java/net/sharewire/googlemapsclustering/SquareTextView.java
new file mode 100644
index 0000000..33007ae
--- /dev/null
+++ b/library/src/main/java/net/sharewire/googlemapsclustering/SquareTextView.java
@@ -0,0 +1,20 @@
+package net.sharewire.googlemapsclustering;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.TextView;
+
+class SquareTextView extends TextView {
+
+ SquareTextView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ int measuredWidth = getMeasuredWidth();
+ //noinspection SuspiciousNameCombination
+ setMeasuredDimension(measuredWidth, measuredWidth);
+ }
+}
diff --git a/library/src/main/res/drawable-hdpi/ic_map_marker.png b/library/src/main/res/drawable-hdpi/ic_map_marker.png
new file mode 100644
index 0000000..a0e47d8
Binary files /dev/null and b/library/src/main/res/drawable-hdpi/ic_map_marker.png differ
diff --git a/library/src/main/res/drawable-mdpi/ic_map_marker.png b/library/src/main/res/drawable-mdpi/ic_map_marker.png
new file mode 100644
index 0000000..3751012
Binary files /dev/null and b/library/src/main/res/drawable-mdpi/ic_map_marker.png differ
diff --git a/library/src/main/res/drawable-xhdpi/ic_map_marker.png b/library/src/main/res/drawable-xhdpi/ic_map_marker.png
new file mode 100644
index 0000000..0795141
Binary files /dev/null and b/library/src/main/res/drawable-xhdpi/ic_map_marker.png differ
diff --git a/library/src/main/res/drawable-xxhdpi/ic_map_marker.png b/library/src/main/res/drawable-xxhdpi/ic_map_marker.png
new file mode 100644
index 0000000..f2c783d
Binary files /dev/null and b/library/src/main/res/drawable-xxhdpi/ic_map_marker.png differ
diff --git a/library/src/main/res/drawable-xxxhdpi/ic_map_marker.png b/library/src/main/res/drawable-xxxhdpi/ic_map_marker.png
new file mode 100644
index 0000000..1beb6fc
Binary files /dev/null and b/library/src/main/res/drawable-xxxhdpi/ic_map_marker.png differ
diff --git a/library/src/main/res/drawable/bg_map_cluster_icon.xml b/library/src/main/res/drawable/bg_map_cluster_icon.xml
new file mode 100644
index 0000000..c35ce14
--- /dev/null
+++ b/library/src/main/res/drawable/bg_map_cluster_icon.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/library/src/main/res/layout/map_cluster_icon.xml b/library/src/main/res/layout/map_cluster_icon.xml
new file mode 100644
index 0000000..7aaf691
--- /dev/null
+++ b/library/src/main/res/layout/map_cluster_icon.xml
@@ -0,0 +1,11 @@
+
+
diff --git a/library/src/main/res/values/colors.xml b/library/src/main/res/values/colors.xml
new file mode 100644
index 0000000..39ec70e
--- /dev/null
+++ b/library/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #E64A3C
+ #FFFFFF
+ #FFFFFF
+
\ No newline at end of file
diff --git a/library/src/main/res/values/dimens.xml b/library/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..c01ee44
--- /dev/null
+++ b/library/src/main/res/values/dimens.xml
@@ -0,0 +1,7 @@
+
+
+ 1dp
+ 14sp
+ 32dp
+ 8dp
+
\ No newline at end of file
diff --git a/library/src/res-public/values/public.xml b/library/src/res-public/values/public.xml
new file mode 100644
index 0000000..e69de29
diff --git a/sample/build.gradle b/sample/build.gradle
new file mode 100644
index 0000000..7973e3c
--- /dev/null
+++ b/sample/build.gradle
@@ -0,0 +1,29 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion rootProject.compileSdkVersion
+ buildToolsVersion rootProject.buildToolsVersion
+
+ defaultConfig {
+ applicationId "net.sharewire.googlemapsclustering.sample"
+ minSdkVersion rootProject.minSdkVersion
+ targetSdkVersion rootProject.targetSdkVersion
+ versionCode 1
+ versionName "1.0"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+ buildToolsVersion '26.0.2'
+}
+
+dependencies {
+ implementation project(':library')
+ implementation "com.android.support:support-v4:$supportVersion"
+ implementation "com.android.support:appcompat-v7:$supportVersion"
+ implementation "com.google.android.gms:play-services-maps:$playServicesVersion"
+}
diff --git a/sample/proguard-rules.pro b/sample/proguard-rules.pro
new file mode 100644
index 0000000..725fef5
--- /dev/null
+++ b/sample/proguard-rules.pro
@@ -0,0 +1,25 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /Users/makovkastar/Library/Android/sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..be745b8
--- /dev/null
+++ b/sample/src/main/AndroidManifest.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sample/src/main/java/com/sharewire/googlemapsclustering/sample/MapsActivity.java b/sample/src/main/java/com/sharewire/googlemapsclustering/sample/MapsActivity.java
new file mode 100644
index 0000000..0dc7348
--- /dev/null
+++ b/sample/src/main/java/com/sharewire/googlemapsclustering/sample/MapsActivity.java
@@ -0,0 +1,73 @@
+package com.sharewire.googlemapsclustering.sample;
+
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v4.app.FragmentActivity;
+import android.util.Log;
+
+import com.google.android.gms.maps.CameraUpdateFactory;
+import com.google.android.gms.maps.GoogleMap;
+import com.google.android.gms.maps.OnMapReadyCallback;
+import com.google.android.gms.maps.SupportMapFragment;
+import com.google.android.gms.maps.model.LatLng;
+import com.google.android.gms.maps.model.LatLngBounds;
+
+import net.sharewire.googlemapsclustering.Cluster;
+import net.sharewire.googlemapsclustering.ClusterManager;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class MapsActivity extends FragmentActivity implements OnMapReadyCallback {
+
+ private static final String TAG = MapsActivity.class.getSimpleName();
+
+ private static final LatLngBounds NETHERLANDS = new LatLngBounds(
+ new LatLng(50.77083, 3.57361), new LatLng(53.35917, 7.10833));
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_maps);
+ setupMapFragment();
+ }
+
+ @Override
+ public void onMapReady(final GoogleMap googleMap) {
+ googleMap.setOnMapLoadedCallback(new GoogleMap.OnMapLoadedCallback() {
+ @Override
+ public void onMapLoaded() {
+ googleMap.animateCamera(CameraUpdateFactory.newLatLngBounds(NETHERLANDS, 0));
+ }
+ });
+
+ ClusterManager clusterManager = new ClusterManager<>(this, googleMap);
+ clusterManager.setCallbacks(new ClusterManager.Callbacks() {
+ @Override
+ public boolean onClusterClick(@NonNull Cluster cluster) {
+ Log.d(TAG, "onClusterClick");
+ return false;
+ }
+
+ @Override
+ public boolean onClusterItemClick(@NonNull SampleClusterItem clusterItem) {
+ Log.d(TAG, "onClusterItemClick");
+ return false;
+ }
+ });
+ googleMap.setOnCameraIdleListener(clusterManager);
+
+ List clusterItems = new ArrayList<>();
+ for (int i = 0; i < 20000; i++) {
+ clusterItems.add(new SampleClusterItem(
+ RandomLocationGenerator.generate(NETHERLANDS)));
+ }
+ clusterManager.setItems(clusterItems);
+ }
+
+ private void setupMapFragment() {
+ SupportMapFragment mapFragment = (SupportMapFragment) getSupportFragmentManager()
+ .findFragmentById(R.id.map);
+ mapFragment.getMapAsync(this);
+ }
+}
diff --git a/sample/src/main/java/com/sharewire/googlemapsclustering/sample/RandomLocationGenerator.java b/sample/src/main/java/com/sharewire/googlemapsclustering/sample/RandomLocationGenerator.java
new file mode 100644
index 0000000..a009cd6
--- /dev/null
+++ b/sample/src/main/java/com/sharewire/googlemapsclustering/sample/RandomLocationGenerator.java
@@ -0,0 +1,27 @@
+package com.sharewire.googlemapsclustering.sample;
+
+import android.support.annotation.NonNull;
+
+import com.google.android.gms.maps.model.LatLng;
+import com.google.android.gms.maps.model.LatLngBounds;
+
+import java.util.Random;
+
+final class RandomLocationGenerator {
+
+ private static final Random RANDOM = new Random();
+
+ @NonNull
+ static LatLng generate(@NonNull LatLngBounds bounds) {
+ double minLatitude = bounds.southwest.latitude;
+ double maxLatitude = bounds.northeast.latitude;
+ double minLongitude = bounds.southwest.longitude;
+ double maxLongitude = bounds.northeast.longitude;
+ return new LatLng(
+ minLatitude + (maxLatitude - minLatitude) * RANDOM.nextDouble(),
+ minLongitude + (maxLongitude - minLongitude) * RANDOM.nextDouble());
+ }
+
+ private RandomLocationGenerator() {
+ }
+}
diff --git a/sample/src/main/java/com/sharewire/googlemapsclustering/sample/SampleClusterItem.java b/sample/src/main/java/com/sharewire/googlemapsclustering/sample/SampleClusterItem.java
new file mode 100644
index 0000000..3e72ad9
--- /dev/null
+++ b/sample/src/main/java/com/sharewire/googlemapsclustering/sample/SampleClusterItem.java
@@ -0,0 +1,39 @@
+package com.sharewire.googlemapsclustering.sample;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import com.google.android.gms.maps.model.LatLng;
+
+import net.sharewire.googlemapsclustering.ClusterItem;
+
+class SampleClusterItem implements ClusterItem {
+
+ private final LatLng location;
+
+ SampleClusterItem(@NonNull LatLng location) {
+ this.location = location;
+ }
+
+ @Override
+ public double getLatitude() {
+ return location.latitude;
+ }
+
+ @Override
+ public double getLongitude() {
+ return location.longitude;
+ }
+
+ @Nullable
+ @Override
+ public String getTitle() {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public String getSnippet() {
+ return null;
+ }
+}
diff --git a/sample/src/main/res/drawable-xhdpi/ic_new_map_marker.png b/sample/src/main/res/drawable-xhdpi/ic_new_map_marker.png
new file mode 100644
index 0000000..9046f6d
Binary files /dev/null and b/sample/src/main/res/drawable-xhdpi/ic_new_map_marker.png differ
diff --git a/sample/src/main/res/layout/activity_maps.xml b/sample/src/main/res/layout/activity_maps.xml
new file mode 100644
index 0000000..5e9dc7f
--- /dev/null
+++ b/sample/src/main/res/layout/activity_maps.xml
@@ -0,0 +1,7 @@
+
diff --git a/sample/src/main/res/mipmap-hdpi/ic_launcher.png b/sample/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..cde69bc
Binary files /dev/null and b/sample/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/sample/src/main/res/mipmap-hdpi/ic_launcher_round.png b/sample/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..9a078e3
Binary files /dev/null and b/sample/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/sample/src/main/res/mipmap-mdpi/ic_launcher.png b/sample/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..c133a0c
Binary files /dev/null and b/sample/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/sample/src/main/res/mipmap-mdpi/ic_launcher_round.png b/sample/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..efc028a
Binary files /dev/null and b/sample/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/sample/src/main/res/mipmap-xhdpi/ic_launcher.png b/sample/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..bfa42f0
Binary files /dev/null and b/sample/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..3af2608
Binary files /dev/null and b/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png b/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..324e72c
Binary files /dev/null and b/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..9bec2e6
Binary files /dev/null and b/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..aee44e1
Binary files /dev/null and b/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..34947cd
Binary files /dev/null and b/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/sample/src/main/res/values/colors.xml b/sample/src/main/res/values/colors.xml
new file mode 100644
index 0000000..6e10013
--- /dev/null
+++ b/sample/src/main/res/values/colors.xml
@@ -0,0 +1,5 @@
+
+ #3F51B5
+ #303F9F
+ #FF4081
+
diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml
new file mode 100644
index 0000000..0eca7db
--- /dev/null
+++ b/sample/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+ Google Maps Clustering Sample
+ Map
+
diff --git a/sample/src/main/res/values/styles.xml b/sample/src/main/res/values/styles.xml
new file mode 100644
index 0000000..93c34e5
--- /dev/null
+++ b/sample/src/main/res/values/styles.xml
@@ -0,0 +1,7 @@
+
+
+
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..77c36d0
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+include ':library', ':sample'