需求:需要一个热力地图,全面的显示中国个省份的数据对比
功能:完整的中国地图(可缩放,平移,点击)
数据颜色区域条(各省颜色按数据所在区间而定)
各省份颜色可设置
各省份具备点击事件接口(点击该省份,黑线描出该省边框)
项目地址:https://github.com/NoEndToLF/ChinaMapView
最终实现效果:
技术路线(简要技术思路,具体实现详见GitHub的Demo):
热力图View技术:
1,解析出assest目录下的SVG文件(典型的XML解析),解析出该地图所有的Path(详见GitHub的Demo中util包中的SVG解析类),该Demo中使用的模拟数据在util包中ColorChangeUtil类中
2,将解析出的Path封装到ChinaMapModel和ProvinceModel对象中
public class ChinaMapModel {
private float Max_x;//地图最大横坐标
private float Min_x;//地图最小横坐标
private float Max_y;//地图最大纵坐标
private float Min_y;//地图最小纵坐标
private List<ProvinceModel> provinceslist;//包含的省份集合
public float getMin_x() {
return Min_x;
}
public void setMin_x(float min_x) {
Min_x = min_x;
}
public float getMax_y() {
return Max_y;
}
public void setMax_y(float max_y) {
Max_y = max_y;
}
public float getMin_y() {
return Min_y;
}
public void setMin_y(float min_y) {
Min_y = min_y;
}
public float getMax_x() {
return Max_x;
}
public List<ProvinceModel> getProvinceslist() {
return provinceslist;
}
public void setProvinceslist(List<ProvinceModel> provinceslist) {
this.provinceslist = provinceslist;
}
public void setMax_x(float max_x) {
Max_x = max_x;
}
}
public class ProvinceModel {
private String name;//省份的名字
private int color;//省份的内部颜色
private int linecolor;//省份的外圈颜色
private List<Path> listpath;//省份的path集合
private List<Region> regionList//每个path对应的Region,用于判断点击位置是否在path内
private boolean isSelect;//是否选中该省份
public boolean isSelect() {
return isSelect;
}
public void setSelect(boolean select) {
isSelect = select;
}
public List<Region> getRegionList() {
return regionList;
}
public void setRegionList(List<Region> regionList) {
this.regionList = regionList;
}
public int getLinecolor() {
return linecolor;
}
public void setLinecolor(int linecolor) {
this.linecolor = linecolor;
}
public List<Path> getListpath() {
return listpath;
}
public void setListpath(List<Path> listpath) {
this.listpath = listpath;
}
public int getColor() {
return color;
}
public void setColor(int color) {
this.color = color;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
3,画View(首先进行大小适配),第一次绘制时,按照取到的该地图的最大横坐标和view的宽度来生成缩放倍数,把所有ProvinceModel内的Path坐标进行缩放,已保证在View的宽度内把整个地图绘制出来,并且根据缩放倍数来动态的控制View的高度,已保证在任何的手机上进行适配。
//初始化准备工作
public ChinaMapView(Context context, AttributeSet attrs) {
super(context, attrs);
//初始化省份内部画笔
innerPaint=new Paint();
innerPaint.setColor(Color.BLUE);
innerPaint.setAntiAlias(true);
//初始化省份外框画笔
outerPaint=new Paint();
outerPaint.setColor(Color.GRAY);
outerPaint.setAntiAlias(true);
outerPaint.setStrokeWidth(1);
outerPaint.setStyle(Paint.Style.STROKE);
//初始化手势帮助类
scrollScaleGestureDetector=new ScrollScaleGestureDetector(this,onScrollScaleGestureListener);
}
@Override
protected void onDraw(Canvas canvas) {
//保证只在初次绘制的时候进行缩放适配
if (isFirst){
viewWidth=getWidth()-getPaddingLeft()-getPaddingRight();
//首先重置所有点的坐标,使得map适应屏幕大小
if (map!=null){
map_scale=viewWidth/map.getMax_x();
}
//缩放所有Path
scalePoints(canvas,map_scale);
isFirst=false;
}else{
//关联缩放和平移后的矩阵
scrollScaleGestureDetector.connect(canvas);
scrollScaleGestureDetector.setScaleMax(3);//最大缩放倍数
scrollScaleGestureDetector.setScalemin(1);//最小缩放倍数
//绘制Map
drawMap(canvas);
}
super.onDraw(canvas);
}
//第一次绘制,缩小map到View指定大小
private void scalePoints(Canvas canvas,float scale) {
if (map.getProvinceslist().size()>0)
//map的左右上下4个临界点
map.setMax_x(map.getMax_x()*scale);
map.setMin_x(map.getMin_x()*scale);
map.setMax_y(map.getMax_y()*scale);
map.setMin_y(map.getMin_y()*scale);
for (ProvinceModel province:map.getProvinceslist()){
innerPaint.setColor(province.getColor());
List<Region> regionList=new ArrayList<>();
List<Path> pathList=new ArrayList<>();
for (Path p:province.getListpath()){
//遍历Path中的所有点,重置点的坐标
Path newpath=resetPath(p, scale, regionList);
pathList.add(newpath);
canvas.drawPath(newpath,innerPaint);
canvas.drawPath(newpath,outerPaint);
}
province.setListpath(pathList);
//判断点是否在path画出的区域内
province.setRegionList(regionList);
}
}
private Path resetPath(Path path,float scale,List<Region> regionList) {
List<PointF> list=new ArrayList<>();
PathMeasure pathmesure=new PathMeasure(path,true);
float[] s=new float[2];
//按照缩放倍数重置Path内的所有点
for (int i=0;i<pathmesure.getLength();i=i+2) {
pathmesure.getPosTan(i, s, null);
PointF p=new PointF(s[0]*scale,s[1]*scale);
list.add(p);
}
//重绘缩放后的Path
Path path1=new Path();
for (int i=0;i<list.size();i++){
if (i==0){
path1.moveTo(list.get(i).x,list.get(i).y);
}else{
path1.lineTo(list.get(i).x, list.get(i).y);
}
}
path1.close();
//构造Path对应的Region,用于判断点击的点是否在Path内
RectF rf=new RectF();
Region re=new Region();
path1.computeBounds(rf,true);
re.setPath(path1,new Region((int)rf.left,(int)rf.top,(int)rf.right,(int)rf.bottom));
regionList.add(re);
return path1;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//不管高度的设置Mode是什么,直接把View的高度按照宽度适配的缩放倍数进行适配
int width=MeasureSpec.getSize(widthMeasureSpec);
if (map!=null){
map_scale=width/map.getMax_x();
}
int height=(int) (map.getMax_y()*map_scale);
setMeasuredDimension(width, height);
}
4,处理该View的事件,包括:
- 点击事件(点击坐标运算和定位,进行重绘,被点击的省份外框黑色,其他省份外框还是默 认灰色)
- 双指缩放事件(对canvas重设缩放后的Matrix)
- 滑动平移事件(对canvas重设平移后的Matrix)
这里把所有的事件封装到了ScrollScaleGestureDetector手势帮助类里,主要解决一下问题
- 根据MotionEvent区分缩放和平移事件
- 根据MotionEvent得到缩放的倍数和平移的距离
- 控制平移的边界点,防止一直平移导致View消失在视野中
- 控制缩放的最大倍数和最小倍数,以及在View平移到设置好的边界后缩放时导致View又超出边界
- 总而言之,就是不管缩放还是平移都要保证View显示在设置的边界内
- 基于缩放和平移操作Matrix,用于View的重绘
- 通过Matrix的RectF来进行坐标换算,把点击事件的坐标换算到初始化的坐标系中,用于判断点击位置是否在某个省份内,因为用于判断的Region是基于初始坐标系的
5,省份的点击事件,包括以下几部分/**缩放平移手势帮助类*/ public class ScrollScaleGestureDetector { private float beforeLength ,afterLength ; // 两触点距离 private float downX ; //单触点x坐标 private float downY ; //单触点y坐标 private float onMoveDownY ; //移动的前一个Y坐标 private float onMoveDownX ; //移动的前一个X坐标 private float scale_temp; //缩放比例 private float downMidX,downMidY; //缩放的中心位置坐标 private float offX,offY; //单指滑动的XY距离 //模式 NONE:无 MOVE:移动 ZOOM:缩放 private int NONE ; private int MOVE ; private int ZOOM ; private int mode = NONE; private int scaleMax;//缩放的最大倍数 private int scalemin;//缩放的最小倍数 private View view;//持有View用于重绘 private Matrix myMatrix; //用来完成缩放 private final float[] matrixValues;//存放矩阵缩放的坐标 private OnScrollScaleGestureListener onScrollScaleGestureListener;//单击事件接口 public interface OnScrollScaleGestureListener{ void onClick(float x,float y); } public ScrollScaleGestureDetector(View view,OnScrollScaleGestureListener onScrollScaleGestureListener){ this.view=view; NONE=0;//无 MOVE=1;//移动 ZOOM=2;//缩放 mode=NONE;// 默认模式 scale_temp=1;//默认缩放比例 myMatrix=new Matrix(); matrixValues=new float[9]; //存放矩阵的9和值 this.onScrollScaleGestureListener=onScrollScaleGestureListener; } //设置最大缩放倍数,最小为1 public void setScaleMax(int scaleMax) { if (scaleMax<=1){ this.scaleMax=1; }else{ this.scaleMax=scaleMax; } } //设置最小缩放倍数,最小为0 public void setScalemin(int scalemin) { if (scalemin<=0){ this.scalemin=0; }else{ this.scalemin=scalemin; } } // 关联View的Canvas和手势操作后的矩阵,用于缩放和平移的展示 public void connect(Canvas canvas) { canvas.concat(myMatrix); } //单触点操作则认为是平移操作 private void onTouchDown(MotionEvent event) { //触电数为1,即单点操作 if(event.getPointerCount()==1){ mode = MOVE; downX = event.getX(); downY = event.getY(); onMoveDownX=event.getX(); onMoveDownY=event.getY(); } } //双触点操作则认为是缩放操作 private void onPointerDown(MotionEvent event) { if (event.getPointerCount() == 2) { mode = ZOOM; beforeLength = getDistance(event); downMidX = getMiddleX(event); downMidY=getMiddleY(event); } } //滑动 private void onTouchMove(MotionEvent event) { //双指缩放操作 if (mode == ZOOM) { afterLength = getDistance(event);// 获取两点的距离 float gapLength = afterLength - beforeLength;// 变化的长度 //缩放倍数采用当前的双指距离除以上一次的双指距离,并且矩阵后乘,即 //在上一次的基础上进行缩放 //达到缩放的最大或者最小值时停止缩放 if (Math.abs(gapLength)>10&&beforeLength!=0){ if (gapLength>0){ if (scaleMax!=0) { if (getScale() < scaleMax) { scale_temp = afterLength / beforeLength; } else { scale_temp = scaleMax / getScale(); } }else { scale_temp = afterLength / beforeLength; } }else{ if (scalemin!=0){ if (getScale()>scalemin){ scale_temp=afterLength/beforeLength; }else{ scale_temp = scalemin / getScale(); } }else { scale_temp=afterLength/beforeLength; } } //设置缩放比例和缩放中心 myMatrix.postScale(scale_temp, scale_temp, downMidX, downMidY); //控制完缩放倍数和缩放中心时,再进行判断,如果此时View已经显示达到边界,平移 //缩放的中心坐标,以保证缩放完后View还在设置的边界内显示 RectF rectF=getMatrixRectF(); if (rectF.left>=view.getWidth()/2){ myMatrix.postTranslate(view.getWidth()/2-rectF.left,0); } if (rectF.right<=view.getWidth()/2){ myMatrix.postTranslate(view.getWidth()/2-rectF.right,0); } if (rectF.top>=view.getHeight()/2){ myMatrix.postTranslate(0,view.getHeight()/2-rectF.top); } if (rectF.bottom<=view.getHeight()/2){ myMatrix.postTranslate(0,view.getHeight()/2-rectF.bottom); } view.invalidate(); beforeLength = afterLength; } } //单指拖动操作 else if(mode == MOVE){ // 计算实际距离 offX = event.getX() - onMoveDownX;//X轴移动距离 offY = event.getY() - onMoveDownY;//y轴移动距离 RectF rectF=getMatrixRectF(); //设置View的平移边界:左右为宽度的一半,上下为高度的一半 //即Map的最左侧允许移动到宽度的一半的位置,右侧、上侧、下侧同理 //上下左右某个平移到边界时不允许该方向的移动 if (rectF.left+offX>=view.getWidth()/2){ offX=view.getWidth()/2-rectF.left; } if (rectF.right+offX<=view.getWidth()/2){ offX=view.getWidth()/2-rectF.right; } if (rectF.top+offY>=view.getHeight()/2){ offY=view.getHeight()/2-rectF.top; } if (rectF.bottom+offY<=view.getHeight()/2){ offY=view.getHeight()/2-rectF.bottom; } //平移的距离为每次移动的叠加 myMatrix.postTranslate(offX,offY); view.invalidate(); onMoveDownX=event.getX(); onMoveDownY=event.getY(); } } //处理手势 public boolean onTouchEvent(MotionEvent event){ switch (event.getAction() & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: onTouchDown(event); break; case MotionEvent.ACTION_POINTER_DOWN: // 多点触摸 onPointerDown(event); break; case MotionEvent.ACTION_MOVE: onTouchMove(event); break; case MotionEvent.ACTION_UP: mode = NONE; //down和up的点横坐标和纵坐标偏差小于10,则认为是点击事件 if (Math.abs(event.getX()-downX)<10&&Math.abs(event.getY()-downY)<10){ if (onScrollScaleGestureListener!=null){ RectF rectF=getMatrixRectF(); //把坐标换算到初始坐标系,用于判断点击坐标是否在某个省份内 PointF pf=new PointF((event.getX() -rectF.left)/getScale() ,(event.getY() -rectF.top)/getScale()); onScrollScaleGestureListener.onClick(pf.x,pf.y); } } break; // 多点松开 case MotionEvent.ACTION_POINTER_UP: mode = NONE; break; } return true; } // 获取两点的距离 private float getDistance(MotionEvent event){ float x = event.getX(0) - event.getX(1); float y = event.getY(0) - event.getY(1); return (float)Math.sqrt(x * x + y * y); } //两点的中心X private float getMiddleX(MotionEvent event){ return (event.getX(1)+event.getX(0))/2; } //两点的中心Y private float getMiddleY(MotionEvent event){ return (event.getY(1)+event.getY(0))/2; } /** * 获得当前的缩放比例 * * @return */ public final float getScale() { myMatrix.getValues(matrixValues); if (matrixValues[Matrix.MSCALE_X]==0){ return 1; }else{ return matrixValues[Matrix.MSCALE_X]; } } /** * 根据当前图片的Matrix获得图片的范围 */ private RectF getMatrixRectF() { Matrix matrix = myMatrix; RectF rect = new RectF(); rect.set(0, 0, view.getWidth(), view.getHeight()); matrix.mapRect(rect); return rect; } }
- 手势帮助类的点击事件接口
- 所点击的省份的接口
- 缩放和平移以及点击后的View绘制
private ScrollScaleGestureDetector.OnScrollScaleGestureListener onScrollScaleGestureListener=new ScrollScaleGestureDetector.OnScrollScaleGestureListener() {
@Override
public void onClick(float x, float y) {
//只有点击在某一个省份内才会触发省份选择接口
for (ProvinceModel p:map.getProvinceslist()){
for (Region region:p.getRegionList()){
if (region.contains((int)x, (int)y)){
//重置上一次选中省份的状态
map.getProvinceslist().get(selectPosition).setSelect(false);
//重置上一次选中省份的边框为初始的棕色map.getProvinceslist().get(selectPosition).setLinecolor(Color.GRAY);
//设置新的选中的省份,并把外框颜色设置为黑色
p.setSelect(true);
p.setLinecolor(Color.BLACK);
//暴露到Activity中的接口,把省的名字传过去
onProvinceClickLisener.onChose(p.getName());
invalidate();
return;
}
}
}
}
};
//选中所点击的省份
public interface onProvinceClickLisener{
public void onChose(String provincename);
}
//绘制整个Map
private void drawMap(Canvas canvas) {
if (map.getProvinceslist().size()>0){
outerPaint.setStrokeWidth(1);
//首先记录下点击的省份的下标,先把其他的省份绘制完,
for (int i=0;i<map.getProvinceslist().size();i++){
if (map.getProvinceslist().get(i).isSelect()){
selectPosition=i;
}else{
//此时绘制其他省份,边框画笔的宽度为1
innerPaint.setColor(map.getProvinceslist().get(i).getColor());
outerPaint.setColor(map.getProvinceslist().get(i).getLinecolor());
for (Path p:map.getProvinceslist().get(i).getListpath()){
canvas.drawPath(p, innerPaint);
canvas.drawPath(p, outerPaint);
}
}
}
//再绘制点击所在的省份,此时画笔宽度设为2.5,以达到着重显示的效果
innerPaint.setColor(map.getProvinceslist().get(selectPosition).getColor());
outerPaint.setColor(map.getProvinceslist().get(selectPosition).getLinecolor());
outerPaint.setStrokeWidth(2.5f);
for (Path p:map.getProvinceslist().get(selectPosition).getListpath()){
canvas.drawPath(p, innerPaint);
canvas.drawPath(p, outerPaint);
}
}
}
6,Activity中使用,详见Demo中的MainActivity中
<com.aiceking.chinamap.view.ChinaMapView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:id="@+id/view"
/>
private void initMap() {
//拿到SVG文件,解析成对象
myMap = new SvgUtil(this).getProvinces();
//传数据
mapview.setMap(myMap);
}
mapview.setOnChoseProvince(new ChinaMapView.onProvinceClickLisener() {
@Override
public void onChose(String provincename) {
//地图点击省份回调接口,listview滚动到相应省份位置
for (int i = 0; i < list.size(); i++) {
if (list.get(i).contains(provincename)) {
adapter.setPosition(i);
province_listview.setSelection(i);
break;
}
}
}
});
渐变色条View技术
1,封装MycolorArea对象
public class MycolorArea {
private int color;//色块颜色
private String text;//对应色块的数值
//get和set省略
2,绘制ColorView
public class ColorView extends View{
//部分代码省略
@Override
protected void onDraw(Canvas canvas) {
if (list==null)return;
if (list.size()>0){
int width_average=getWidth()/list.size();
for (int i=0;i<list.size();i++){
colorPaint.setColor(list.get(i).getColor());
canvas.drawRect(i * width_average, 0, (i + 1) * width_average, getHeight() / 3, colorPaint);
colorPaint.setColor(Color.BLACK);
colorPaint.setTextSize(getHeight()/3);
canvas.drawText(list.get(i).getText(),width_average/2+i * width_average,getHeight()/3*5/2,colorPaint);
}
}
super.onDraw(canvas);
}
3,Activity中使用
<com.example.vmmet.mymapview.view.ColorView
android:layout_width="match_parent"
android:layout_height="33dp"
android:id="@+id/colorView"/>
//设置颜色渐变条
setColorView();
private void setColorView() {
colorView_hashmap = new HashMap<>();
for (int i = 0; i < ColorChangeHelp.nameStrings.length; i++) {
String colors[] = ColorChangeHelp.colorStrings[i].split(",");
String texts[] = ColorChangeHelp.textStrings[i].split(",");
List<MycolorArea> list = new ArrayList<>();
for (int j = 0; j < colors.length; j++) {
MycolorArea c = new MycolorArea();
c.setColor(Color.parseColor(colors[j]));
c.setText(texts[j]);
list.add(c);
}
colorView_hashmap.put(ColorChangeHelp.nameStrings[i], list);
}
colorView.setList(colorView_hashmap.get(ColorChangeHelp.nameStrings[0]) );
}
做开发,需要脚踏实地,日积月累,愿你我共勉