丨版權說明 : 《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
結束了!結束了!結束了!歡迎童鞋們留言提問,給出寶貴的意見,博客和源碼會不定期更新~~