Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions HeatMapLib/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ android {

dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:support-annotations:24.2.1'
}

apply plugin: 'com.github.dcendents.android-maven'
Expand Down
95 changes: 81 additions & 14 deletions HeatMapLib/src/main/java/ca/hss/heatmaplib/HeatMap.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
import android.graphics.PorterDuff;
import android.graphics.RadialGradient;
import android.graphics.Shader;
import android.support.annotation.AnyThread;
import android.support.annotation.WorkerThread;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
Expand Down Expand Up @@ -136,10 +138,13 @@ public class HeatMap extends View implements View.OnTouchListener {

private Canvas mShadowCanvas = null;

private final Object tryRefreshLock = new Object();

/**
* Set the blur factor for the heat map. Must be between 0 and 1.
* @param blur The blur factor
*/
@AnyThread
public void setBlur(double blur) {
if (blur > 1.0 || blur < 0.0)
throw new IllegalArgumentException("Blur must be between 0 and 1.");
Expand All @@ -148,6 +153,7 @@ public void setBlur(double blur) {
/**
* Get the heat map's blur factor.
*/
@AnyThread
public double getBlur() { return mBlur; }

/**
Expand All @@ -156,6 +162,7 @@ public void setBlur(double blur) {
* This should be greater than the minimum value.
* @param max The maximum value.
*/
@AnyThread
public void setMaximum(double max) { this.max = max; }

/**
Expand All @@ -164,37 +171,43 @@ public void setBlur(double blur) {
* This should be less than the maximum value.
* @param min The minimum value.
*/
@AnyThread
public void setMinimum(double min) { this.min = min; }

/**
* Set the opacity to be used in the heat map. This opacity will be used for the entire map.
* @param opacity The opacity in the range [0,255].
*/
@AnyThread
public void setOpacity(int opacity) { this.opacity = opacity; }

/**
* Set the minimum opacity to be used in the map. Only used when {@link HeatMap#opacity} is 0.
* @param min The minimum opacity in the range [0,255].
*/
@AnyThread
public void setMinimumOpactity(int min) { this.minOpacity = min; }

/**
* Set the maximum opacity to be used in the map. Only used when {@link HeatMap#opacity} is 0.
* @param max The maximum opacity in the range [0,255].
*/
@AnyThread
public void setMaximumOpactity(int max) { this.maxOpacity = max; }

/**
* Set the circles radius when drawing data points.
* @param radius The radius in pixels.
*/
@AnyThread
public void setRadius(double radius) { this.mRadius = radius; }

/**
* Set the color stops used for the heat map's gradient. There needs to be at least 2 stops
* and there should be one at a position of 0 and one at a position of 1.
* @param stops A map from stop positions (as fractions of the width in [0,1]) to ARGB colors.
*/
@AnyThread
public void setColorStops(Map<Float, Integer> stops) {
if (stops.size() < 2)
throw new IllegalArgumentException("There must be at least 2 color stops");
Expand All @@ -214,6 +227,7 @@ public void setColorStops(Map<Float, Integer> stops) {
* Does not refresh the display. See {@link HeatMap#forceRefresh()} in order to redraw the heat map.
* @param point A new data point.
*/
@AnyThread
public void addData(DataPoint point) {
dataBuffer.add(point);
dataModified = true;
Expand All @@ -224,6 +238,7 @@ public void addData(DataPoint point) {
*
* Does not refresh the display. See {@link HeatMap#forceRefresh()} in order to redraw the heat map.
*/
@AnyThread
public void clearData() {
dataBuffer.clear();
dataModified = true;
Expand Down Expand Up @@ -315,23 +330,69 @@ private void initialize() {
this.setDrawingCacheBackgroundColor(Color.TRANSPARENT);
}

private void redrawShadow() {
@AnyThread
private void redrawShadow(Bitmap drawingCache, int width, int height) {
mRenderBoundaries[0] = 10000;
mRenderBoundaries[1] = 10000;
mRenderBoundaries[2] = 0;
mRenderBoundaries[3] = 0;

mShadow = getDrawingCache();
mShadow = drawingCache;
mShadowCanvas = new Canvas(mShadow);

drawTransparent(mShadowCanvas);
drawTransparent(mShadowCanvas, width, height);
}

/**
* Draws the heatmap from a background thread.
*
* This allows offloading some of the work that would usualy be done in
* {@link #onDraw(Canvas)} into a background thread. If the view is redrawn
* for some reason while this operation is still ongoing, the UI thread
* will block until this call is finished.
*
* The caller should take care to invalidate the view on the UI thread
* afterwards, but not before this call has finished.
*
* <pre>{@code
* final HeatMap heatmap = (HeatMap) findViewById(R.id.heatmap);
* new AsyncTask<Void,Void,Void>() {
* protected Void doInBackground(Void... params) {
* Random rand = new Random();
* //add 20 random points of random intensity
* for (int i = 0; i < 20; i++) {
* heatmap.addData(getRandomDataPoint());
* }
*
* heatmap.refreshImmediateInBackgroundThread();
*
* return null;
* }
*
* protected void onPostExecute(Void aVoid) {
* heatmap.invalidate();
* heatmap.setAlpha(0.0f);
* heatmap.animate().alpha(1.0f).setDuration(700L).start();
* }
* }.execute();
* }</pre>
*/
@WorkerThread
public void forceRefreshOnWorkerThread() {
synchronized (tryRefreshLock) {
// These getters are in fact available on this thread. The caller will have to
// take care that the view is in an acceptable state here.
// noinspection WrongThread
tryRefresh(true, getDrawingCache(), getWidth(), getHeight());
}
}

/**
* If needed, refresh the palette.
*/
private void tryRefresh() {
if (needsRefresh) {
@AnyThread
private void tryRefresh(boolean forceRefresh, Bitmap drawingCache, int width, int height) {
if (forceRefresh || needsRefresh) {
Bitmap bit = Bitmap.createBitmap(256, 1, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bit);
LinearGradient grad;
Expand All @@ -350,13 +411,13 @@ private void tryRefresh() {
dataModified = false;
}

redrawShadow();
}
else if (sizeChange) {
redrawShadow();
redrawShadow(drawingCache, width, height);
} else if (sizeChange) {
sizeChange = false;
redrawShadow(drawingCache, width, height);
}

needsRefresh = false;
sizeChange = false;
}

@Override
Expand All @@ -372,7 +433,9 @@ public void onSizeChanged(int w, int h, int oldw, int oldh) {
*/
@Override
protected void onDraw(Canvas canvas) {
tryRefresh();
synchronized (tryRefreshLock) {
tryRefresh(false, getDrawingCache(), getWidth(), getHeight());
}

drawColour(canvas);
}
Expand All @@ -388,6 +451,7 @@ protected void onDraw(Canvas canvas) {
* @param blurFactor A factor to scale the circles width by.
* @param alpha The transparency of the gradient.
*/
@AnyThread
private void drawDataPoint(Canvas canvas, float x, float y, double radius, double blurFactor, double alpha) {
if (blurFactor == 1) {
canvas.drawCircle(x, y, (float)radius, mBlack);
Expand All @@ -407,8 +471,11 @@ private void drawDataPoint(Canvas canvas, float x, float y, double radius, doubl
* version.
*
* @param canvas Canvas to draw into.
* @param width The width of the view
* @param height The height of the view
*/
private void drawTransparent(Canvas canvas) {
@AnyThread
private void drawTransparent(Canvas canvas, int width, int height) {
//invert the blur factor
double blur = 1 - mBlur;

Expand All @@ -417,8 +484,8 @@ private void drawTransparent(Canvas canvas) {

//loop through the data points
for (DataPoint point : data) {
float x = point.x * getWidth();
float y = point.y * getHeight();
float x = point.x * width;
float y = point.y * height;
double value = Math.max(min, Math.min(point.value, max));
//the edge of the bounding rectangle for the circle
double rectX = x - mRadius;
Expand Down