最近在帮老师做一个项目,类似于景点通的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资源。
Android开发--仿景点通景区地图SurfaceView实现
原文地址:http://blog.csdn.net/smbroe/article/details/44520881