最近在帮老师做一个项目,类似于景点通的App手机应用,我们是要精细化一些室内的地图,室内的地图采用的是自己的一套定位机制,所有室内地图也要自己来实现,参考了网上一些例子,考虑到效率的问题,最后决定使用SurfaceView来进行地图绘制,实现的功能有:
- 双击放大
- 多点触摸放大
- 地图拖拽
- 添加地图标记
效果图一张:
代码思路
1.处理缩放和拖拽事件
在这里我利用了Matrix类提供的图片操作方法去进行图片的缩放和平移处理,关于该方面的知识可以参考
Android开发–利用Matrix进行图片操作
2.双击放大
为了实现双击放大,在这里我们MyMap类中设置了一个成员变量lastClickTime用来记录上一次点击屏幕的时间(点击屏幕的时间值可以通过MotionEvent的getEventTime方法去获得,单位是ms),如果当前点击事件的时间与上次点击事件的时间差值小于300ms则执行放大事件。
3.多点触摸放大
通过MotionEvent中的方法来获得两个触摸点之间的距离大小, 如下:
//计算两个触摸点的距离private float spacing(MotionEvent event) { float x = event.getX(0) - event.getX(1); float y = event.getY(0) - event.getY(1); return (float) Math.sqrt(x * x + y * y);}
利用一个变量oldDist表示前一次两个触摸点的距离,利用一个oldRate表示前一次的缩放,在onTouchEvent方法中move的情况下不断更新当前缩放mCurrentScale = oldRate * (newDist / oldDist);
4.地图拖拽
利用一个PointF变量mapCenter表示当前地图中心的位置在手机屏幕上的坐标,当拖拽事件发生时通过手指移动的距离来不同更新mapCenter的值,并在draw方法中利用Matrix类操作图片
matrix.postTranslate(mapCenter.x - mBitmap.getWidth() / 2, mapCenter.y - mBitmap.getHeight() / 2);
5.添加地图标记
编写一个MarkObject类来表示地图标记,在该类之下存储了标记的Bitmap对象,该标记相对于整张地图的位置,以及点击标记的回调事件的处理。
在MyMap类中利用一个List变量markList来记录所有已经添加的地图标记。
1)处理标记随拖拽和缩放事件而改变位置:这里主要是根据mapCenter的点来进行计算,具体的计算大家可以参考代码;
2)处理点击事件:在onTouchEvent方法中up情况时,遍历markList中的MarkObject进行判断当前触摸点是否被包含在当前的标记区域中;
参考代码
MyMap类:
package com.example.maptest;import java.lang.reflect.Field;import java.util.ArrayList;import java.util.List;import android.content.Context;import android.graphics.Bitmap;import android.graphics.Canvas;import android.graphics.Color;import android.graphics.Matrix;import android.graphics.Paint;import android.graphics.PointF;import android.util.AttributeSet;import android.util.Log;import android.view.MotionEvent;import android.view.SurfaceHolder;import android.view.SurfaceView;public class MyMap extends SurfaceView implements SurfaceHolder.Callback { private static final String TAG = MyMap.class.getSimpleName(); private static final long DOUBLE_CLICK_TIME_SPACE = 300; private float mCurrentScaleMax; private float mCurrentScale; private float mCurrentScaleMin; private float windowWidth, windowHeight; private Bitmap mBitmap; private Paint mPaint; private PointF mStartPoint, mapCenter;// mapCenter表示地图中心在屏幕上的坐标 private long lastClickTime;// 记录上一次点击屏幕的时间,以判断双击事件 private Status mStatus = Status.NONE; private float oldRate = 1; private float oldDist = 1; private float offsetX, offsetY; private boolean isShu = true; private enum Status { NONE, ZOOM, DRAG }; private List<MarkObject> markList = new ArrayList<MarkObject>(); public MyMap(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); // TODO Auto-generated constructor stub init(); } public MyMap(Context context, AttributeSet attrs) { super(context, attrs); // TODO Auto-generated constructor stub init(); } public MyMap(Context context) { super(context); // TODO Auto-generated constructor stub init(); } private void init() { SurfaceHolder holder = getHolder(); holder.addCallback(this); // 获取屏幕的宽和高 windowWidth = getResources().getDisplayMetrics().widthPixels; windowHeight = getResources().getDisplayMetrics().heightPixels - getStatusBarHeight(); mPaint = new Paint(); mStartPoint = new PointF(); mapCenter = new PointF(); } public void setBitmap(Bitmap bitmap) { this.mBitmap = bitmap; // 设置最小缩放为铺满屏幕,最大缩放为最小缩放的4倍 mCurrentScaleMin = Math.min(windowHeight / mBitmap.getHeight(), windowWidth / mBitmap.getWidth()); mCurrentScale = mCurrentScaleMin; mCurrentScaleMax = mCurrentScaleMin * 4; mapCenter.set(mBitmap.getWidth() * mCurrentScale / 2, mBitmap.getHeight() * mCurrentScale / 2); float bitmapRatio = mBitmap.getHeight() / mBitmap.getWidth(); float winRatio = windowHeight / windowWidth; // 判断屏幕铺满的情况,isShu为true表示屏幕横向被铺满,为false表示屏幕纵向被铺满 if (bitmapRatio <= winRatio) { isShu = true; } else { isShu = false; } draw(); } /** * 为当前地图添加标记 * * @param object */ public void addMark(MarkObject object) { markList.add(object); } /** * 地图放大 */ public void zoomIn() { mCurrentScale *= 1.5f; if (mCurrentScale > mCurrentScaleMax) { mCurrentScale = mCurrentScaleMax; } draw(); } /** * 地图缩小 */ public void zoomOut() { mCurrentScale /= 1.5f; if (mCurrentScale < mCurrentScaleMin) { mCurrentScale = mCurrentScaleMin; } if (isShu) { if (mapCenter.x - mBitmap.getWidth() * mCurrentScale / 2 > 0) { mapCenter.x = mBitmap.getWidth() * mCurrentScale / 2; } else if (mapCenter.x + mBitmap.getWidth() * mCurrentScale / 2 < windowWidth) { mapCenter.x = windowWidth - mBitmap.getWidth() * mCurrentScale / 2; } if (mapCenter.y - mBitmap.getHeight() * mCurrentScale / 2 > 0) { mapCenter.y = mBitmap.getHeight() * mCurrentScale / 2; } } else { if (mapCenter.y - mBitmap.getHeight() * mCurrentScale / 2 > 0) { mapCenter.y = mBitmap.getHeight() * mCurrentScale / 2; } else if (mapCenter.y + mBitmap.getHeight() * mCurrentScale / 2 < windowHeight) { mapCenter.y = windowHeight - mBitmap.getHeight() * mCurrentScale / 2; } if (mapCenter.x - mBitmap.getWidth() * mCurrentScale / 2 > 0) { mapCenter.x = mBitmap.getWidth() * mCurrentScale / 2; } } draw(); } // 处理拖拽事件 private void drag(MotionEvent event) { PointF currentPoint = new PointF(); currentPoint.set(event.getX(), event.getY()); offsetX = currentPoint.x - mStartPoint.x; offsetY = currentPoint.y - mStartPoint.y; // 以下是进行判断,防止出现图片拖拽离开屏幕 if (offsetX > 0 && mapCenter.x + offsetX - mBitmap.getWidth() * mCurrentScale / 2 > 0) { offsetX = 0; } if (offsetX < 0 && mapCenter.x + offsetX + mBitmap.getWidth() * mCurrentScale / 2 < windowWidth) { offsetX = 0; } if (offsetY > 0 && mapCenter.y + offsetY - mBitmap.getHeight() * mCurrentScale / 2 > 0) { offsetY = 0; } if (offsetY < 0 && mapCenter.y + offsetY + mBitmap.getHeight() * mCurrentScale / 2 < windowHeight) { offsetY = 0; } mapCenter.x += offsetX; mapCenter.y += offsetY; draw(); mStartPoint = currentPoint; } // 处理多点触控缩放事件 private void zoomAction(MotionEvent event) { float newDist = spacing(event); if (newDist > 10.0f) { mCurrentScale = oldRate * (newDist / oldDist); if (mCurrentScale < mCurrentScaleMin) { mCurrentScale = mCurrentScaleMin; } else if (mCurrentScale > mCurrentScaleMax) { mCurrentScale = mCurrentScaleMax; } if (isShu) { if (mapCenter.x - mBitmap.getWidth() * mCurrentScale / 2 > 0) { mapCenter.x = mBitmap.getWidth() * mCurrentScale / 2; } else if (mapCenter.x + mBitmap.getWidth() * mCurrentScale / 2 < windowWidth) { mapCenter.x = windowWidth - mBitmap.getWidth() * mCurrentScale / 2; } if (mapCenter.y - mBitmap.getHeight() * mCurrentScale / 2 > 0) { mapCenter.y = mBitmap.getHeight() * mCurrentScale / 2; } } else { if (mapCenter.y - mBitmap.getHeight() * mCurrentScale / 2 > 0) { mapCenter.y = mBitmap.getHeight() * mCurrentScale / 2; } else if (mapCenter.y + mBitmap.getHeight() * mCurrentScale / 2 < windowHeight) { mapCenter.y = windowHeight - mBitmap.getHeight() * mCurrentScale / 2; } if (mapCenter.x - mBitmap.getWidth() * mCurrentScale / 2 > 0) { mapCenter.x = mBitmap.getWidth() * mCurrentScale / 2; } } } draw(); } // 处理点击标记的事件 private void clickAction(MotionEvent event) { int clickX = (int) event.getX(); int clickY = (int) event.getY(); for (MarkObject object : markList) { Bitmap location = object.getmBitmap(); int objX = (int) (mapCenter.x - location.getWidth() / 2 - mBitmap.getWidth() * mCurrentScale / 2 + mBitmap .getWidth() * object.getMapX() * mCurrentScale); int objY = (int) (mapCenter.y - location.getHeight() - mBitmap.getHeight() * mCurrentScale / 2 + mBitmap .getHeight() * object.getMapY() * mCurrentScale); // 判断当前object是否包含触摸点,在这里为了得到更好的点击效果,我将标记的区域放大了 if (objX - location.getWidth() < clickX && objX + location.getWidth() > clickX && objY + location.getHeight() > clickY && objY - location.getHeight() < clickY) { if (object.getMarkListener() != null) { object.getMarkListener().onMarkClick(clickX, clickY); } break; } } } // 计算两个触摸点的距离 private float spacing(MotionEvent event) { float x = event.getX(0) - event.getX(1); float y = event.getY(0) - event.getY(1); return (float) Math.sqrt(x * x + y * y); } private void draw() { new Thread(new Runnable() { @Override public void run() { // TODO Auto-generated method stub Canvas canvas = getHolder().lockCanvas(); if (canvas != null && mBitmap != null) { canvas.drawColor(Color.GRAY); Matrix matrix = new Matrix(); matrix.setScale(mCurrentScale, mCurrentScale, mBitmap.getWidth() / 2, mBitmap.getHeight() / 2); matrix.postTranslate(mapCenter.x - mBitmap.getWidth() / 2, mapCenter.y - mBitmap.getHeight() / 2); canvas.drawBitmap(mBitmap, matrix, mPaint); for (MarkObject object : markList) { Bitmap location = object.getmBitmap(); matrix.setScale(1.0f, 1.0f); // 使用Matrix使得Bitmap的宽和高发生变化,在这里使用的mapX和mapY都是相对值 matrix.postTranslate( mapCenter.x - location.getWidth() / 2 - mBitmap.getWidth() * mCurrentScale / 2 + mBitmap.getWidth() * object.getMapX() * mCurrentScale, mapCenter.y - location.getHeight() - mBitmap.getHeight() * mCurrentScale / 2 + mBitmap.getHeight() * object.getMapY() * mCurrentScale); canvas.drawBitmap(location, matrix, mPaint); } } if (canvas != null) { getHolder().unlockCanvasAndPost(canvas); } } }).start(); } @Override public boolean onTouchEvent(MotionEvent event) { // TODO Auto-generated method stub switch (event.getAction() & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: if (event.getPointerCount() == 1) { // 如果两次点击时间间隔小于一定值,则默认为双击事件 if (event.getEventTime() - lastClickTime < DOUBLE_CLICK_TIME_SPACE) { zoomIn(); } else { mStartPoint.set(event.getX(), event.getY()); mStatus = Status.DRAG; } } lastClickTime = event.getEventTime(); break; case MotionEvent.ACTION_POINTER_DOWN: float distance = spacing(event); if (distance > 10f) { mStatus = Status.ZOOM; oldDist = distance; } break; case MotionEvent.ACTION_MOVE: if (mStatus == Status.DRAG) { drag(event); } else if (mStatus == Status.ZOOM) { zoomAction(event); } break; case MotionEvent.ACTION_UP: if (mStatus != Status.ZOOM) { clickAction(event); } case MotionEvent.ACTION_POINTER_UP: oldRate = mCurrentScale; mStatus = Status.NONE; break; default: break; } return true; } @Override public void surfaceCreated(SurfaceHolder holder) { // TODO Auto-generated method stub draw(); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { // TODO Auto-generated method stub } @Override public void surfaceDestroyed(SurfaceHolder holder) { // TODO Auto-generated method stub if (mBitmap != null) { mBitmap.recycle(); } for (MarkObject object : markList) { if (object.getmBitmap() != null) { object.getmBitmap().recycle(); } } } // 获得状态栏高度 private int getStatusBarHeight() { Class<?> c = null; Object obj = null; Field field = null; int x = 0; try { c = Class.forName("com.android.internal.R$dimen"); obj = c.newInstance(); field = c.getField("status_bar_height"); x = Integer.parseInt(field.get(obj).toString()); return getResources().getDimensionPixelSize(x); } catch (Exception e1) { e1.printStackTrace(); return 75; } }}
MarkObject类用于存储标记信息:
package com.example.maptest;import android.graphics.Bitmap;import android.graphics.Rect;public class MarkObject { private Bitmap mBitmap; private float mapX; private float mapY; private MarkClickListener listener; public MarkObject() { } public MarkObject(Bitmap mBitmap, float mapX, float mapY) { super(); this.mBitmap = mBitmap; this.mapX = mapX; this.mapY = mapY; } /** * @return the mBitmap */ public Bitmap getmBitmap() { return mBitmap; } /** * @param mBitmap * the mBitmap to set */ public void setmBitmap(Bitmap mBitmap) { this.mBitmap = mBitmap; } /** * @return the mapX */ public float getMapX() { return mapX; } /** * @param mapX * the mapX to set */ public void setMapX(float mapX) { this.mapX = mapX; } /** * @return the mapY */ public float getMapY() { return mapY; } /** * @param mapY * the mapY to set */ public void setMapY(float mapY) { this.mapY = mapY; } public MarkClickListener getMarkListener() { return listener; } public void setMarkListener(MarkClickListener listener) { this.listener = listener; } public interface MarkClickListener { public void onMarkClick(int x, int y); }}
注意问题
1.每次使用Matrix进行缩放时,均设置缩放中心为图片地图中心(这里是相对图片来说的,所以是
(mBitmap.getWidth() / 2, mBitmap.getHeight() / 2)的位置,而不是mapCenter;),这样在我们处理图片的缩放时mapCenter的位置不会改变,如果不这样做的话,处理mapCenter的位置变化十分困难。
2.为了避免不同分辨率的手机获得的图片高度,宽度不一致的情况,这里采用的是标记相对于图片的整体位置值,即标记在图片中的像素坐标除以图片的高或宽。
3.为了获得良好的用户体验,当我们拖拽图片离开了屏幕边缘的时候,应当重新设定mapCenter以避免这种情况;同时在处理缩放事件时也应当注意。
4.为了获得高效率,我们利用SurfaceView来做,并在异步线程中进行地图更新,关于SurfaceView的用法可以参考
Android开发–SurfaceView的基本用法
5.在surfaceDestroyed记得回收Bitmap资源。
源码下载
点击下载源码