1. 程式人生 > >自定義View進階--手繪地圖(一)

自定義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 = 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);
}
}

這邊是