自定义View进阶--手绘地图

Posted LZ涸泽而渔

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自定义View进阶--手绘地图相关的知识,希望对你有一定的参考价值。

一:最近学习了自定义view,刚好就接到了相关的需求,于是就上手做了,下面描述一下需求

       需求:简单的来说就是做一个地图,不同的是,为了追求美观,于是地图是一张由UI出的图片,poi点

       为运营采集点,实现地图的缩放,移动,poi打点,以及其他的东西,由于涉及到的东西较多,因此本次就说这些

        内容,包括分析、实现、踩坑等内容

-------------------分割线------------------------------

    本篇适合有一些自定义View基础的朋友食用

二:效果



这些就是实现的效果了,因为舍不得钱开会员,没找到合适的网站,因此就分成了三个gif供看观浏览,接下来进入分析阶段

三:分析

    1.首先考虑大方向的实现方式,比较简单的是自定义viewGroup,稍复杂的是自定义view

    2.需求中包含几个内容,第一个为图片的初始化放置,第二为图片的缩放与位移,第三为图片的边界回弹,第四位poi点的位          置确定,第五为poi的点击事件

大致需求就是这些,当然还有一些双击放大,层级变换等等,不过做完这些也就一通白通了


四:实现

    本篇主要讲自定义ViewGroup的实现方式,自定义View的在下一篇进行

1.自定义一个类,集成ViewGroup,继承构造方法

public class MapLayout extends RelativeLayout 

然后集成构造方法

public MapLayout(Context context, @Nullable AttributeSet attrs) 
    super(context, attrs);
    mContext = context;
    init();

这里说一下双参和单参构造函数的区别(一般只关注这两种),单参为你的自定义view被实例化时调用,而双参中调用了一些自定义属性,也就是说只要在xml中使用了自定义view都需要调用双参,相信机智的你明白了

2.接着进行:

实例化画笔,上下文,以及将要使用到的东西。这里先将成员变量放出来,后面大家可对照查看,每个一个成员变量都打上了注释

private Context mContext;
//画笔
Paint mPaint;
//控件宽度
private int mViewWidth;
//控件高度
private int mViewHeight;
//控制画板的矩阵
private Matrix mMapMatrix;
//地图初始化需要位移
private float mInitTranslateX;
private float mInitTranslateY;
//地图Bitmap
private Bitmap mMapBitmap;
//此处手指情况只考虑单指移动和双指缩放
//上次手指停留位置(单手指)
private float mLastSinglePointX;
private float mLastSinglePointY;
//用于双指缩放
private float mLastDistancce;
//最小缩放倍数
private float mMinScale = 0.8f;
//最大缩放倍数
private float mMaxScale = 4.0f;

//上次手机离开时缩放倍数
private float mLastScale;

//是否能够移动的标志
private boolean mCouldMove = true;
//矩阵对应的值
float[] mNowMatrixvalues;

//x位移最大值
private float mMaxTranslateX;
//Y位移最大值
private float mMaxTranslateY;

/**
 * 边界回弹状态  边界起头:1   例:11
 *
 * @param context
 */
private int mNowBoundStates = 0;
//只向上恢复
private static final int BOUND_ONLY_TOP = 11;
//只向左恢复
private static final int BOUND_ONLY_LEFT = 12;
//同时向左和上恢复
private static final int BOUND_TOPANDLEFT = 13;
//只向右恢复
private static final int BOUND_ONLY_RIGHT = 14;
//同时向右上恢复
private static final int BOUND_RIGHTANDTOP = 15;
//只向下恢复
private static final int BOUND_ONLY_BOTTOM = 16;
//同时向右下恢复
private static final int BOUND_RIGHTANDBOTTOM = 17;
//同时向左下恢复
private static final int BOUND_LEFTANDBOTTOM = 18;
//属性动画起始和结束值
private static final int REBOUND_ANIMATION_START_VALUE = 0;
private static final int REBOUND_ANIMATION_END_VALUE = 100;
private static final int REBOUND_ANIMATION_TIME = 200;

