一:最近学习了自定义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 = getMatrixRectF(); float nowScale = mNowMatrixvalues[Matrix.MSCALE_X]; if (mNowMatrixvalues[Matrix.MSCALE_X] < 1 && (mNowMatrixvalues[Matrix.MSCALE_X] + 0.001f) > 1) { //消除缩放误差 nowScale = 1; } if (nowScale < 1) { //缩小的情况,缩小以四个顶角为基准,并且向位移多的地方恢复 scaleBoundAnimation(matrixRectF); } else { //情况判断,放大或者一倍的情况 if (matrixRectF.top > 0) { //头在边界下 if (matrixRectF.left > 0) { //向左上恢复的情况 mNowBoundStates = BOUND_TOPANDLEFT; } else if (matrixRectF.right < mViewWidth) { //向右上恢复的情况 mNowBoundStates = BOUND_RIGHTANDTOP; } else { //只向上恢复的情况 mNowBoundStates = BOUND_ONLY_TOP; } } else if (matrixRectF.top < 0 && matrixRectF.bottom > mViewHeight) { //头在边界上,底在边界下 if (matrixRectF.left > 0) { //只向左恢复的情况 mNowBoundStates = BOUND_ONLY_LEFT; } else if (matrixRectF.right < mViewWidth) { //只向右恢复的情况 mNowBoundStates = BOUND_ONLY_RIGHT; } } else if (matrixRectF.top < 0 && matrixRectF.bottom < mViewHeight) { //底在边界上 if (matrixRectF.left > 0) { //向左下恢复的情况 mNowBoundStates = BOUND_LEFTANDBOTTOM; } else if (matrixRectF.right < mViewWidth) { //向右下恢复的情况 mNowBoundStates = BOUND_RIGHTANDBOTTOM; } else { //只向下恢复的情况 mNowBoundStates = BOUND_ONLY_BOTTOM; } } translateBoundAnimation(matrixRectF); } }
这边是先将状态进行赋值,用于后面的计算,回弹区分两种,缩小时的回弹与放大时的回弹,先说放大时,放大的时候只需要进行位移回弹即可
/** * 边界回弹动画(位移动画) * * @param matrixRectF */ public void translateBoundAnimation(final RectF matrixRectF) { //应该位移的x值与y值 float boundTranslateX = 0.0f; float boundTranslateY = 0.0f; //右边恢复需要的位移值 float translateRight = mViewWidth - matrixRectF.right; //下边恢复需要的位移值 float translateBottom = mViewHeight - matrixRectF.bottom; //放大或者原图状态,直接进行位移操作 switch (mNowBoundStates) { case BOUND_ONLY_TOP: boundTranslateY = -matrixRectF.top; break; case BOUND_ONLY_LEFT: boundTranslateX = -matrixRectF.left; break; case BOUND_ONLY_RIGHT: boundTranslateX = translateRight; break; case BOUND_ONLY_BOTTOM: boundTranslateY = translateBottom; break; case BOUND_TOPANDLEFT: boundTranslateY = -matrixRectF.top; boundTranslateX = -matrixRectF.left; break; case BOUND_RIGHTANDTOP: boundTranslateY = -matrixRectF.top; boundTranslateX = translateRight; break; case BOUND_RIGHTANDBOTTOM: boundTranslateX = translateRight; boundTranslateY = translateBottom; break; case BOUND_LEFTANDBOTTOM: boundTranslateX = -matrixRectF.left; boundTranslateY = translateBottom; break; default: break; } //位移属性动画 ValueAnimator valueAnimator = ValueAnimator.ofInt(REBOUND_ANIMATION_START_VALUE, REBOUND_ANIMATION_END_VALUE); valueAnimator.setDuration(REBOUND_ANIMATION_TIME); valueAnimator.setInterpolator(new LinearInterpolator()); final float finalBoundTranslateX = boundTranslateX; final float finalBoundTranslateY = boundTranslateY; //上一次valueAnimation移动的位置[x,y] final float[] lastTranslate = new float[2]; valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { Integer numerator = (Integer) valueAnimator.getAnimatedValue(); float total = REBOUND_ANIMATION_END_VALUE - REBOUND_ANIMATION_START_VALUE; float shouldTranslateX = finalBoundTranslateX * ((float) numerator / total) - lastTranslate[0]; lastTranslate[0] = finalBoundTranslateX * ((float) numerator / total); float shouldTranslateY = finalBoundTranslateY * ((float) numerator / total) - lastTranslate[1]; lastTranslate[1] = finalBoundTranslateY * ((float) numerator / total); mMapMatrix.postTranslate(shouldTranslateX, shouldTranslateY); refreshUI(); } }); valueAnimator.start(); mNowBoundStates = 0; }
此处就是计算现在的x、y值与应该所处得x、y值的差值,然后运用ValueAnimation进行动画回弹,ValueAnimation也就是将目标值与初始值,按照给的时间点,分配若干进度,手机越好的,按理来说就会分的越多,效果越好,至于里面的算法,也就是这一次应该移动的值为本次目标值减去上一次移动值,同样的后面缩放也类似,不过是加减法变成乘除法
然后考虑缩放的情况:
/** * 边界回弹动画(缩放动画) */ public void scaleBoundAnimation(final RectF martrixRectF) { final float startScale = mNowMatrixvalues[Matrix.MSCALE_X]; //位移属性动画 //上一次的缩放倍数 final float[] lastScale = {0.0f}; ValueAnimator valueAnimator = ValueAnimator.ofInt(REBOUND_ANIMATION_START_VALUE, REBOUND_ANIMATION_END_VALUE); valueAnimator.setDuration(REBOUND_ANIMATION_TIME); valueAnimator.setInterpolator(new LinearInterpolator()); valueAnimator.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animator) { } @Override public void onAnimationEnd(Animator animator) { //重新获取一下值 mMapMatrix.getValues(mNowMatrixvalues); //缩放结束再查看边界进行位移缩放 controlBound(); } @Override public void onAnimationCancel(Animator animator) { } @Override public void onAnimationRepeat(Animator animator) { } }); valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { Integer numerator = (Integer) valueAnimator.getAnimatedValue(); float total = REBOUND_ANIMATION_END_VALUE - REBOUND_ANIMATION_START_VALUE; if (lastScale[0] != 0) { float shouldScale = (((float) numerator / total) * (1 - startScale) + startScale) / lastScale[0]; mMapMatrix.postScale(shouldScale, shouldScale, martrixRectF.left + martrixRectF.width() / 2, martrixRectF.top + martrixRectF.height() / 2); refreshUI(); } lastScale[0] = ((float) numerator / total) * (1 - startScale) + startScale; } }); valueAnimator.start(); }
之所以把两种情况分开就是为了方便,因为计划的是x、y轴都缩放同样的倍数保证图片宽高比不变,但是缩放的中心不可控,因此回弹放大如果不进行位移,必然会留边,因此此处操作是先进行缩放操作,然后再进行位移操作,这样的话目的就打成了,当然只是这样写比较方便,你也可以尝试一变缩放,一遍进行位移控制。此处是加上了动画监听,缩放结束时,进行位移回弹
-----------------------分割线----------------------------------------
至此,图片的操作也就完成了,回顾一下,实现了图片初始位置放置,缩放,平移,边界回弹
先放上绘制的方法,当然特别简单
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (mMapBitmap == null) return; //绘制地图 canvas.drawBitmap(mMapBitmap, mMapMatrix, mPaint); }
并且,使用bitmap时,一定要注意资源管理,不然oom会经常临幸你的
//回收同步生命周期 public void onDestroy() { if (mMapBitmap != null) { mMapBitmap = null; } }
@Override protected void onDestroy() { super.onDestroy(); mMap.onDestroy(); }
9.绘制点
点的思路为点相对图片的位置百分比,转化为地图百分比,然后由地图百分比转化为屏幕像素值,并且在地图进行操作时,对点的位置进行更新
首先定义实体,然后提供假数据,并且自定义view对外提供方法
private String name; //poi点的状态 private int status; //百分比位置 private float precentageTop; private float precentageLeft; private int i;这是我定义的实体,很直观,然后进行假数据操作
mMapPoiEntityList=new ArrayList<>(); for (int i = 0; i < 10; i++) { MapPoiEntity mapPoiEntity=new MapPoiEntity(); mapPoiEntity.setName("天安门:"+i); mapPoiEntity.setPrecentageLeft(i*5); mapPoiEntity.setPrecentageTop(i*5); mapPoiEntity.setI(i); mMapPoiEntityList.add(mapPoiEntity); }
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里面的方法,第二个也就是添加点的方法
/** * 设置poi内容 */ public void setMapPoiEntityList(List<MapPoiEntity> mapPoiEntityList) { mMapPoiEntityList = mapPoiEntityList; if (mMapPoiEntityList != null) { for (int i = 0; i < mMapPoiEntityList.size(); i++) { View view = LayoutInflater.from(mContext).inflate(R.layout.poi_point_layout_default, null); final int finalI = i; view.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { Toast.makeText(mContext, "被点击了" + mMapPoiEntityList.get(finalI).getI(), Toast.LENGTH_SHORT).show(); } }); addView(view); } } requestLayout(); }
很直观的直接addView就可以将xml绘制的View添加到我们的自定义View上面,调用requestLayout意味着进行从新测量布局,我们趁此进行子控件的布局分配
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); if (mMapPoiEntityList == null || mMapPoiEntityList.size() < 1) { return; } RectF matrixRectF = getMatrixRectF(); mMapMatrix.getValues(mNowMatrixvalues); for (int i = 0; i < getChildCount(); i++) { MapPoiEntity mapPoiEntity = mMapPoiEntityList.get(i); //三角尖的位置 int left = 0; int top = 0; left = (int) (matrixRectF.width() * (mapPoiEntity.getPrecentageLeft() / 100.0f) + matrixRectF.left); top = (int) (matrixRectF.height() * (mapPoiEntity.getPrecentageTop() / 100.0f) + matrixRectF.top); //三角尖位置记得转化为控件的左上右下 getChildAt(i).layout(left - getChildAt(i).getMeasuredWidth() / 2, top - getChildAt(i).getMeasuredHeight(), left + getChildAt(i).getMeasuredWidth() / 2, top); } }
这里需要注意的也就是矩阵需要实时获取,才会有正确的值,还有子空间的位置中心点,应该是之前效果图的三角尖位置,不然效果就是poi点看起来会移动
还记得之前重复出现的refreshUI么,其实也就是。。。这个
/** * 刷新视图 */ private void refreshUI() { invalidate(); requestLayout(); }
这样的话,也就完全的实现了需求了,并且点击事件实现起来毫不费力,但是,个人觉得完全的自定义View更加流畅,自定义View实现方法将在自定义View--手绘地图(二)中讲解
以下为踩坑与注意事项总结:
1.成员变量命名请规范,不然你自己都会懵逼,小写m+大写单词,这样的命名方式爽爆
2.先搞清楚生命周期以及必然方法的先后顺序,避免后面补坑(post方法里面的泪)
3.双指事件,单指事件两者事件切换的管理与双指抬起的监听
4.请先想好实现方式与逻辑再写代码(尤其是算法方面),不然很容易被已写的代码影响思路,用笔在纸上画一画是很有必要的事情(如果脑子不能清楚的想明白的话)
5.多来写注释挺好的,方便自己,也方便以后的人(说不定呢。避免被骂)
6.onDraw里面如果不是非常必要,不要new对象,就算new了,也请控制
另外附上源码:点击打开链接