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

LZ涸泽而渔 LZ涸泽而渔     2022-12-07     671

关键词:

一:最近学习了自定义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 = 

androidui系列-自定义view手绘小黄人

总是想尝试各种自定义控件,来熟悉谷歌提供的一些自定绘图的方法,那就画一个小黄人吧。我在git上找到一个小换人的源码。它是按照比例计算的,有一定的公式,我觉得太麻烦了。就用自己的理解画了一个写... 查看详情

android进阶之旅-自定义view篇

...同行。经过反复的思考,首先分享*Android进阶之旅-自定义View篇*。  跟内涵段子项目不一样的是,我会先从自定义View的最基础开始,一直讲到各种高级效果。当然也需要看源码,比如对于onMeasure()方法,以 查看详情

android进阶之自定义view实战九宫格手势解锁实现

一.引言在上篇博客Android进阶之自定义View实战(一)仿iOSUISwitch控件实现中我们主要介绍了自定义View的最基本的实现方法。作为自定义View的入门篇,仅仅介绍了Canvas的基本使用方法,而对用户交互层面仅仅处理了单击事件... 查看详情

安卓自定义view进阶-canvas之绘制基本形状

...形状作者微博:@GcsSloop【本系列相关文章】在上一篇自定义View分类与流程中我们了解自定义View相关的基本知识,不过,这些东西依旧还是理论,并不能拿来(zhuang)用(B),这一次我们就了解一些能(zhaung)用(B)的东西。在... 查看详情

android进阶知识——view的工作原理(代码片段)

...系4.View的工作流程3.1measure过程3.2layout过程3.3draw过程4.自定义View4.1自定义View的分类4.2自定义View须知4.3自定义View示例本章我们主要介绍两个方面的内容,首 查看详情

进阶篇-用户界面:5.android绘图api自定义view(视图)

1.自定义视图并为其添加属性  我们平时用的Button啊TextView啊都是安卓中系统自带的控件供开发者使用,但是,这些事远远不够的,有时候我们需要自定义控件。(1)新建一个类MyView使其继承View类importandroid.content.Context;imp... 查看详情

android进阶之自定义view实战贝塞尔曲线应用

Android进阶之自定义View实战(三)贝塞尔曲线应用一、引言在自定义View中,常常看到这样一些非常规的UI效果,如水滴、心型、水波、仿真书页翻动、弹射床等效果,这里面都包含一个重要的要素:贝塞尔曲线(Béziercurve&#x... 查看详情

安卓自定义view进阶-事件分发机制原理(代码片段)

...前讲解了很多与View绘图相关的知识,你可以在安卓自定义View教程目录中查看到这些文章,如果你理解了这些文章,那么至少2D绘图部分不是难题了,大部分的需求都能满足,但是关于View还有很多知识点,... 查看详情

android进阶之自定义view实战仿iosuiswitch控件实现

一.引言个人觉得,自定义View一直是Android开发最变换莫测、最难掌握、最具吸引力的地方。因为它涉及到的知识点比较多,想在实际应用中驾轻就熟,由浅入深,你需要掌握以下知识点:1.View的绘制机制以及Canvas、... 查看详情

android自定义view进阶-xfermode(代码片段)

在Android自定义控件中,Xfermode知识点占有很重要的地位,它能帮助我们实现很多炫酷的效果。例如,实现各种形状的图片控件;结合属性动画实现渐变效果。Xfermode介绍Xfermode主要是通过paint.setXfermode(Xfermodexfermode)... 查看详情

安卓自定义view进阶-canvas之图片文字

...话,那么恭喜你,本篇结束之后,大部分的自定义View已经难不倒你了,当然了,这并不是终点 查看详情

安卓自定义view进阶-matrixcamera(代码片段)

首发地址:http://www.gcssloop.com/customview/matrix-3d-camera本篇依旧属于Matrix,主要讲解Camera,Android下有很多相机应用,其中的美颜相机更是不少,不过今天这个Camera可不是我们平时拍照的那个相机,而是graphic包... 查看详情

android进阶之自定义view(文字圆形边框)(代码片段)

最近写自定义的view写了很多,打算好好完整学习一下,顺便也是记录下,首先来看看效果大概是实现上面的效果吧,其实做起来很简单,只需要继承TextView,然后在外面画上一个框即可,代码如下:public... 查看详情

安卓自定义view进阶-matrix原理(代码片段)

Matrix原理作者微博:@GcsSloop【本系列相关文章】前言本文内容偏向理论,和画布操作有重叠的部分,本文会让你更加深入的了解其中的原理。本篇的主角Matrix,是一个一直在后台默默工作的劳动模范,虽然我们... 查看详情

android进阶知识——view的工作原理(代码片段)

...系4.View的工作流程3.1measure过程3.2layout过程3.3draw过程4.自定义View4.1自定义View的分类4.2自定义View须知4.3自定义View示例本章我们主要介绍两个方面的内容,首先介绍View的工作原理,接着介绍自定义View的实现方式。有的时候... 查看详情

安卓自定义view进阶-canvas之画布操作

Canvas之画布操作作者微博:@GcsSloop【本系列相关文章】上一篇Canvas之绘制基本形状中我们了解了如何使用Canvas绘制基本图形,本次了解一些基本的画布操作。本来想把画布操作放到后面部分的,但是发现很多图形绘制都... 查看详情

安卓自定义view进阶-贝塞尔曲线

Path之贝塞尔曲线作者微博:@GcsSloop【本系列相关文章】在上一篇文章Path之基本图形中我们了解了Path的基本使用方法,本次了解Path中非常非常非常重要的内容-贝塞尔曲线。一.Path常用方法表为了兼容性(偷懒)本表格中去除... 查看详情

安卓自定义view进阶-path基本操作

Path之基本操作作者微博:@GcsSloop【本系列相关文章】在上一篇Canvas之图片文字中我们了解了如何使用Canvas中绘制图片文字,结合前几篇文章,Canvas的基本操作已经差不多完结了,然而Canvas不仅仅具有这些基本的操作... 查看详情