自定义View进阶--手绘地图(一)

canvas画线API实现简单地图 做一个青海项目时,涉及到地图,简单显示青海省个地区的用了一个svg插件,要求显示热点数据的就用了百度的地图API。地图的确高大上。 回顾一、后台老大哥负责跟客户沟通的,突然有一天客户要求说登录页显示地区的地图要有3D效果。心里顿时有一万匹草泥马奔过~~                 百度许久,没找到可用的方案,最终这种需求被开发说了No! 回顾二、另一个系统的登录页,因为用户要选择自己 阅读详情

一:最近学习了自定义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了,也请控制

另外附上源码:点击打开链接

canvas实现地图放大缩小拖拽 前言 canvas 基础 拖拽功能 边界条件 放大与缩小 阅读详情

相关推荐

VirtualBox安装Windows 98步骤

一、准备工作 1、下载Windows 98 SE光盘镜像,网上下载307.3M的光盘镜像,大部分网站都是这个镜像,如下载地址:http://www.itmop.com/downinfo/14216.html,这个镜像已做成启动盘,还是比较方便。但是这个镜像没有集成KEY,后期输入有点不方便,网上还有个RAR压缩的安装文件,解开是WIN98和WIN98SE两个版本的安装文件,WIN98SE已集成了KEY。用UltraISO打开307.3M的光盘镜像删除win98下的文件,将已集成了KEY的WIN98SE的文件

whziwen的博客 1万+

超简单的canvas绘制地图

    本文使用geojson数据,通过缩放和平移把地图的地理坐标系转换canvas的屏幕坐标系,然后将转换后的数据绘制到canvas上。     首先要计算数据的最大最小值,遍历所有坐标点的最大最小经纬度。在这个步骤要注意坐标点是否都遍历到,因为数据中可能会有多面和洞的存在。     第二步就是转换坐标系,地理坐标系转换canvas的屏幕坐标系。为了让地图能完整在页面展示,我们要有数据的最大最小...

weixin_34387468的博客 2063

为了测试一个老掉牙的浏览器兼容性,我折腾了VirtualBox 6.1、Win98和Dreamweaver 3.0

本文详细介绍了如何在VirtualBox 6.1中搭建Windows 98环境,用于测试老旧浏览器的网页兼容性。通过配置虚拟机、安装Dreamweaver 3.0以及解决网络和显示问题,开发者可以复现古董级系统的运行环境,确保现代网页在旧浏览器中的正常显示和功能。

weixin_42554151的博客 305

canvas根据坐标完成室内地图路线的绘制

canvas根据坐标完成室内地图路线的绘制 注释:此版本为低版本,后续会完善,只是提供一个思路 html: <canvas id="canvas" width="5083px" height="6579px"></canvas> style: <script> var c=document.getElementById( "canvas"); var ctx=c.getContext( "2d");

一杯清茶的博客 1690

Windows 98光盘引导核心文件win98.bif深度解析

Shell=指令指定在启动完成后加载的命令解释器。默认为,但可替换为其他兼容DOS的shell程序。应用场景使用更强大的第三方shell(如4DOS)可提供增强的命令补全、历史记录和脚本功能,适用于专业维护场景。在深入理解win98.bif文件的结构与系统引导机制后,实际应用层面的操作能力成为掌握该技术的关键。Windows 98 的可定制性虽受限于当时的硬件环境和操作系统架构,但其基于文本配置的引导方式为用户提供了高度灵活的手动干预空间。通过合理修改win98.bif。

weixin_33812391的博客 1174

Jquery+h5绘制地图

Jquery+h5绘制地图

如何用canvans实现地图上的运动轨迹

2.通过canvas绘制出运动轨迹。1.先需要一个地图的图片。

cuiyuchen111的博客 1907

基于mAppwidget实现的手绘地图demo

可使用不同的图片作为地图,支持缩放,pinch缩放,双击缩放,pop弹窗,语音导游,用户定位等功能,展示demo

虚拟机中安装Win98

Win98是微软于1998年发布的16位与32位混合的操作系统,也是一代经典的操作系统,期间出现了不少经典的软件与游戏,还是值得怀念的。为此笔者本文就介绍一下在VMWare中以及Qemu中安装Win98。

witton的专栏 7799

微信小程序地图map使用demoMapDemo-master.zip

微信小程序地图map使用demoMapDemo-master.zip

验证(uvm常见问题)--子类和父类的句柄和对象问题

