Android自定义导览地图组件(二)

原创 cheny  2017-10-17 22:05  阅读 77 次 评论 0 条
摘要:

Android自定义导览地图组件(一)主要讲述了导览地图的概览,实现思路以及大图浏览“MapView”的实现,本篇围绕“地图坐标“的实现展开叙述,完成整体导览地图功能。

版权说明 : Android自定义导览地图组件(二)于当前乘月网CSDN博客属同一原创,转载请说明出处,谢谢。

 

前段时间一直忙碌加上难得的8天假直至今日才得以调整,向大家表以歉意。上一篇《Android自定义导览地图组件(一)》主要讲述了导览地图的概览,实现思路以及大图浏览“MapView”的实现,本篇围绕“地图坐标“的实现展开叙述,完成整体导览地图功能,下面继续:

   二、定位图标Marker

一个marker需要哪些元素?

1.作为图片显示的载体,ImageView肯定是家中必备良品;
2.哦对,图片呢?OK,配上小图标资源id;
3.显示在哪啊?给个X、Y坐标呗。 ↓↓↓↓↓↓↓↓↓↓↓下方高能,如有不适,也要看完。。。

嗯哼,理论上是这样的,这里是以图片像素点作为坐标,比如一张240320分辨率的图片(地图),左上角为原点,那么marker在图片上的显示坐标范围就是(0,0)到(240,320)。由于地图是可缩放的,图片的分辨率会发生变化,marker坐标也需要动态调整,显然不能取固定值,于是坐标比例方案孕育而生,以上面提到的图片为例:要显示一个坐标为(60,240)的marker,其坐标比例scaleX=601f/240=0.25,scaleY=2401f/320=0.75,如果图片放大2倍(分辨率为480640)时,marker坐标需变为(480scaleX,640 scaleY)即(120,480),下面为示意图:

Android自定义导览地图组件(二) Android 第1张

OK,这样就可以建起一个Marker实体类,代码如下:

package cn.icheny.guide_map;

import android.widget.ImageView;

/**
 * 地图上的小标记图标
 * @author www.icheny.cn
 * @date 2017/10/17
 */

public class Marker {
    private float scaleX;//x坐标比例,用比例值来自适应缩放的地图
    private float scaleY;//y坐标比例
    private ImageView markerView;//标记图标
    private int imgSrcId;//标记图标资源id

    public Marker() {
    }

    public Marker(float scaleX, float scaleY, int imgSrcId) {
        this.scaleX = scaleX;
        this.scaleY = scaleY;
        this.imgSrcId = imgSrcId;
    }

    public float getScaleX() {
        return scaleX;
    }

    public void setScaleX(float scaleX) {
        this.scaleX = scaleX;
    }

    public float getScaleY() {
        return scaleY;
    }

    public void setScaleY(float scaleY) {
        this.scaleY = scaleY;
    }

    public void setMarkerView(ImageView markerView) {
        this.markerView = markerView;
    }

    public int getImgSrcId() {
        return imgSrcId;
    }

    public void setImgSrcId(int imgSrcId) {
        this.imgSrcId = imgSrcId;
    }

    public ImageView getMarkerView() {
        return markerView;
    }
}

三、给导览地图配置初始化属性map_attr.xml.xml

直接看xml代码:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="MapView">
        <attr name="marker_width" format="dimension" />
        <attr name="marker_height" format="dimension" />
        <attr name="marker_anim_duration" format="integer" />
    </declare-styleable>
</resources>

顾名思义,分别是marker(定位图标)显示的宽、高和下落动画时间属性,具体怎么用,下文见晓。

四、自定义MapContainer

先看代码:

package cn.icheny.guide_map;

import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;

/**
 * 地图界面承载容器,ViewGroup
 *
 * @author www.icheny.cn
 * @date 2017/10/18
 */
public class MapContainer extends ViewGroup {
    private Context mContext;//上下文
    private int MARKER_ANIM_DURATION;//动画时间
    private int MARKER_WIDTH; //marker宽度
    private int MARKER_HEIGHT; //marker高度

    /**
     * 这个Flag标记是为了不让ViewGroup不断地绘制子View,
     * 导致不断地重置,  因为之后MapView的缩放,
     * 移动以及markerView的移动等所涉及的重绘都是由逻辑代码控制好了
     */
    private boolean isFirstLayout = true;

    public MapContainer(Context context) {
        this(context, null);
    }