//poi实体集合
List<MapPoiEntity> mMapPoiEntityList;

3.开始的话,不用过多的关注成员变量,也算是看代码的一种技巧,需要知道什么回来查找就好了

接着开始测量自定义View的宽高,因为我知道自己的使用情况,因此就定义了一种情况来测量宽高(match_parent,或者固定宽高时)

/**
 * 测量控件宽高
 *
 * @param widthMeasureSpec
 * @param heightMeasureSpec
 */
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) 
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) 
        mViewWidth = MeasureSpec.getSize(widthMeasureSpec);
    
    if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY) 
        mViewHeight = MeasureSpec.getSize(heightMeasureSpec);
    
    mMaxTranslateX = mViewWidth / 6;
    mMaxTranslateY = mViewHeight / 8;
    setMeasuredDimension(mViewWidth, mViewHeight);

中间有成员变量,请对照之前查看,后面就不在复述了

5.xml中使用

<com.example.a12280.maptestproject.MapView
    android:layout_width="match_parent"
    android:clipChildren="false"
    android:id="@+id/map"
    android:background="@color/transparent"
    android:layout_centerInParent="true"
    android:layout_height="match_parent"
  />

activity中申明并初始化

mMap.post(new Runnable() 
    @Override
    public void run() 
        Glide.with(MainActivity.this).load(R.drawable.map).asBitmap().into(new SimpleTarget<Bitmap>() 
            @Override
            public void onResourceReady(Bitmap resource, GlideAnimation<? super Bitmap> glideAnimation) 
                mMap.initImageWH(resource);
                mMap.setMapPoiEntityList(mMapPoiEntityList);
            
        );
    
);

可以看到此处调用了控件的post方法,标识控件绘制结束,也就是可以获取正确宽高的时刻,并且此处使用Glide加载图片获取到了bitmap,同时实现网络加载,三级缓存,图片压缩,还是很方便的,至于拿到bitmap之后做的事情,会在后面将到

6.初始化图片

首先将图片放置到屏幕上面,一般来说,图片的比例不会和屏幕的比例完全吻合,因此需要对图片进行合适的缩放,此处采用的方式是保护一边,即不管怎样至少有一边完全贴合屏幕的一边,另一边进行居中显示

/**
 * 初始化图片的宽高
 */
public void initImageWH(Bitmap mapImg) 
    float imgHeight = mapImg.getHeight();
    float imgWidth = mapImg.getWidth();
    float changeWidth = 0.0f;
    float changeHeight = 0.0f;
    float scaleWidth = mViewWidth / imgWidth;
    float scaleHeight = mViewHeight / imgHeight;
    //对图片宽高进行缩放
    if (scaleHeight > scaleWidth) 
        changeHeight = mViewHeight;
        changeWidth = mViewHeight * imgWidth / imgHeight;
        mInitTranslateY = 0;
        mInitTranslateX = -Math.abs((changeWidth - mViewWidth) / 2);
     else 
        changeWidth = mViewWidth;
        changeHeight = mViewWidth * imgHeight / imgWidth;
        mInitTranslateY = -Math.abs((changeHeight - mViewHeight) / 2);
        mInitTranslateX = 0;
    
    Matrix matrix = new Matrix();
    matrix.postScale(changeWidth / imgWidth, changeHeight / imgHeight);
    mMapBitmap = Bitmap.createBitmap(mapImg, 0, 0, (int) imgWidth, (int) imgHeight, matrix, true);
    if (mapImg!=null&&mMapBitmap!=null&&!mapImg.equals(mMapBitmap)&&!mapImg.isRecycled())
        mapImg=null;
    
    //初次加载时,将Matrix移动到正确位置
    mMapMatrix.postTranslate(mInitTranslateX,mInitTranslateY);
    refreshUI();