sv中的子类和父类 在sv中,有如下两个类,child继承于parent。然后在test module中,正确转换的方法如下面所示。其中c1,c2,p1 都指向了同一个子类的对象c1。总结如下: 一个子类的对象是包括子类的成员变量,方法和父类的成员变量,方法。 子类的句柄c1可以直接赋值给父类的句柄p1,此时p1只可以访问父类的成员变量和方法,或者子类的虚方法,比如下面调用p1.display是...

q876507447的博客 7633

win98 支持html5,win98 ghost ghost在WIN98怎么使用

我想下载一个win98的ghost的操作系统,哪里有?ghost是没有,你也可以在网上搜。但是你可以下载原版的。CSS布局HTML小编今天和大家分享 win98 ghost 版系统盘 或ghost映像 下载地址我想知道关于Windows98系统上的Ghost系统使用方法我想问一下我家Windows98的操作系统安装了Ghost怎么在DOS操作系统中使用98系统因为包含DOS 所以更简单。重起机器 ...

weixin_39644377的博客 1051

微信公众号数据采集实战:历史文章、互动数据与评论详情全解析

本文提供了一套完整的微信公众号数据采集实战方案,详细解析了如何通过接口调用获取公众号的历史文章列表、单篇文章的互动数据以及详细的用户评论。文章从环境准备、核心参数解析到分页逻辑、批量处理和异常处理,提供了可运行的Python代码示例,旨在帮助开发者高效、稳定地构建自动化采集工具,用于竞品分析、内容研究和舆情监控。

u5v6w7的博客 1599

如何在64位win10中装个win98虚拟机

突发奇想,想装个win98系统玩一玩,于是就开始了我的尝试。 但是发现win10在64位长模式下没有办法安装16/32位混合的win98虚拟机,出现了一大堆应用程序错误,最后我在winXP虚拟机里面又嵌套了一层终于搞定了。以下为本文目录: 目录 一、碰壁经过 1、用windows自带的Hyper-V 2、用Oracle VirtualBox 3、用Vmware Workstation 4、下定决心玩套娃 二、成功方案 1、下载VirtualBox安装winXP 2、在winXP里安装wi

fcdm_的博客 7万+

COMSOL多物理场仿真:瓷绝缘子电热耦合建模与优化策略

本文详细介绍了使用COMSOL Multiphysics进行瓷绝缘子电热耦合仿真的完整流程与优化策略。文章阐述了电热耦合效应的工程意义,并逐步讲解了从几何建模、材料定义、物理场设置到边界条件与网格划分的关键步骤。重点分析了如何通过焦耳热耦合实现电场与温度场的双向交互,以及如何利用仿真结果进行参数化优化和瞬态故障模拟,为提升电力设备可靠性提供有效的数字化设计工具。

n8m7b6v5c4的博客 884

DevExpress学习之GridControl导入导出至EXCEL