    public MapContainer(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MapContainer(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.mContext = context;
        initAttributes(attrs);
    }

    /**
     * 初始化地图属性配置
     * @param attrs
     */
    private void initAttributes(AttributeSet attrs) {
        if (attrs == null) {
            return;
        }
        TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.MapView);
        MARKER_WIDTH = a.getDimensionPixelOffset(R.styleable.MapView_marker_width, 30);//默认30px
        MARKER_HEIGHT = a.getDimensionPixelOffset(R.styleable.MapView_marker_height, 60);//默认60px
        MARKER_ANIM_DURATION = a.getInteger(R.styleable.MapView_marker_anim_duration, 1200);//默认1.2完成下落动画
        a.recycle();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (changed) {
            if (isFirstLayout) {
                int count = getChildCount();
                for (int i = 0; i < count; i++) {
                    View child = getChildAt(i);
                    child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight());
                }
            }
        }
    }
}

自定义ViewGroup为MapContainer类,构造方法中获取配置参数并初始化marker需要显示的宽、高和下落动画时间的值。这里MapContainer仅仅作为承载地图和marker的容器(父View)以及作为MapView与maker们的沟通桥梁,没有onMeasure和onLayout什么事,只作了简单的实现,关于“isFirstLayout”这个flag的注释一定要好好看看。

好了,终于可以让小marker们上场了,先看代码:

    ......
    private List<Marker> mMarkers;//marker集合

    /**
     * 传入marker集合
     * @param markers
     */
    public void setMarkers(List<Marker> markers) {
        this.mMarkers = markers;

        /*移除上次传入的所有marker(即移除已显示的markers),至于要不要移除看需求,这里仅仅提供方法*/
        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            Object tag = child.getTag(R.id.is_marker);
            if (tag instanceof Boolean && (((Boolean) tag).booleanValue())) {
                //确认当前child是markerView即从ViewGroup中移除
                removeView(child);
            }
        }

        //初始化marker
        initMarkers();
    }

    /**
     * 初始化所有的标记图标(marker)
     */
    private void initMarkers() {
        if (mMarkers != null) {
            return;
        }

        //markerview布局参数,设定宽高
        LayoutParams params = new LayoutParams(MARKER_WIDTH, MARKER_HEIGHT);

        /* 遍历所有marker对象并新建ImageView对象markerView,作相关赋值*/
        for (int i = 0, size = mMarkers.size(); i < size; i++) {

            Marker marker = mMarkers.get(i);
            final ImageView markerView = new ImageView(mContext);
            marker.setMarkerView(markerView);
            addView(markerView);

            //设定tag标识,便于根据tag判定是否是markerView
            markerView.setTag(R.id.is_marker, true);
            markerView.setLayoutParams(params);
            markerView.setImageResource(marker.getImgSrcId());
            final int position = i;
            markerView.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (onMarkerClickListner != null) {
                        //点击事件交给业务类处理
                        onMarkerClickListner.onClick(markerView, position);
                    }
                }
            });
        }
    }

    private OnMarkerClickListner onMarkerClickListner;//maker被点击监听接口对象

    /**
     * 传入需要处理marker点击事件的业务类对象
     * @param l
     */
    public void setOnMarkerClickListner(OnMarkerClickListner l) {
        this.onMarkerClickListner = l;
    }

    /**
     * maker被点击监听接口,便于回调给业务类处理事件
     */
    public interface OnMarkerClickListner {
        void onClick(View view, int position);
    }
    ......

开放setMarkers()方法便于传入marker数据,每次传入的时候移除已显示的marker(要不要移除看需求),接下来initMarkers()方法是对markers初始化赋值以及为业务类(OnMarkerClickListner或其实现类)绑定marker点击事件。
上述代码提到了R.id.is_marker,这个资源为values文件下新建的map_ids.xml文件,其代码如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="is_marker" type="id" />
</resources>

OK,继续折腾,在MapContainer构造方法里完成MapView的初始化创建,考虑到一般地图都是动态从后台API获取的,所以开放了getMapView()方法给相关业务类获取MapView对象以便于加载本地或网络图片,看代码:

    ......
    public MapContainer(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.mContext = context;
        initAttributes(attrs);
        initMapView();
    }

    /**
     * 初始化MapView并添加到MapContainer中
     */
    private void initMapView() {
        LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
        mMapView = new MapView(mContext);
        addView(mMapView);
        mMapView.setLayoutParams(params);
    }
    /**
     * 获取MapView对象,建议仅仅拿来加载图片资源
     * @return
     */
    public MapView getMapView() {
        return this.mMapView;
    }
    ......

