前言:簡書菜鳥一枚,主要用于記錄彭则。如有侵權(quán)鳍刷,望告知,我會下架文章俯抖。
話不多输瓜,先上圖:
heatMap_both.png
最近公司在做一款能彩集壓力數(shù)據(jù)的坐墊。怎么樣能將這個功能高大上的體現(xiàn)出牛逼樣兒。這時同事們就想到了熱力圖了尤揣。如圖示搔啊,不明覺厲,確實高大上芹缔。
一坯癣、找資源
實際工作的開發(fā)過程中,因為時間限定研發(fā)成本等原因最欠,每遇到新玩意示罗,首先都是找“輪子”。我運氣不錯(天選打工人一枚)一個早上就找到2個芝硬,并且在接入時發(fā)現(xiàn)第一個就很不錯蚜点。大佬輪子鏈接https://github.com/ChristianFF/HeatMapForAndroid。效果也貼一個:
image.png
二拌阴、接入
由于直接效果略有出入绍绘,感覺要對應自己細節(jié)調(diào)整所以我是直接下載源碼接入,最終關鍵代碼沒變迟赃。配置略有調(diào)整陪拘,關鍵類Gradient、HeatMap纤壁、WeightedPoint左刽;關鍵代碼如下:
// 轉(zhuǎn)換數(shù)據(jù)
private List<WeightedPoint> generateHeatMapData(Object[] obs) {
List<WeightedPoint> data = new ArrayList<>();
for (int i = 0; i < obs.length; i++) {
data.add(new WeightedPoint(obs[i].getOx() + mHeatMapPointRadius,
obs[i].getOy() + mHeatMapPointRadius, obs[i].getPressure()));
}
return data;
}
// 生成熱力圖
List<WeightedPoint> data = generateHeatMapData(mMotorCircles);
HeatMap heatMap = new HeatMap.Builder().weightedData(data).radius(mHeatMapPointRadius)
.width(view.getWidth() + mHeatMapPointRadius).height(view.getHeight() + mHeatMapPointRadius).build();
Bitmap heatMap = heatMap.generateMap();
/**
* 顏色配置(色階漸變)
* 色彩區(qū)間,參數(shù)1色階酌媒。參數(shù)2色階占比(按0~1分段)欠痴,
*/
private static final Gradient DEFAULT_MY_GRADIENT = new Gradient(
new int[]{0xFF7C89DF, Color.GREEN, Color.YELLOW, Color.RED},
new float[]{0.2f, 0.4f, 0.7f, 1f});
三、源碼分析
我感覺這個東西很有意思秒咨,加上預防需求變化有調(diào)整喇辽。就具體看了看源碼,以下是我的個人收獲雨席。記錄一番菩咨。
1、設計思路:與二維碼相似陡厘,通過描繪像素點形成位圖Bitmap抽米;不同的是這是給圖片的不同像素點上色不同顏色值,以一個坐標點為圓心雏亚,圓形向外漸變顏色缨硝,如:
image.png
image.png
散點摩钙、相鄰點:
image.png
image.png
色彩與像素點的映射關系:(參考一個點的圖對比)
image.png
2罢低、代碼實現(xiàn)
設計:
image.png
關鍵計算代碼:
1.生成位圖方法(注釋是我個人的理解,僅供參考)
public Bitmap generateMap() {
// double[][] intensity = new double[mWidth][mHeight];
// 強度二維數(shù)組(坐標強度)
double[][] intensity = new double[mWidth + mRadius * 2][mHeight + mRadius * 2];
for (WeightedPoint w : mData) {
// 數(shù)據(jù)集點,熱力中心點网持,然后周邊擴散
int bucketX = w.x;
int bucketY = w.y;
if (bucketX < mWidth && bucketX >= 0 && bucketY < mHeight && bucketY >= 0) {
intensity[bucketX][bucketY] += w.intensity;
}
}
// convolved 卷積宜岛;intensity 強度;Kernel 內(nèi)核
double[][] convolved = convolve(intensity, mKernel);
return colorize(convolved, mColorMap, mMaxIntensity);
}
2.點與點之間交叉重合的處理(強度相容)-重點
/**
* 使纏繞功舀,將坐標之間的強度相接
*
* @param grid 像素點集(x萍倡,y)
* @param kernel 坐標(漸變系數(shù),圓心~圓邊:(1,0]辟汰,0是透明)
* @return 具體圖片的像素點集(帶強度)
*/
protected double[][] convolve(double[][] grid, double[] kernel) {
Logc.i("radius = " + mRadius);
int dimOldW = grid.length;
int dimOldH = grid[0].length;
int dimW = dimOldW - 2 * mRadius;
int dimH = dimOldH - 2 * mRadius;
int lowerLimit = mRadius;
int upperLimitW = mRadius + dimW - 1; // 向外伸展列敲,目的:是支持強度(紅點)在邊界上。
int upperLimitH = mRadius + dimH - 1;
// 中間的; 中級的; (兩地帖汞、兩物戴而、兩種狀態(tài)等)之間的
double[][] intermediate = new double[dimOldW][dimOldH];
int x, y, x2, xUpperLimit, initial;
double val;
for (x = 0; x < dimOldW; x++) {
for (y = 0; y < dimOldH; y++) {
val = grid[x][y];
if (val != 0) {
// 寬上限:x軸 + 半徑
xUpperLimit = ((upperLimitW < x + mRadius) ? upperLimitW : x + mRadius);
// 初始值
initial = (lowerLimit > x - mRadius) ? lowerLimit : x - mRadius;
// 遍歷x,賦予強度值.(一維數(shù)組翩蘸,同y軸上的強度)
for (x2 = initial; x2 < xUpperLimit; x2++) {
double v = kernel[x2 - (x - mRadius)];
intermediate[x2][y] += val * v;
// Logc.d("有坐標的所意,x2 = " + x2 + ", y = " + y + ", val = " + val + " , v = " + v + ", (x - mRadius) = " + (x - mRadius) + ", old = " + old + ", new =" + (val * v));
}
}
}
}
// 輸出的網(wǎng)格
double[][] outputGrid = new double[dimW][dimH];
int y2, yUpperLimit;
// 坐標范圍的像素點強度 整圖范圍的x軸(radius ~ radiusX + mWidth - 1)
for (x = lowerLimit; x < upperLimitW + 1; x++) {
for (y = 0; y < dimOldH; y++) {
val = intermediate[x][y];
// val != 0 :有強度的,設置坐標了的
if (val != 0) {
yUpperLimit = ((upperLimitH < y + mRadius) ? upperLimitH : y + mRadius);
// 初始y值
initial = (lowerLimit > y - mRadius) ? lowerLimit : y - mRadius;
// 遍歷y催首,賦予強度值.
for (y2 = initial; y2 < yUpperLimit; y2++) {
//
double v = kernel[y2 - (y - mRadius)];
// 不同點之間有交集所以用 +=扶踊;
outputGrid[x - mRadius][y2 - mRadius] += val * v;
}
}
}
}
return outputGrid;
}
3.位圖上色,如zxing二維碼一樣郎任,給圖片的每個像素點按強度上色
/**
* 像素點上色秧耗,即畫位圖
*
* @param grid 像素點集(x,y)
* @param colorMap 設置的顏色變化色彩集(比如4中顏色淡、弱涝滴、中绣版、強且按排序的色彩集)
* @param max 最大強度
* @return
*/
private Bitmap colorize(double[][] grid, int[] colorMap, double max) {
int maxColor = colorMap[colorMap.length - 1];
double colorMapScaling = (colorMap.length - 1) / max;
int dimW = mWidth;
int dimH = mHeight;
int i, j, index, col;
double val;
int[] colors = new int[dimW * dimH];
for (i = 0; i < dimH; i++) {
for (j = 0; j < dimW; j++) {
val = grid[j][i];// 強度,最大值:max
index = i * dimW + j;
// val * colorMapScaling歼疮,結(jié)合之前的 colorMapScaling = (colorMap.length - 1) / max;
// 算出某強度在顏色集的下標
col = (int) (val * colorMapScaling);
if (val != 0) {
if (col < colorMap.length) {
colors[index] = colorMap[col];
} else {
colors[index] = maxColor;
}
} else {
colors[index] = Color.TRANSPARENT;
}
}
}
Bitmap tile = Bitmap.createBitmap(dimW, dimH, Bitmap.Config.ARGB_8888);
tile.setPixels(colors, 0, dimW, 0, 0, dimW, dimH);
return tile;
}
4.色彩映射按強度形成數(shù)據(jù)便于與強度關聯(lián)然后位圖上色(注釋是我個人的理解杂抽,僅供參考)
/**
* 生成顏色集(按淡、弱韩脏、中缩麸、強(colors)排序的)
* @param opacity 不透明度
* @return
*/
public int[] generateColorMap(double opacity) {
HashMap<Integer, ColorInterval> colorIntervals = generateColorIntervals();
int[] colorMap = new int[colorMapSize]; // (各色階依據(jù)色階段比,在色彩集中也對應分段)
ColorInterval interval = colorIntervals.get(0);
// 初始顏色
int start = 0;
for (int i = 0; i < colorMapSize; i++) {
// 從色階分段中獲取分段信息(紅-黃赡矢、黃-綠杭朱、綠-藍、藍-透明)
if (colorIntervals.containsKey(i)) {
// 區(qū)間范圍(色階分段范圍)
interval = colorIntervals.get(i);
// 色階初始值分段中的初始值吹散,類似(紅-黃[100~80]弧械、黃-綠[80~50]、綠-藍(50~20)空民、藍-透明(20~0))
start = i; // 100\80\50\20
}
// ratio : 比率刃唐;colorEnd時 = 1羞迷。;如i = 90画饥,色階長度是20衔瓮,初始值是80,- 》 比率 = 0.5
float ratio = (i - start) / interval.interval; // interval.interval // 色階長度(如紅黃[100~80] 是20)
colorMap[i] = interpolateColor(interval.colorStart, interval.colorEnd, ratio);
}
// 轉(zhuǎn)成帶透明度的顏色值
if (opacity != 1) {
for (int i = 0; i < colorMapSize; i++) {
int c = colorMap[i];
colorMap[i] = Color.argb((int) (Color.alpha(c) * opacity),
Color.red(c), Color.green(c), Color.blue(c));
}
}
return colorMap;
}
/**
* 生成色階集合
* @return
*/
private HashMap<Integer, ColorInterval> generateColorIntervals() {
// 顏色區(qū)間數(shù)數(shù)組
HashMap<Integer, ColorInterval> colorIntervals = new HashMap<>();
// Create first color if not already created
// The initial color is transparent by default : 初始顏色默認為透明
// 其實默認色階比colors多一個透明
if (startPoints[0] != 0) {
int initialColor = Color.argb(
0, Color.red(colors[0]), Color.green(colors[0]), Color.blue(colors[0]));
colorIntervals.put(0, new ColorInterval(initialColor, colors[0], colorMapSize * startPoints[0]));
}
// Generate color intervals
// 生成色階集
for (int i = 1; i < colors.length; i++) {
colorIntervals.put(((int) (colorMapSize * startPoints[i - 1])),
new ColorInterval(colors[i - 1], colors[i],
(colorMapSize * (startPoints[i] - startPoints[i - 1]))));
}
// If color for 100% intensity is not given, the color of highest intensity is used. 如果沒有給出100%強度的顏色抖甘,則使用最高強度的顏色热鞍。
// 即最強強度去掉透明度
if (startPoints[startPoints.length - 1] != 1) {
int i = startPoints.length - 1;
colorIntervals.put(((int) (colorMapSize * startPoints[i])),
new ColorInterval(colors[i], colors[i], colorMapSize * (1 - startPoints[i])));
}
return colorIntervals;
}
/**
* 插入/篡改 顏色
* @param colorStart 初始顏色
* @param colorEnd 結(jié)束顏色
* @param ratio 比率
* @return
*/
private int interpolateColor(int colorStart, int colorEnd, float ratio) {
int alpha = (int) ((Color.alpha(colorEnd) - Color.alpha(colorStart)) * ratio + Color.alpha(colorStart));
float[] hsvStart = new float[3];
// https://blog.csdn.net/qq_42271561/article/details/115465061
// HSV是一種比較直觀的顏色模型,所以在許多圖像編輯工具中應用比較廣泛衔彻,這個模型中顏色的參數(shù)分別是:色調(diào)(H,Hue:0°~360°)薇宠,飽和度(S,Saturation:0%~100%),明度(V, Value:0%(黑)到100%(白))艰额。
Color.RGBToHSV(Color.red(colorStart), Color.green(colorStart), Color.blue(colorStart), hsvStart);
float[] hsvEnd = new float[3];
Color.RGBToHSV(Color.red(colorEnd), Color.green(colorEnd), Color.blue(colorEnd), hsvEnd);
// 就近漸變昼接,避免漸變經(jīng)過大于180的色彩范圍
if (hsvStart[0] - hsvEnd[0] > 180) {
hsvEnd[0] += 360;
} else if (hsvEnd[0] - hsvStart[0] > 180) {
hsvStart[0] += 360;
}
float[] hsvResult = new float[3];
// 轉(zhuǎn)換HSV顏色數(shù)據(jù)后按比率變換
for (int i = 0; i < 3; i++) {
// (結(jié)束色彩 - 初始色彩)* 比率
hsvResult[i] = (hsvEnd[i] - hsvStart[i]) * (ratio) + hsvStart[i];
}
return Color.HSVToColor(alpha, hsvResult);
}