导出: private void EXCELout_Click(object sender, EventArgs e) { SaveFileDialog saveFileDialog = new SaveFileDialog(); saveFileDialog.Title = "导出Excel"; saveFileDialog.Filter = "Excel文件(*.xls)|*.xls";

I Can -Jiang 2754

Win98USB驱动:让Win98兼容现代USB设备的核心工具

Win98USB驱动:让Win98兼容现代USB设备的核心工具 【下载地址】Win98USB驱动 这款开源项目提供了一个经过验证的Win98操作系统USB驱动程序,被誉为“万能驱动”,适用于多种USB设备,有效解决了Win98系统下USB设备的兼容性问题。经过实际使用测试,该驱动表现出色,稳定可靠,能够显著提升Win98...

gitblog_06779的博客 566

ABAP CDS 和 HANA SQL 的关系

摘要: ABAP CDS与HANA SQL既有紧密联系又有本质区别。ABAP CDS作为语义数据建模框架,在SQL基础上扩展了业务语义层,通过注解(annotations)、关联关系(associations)等机制为数据附加业务含义,并支持权限控制、客户端处理等ABAP平台特性。虽然CDS实体最终会转换为HANA数据库对象执行,但它并非简单的SQL封装,而是构建了独立于数据库的语义模型。二者在数据库执行层深度融合,ABAP CDS通过代码下推技术将计算任务优化到HANA层完成。实际开发中应优先使用ABAP

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 863

win98 支持html5,穿越1999:如何装一台Win98时代的PC

拼 命 加 载 中 ...回忆总是美好的,如今用着四核i7+顶级显卡+大屏幕显示器+Win7的电脑,依然有人怀念10多年前的PC时代,那时候还没有双核,Windows系统也没有那么稳定,动不动就给你蓝脸看,内存还没有现在的SSD缓存容量大,游戏画面粗糙的很儿童画一样。那个时代的系统那个时代的游戏那时候的配置现在看来非常烂,但是Win98时代配置一台属于自己的电脑恐怕是很多人奢侈的梦想,更重要的是当...

weixin_28973855的博客 1397

CAD二次开发c# .net加载cui\cuix菜单文件

public static void LoadMyCui(string cuiFile) { Document doc = Application.DocumentManager.MdiActiveDocument; object oldCmdEcho = Application.GetSystemVariable("CMDECHO"); object oldFileDia = Application.GetSys..

2128

Python爬虫教程:抓取短视频平台数据(以抖音和快手为例)

为什么抓取短视频平台数据?内容分析:通过抓取短视频平台上的视频内容、标题、标签等数据,可以进行内容分类、趋势分析、话题分析等。情感分析:抓取视频的评论数据,并进行情感分析,帮助企业或组织了解公众的情绪反馈。用户行为分析市场监测:通过抓取特定关键词或标签下的视频数据,分析市场趋势,进行竞争对手分析。短视频平台数据类型:视频信息:包括视频标题、描述、标签、作者等。用户信息:包括用户的昵称、粉丝数、关注数等。互动数据评论数据:视频下的用户评论内容及用户信息。

2201_76125261的博客 3663

win98 支持html5,win98支持1G以上内存的解决办法

如果计算机上有1G或以上的内存,而操作系统是Win95/98的话,Windows将不能启动,当你重启时,会看到如下的错误信息:Insufficient memory to initialize Windows.Quit one or more memory-resident programs or remove unnecessaryutilities from your CONFIG.SYS a...

weixin_34902502的博客 6643

【OpenAI】Python:(1) 基于 Gym-CarRacing 的自动驾驶项目 | 前置知识介绍 | 项目环境准备 | 手把手带你一步步实现

本篇是关于多伦多大学自动驾驶专业项目 Gym-CarRacing 的博客。GYM-Box2D CarRacing 是一种在 OpenAI Gym 平台上开发和比较强化学习算法的模拟环境。它是流行的 Box2D 物理引擎的一个版本,经过修改以支持模拟汽车在赛道上行驶的物理过程。由于内容比较多所以分多次更新,本篇是关于前置知识介绍,以及项目环境准备的。

有道无术, 术尚可求。有术无道, 止于术。 5053

Win98 USB驱动 万用驱动

Win98 USB驱动 万用驱动 【下载地址】Win98USB驱动万用驱动 这款“Win98 USB驱动 万用驱动”专为Windows 98系统设计,解决了该系统在USB支持上的历史局限。通过安装此驱动集合,用户能够轻松连接和使用现代USB设备,如U盘、鼠标和键盘,极大地扩展了Windows 98的功能性。简单易用的安装...

gitblog_06756的博客 792

pscad IEEE标准系统搭建说明

pscad IEEE标准系统搭建说明,包含典型的IEEE 9节点、14节点、30节点、39节点和118节点系统。内含标准测试系统的原始数据和潮流结果。

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

WIN98一键GHOST资源下载介绍:一键 ghost WIN98,轻松备份与还原

WIN98一键GHOST资源下载介绍:一键 ghost WIN98,轻松备份与还原 【下载地址】WIN98一键GHOST资源下载介绍 WIN98一键GHOST是一款专为怀旧经典操作系统设计的便捷工具,帮助用户轻松备份或恢复WIN98系统。无论是重新安装还是数据备份,这款工具都能一键完成,操作简单,快速上手。它让您在现代化...

gitblog_06723的博客 944

锂电池充电器电路设计 里面有电路

锂电池充电器电路设计 里面有电路 3.7V升压到5V电路设计。

Win98 USB 驱动

Win98 USB 驱动 【下载地址】Win98USB驱动 这款开源项目提供了一个经过验证的Win98操作系统USB驱动程序,被誉为“万能驱动”,适用于多种USB设备,有效解决了Win98系统下USB设备的兼容性问题。经过实际使用测试,该驱动表现出色,稳定可靠,能够显著提升Win98系统对USB设备的支持能力。无论是老式...

gitblog_06705的博客 506

visio安装包

Microsoft Office Visio 是一款专业的制图软件。利用经过更新的形状、协作工具和数据链接图表,创建专业级图表以简化复杂的信息。

上一篇: android自定义view学习(一)之时尚表盘
下一篇: 自定义View进阶-手绘地图(二)
LZ涸泽而渔
博客等级 码龄10年 269粉丝 21原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值