下面分析下导览地图一开始显示过程中所需的代码设计流程:

  1.地图(图片)加载 ----> 2.地图加载完成后计算自适应屏幕缩放比例 ----> 3.地图自适应屏幕显示 ----> 4.markers计算坐标并显示 ----> 5.markers执行下落动画

OK,着重说说流程4,其代码执行是在流程3后,这不是废话么?呜呜~ ~,只是强调下嘛,流程3执行完后告知流程4:XXX歌星(地图)已经画好妆上台表演了,我把它最新的方位(尺寸和坐标)告诉你,你可以拿去参考下找准配角们(markers)的出场位置后喊他们去表演吧。嗯哼,还是说了一大堆废话,就只是强调那个告知的接口方法onChanged (RectF rectF),rectF便是尺寸和坐标喽。

下面再分析下导览地图操作场景中所需的代码设计流程:
场景一:1.地图移动 ----> 2.markers计算坐标并显示
场景二:1.地图缩放 ----> 2.markers计算坐标并显示
场景三:1.地图同时缩放并移动 ----> 2.markers计算坐标并显示

OK,场景三直接忽略吧,同时调用场景一和场景二的接口方法就得了。你还在想场景一的解决方案是地图朝哪个方向移动n距离,marker就跟着移动n距离吗?还在想场景二的解决方案是根据缩放点(上一篇文章提到的getFocusX,getFocusY)计算偏移方向和距离决定marker的坐标吗?需要两个对应的接口方法?恩,是可以实现,不过这条路走得可真是迂回婉转。
其实只需要上文提到的onChanged (RectF rectF)就够啦!Excuse Me?又是它?就是这么简单,你(地图)移动和缩放跟我(marker)有半毛钱关系?你只要在移动和缩放的时候告诉我你最新的尺寸和坐标,我自己计算自己的坐标不就好了咩!说了那么多,是不是很期待这个onChanged()了呢?上代码:

    private void initMapView() {
        ......
        mMapView = new MapView(mContext);
        mMapView.setOnMapStateChangedListner(this);
        addView(mMapView);
        ......
    }

    private boolean isAnimFinished = false;

    /**
     * 地图自适应屏幕缩放、手势移动以及缩放的状态变化触发的方法
     * @param rectF 地图Rect矩形
     */
    @Override
    public void onChanged(RectF rectF) {
        if (mMarkers == null) {
            return;
        }
        float pWidth = rectF.width();//地图宽度
        float pHeight = rectF.height();//地图高度
        float pLeft = rectF.left;//地图左边x坐标
        float pTop = rectF.top;//地图顶部y坐标

        Marker marker = null;
        for (int i = 0, size = mMarkers.size(); i < size; i++) {

            marker = mMarkers.get(i);

           /* 计算marker显示的矩形坐标,定位坐标以marker的中下边为基准*/
            int left = roundValue(pLeft + pWidth * marker.getScaleX() - MARKER_WIDTH * 1f / 2);
            int top = roundValue(pTop + pHeight * marker.getScaleY() - MARKER_HEIGHT);
            int right = roundValue(pLeft + pWidth * marker.getScaleX() + MARKER_WIDTH * 1f / 2);
            int bottom = roundValue(pTop + pHeight * marker.getScaleY());

            if (!isAnimFinished) {//下落动画,第一次状态改变会调用,即地图自适应屏幕缩放后会调用
                TranslateAnimation ta = new TranslateAnimation(0, 0, -top, 0);
                ta.setDuration(MARKER_ANIM_DURATION);
                marker.getMarkerView().startAnimation(ta);
            }

            //移动marker
            marker.getMarkerView().layout(left, top, right, bottom);
        }
        isAnimFinished = true;
    }

    /**
     * 此方法返回参数的最接近的整数,目的是为了减小误差
     * 否则marker容易变大或变小,坐标偏差也会越来越大,
     * 毕竟markerView.layout只能传入整数
     * @param value
     * @return
     */
    private int roundValue(float value) {
        return Math.round(value);
    }

MapView.java里的代码:

    private OnMapStateChangedListner onChangedListner;//地图状态变化监听对象

    public void setOnMapStateChangedListner(OnMapStateChangedListner l) {
        onChangedListner = l;
    }

    /**
     * 监听地图自适应屏幕缩放、手势移动以及缩放的状态变化接口
     */
    public interface OnMapStateChangedListner {
        void onChanged(RectF rectF);
    }

代码注释得很详细,不作赘述了。

