自定義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); } }
這邊是