当前位置: 代码迷 >> Android >> Android开发-仿景点通景区mapSurfaceView实现
  详细解决方案

Android开发-仿景点通景区mapSurfaceView实现

热度:34   发布时间:2016-04-28 01:55:07.0
Android开发--仿景点通景区地图SurfaceView实现

最近在帮老师做一个项目,类似于景点通的App手机应用,我们是要精细化一些室内的地图,室内的地图采用的是自己的一套定位机制,所有室内地图也要自己来实现,参考了网上一些例子,考虑到效率的问题,最后决定使用SurfaceView来进行地图绘制,实现的功能有:

  1. 双击放大
  2. 多点触摸放大
  3. 地图拖拽
  4. 添加地图标记
    效果图一张:

代码思路

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资源。

源码下载

点击下载源码

  相关解决方案