Android自定義導覽地圖組件(二)

版權說明 : 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

結束了!結束了!結束了!歡迎童鞋們留言提問,給出寶貴的意見,博客和源碼會不定期更新~~









發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章