Android自定义导览地图组件(二)
来源:互联网 发布:java 工厂模式 编辑:程序博客网 时间:2024/05/16 04:09
丨版权说明 : 《Android自定义导览地图组件(二)》于当前CSDN博客和乘月网属同一原创,转载请说明出处,谢谢。
前段时间一直忙碌加上难得的8天假直至今日才得以调整,向大家表以歉意。上一篇《Android自定义导览地图组件(一)》主要讲述了导览地图的概览,实现思路以及大图浏览“MapView”的实现,本篇围绕“地图坐标“的实现展开叙述,完成整体导览地图功能,下面继续:
二、定位图标Marker
一个marker需要哪些元素?
1.作为图片显示的载体,ImageView肯定是家中必备良品;2.哦对,图片呢?OK,配上小图标资源id;
3.显示在哪啊?给个X、Y坐标呗。 ↓↓↓↓↓↓↓↓↓↓↓下方高能,如有不适,也要看完。。。
嗯哼,理论上是这样的,这里是以图片像素点作为坐标,比如一张240*320分辨率的图片(地图),左上角为原点,那么marker在图片上的显示坐标范围就是(0,0)到(240,320)。由于地图是可缩放的,图片的分辨率会发生变化,marker坐标也需要动态调整,显然不能取固定值,于是坐标比例方案孕育而生,以上面提到的图片为例:要显示一个坐标为(60,240)的marker,其坐标比例scaleX=60*1f/240=0.25,scaleY=240*1f/320=0.75,如果图片放大2倍(分辨率为480*640)时,marker坐标需变为(480*scaleX,640* scaleY)即(120,480),下面为示意图:
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自定义导览地图组件_GuideMap》,GitHub下载地址:https://github.com/ausboyue/GuideMap
结束了!结束了!结束了!欢迎童鞋们留言提问,给出宝贵的意见,博客和源码会不定期更新~~
- Android自定义导览地图组件(二)
- Android自定义导览地图组件(一)
- Android自定义组件(二)
- Android自定义组件(一)(二)
- android中自定义组合组件(二)
- Android 自定义view(二) 如何实现自定义组件
- 自定义文本编辑组件(二)
- 自定义组件(二)
- Recycler自定义组件二
- 自定义组件(二)
- SuperMap IObjects C++组件学习笔记(二) - Qt接管下IObjectsC++组件的自定义地图绘制
- Android 自定义 地图 室内
- 【Android】Android自定义组件
- 高德地图组件在Android的应用以及Android与JavaScript的交互(二)
- Android百度地图(二)
- 自定义android RadioPreference组件
- android自定义组件
- Android自定义组件
- Discuz常见小问题-如何设置为人工审核
- Discuz常见小问题-如何设置QQ邮箱注册验证
- NEUQACM OJ:1071谭浩强C语言(第三版)习题9.2
- Discuz常见小问题-如何设置163邮箱注册验证
- Discuz常见小问题-如何删除用户
- Android自定义导览地图组件(二)
- Discuz常见小问题-如何发布站点公告
- for循环failfast结果
- Discuz常见小问题-如何禁止用户发言,快速删除某个用户的所有帖子
- Discuz常见小问题-如何快速清除帖子
- Promise
- C#基础视频教程1 背景知识和安装配置
- C#基础视频教程3.1 常见控件类型和使用方法
- C#基础视频教程2 常见数据类型和属性方法