2012年3月4日 星期日

Android - MapView上面的圖片跟著縮放 Part 2

隔了七個多月的時間,我之前寫的Android - MapView上面的圖片跟著縮放 在ptt上面又被挖出來了,這次比較不一樣的是,原本由程式帶入的點位,要改成由使用者在地圖上面長按後,將所按壓的位子放一個圖片,而這個圖片一樣要可以隨著地圖縮放!

所以我下班之後花了一點時間把之前的寫的範例修改成本次的需求,不過有一點要特別說明的是,之前的圖片縮放法太耗資源了,所以我改成了用 Matrix 的方式去縮放。

以下就是本次的範例說明



Layout 配置
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
 xmlns:android="http://schemas.android.com/apk/res/android"
 android:orientation="vertical"
 android:layout_width="fill_parent"
 android:layout_height="fill_parent">
 <com.google.android.maps.MapView
  android:id="@+id/mView_Main"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent"
  android:apiKey="你自己的key">
 </com.google.android.maps.MapView>
</LinearLayout>
 
跟上次的範例一模一樣。

接下來是我改寫的點位物件,與之前不一樣的是,我拿掉了 getter 跟 setter,變成可以直接存取,然後新增了一個可以直接取得這個物件的位置函式。

package tw.tericky.test;

import com.google.android.maps.GeoPoint;

public class ImageEntity {
 public int  mLatitude;
 public int  mLongitude;
 public String mName;

 public ImageEntity() {
  mLatitude = 0;
  mLongitude = 0;
  mName = "";
 }

 public ImageEntity(int aLatitude, int aLongitude, String aName) {
  mLatitude = aLatitude;
  mLongitude = aLongitude;
  mName = aName;
 }

 public GeoPoint getGeoPoint() {
  return new GeoPoint(mLatitude, mLongitude);
 }
}
接著看的是存放點位的Overlay。
package tw.tericky.test;

import java.util.ArrayList;
import java.util.List;

import android.graphics.Bitmap;
import android.graphics.Matrix;
import android.graphics.PointF;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.view.MotionEvent;

import com.google.android.maps.GeoPoint;
import com.google.android.maps.ItemizedOverlay;
import com.google.android.maps.MapView;
import com.google.android.maps.OverlayItem;

public class MarkerOverlay extends ItemizedOverlay<overlayitem> {
 private static final int mTouchMarker = 1000; // 長壓作用時間(毫秒)
 private static final int mMoveRate  = 5; // 地圖移動基數
 private static int   mOldZoomLevel;   // 紀錄地圖的縮放等級
 private static long   mTouchTime;   // 紀錄手指按下的時間
 private static PointF  mStartPoint;   // 紀錄手指按下的坐標
 private static int   mBaseLevel  = 10; // 圖片原大小對應的地圖縮放層級
 private static float  mLevelRate  = 0.2f; // 每一級之間的縮放比例

 private static Drawable  mMarkerDrawable;  // Drawable物件
 private static Bitmap  mMarkerBmp;   // 原本所用的圖示,設成全域變數避免每次重複生成

 private List<imageentity> mItems;    // 存放點位資料

 public MarkerOverlay(Bitmap aDefaultMarker) {
  super(boundCenterBottom(mMarkerDrawable = new BitmapDrawable(aDefaultMarker)));
  mItems = new ArrayList<imageentity>();
  mMarkerBmp = aDefaultMarker;
  mStartPoint = new PointF();
  mOldZoomLevel = mBaseLevel;

  // 一定要加,不然當mItems的個數是0個時,會造成crash
  populate();
 }

 public Boolean addItem(ImageEntity aImageEntity) {
  Boolean result = aImageEntity != null ? mItems.add(aImageEntity) : false;

  // 更新OverlayItem前,先把focus移走,不然會出錯
  setLastFocusedIndex(-1);
  // 通知ItemizedOverlay更新OverlayItem
  populate();

  return result;
 }

 public Boolean addItems(List<imageentity> aImageEntityList) {
  Boolean result = aImageEntityList != null ? mItems.addAll(aImageEntityList) : false;

  // 更新OverlayItem前,先把focus移走,不然會出錯
  setLastFocusedIndex(-1);
  // 通知ItemizedOverlay更新OverlayItem
  populate();

  return result;
 }

 public void recycle() {
  // 回收記憶體,避免造成 memory leak
  if (!mMarkerBmp.isRecycled()) {
   mMarkerBmp.recycle();
   mMarkerBmp = null;
  }

  mMarkerDrawable.setCallback(null);
 }

 @Override
 protected OverlayItem createItem(int aPosition) {
  OverlayItem item = new OverlayItem(mItems.get(aPosition).getGeoPoint(), mItems.get(aPosition).mName, "Marker");
  // 要自己重新設定Market,否則沒有效果
  item.setMarker(mMarkerDrawable);
  return item;
 }

 @Override
 public int size() {
  return mItems.size();
 }

