自定义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进阶--手绘地图的主要内容,如果未能解决你的问题,请参考以下文章