此处将我们自定义view的初始化放在其post方法中使用的用处就来了,因为对图片缩放需要拿到控件的宽高,而这种异步的事情不可控,因此就等待其宽高确定再进行初始化(不要问我为啥知道。。。),然后就是初始化矩阵mMapMatrix

另外,此处说明一下,地图的缩放与移动都将采用Matrix作为中间实现对象,不明白Matrix还是先理解一下再向下看吧

然后解释一下矩阵的各个值位置


--------------分割线--------------------------

经过上面的步骤也就正确的将图片放置在了屏幕上了,接下来对其进行移动和缩放

7.移动和缩放

首先,我并没有采用手势的方案(不熟悉手势使用方法,暂时没有去看,或许会简单,或许不会),而是直接采用监听onTouch的方式,要监听ouTouch,首先得将其返回值变为true,事件分发都是通过返回值来确定行为的

/**
 * 用户触控事件
 *
 * @param event
 * @return
 */
@Override
public boolean onTouchEvent(MotionEvent event) 
    mMapMatrix.getValues(mNowMatrixvalues);
    //缩放
    scaleCanvas(event);
    //位移
    translateCanvas(event);
    return true;

贴一个方法为获取bitmap对应的矩阵

/**
 * 获取当前bitmap矩阵的RectF,以获取宽高与margin
 *
 * @return
 */
private RectF getMatrixRectF() 
    RectF rectF = new RectF();
    if (mMapBitmap != null) 
        rectF.set(0, 0, mMapBitmap.getWidth(), mMapBitmap.getHeight());
        mMapMatrix.mapRect(rectF);
    
    return rectF;

首先看位移事件:

/**
 * 用户手指的位移操作
 *
 * @param event
 */
public void translateCanvas(MotionEvent event) 
    if (event.getPointerCount() == 1) 
        switch (event.getAction()) 
            case MotionEvent.ACTION_DOWN:
                //获取到上一次手指位置
                mLastSinglePointX = event.getX();
                mLastSinglePointY = event.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                if (mCouldMove) 
                    float translateX = event.getX() - mLastSinglePointX;
                    float translateY = event.getY() - mLastSinglePointY;
                    RectF matrixRectF = getMatrixRectF();
                    //边界控制
                    //left不能大于mMaxTranslateX,right值不能小于mViewwidth-mMaxTranslateX
                    if ((matrixRectF.left >= mMaxTranslateX && translateX > 0) || ((matrixRectF.right <= mViewWidth - mMaxTranslateX) && translateX < 0)) 
                        translateX = 0;
                    
                    //top不能大于mMaxTranslateY,bottom值不能小于mViewHeight-mMaxTranslateY
                    if ((matrixRectF.top >= mMaxTranslateY && translateY > 0) || ((matrixRectF.bottom <= mViewHeight - mMaxTranslateY) && translateY < 0)) 
                        translateY = 0;
                    
                    //对本次移动造成的超过范围做调整
                    if (translateX > 0 && ((matrixRectF.left + translateX) > mMaxTranslateX)) 
                        translateX = mMaxTranslateX - matrixRectF.left;
                    
                    if (translateX < 0 && ((matrixRectF.right + translateX) < mViewWidth - mMaxTranslateX)) 
                        translateX = -(mMaxTranslateX - (mViewWidth - matrixRectF.right));
                    
                    if (translateY > 0 && ((matrixRectF.top + translateY) > mMaxTranslateY)) 
                        translateY = mMaxTranslateY - matrixRectF.top;
                    
                    if (translateY < 0 && ((matrixRectF.bottom + translateY) < mViewHeight - mMaxTranslateY)) 
                        translateY = -(mMaxTranslateY - (mViewHeight - matrixRectF.bottom));
                    
                    mMapMatrix.postTranslate(translateX, translateY);
                    mLastSinglePointX = event.getX();
                    mLastSinglePointY = event.getY();
                    refreshUI();
                
                break;
            case MotionEvent.ACTION_UP:
                mLastSinglePointX = 0;
                mLastSinglePointY = 0;
                mLastDistancce = 0;
                mCouldMove = true;
                controlBound();
                break;
        
    

