丨版权说明 : 《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),下面为示意图:
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
结束了!结束了!结束了!欢迎童鞋们留言提问,给出宝贵的意见,博客和源码会不定期更新
暂无评论内容