├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── ff │ │ └── heatmap │ │ └── ApplicationTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── ff │ │ │ └── heatmap │ │ │ ├── MainActivity.java │ │ │ └── heatmap │ │ │ ├── Gradient.java │ │ │ ├── HeatMap.java │ │ │ ├── HeatMapOverlay.java │ │ │ ├── LatLng.java │ │ │ └── WeightedLatLng.java │ └── res │ │ ├── drawable-nodpi │ │ └── ic_map_white_24dp.xml │ │ ├── drawable-v21 │ │ └── ic_map_white_24dp.xml │ │ ├── drawable │ │ └── ic_map_white_24dp.xml │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── values-v21 │ │ └── styles.xml │ │ ├── values-w820dp │ │ └── dimens.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── ff │ └── heatmap │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── screenshot ├── main.png └── main2.png └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HeatMapForAndroid 2 | Android绘制热力图 3 | * HeatMap 使用高斯核密度估计方法绘制 4 | * HeatMapOverlay 使用绘制点阴影的方式绘制 5 | 6 | ![main](/screenshot/main.png) 7 | ![main](/screenshot/main2.png) -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 23 5 | buildToolsVersion "23.0.2" 6 | 7 | defaultConfig { 8 | applicationId "com.ff.heatmap" 9 | minSdkVersion 19 10 | targetSdkVersion 23 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(include: ['*.jar'], dir: 'libs') 24 | testCompile 'junit:junit:4.12' 25 | compile 'com.android.support:appcompat-v7:23.1.1' 26 | compile 'com.android.support:design:23.1.1' 27 | } 28 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in C:\Users\FF\AppData\Local\Android\Sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/ff/heatmap/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.ff.heatmap; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/ff/heatmap/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.ff.heatmap; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.NonNull; 5 | import android.support.design.widget.FloatingActionButton; 6 | import android.support.design.widget.Snackbar; 7 | import android.support.v7.app.AppCompatActivity; 8 | import android.support.v7.widget.Toolbar; 9 | import android.util.DisplayMetrics; 10 | import android.view.View; 11 | import android.widget.ImageView; 12 | 13 | import com.ff.heatmap.heatmap.HeatMap; 14 | import com.ff.heatmap.heatmap.HeatMapOverlay; 15 | import com.ff.heatmap.heatmap.WeightedLatLng; 16 | 17 | import java.util.ArrayList; 18 | import java.util.List; 19 | 20 | public class MainActivity extends AppCompatActivity { 21 | 22 | private int screenWidth; 23 | private int screenHeight; 24 | private ImageView imageView; 25 | private HeatMap heatMap; 26 | private HeatMapOverlay heatMapOverlay; 27 | 28 | @Override 29 | protected void onCreate(Bundle savedInstanceState) { 30 | super.onCreate(savedInstanceState); 31 | setContentView(R.layout.activity_main); 32 | Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); 33 | imageView = (ImageView) findViewById(R.id.image); 34 | FloatingActionButton fab1 = (FloatingActionButton) findViewById(R.id.fab1); 35 | FloatingActionButton fab2 = (FloatingActionButton) findViewById(R.id.fab2); 36 | 37 | setSupportActionBar(toolbar); 38 | getSupportActionBar().setTitle("HeatMap"); 39 | 40 | fab1.setOnClickListener(new View.OnClickListener() { 41 | @Override 42 | public void onClick(View view) { 43 | Snackbar.make(view, "regenerate heatmap data", Snackbar.LENGTH_LONG) 44 | .setAction("ok", new View.OnClickListener() { 45 | @Override 46 | public void onClick(View v) { 47 | List data = generateHeatMapData(); 48 | heatMap.setWeightedData(data); 49 | imageView.setImageBitmap(heatMap.generateMap()); 50 | } 51 | }).show(); 52 | } 53 | }); 54 | 55 | fab2.setOnClickListener(new View.OnClickListener() { 56 | @Override 57 | public void onClick(View view) { 58 | Snackbar.make(view, "Regenerate HeatMapOverlay Data", Snackbar.LENGTH_LONG) 59 | .setAction("ok", new View.OnClickListener() { 60 | @Override 61 | public void onClick(View v) { 62 | List data = generateHeatMapData(); 63 | heatMapOverlay.setWeightedData(data); 64 | imageView.setImageBitmap(heatMapOverlay.generateMap()); 65 | } 66 | }).show(); 67 | } 68 | }); 69 | 70 | 71 | measureScreen(); 72 | List data = generateHeatMapData(); 73 | heatMap = new HeatMap.Builder().weightedData(data).radius(35).width(screenWidth).height(screenHeight).build(); 74 | heatMapOverlay = new HeatMapOverlay.Builder().weightedData(data).radius(35).width(screenWidth).height(screenHeight).build(); 75 | imageView.setImageBitmap(heatMap.generateMap()); 76 | } 77 | 78 | private void measureScreen() { 79 | DisplayMetrics displayMetrics = new DisplayMetrics(); 80 | getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); 81 | screenWidth = displayMetrics.widthPixels; 82 | screenHeight = displayMetrics.heightPixels; 83 | } 84 | 85 | @NonNull 86 | private List generateHeatMapData() { 87 | List data = new ArrayList<>(); 88 | for (int i = 0; i < 1000; i++) { 89 | data.add(new WeightedLatLng((int) (Math.random() * screenWidth), 90 | (int) (Math.random() * screenHeight), 91 | Math.random() * 100)); 92 | } 93 | return data; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /app/src/main/java/com/ff/heatmap/heatmap/Gradient.java: -------------------------------------------------------------------------------- 1 | package com.ff.heatmap.heatmap; 2 | 3 | import android.graphics.Color; 4 | 5 | import java.util.HashMap; 6 | 7 | /** 8 | * A class to generate a color map of given size from a given array of colors and the fractions 9 | */ 10 | public class Gradient { 11 | 12 | private static final int DEFAULT_COLOR_MAP_SIZE = 1000; 13 | public final int colorMapSize; 14 | public int[] colors; 15 | public float[] startPoints; 16 | 17 | public Gradient(int[] colors, float[] startPoints) { 18 | this(colors, startPoints, DEFAULT_COLOR_MAP_SIZE); 19 | } 20 | 21 | public Gradient(int[] colors, float[] startPoints, int colorMapSize) { 22 | if (colors.length != startPoints.length) { 23 | throw new IllegalArgumentException("colors and startPoints must have same length"); 24 | } else if (colors.length == 0) { 25 | throw new IllegalArgumentException("no colors have been defined"); 26 | } 27 | for (int i = 1; i < startPoints.length; i++) { 28 | if (startPoints[i] <= startPoints[i - 1]) { 29 | throw new IllegalArgumentException("startPoints should be in increasing order"); 30 | } 31 | } 32 | this.colorMapSize = colorMapSize; 33 | this.colors = new int[colors.length]; 34 | this.startPoints = new float[startPoints.length]; 35 | System.arraycopy(colors, 0, this.colors, 0, colors.length); 36 | System.arraycopy(startPoints, 0, this.startPoints, 0, startPoints.length); 37 | } 38 | 39 | static int interpolateColor(int color1, int color2, float ratio) { 40 | 41 | int alpha = (int) ((Color.alpha(color2) - Color.alpha(color1)) * ratio + Color.alpha(color1)); 42 | 43 | float[] hsv1 = new float[3]; 44 | Color.RGBToHSV(Color.red(color1), Color.green(color1), Color.blue(color1), hsv1); 45 | float[] hsv2 = new float[3]; 46 | Color.RGBToHSV(Color.red(color2), Color.green(color2), Color.blue(color2), hsv2); 47 | 48 | if (hsv1[0] - hsv2[0] > 180) { 49 | hsv2[0] += 360; 50 | } else if (hsv2[0] - hsv1[0] > 180) { 51 | hsv1[0] += 360; 52 | } 53 | 54 | float[] result = new float[3]; 55 | for (int i = 0; i < 3; i++) { 56 | result[i] = (hsv2[i] - hsv1[i]) * (ratio) + hsv1[i]; 57 | } 58 | 59 | return Color.HSVToColor(alpha, result); 60 | } 61 | 62 | private HashMap generateColorIntervals() { 63 | HashMap colorIntervals = new HashMap<>(); 64 | // Create first color if not already created 65 | // The initial color is transparent by default 66 | if (startPoints[0] != 0) { 67 | int initialColor = Color.argb( 68 | 0, Color.red(colors[0]), Color.green(colors[0]), Color.blue(colors[0])); 69 | colorIntervals.put(0, new ColorInterval(initialColor, colors[0], colorMapSize * startPoints[0])); 70 | } 71 | // Generate color intervals 72 | for (int i = 1; i < colors.length; i++) { 73 | colorIntervals.put(((int) (colorMapSize * startPoints[i - 1])), 74 | new ColorInterval(colors[i - 1], colors[i], 75 | (colorMapSize * (startPoints[i] - startPoints[i - 1])))); 76 | } 77 | // If color for 100% intensity is not given, the color of highest intensity is used. 78 | if (startPoints[startPoints.length - 1] != 1) { 79 | int i = startPoints.length - 1; 80 | colorIntervals.put(((int) (colorMapSize * startPoints[i])), 81 | new ColorInterval(colors[i], colors[i], colorMapSize * (1 - startPoints[i]))); 82 | } 83 | return colorIntervals; 84 | } 85 | 86 | int[] generateColorMap(double opacity) { 87 | HashMap colorIntervals = generateColorIntervals(); 88 | int[] colorMap = new int[colorMapSize]; 89 | ColorInterval interval = colorIntervals.get(0); 90 | int start = 0; 91 | for (int i = 0; i < colorMapSize; i++) { 92 | if (colorIntervals.containsKey(i)) { 93 | interval = colorIntervals.get(i); 94 | start = i; 95 | } 96 | float ratio = (i - start) / interval.duration; 97 | colorMap[i] = interpolateColor(interval.colorStart, interval.colorEnd, ratio); 98 | } 99 | if (opacity != 1) { 100 | for (int i = 0; i < colorMapSize; i++) { 101 | int c = colorMap[i]; 102 | colorMap[i] = Color.argb((int) (Color.alpha(c) * opacity), 103 | Color.red(c), Color.green(c), Color.blue(c)); 104 | } 105 | } 106 | 107 | return colorMap; 108 | } 109 | 110 | private class ColorInterval { 111 | private final int colorStart; 112 | private final int colorEnd; 113 | 114 | private final float duration; 115 | 116 | private ColorInterval(int color1, int color2, float duration) { 117 | this.colorStart = color1; 118 | this.colorEnd = color2; 119 | this.duration = duration; 120 | } 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /app/src/main/java/com/ff/heatmap/heatmap/HeatMap.java: -------------------------------------------------------------------------------- 1 | package com.ff.heatmap.heatmap; 2 | 3 | import android.graphics.Bitmap; 4 | import android.graphics.Color; 5 | 6 | import java.util.Collection; 7 | 8 | /** 9 | * 热力图 10 | */ 11 | public class HeatMap { 12 | 13 | private static final double DEFAULT_OPACITY = 0.6; 14 | 15 | private static final int DEFAULT_RADIUS = 36; 16 | 17 | private static final Gradient DEFAULT_GRADIENT = new Gradient( 18 | new int[]{Color.TRANSPARENT, Color.BLUE, Color.GREEN, Color.YELLOW, Color.RED}, 19 | new float[]{0f, 0.25f, 0.55f, 0.85f, 1f}); 20 | 21 | private static final int MIN_RADIUS = 10; 22 | 23 | private static final int MAX_RADIUS = 50; 24 | 25 | private Collection mData; 26 | 27 | private int mRadius; 28 | 29 | private Gradient mGradient; 30 | 31 | private int[] mColorMap; 32 | 33 | private double[] mKernel; 34 | 35 | private double mOpacity; 36 | 37 | private double mMaxIntensity; 38 | 39 | private int mWidth; 40 | 41 | private int mHeight; 42 | 43 | private HeatMap(Builder builder) { 44 | mData = builder.data; 45 | 46 | mRadius = builder.radius; 47 | mGradient = builder.gradient; 48 | mOpacity = builder.opacity; 49 | 50 | mWidth = builder.width; 51 | mHeight = builder.height; 52 | 53 | mKernel = generateKernel(mRadius, mRadius / 3.0); 54 | 55 | setGradient(mGradient); 56 | 57 | setWeightedData(mData); 58 | } 59 | 60 | static double[] generateKernel(int radius, double sd) { 61 | double[] kernel = new double[radius * 2 + 1]; 62 | for (int i = -radius; i <= radius; i++) { 63 | kernel[i + radius] = (Math.exp(-i * i / (2 * sd * sd))); 64 | } 65 | return kernel; 66 | } 67 | 68 | static double[][] convolve(double[][] grid, double[] kernel) { 69 | int radius = (int) Math.floor((double) kernel.length / 2.0); 70 | 71 | int dimOldW = grid.length; 72 | int dimOldH = grid[0].length; 73 | 74 | int dimW = dimOldW - 2 * radius; 75 | int dimH = dimOldH - 2 * radius; 76 | 77 | int lowerLimit = radius; 78 | int upperLimitW = radius + dimW - 1; 79 | int upperLimitH = radius + dimH - 1; 80 | 81 | 82 | double[][] intermediate = new double[dimOldW][dimOldH]; 83 | 84 | int x, y, x2, xUpperLimit, initial; 85 | double val; 86 | for (x = 0; x < dimOldW; x++) { 87 | for (y = 0; y < dimOldH; y++) { 88 | val = grid[x][y]; 89 | if (val != 0) { 90 | xUpperLimit = ((upperLimitW < x + radius) ? upperLimitW : x + radius) + 1; 91 | initial = (lowerLimit > x - radius) ? lowerLimit : x - radius; 92 | for (x2 = initial; x2 < xUpperLimit; x2++) { 93 | intermediate[x2][y] += val * kernel[x2 - (x - radius)]; 94 | } 95 | } 96 | } 97 | } 98 | 99 | double[][] outputGrid = new double[dimW][dimH]; 100 | 101 | int y2, yUpperLimit; 102 | 103 | for (x = lowerLimit; x < upperLimitW + 1; x++) { 104 | for (y = 0; y < dimOldH; y++) { 105 | val = intermediate[x][y]; 106 | if (val != 0) { 107 | yUpperLimit = ((upperLimitH < y + radius) ? upperLimitH : y + radius) + 1; 108 | initial = (lowerLimit > y - radius) ? lowerLimit : y - radius; 109 | for (y2 = initial; y2 < yUpperLimit; y2++) { 110 | outputGrid[x - radius][y2 - radius] += val * kernel[y2 - (y - radius)]; 111 | } 112 | } 113 | } 114 | } 115 | 116 | return outputGrid; 117 | } 118 | 119 | static Bitmap colorize(double[][] grid, int[] colorMap, double max) { 120 | int maxColor = colorMap[colorMap.length - 1]; 121 | double colorMapScaling = (colorMap.length - 1) / max; 122 | 123 | int dimW = grid.length; 124 | int dimH = grid[0].length; 125 | 126 | int i, j, index, col; 127 | double val; 128 | int colors[] = new int[dimW * dimH]; 129 | for (i = 0; i < dimH; i++) { 130 | for (j = 0; j < dimW; j++) { 131 | val = grid[j][i]; 132 | index = i * dimW + j; 133 | col = (int) (val * colorMapScaling); 134 | 135 | if (val != 0) { 136 | if (col < colorMap.length) colors[index] = colorMap[col]; 137 | else colors[index] = maxColor; 138 | } else { 139 | colors[index] = Color.TRANSPARENT; 140 | } 141 | } 142 | } 143 | 144 | Bitmap tile = Bitmap.createBitmap(dimW, dimH, Bitmap.Config.ARGB_8888); 145 | tile.setPixels(colors, 0, dimW, 0, 0, dimW, dimH); 146 | return tile; 147 | } 148 | 149 | public void setWeightedData(Collection data) { 150 | if (data.isEmpty()) { 151 | throw new IllegalArgumentException("No input points."); 152 | } 153 | mData = data; 154 | mMaxIntensity = getMaxIntensities(); 155 | } 156 | 157 | public Bitmap generateMap() { 158 | double[][] intensity = new double[mWidth + mRadius * 2][mHeight + mRadius * 2]; 159 | for (WeightedLatLng w : mData) { 160 | //if you are using LatLng,transform them into screen coordinates 161 | int bucketX = w.x; 162 | int bucketY = w.y; 163 | if (bucketX < mWidth && bucketX >= 0 164 | && bucketY < mHeight && bucketY >= 0) 165 | intensity[bucketX][bucketY] += w.intensity; 166 | } 167 | 168 | double[][] convolved = convolve(intensity, mKernel); 169 | 170 | return colorize(convolved, mColorMap, mMaxIntensity); 171 | } 172 | 173 | public void setGradient(Gradient gradient) { 174 | mGradient = gradient; 175 | mColorMap = gradient.generateColorMap(mOpacity); 176 | } 177 | 178 | public void setRadius(int radius) { 179 | mRadius = radius; 180 | mKernel = generateKernel(mRadius, mRadius / 3.0); 181 | mMaxIntensity = getMaxIntensities(); 182 | } 183 | 184 | public void setOpacity(double opacity) { 185 | mOpacity = opacity; 186 | setGradient(mGradient); 187 | } 188 | 189 | private double getMaxIntensities() { 190 | double maxIntensity = 0; 191 | 192 | for (WeightedLatLng l : mData) { 193 | double value = l.intensity; 194 | if (value > maxIntensity) maxIntensity = value; 195 | } 196 | return maxIntensity; 197 | } 198 | 199 | public static class Builder { 200 | private Collection data; 201 | 202 | private int radius = DEFAULT_RADIUS; 203 | private Gradient gradient = DEFAULT_GRADIENT; 204 | private double opacity = DEFAULT_OPACITY; 205 | private int width = 0; 206 | private int height = 0; 207 | 208 | public Builder() { 209 | } 210 | 211 | public Builder weightedData(Collection val) { 212 | this.data = val; 213 | 214 | if (this.data.isEmpty()) { 215 | throw new IllegalArgumentException("No input points."); 216 | } 217 | return this; 218 | } 219 | 220 | public Builder radius(int val) { 221 | radius = val; 222 | if (radius < MIN_RADIUS || radius > MAX_RADIUS) { 223 | throw new IllegalArgumentException("Radius not within bounds."); 224 | } 225 | return this; 226 | } 227 | 228 | public Builder gradient(Gradient val) { 229 | gradient = val; 230 | return this; 231 | } 232 | 233 | public Builder opacity(double val) { 234 | opacity = val; 235 | if (opacity < 0 || opacity > 1) { 236 | throw new IllegalArgumentException("Opacity must be in range [0, 1]"); 237 | } 238 | return this; 239 | } 240 | 241 | public Builder width(int val) { 242 | this.width = val; 243 | if (width <= 0) { 244 | throw new IllegalArgumentException("Width must be above 0"); 245 | } 246 | return this; 247 | } 248 | 249 | public Builder height(int val) { 250 | this.height = val; 251 | if (height <= 0) { 252 | throw new IllegalArgumentException("Height must be above 0"); 253 | } 254 | return this; 255 | } 256 | 257 | public HeatMap build() { 258 | if (data == null || width == 0 || height == 0) { 259 | throw new IllegalStateException("No input data: you must use .weightedData&&.width&&.height before building"); 260 | } 261 | 262 | return new HeatMap(this); 263 | } 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /app/src/main/java/com/ff/heatmap/heatmap/HeatMapOverlay.java: -------------------------------------------------------------------------------- 1 | package com.ff.heatmap.heatmap; 2 | 3 | import android.graphics.Bitmap; 4 | import android.graphics.Canvas; 5 | import android.graphics.Color; 6 | import android.graphics.Paint; 7 | import android.graphics.RadialGradient; 8 | import android.graphics.Shader; 9 | 10 | import java.util.Collection; 11 | 12 | public class HeatMapOverlay { 13 | private static final int DEFAULT_RADIUS = 36; 14 | private static final int MAX_RADIUS = 50; 15 | private static final int MIN_RADIUS = 10; 16 | private static final Gradient DEFAULT_GRADIENT = new Gradient( 17 | new int[]{Color.TRANSPARENT, Color.rgb(255, 191, 255), Color.rgb(255, 128, 255), Color.rgb(255, 64, 255), Color.rgb(255, 0, 255)}, 18 | new float[]{0f, 0.25f, 0.55f, 0.85f, 1f}); 19 | private static final double DEFAULT_OPACITY = 0.6; 20 | 21 | private static Bitmap backBuffer = null; 22 | private static int[] pixels; 23 | private int screenWidth = 0; 24 | private int screenHeight = 0; 25 | private Collection data; 26 | private int radius; 27 | private double maxIntensity; 28 | private Gradient gradient; 29 | private double opacity; 30 | private int[] colorMap; 31 | 32 | private Canvas myCanvas; 33 | 34 | public static class Builder { 35 | private Collection data; 36 | private int width; 37 | private int height; 38 | 39 | private int radius = DEFAULT_RADIUS; 40 | private Gradient gradient = DEFAULT_GRADIENT; 41 | private double opacity = DEFAULT_OPACITY; 42 | 43 | public Builder() { 44 | } 45 | 46 | public Builder weightedData(Collection val) { 47 | this.data = val; 48 | if (this.data.isEmpty()) { 49 | throw new IllegalArgumentException("No input points."); 50 | } 51 | return this; 52 | } 53 | 54 | public Builder radius(int val) { 55 | radius = val; 56 | if (radius < MIN_RADIUS || radius > MAX_RADIUS) { 57 | throw new IllegalArgumentException("Radius not within bounds."); 58 | } 59 | return this; 60 | } 61 | 62 | /** 63 | * if u want generate a heat map with different colors,u need to set this 64 | */ 65 | public Builder gradient(Gradient val) { 66 | gradient = val; 67 | return this; 68 | } 69 | 70 | public Builder opacity(double val) { 71 | opacity = val; 72 | if (opacity < 0 || opacity > 1) { 73 | throw new IllegalArgumentException("Opacity must be in range [0, 1]"); 74 | } 75 | return this; 76 | } 77 | 78 | public Builder width(int val) { 79 | this.width = val; 80 | if (width <= 0) { 81 | throw new IllegalArgumentException("Width must be above 0"); 82 | } 83 | return this; 84 | } 85 | 86 | public Builder height(int val) { 87 | this.height = val; 88 | if (height <= 0) { 89 | throw new IllegalArgumentException("Height must be above 0"); 90 | } 91 | return this; 92 | } 93 | 94 | public HeatMapOverlay build() { 95 | if (data == null || width == 0 || height == 0) { 96 | throw new IllegalStateException("No input data: you must use .weightedData&&.width&&.height before building"); 97 | } 98 | 99 | return new HeatMapOverlay(this); 100 | } 101 | } 102 | 103 | public HeatMapOverlay(Builder builder) { 104 | data = builder.data; 105 | screenWidth = builder.width; 106 | screenHeight = builder.height; 107 | radius = builder.radius; 108 | opacity = builder.opacity; 109 | gradient = builder.gradient; 110 | 111 | maxIntensity = getMaxIntensities(); 112 | pixels = new int[screenWidth * screenHeight]; 113 | backBuffer = Bitmap.createBitmap(screenWidth, screenHeight, Bitmap.Config.ARGB_8888); 114 | colorMap = gradient.generateColorMap(opacity); 115 | } 116 | 117 | public void setWeightedData(Collection data) { 118 | if (this.data.isEmpty()) { 119 | throw new IllegalArgumentException("No input points."); 120 | } 121 | this.data = data; 122 | maxIntensity = getMaxIntensities(); 123 | } 124 | 125 | private double getMaxIntensities() { 126 | double maxIntensity = 0; 127 | for (WeightedLatLng l : data) { 128 | double value = l.intensity; 129 | if (value > maxIntensity) maxIntensity = value; 130 | } 131 | return maxIntensity; 132 | } 133 | 134 | public Bitmap generateMap() { 135 | backBuffer.eraseColor(Color.TRANSPARENT); 136 | myCanvas = new Canvas(backBuffer); 137 | for (WeightedLatLng p : data) { 138 | drawAlphaCircle(p.x, p.y, p.intensity); 139 | } 140 | return colorize(); 141 | } 142 | 143 | private void drawAlphaCircle(float x, float y, double intensity) { 144 | RadialGradient g = new RadialGradient(x, y, radius, Color.argb( 145 | (int) (intensity / maxIntensity * 255), 0, 0, 0), Color.TRANSPARENT, 146 | Shader.TileMode.CLAMP); 147 | Paint gp = new Paint(); 148 | gp.setShader(g); 149 | myCanvas.drawCircle(x, y, radius, gp); 150 | } 151 | 152 | private Bitmap colorize() { 153 | pixels = new int[screenWidth * screenHeight]; 154 | backBuffer.getPixels(pixels, 0, screenWidth, 0, 0, screenWidth, screenHeight); 155 | for (int i = 0; i < pixels.length; i++) { 156 | int alpha = pixels[i] >>> 24; 157 | pixels[i] = colorMap[alpha * 1000 / 256]; 158 | } 159 | Bitmap tile = Bitmap.createBitmap(screenWidth, screenHeight, Bitmap.Config.ARGB_8888); 160 | tile.setPixels(pixels, 0, screenWidth, 0, 0, screenWidth, screenHeight); 161 | return tile; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /app/src/main/java/com/ff/heatmap/heatmap/LatLng.java: -------------------------------------------------------------------------------- 1 | package com.ff.heatmap.heatmap; 2 | 3 | /** 4 | * point coordinate and latitude and longitude 5 | * Created by FF on 2016/3/9. 6 | */ 7 | public class LatLng { 8 | private double latitude; 9 | private double longitude; 10 | 11 | public LatLng(double latitude, double longitude) { 12 | this.latitude = latitude; 13 | this.longitude = longitude; 14 | } 15 | 16 | public double getLatitude() { 17 | return latitude; 18 | } 19 | 20 | public void setLatitude(double latitude) { 21 | this.latitude = latitude; 22 | } 23 | 24 | public double getLongitude() { 25 | return longitude; 26 | } 27 | 28 | public void setLongitude(double longitude) { 29 | this.longitude = longitude; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/ff/heatmap/heatmap/WeightedLatLng.java: -------------------------------------------------------------------------------- 1 | package com.ff.heatmap.heatmap; 2 | 3 | /** 4 | * Point coordinates of the LatLng and the intensity 5 | */ 6 | public class WeightedLatLng { 7 | 8 | /** 9 | * default intensity 10 | */ 11 | private static final double DEFAULT_INTENSITY = 1.0; 12 | 13 | // /** 14 | // * latitude and longitude 15 | // */ 16 | // public final LatLng latLng; 17 | 18 | public final int x; 19 | 20 | public final int y; 21 | 22 | /** 23 | * intensity must be over zero 24 | */ 25 | public final double intensity; 26 | 27 | // 28 | // public WeightedLatLng(LatLng latlng, double intensity) { 29 | // this.latLng = latlng; 30 | // if (intensity < 0) { 31 | // throw new IllegalStateException("Intensity must be over zero!"); 32 | // } 33 | // this.intensity = intensity; 34 | // } 35 | 36 | // public WeightedLatLng(LatLng latlng) { 37 | // this.latLng = latlng; 38 | // this.intensity = DEFAULT_INTENSITY; 39 | // } 40 | 41 | //I don't have points of geo-coordinates,so I use points of screen coordinates instead 42 | //If you want generate a heat map use LatLng data,you should use the constructor above this 43 | //And you should transform LatLng into screen coordinates when generate the heat map 44 | public WeightedLatLng(int x, int y, double intensity) { 45 | this.x = x; 46 | this.y = y; 47 | if (intensity < 0) { 48 | throw new IllegalStateException("Intensity must be over zero!"); 49 | } 50 | this.intensity = intensity; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/ic_map_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/ic_map_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_map_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 14 | 15 | 21 | 22 | 23 | 24 | 28 | 29 | 36 | 37 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristianFF/HeatMapForAndroid/f900b54fdfb341147de08f8b8c8b1b73e8135fa1/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristianFF/HeatMapForAndroid/f900b54fdfb341147de08f8b8c8b1b73e8135fa1/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristianFF/HeatMapForAndroid/f900b54fdfb341147de08f8b8c8b1b73e8135fa1/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristianFF/HeatMapForAndroid/f900b54fdfb341147de08f8b8c8b1b73e8135fa1/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristianFF/HeatMapForAndroid/f900b54fdfb341147de08f8b8c8b1b73e8135fa1/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values-v21/styles.xml: -------------------------------------------------------------------------------- 1 | > 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 16dp 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | HeatMapForAndroid 3 | Settings 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 15 | 16 |