android自定义View,区域热力地图(具备每个省份的点击接口)

需求:需要一个热力地图,全面的显示中国个省份的数据对比

 

功能:完整的中国地图(可缩放,平移,点击)

           数据颜色区域条(各省颜色按数据所在区间而定)

           各省份颜色可设置

           各省份具备点击事件接口(点击该省份,黑线描出该省边框)

 

项目地址: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是基于初始坐标系的
     
    /**缩放平移手势帮助类*/
    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,省份的点击事件,包括以下几部分
  • 手势帮助类的点击事件接口
  • 所点击的省份的接口
  • 缩放和平移以及点击后的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])             );
             }

 

      做开发,需要脚踏实地,日积月累,愿你我共勉

 

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值