此处用了一个布尔值mCouldMove,用于消除双指与单指交互时的错误性,感兴趣的可以试试不加这个布尔值的效果,总的来说就是将两次位移的差值体现在矩阵中,然后绘制上去,并且需要注意,手指抬起时置空上一次按下的x、y值,controlBound为边界控制,等会再说

接下来是双指缩放:

/**
 * 用户双指缩放操作
 *
 * @param event
 */
public void scaleCanvas(MotionEvent event) 
    if (event.getPointerCount() == 2) 
        mCouldMove = false;
        switch (event.getAction()) 
            case MotionEvent.ACTION_DOWN:
                float lastlengthOFY = Math.abs(event.getY(1) - event.getY(0));
                float lastlengthOFX = Math.abs(event.getX(1) - event.getX(0));
                mLastDistancce = (float) Math.sqrt(lastlengthOFX * lastlengthOFX + lastlengthOFY * lastlengthOFY);
                break;
            case MotionEvent.ACTION_MOVE:
                float lengthOFY = Math.abs(event.getY(1) - event.getY(0));
                float lengthOFX = Math.abs(event.getX(1) - event.getX(0));
                float distance = (float) Math.sqrt(lengthOFX * lengthOFX + lengthOFY * lengthOFY);

                float scale = distance / mLastDistancce;
                if (mLastDistancce != 0) 
                    //缩放大小控制
                    float nowScale = mNowMatrixvalues[Matrix.MSCALE_X];
                    if ((nowScale > mMaxScale && scale > 1.0f) || (nowScale < mMinScale && scale < 1.0f)) 
                        return;
                    
                    mMapMatrix.postScale(scale, scale,
                            event.getX(0) + (event.getX(1) - event.getX(0)) / 2,
                            event.getY(0) + (event.getY(1) - event.getY(0)) / 2);
                
                mLastDistancce = distance;
                refreshUI();
                break;
            case MotionEvent.ACTION_POINTER_UP:
                mLastDistancce = 0;
                break;
            case MotionEvent.ACTION_POINTER_2_UP:
                mLastDistancce = 0;
                break;
            default:
                break;
        
    

同样的,双指缩放拿到缩放倍数,然后体现到矩阵中去,缩放比例为第一次的x,y值获取到的三角边长与第二次的比例,需要注意的是,双指缩放,单指抬起时的时间监听(。。蒙蔽了一段时间,没法办打印事件值才找到,感兴趣的可以试验一下,两根手指,第一根和第二根的触发事件不同),缩放中心为本次move的两指中心,并且加上缩放倍数的控制,同样的需要置空之前的值

至此也就完成了缩放与移动操作,用矩阵实现还是很简单的,需要注意的是,矩阵的操作,前一次对后一次有影响,也就是对矩阵进行操作,是将要操作多少,而不是操作到多少,举个栗子:将图片进行缩放1.4倍,如果第一次缩放了1.2倍,则第二次需要缩放1.4/1.2倍,位移也是一样的

8.边界回弹

首先做边界回弹的话,需要区分一下状态,之前的成员变量表应该已经显示出来了,并且下方代码的注释中解释的很明白:

/**
 * 用于控制用户的手指抬起时,对留边的情况进行控制
 */
private void controlBound() 
    RectF matrixRectF = 

以上是关于自定义View进阶--手绘地图的主要内容,如果未能解决你的问题,请参考以下文章

AndroidUI系列 - 自定义View手绘小黄人

自定义View实现跟随手指的小球

自定义View--滚动View

Android 自定义View 进阶

安卓自定义View进阶-分类与流程

百度地图画圈搜索功能探索