 public void reSize(int aMapZoomLevel) {
  // 計算縮放率,記得一定要 > 0
  float scale = 1 - (mBaseLevel - aMapZoomLevel) * mLevelRate * 1.1f;

  if (scale > 0) {
   Matrix matrix = new Matrix();
   matrix.postScale(scale, scale);

   mMarkerDrawable.setCallback(null);
   mMarkerDrawable = boundCenterBottom(new BitmapDrawable(Bitmap.createBitmap(mMarkerBmp, 0, 0, mMarkerBmp.getWidth(), mMarkerBmp.getHeight(), matrix, false)));

   // 更新OverlayItem前,先把focus移走,不然會出錯
   setLastFocusedIndex(-1);
   // 通知ItemizedOverlay更新OverlayItem
   populate();
  }
 }

 @Override
 public boolean onTouchEvent(MotionEvent event, MapView mapView) {
  try {
   if (event.getAction() == MotionEvent.ACTION_UP) {
    float start = mStartPoint.x + mStartPoint.y;
    float end = event.getX() + event.getY();
    int rate = mMoveRate * mapView.getZoomLevel();

    // 在手指按壓後,幾乎沒有移動的情況下或是移動量不大的情況下才會觸發事件
    if (Math.abs(end - start) < rate) {
     // 按壓超過設定時間才會觸發
     if (event.getEventTime() - mTouchTime > mTouchMarker) {
      // 取得按壓點轉換成經緯度,放入點位串列中
      GeoPoint mPoint = mapView.getProjection().fromPixels((int) event.getX(), (int) event.getY());
      this.addItem(new ImageEntity(mPoint.getLatitudeE6(), mPoint.getLongitudeE6(), mPoint.toString()));
      // 刷新地圖
      mapView.invalidate();
     }
    }

    // 判斷是否有地圖縮放的情況
    if (mOldZoomLevel != mapView.getZoomLevel()) {
     mOldZoomLevel = mapView.getZoomLevel();
     this.reSize(mOldZoomLevel);
    }
   } else if (event.getAction() == MotionEvent.ACTION_DOWN) {
    // 紀錄起始按壓位置與時間
    mStartPoint.set(event.getX(), event.getY());
    mTouchTime = event.getDownTime();
   }
  } catch (ArrayIndexOutOfBoundsException e) {
   e.printStackTrace();
  }

  return super.onTouchEvent(event, mapView);
 }

}
最後是MapActivity的寫法。
package tw.tericky.test;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BitmapFactory.Options;
import android.os.Bundle;
import android.widget.ZoomButtonsController;
import android.widget.ZoomButtonsController.OnZoomListener;

import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapActivity;
import com.google.android.maps.MapView;

public class AndroidMapTest2Activity extends MapActivity {
 private MapView   mMViewMap;
 private MarkerOverlay mMarkerOverlay;

 private OnZoomListener mMapViewOnZoom = new OnZoomListener() {
            @Override
            public void onZoom(boolean zoomIn) {
             if (zoomIn) {
              mMViewMap.getController().zoomIn();
             } else {
              mMViewMap.getController().zoomOut();
             }
             // 縮放圖標大小
             mMarkerOverlay.reSize(mMViewMap.getZoomLevel());
            }

            @Override
            public void onVisibilityChanged(boolean visible) {

            }
           };

 @Override
 public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);

  // 原圖尺寸 711 * 384
  // 將大圖的尺寸預先縮減
  Options opts = new Options();
  opts.inSampleSize = 3;
  Bitmap marker = BitmapFactory.decodeResource(getResources(), R.drawable.qb, opts);

  // 生成圖層
  mMarkerOverlay = new MarkerOverlay(marker);

  mMViewMap = (MapView) findViewById(R.id.MView_Map);
  // 加入圖層
  mMViewMap.getOverlays().add(mMarkerOverlay);

  // 刷新
  mMViewMap.invalidate();
  mMViewMap.setBuiltInZoomControls(true);
  mMViewMap.setClickable(true);
  mMViewMap.setEnabled(true);
  mMViewMap.getController().setZoom(10);
  ZoomButtonsController zoomButton = mMViewMap.getZoomButtonsController();
  zoomButton.setOnZoomListener(mMapViewOnZoom);

  // zoom 到台灣
  mMViewMap.getController().animateTo(new GeoPoint(23949024, 120992340));
 }

 @Override
 protected void onDestroy() {
  mMarkerOverlay.recycle();
  super.onDestroy();
  System.exit(0);
 }

 @Override
 protected boolean isRouteDisplayed() {
  return false;
 }
}
 
最後要說的是,為什麼不直接將onTouch事件設給MapView而是設在Overlay裡面,是因為......該死的ZoomButtonsController的Touch事件跟MapView的Touch互衝,所以MapView的Touch只會作用一次而已,這邊害我卡了很久...

按照慣例,附上Source Code(AndroidMapTest2.rar)





有任何疑問歡迎討論!

沒有留言: