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

源码下载

点击下载源码

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。