需求:需要一个热力地图,全面的显示中国个省份的数据对比
功能:完整的中国地图(可缩放,平移,点击)
数据颜色区域条(各省颜色按数据所在区间而定)
各省份颜色可设置
各省份具备点击事件接口(点击该省份,黑线描出该省边框)
项目地址: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 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 getProvinceslist() {
return provinceslist;
}
public void setProvinceslist(List 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 listpath;//省份的path集合
private List regionList//每个path对应的Region,用于判断点击位置是否在path内
private boolean isSelect;//是否选中该省份
public boolean isSelect() {
return isSelect;
}
public void setSelect(boolean select) {
isSelect = select;
}
public List getRegionList() {
return regionList;
}
public void setRegionList(List regionList) {
this.regionList = regionList;
}
public int getLinecolor() {
return linecolor;
}
public void setLinecolor(int linecolor) {
this.linecolor = linecolor;
}
public List getListpath() {
return listpath;
}
public void setListpath(List 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 regionList=new ArrayList<>();
List 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 regionList) {
List list=new ArrayList<>();
PathMeasure pathmesure=new PathMeasure(path,true);
float[] s=new float[2];
//按照缩放倍数重置Path内的所有点
for (int i=0;i
4,处理该View的事件,包括:
这里把所有的事件封装到了ScrollScaleGestureDetector手势帮助类里,主要解决一下问题
/**缩放平移手势帮助类*/
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;
}
}
5,省份的点击事件,包括以下几部分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
6,Activity中使用,详见Demo中的MainActivity中
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
3,Activity中使用
//设置颜色渐变条
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 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]) );
}
做开发,需要脚踏实地,日积月累,愿你我共勉