上文提到接口OnMapStateChangedListner下的方法onChanged(RectF rectF)触发场景,即:自适应屏幕缩放、手势移动以及缩放的状态变化,那么只要在MapView.java里会发生变化的代码处----setImageMatrix( matrix ) 补上"onChangedListner.onChanged( rectF )"即可:

    @Override
    public void onGlobalLayout() {
            ......
            //执行偏移和缩放
            setImageMatrix(mScaleMatrix);
            onChangedListner.onChanged(getMatrixRect());

            //根据当前图片的缩放情况,重新调整图片的最大最小缩放值
            ......
        }
    }

    @Override
    public boolean onScale(ScaleGestureDetector detector) {
            ......
            //执行缩放
            setImageMatrix(mScaleMatrix);
            onChangedListner.onChanged(getMatrixRect());
            ......
    }

    private class AutoScaleTask implements Runnable {
        ......
        @Override
        public void run() {
            ......
            setImageMatrix(mScaleMatrix);
            onChangedListner.onChanged(getMatrixRect());
            //当前缩放值
            ......
            if (tmpScale > 1 && scale < targetScale || scale > targetScale && tmpScale < 1) {
                ......
            } else {//缩放的略微过头了,需要强制设定为目标缩放值
                ......
                setImageMatrix(mScaleMatrix);
                onChangedListner.onChanged(getMatrixRect());
                ......
            }
        }
    }

    private void moveByTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE://手势移动
                     ......
                    setImageMatrix(mScaleMatrix);
                    onChangedListner.onChanged(getMatrixRect());
                    ......
    }

好了,写个Demo测试下效果,MainActivity.java:

/**
 * 使用Demo
 * @author www.icheny.cn
 * @date 2017/10/18
 */
public class MainActivity extends AppCompatActivity implements MapContainer.OnMarkerClickListner {
    MapContainer mMapContainer;
    ArrayList<Marker> mMarkers;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setNavigationBarVisibility(false);
        setContentView(R.layout.activity_main);
        mMapContainer = (MapContainer) findViewById(R.id.mc_map);
        //这里用女神赵丽颖的照片作地图~~
        mMapContainer.getMapView().setImageResource(R.drawable.zhaoliyin);
        mMarkers = new ArrayList<>();
        mMarkers.add(new Marker(0.1f, 0.2f, R.drawable.location));
        mMarkers.add(new Marker(0.3f, 0.7f, R.drawable.location));
        mMarkers.add(new Marker(0.3f, 0.3f, R.drawable.location));
        mMarkers.add(new Marker(0.2f, 0.4f, R.drawable.location));
        mMarkers.add(new Marker(0.8f, 0.4f, R.drawable.location));
        mMarkers.add(new Marker(0.5f, 0.6f, R.drawable.location));
        mMarkers.add(new Marker(0.8f, 0.8f, R.drawable.location));
        mMapContainer.setMarkers(mMarkers);
        mMapContainer.setOnMarkerClickListner(this);
    }

    @Override
    public void onClick(View view, int position) {
        Toast.makeText(MainActivity.this, "你点击了第" + position + "个marker", Toast.LENGTH_SHORT).show();
    }

    /**
     * 设置导航栏显示状态
     *
     * @param visible
     */
    private void setNavigationBarVisibility(boolean visible) {
        int flag = 0;
        if (!visible) {
            flag = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                    | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
                    | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
        }
        getWindow().getDecorView().setSystemUiVisibility(flag);
        //透明导航栏
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
    }
}

布局文件activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<cn.icheny.guide_map.MapContainer xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/mc_map"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:marker_anim_duration="1200"
    app:marker_height="94px"
    app:marker_width="66px" />

效果如下:

Android自定义导览地图组件(二) Android 第2张

恩~ ~  效果还不错,终于结束了这场写博之路,坎坷,漫长。。。

下载源码:《Android自定义导览地图组件_GuideMap》,GitHub下载地址:https://github.com/ausboyue/GuideMap

结束了!结束了!结束了!欢迎童鞋们留言提问,给出宝贵的意见,博客和源码会不定期更新

本文地址:https://www.icheny.cn/android%e8%87%aa%e5%ae%9a%e4%b9%89%e5%af%bc%e8%a7%88%e5%9c%b0%e5%9b%be%e7%bb%84%e4%bb%b6%e4%ba%8c/
关注我们:加我微信:扫描二维码乘月网的微信号,微信号:ausboyue
版权声明:本文为原创文章,版权归 cheny 所有,欢迎分享本文,转载请保留出处!

发表评论


表情