前言:简书菜鸟一枚,主要用于记录。如有侵权,望告知,我会下架文章。
话不多,先上图:
最近公司在做一款能彩集压力数据的坐垫。怎么样能将这个功能高大上的体现出牛逼样儿。这时同事们就想到了热力图了。如图示,不明觉厉,确实高大上。
一、找资源
实际工作的开发过程中,因为时间限定研发成本等原因,每遇到新玩意,首先都是找“轮子”。我运气不错(天选打工人一枚)一个早上就找到2个,并且在接入时发现第一个就很不错。大佬轮子链接https://github.com/ChristianFF/HeatMapForAndroid。效果也贴一个:
二、接入
由于直接效果略有出入,感觉要对应自己细节调整所以我是直接下载源码接入,最终关键代码没变。配置略有调整,关键类Gradient、HeatMap、WeightedPoint;关键代码如下:
// 转换数据
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();
/**
* 颜色配置(色阶渐变)
* 色彩区间,参数1色阶。参数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});
三、源码分析
我感觉这个东西很有意思,加上预防需求变化有调整。就具体看了看源码,以下是我的个人收获。记录一番。
1、设计思路:与二维码相似,通过描绘像素点形成位图Bitmap;不同的是这是给图片的不同像素点上色不同颜色值,以一个坐标点为圆心,圆形向外渐变颜色,如:
散点、相邻点:
色彩与像素点的映射关系:(参考一个点的图对比)
2、代码实现
设计:
关键计算代码:
1.生成位图方法(注释是我个人的理解,仅供参考)
public Bitmap generateMap() {
// double[][] intensity = new double[mWidth][mHeight];
// 强度二维数组(坐标强度)
double[][] intensity = new double[mWidth + mRadius * 2][mHeight + mRadius * 2];
for (WeightedPoint w : mData) {
// 数据集点,热力中心点,然后周边扩散
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 内核
double[][] convolved = convolve(intensity, mKernel);
return colorize(convolved, mColorMap, mMaxIntensity);
}
2.点与点之间交叉重合的处理(强度相容)-重点
/**
* 使缠绕,将坐标之间的强度相接
*
* @param grid 像素点集(x,y)
* @param kernel 坐标(渐变系数,圆心~圆边:(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;
// 中间的; 中级的; (两地、两物、两种状态等)之间的
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,赋予强度值.(一维数组,同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));
}
}
}
}
// 输出的网格
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,结合之前的 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.色彩映射按强度形成数据便于与强度关联然后位图上色(注释是我个人的理解,仅供参考)
/**
* 生成颜色集(按淡、弱、中、强(colors)排序的)
* @param opacity 不透明度
* @return
*/
public int[] generateColorMap(double opacity) {
HashMap<Integer, ColorInterval> colorIntervals = generateColorIntervals();
int[] colorMap = new int[colorMapSize]; // (各色阶依据色阶段比,在色彩集中也对应分段)
ColorInterval interval = colorIntervals.get(0);
// 初始颜色
int start = 0;
for (int i = 0; i < colorMapSize; i++) {
// 从色阶分段中获取分段信息(红-黄、黄-绿、绿-蓝、蓝-透明)
if (colorIntervals.containsKey(i)) {
// 区间范围(色阶分段范围)
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);
}
// 转成带透明度的颜色值
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() {
// 颜色区间数数组
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 结束颜色
* @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是一种比较直观的颜色模型,所以在许多图像编辑工具中应用比较广泛,这个模型中颜色的参数分别是:色调(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);
// 就近渐变,避免渐变经过大于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];
// 转换HSV颜色数据后按比率变换
for (int i = 0; i < 3; i++) {
// (结束色彩 - 初始色彩)* 比率
hsvResult[i] = (hsvEnd[i] - hsvStart[i]) * (ratio) + hsvStart[i];
}
return Color.HSVToColor(alpha, hsvResult);
}