自定义View:地图View

最终效果

功能描述

基于公司的业务描述做的一个小demo,在一个地图上移动的效果,包括:

  1. 缩放地图
  2. 地图图片全屏适配
  3. 移动动画
  4. 镜头跟随(定位点永远在屏幕中心)
  5. 移动路径显示与否

难点记录

这是一个继承自ImageView的View,主要涉及的内容就是通过手指来放大或缩小地图,并计算对应的缩放倍数zoomP(缩放系数)。我们以图片适配全屏后的大小定为基点,缩放系数是1。
放大即是zoomP>1的时候,缩小即是0 < zoomP <1。其中稍微复杂一点的是当地图放大缩小后,再移动画布(地图)的话就是乘上缩放系数后的移动,需要处理好。其他地方在代码注释中都有解释。

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.animation.TypeEvaluator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.animation.LinearInterpolator;
import android.widget.ImageView;

/**
* 缩放View
*/
public class ScaleView extends ImageView {
final public static int DRAG = 1;
final public static int ZOOM = 2;

public int mode = 0;

private Matrix matrix = new Matrix();
private Matrix matrix1 = new Matrix();
private Matrix saveMatrix = new Matrix();

private float x_down = 0;
private float y_down = 0;

private Bitmap touchImg;

private PointF mid = new PointF();
private float initDis = 1f;
private int screenWidth, screenHeight;
private float[] x = new float[4];
private float[] y = new float[4];

private boolean flag = false;
boolean isfrist = true;//第一次进来,需适配全屏
private Paint pointPaint;//画定位点的笔
private boolean isFollow = false;//画面跟着坐标点移动
private PointF centerImagepointF;//图片的中心点
private boolean isMove = false;//手指是否在滑动
private float mx, my;//定位点相对于屏幕坐标点
private float zoomP;//图片默认缩放系数
private float zoomLib = 3f;//跟随模式的放大系数
float before_x;//记录上次的坐标点,用于划线
float before_y;
private boolean isLine = true;//默认绘制路线
private Paint startRectPaint;//画路径,画有效范围的笔

public ScaleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}

public ScaleView(Context context, AttributeSet attrs) {
super(context, attrs);
touchImg = BitmapFactory.decodeResource(getResources(), R.mipmap.image2).copy(Bitmap.Config.ARGB_8888, true);
matrix = new Matrix();
centerImagepointF = new PointF();
//设置画点的参数
pointPaint = new Paint();
pointPaint.setColor(Color.RED);
pointPaint.setAntiAlias(true);
pointPaint.setStrokeWidth(20);
//画路径范围的笔
startRectPaint = new Paint();
startRectPaint.setAntiAlias(true);
startRectPaint.setStrokeWidth(3);
startRectPaint.setStyle(Paint.Style.STROKE);//空心
startRectPaint.setColor(Color.BLACK);
}

public ScaleView(Context context) {
super(context);

}

/**
* 坐标点,相对地图的坐标
*
* @param x
* @param y
* @param switchStatus 开关锁状态
*/
public void setPointLocal(float x, float y) {
mx = x * zoomP;//相对于屏幕坐标点
my = y * zoomP;

if (isFollow && !isMove) {//是跟随状态下,且没有手指移动过
// matrix.postTranslate(centerImagepointF.x - zoomLib * x * zoomP, centerImagepointF.y - zoomLib * y * zoomP);// 平移
setAnim(centerImagepointF.x - zoomLib * zoomP *x ,centerImagepointF.y - zoomLib * zoomP *y );
centerImagepointF.x = zoomLib * zoomP *x;
centerImagepointF.y = zoomLib * zoomP *y;
}
Canvas canvas;
if(isLine){
canvas = new Canvas(touchImg);
drawLine(canvas,x,y);//画路径
}else{
touchImg = BitmapFactory.decodeResource(getResources(), R.mipmap.image2).copy(Bitmap.Config.ARGB_8888, true);
canvas = new Canvas(touchImg);
}
//记录上个点
before_x = x;
before_y = y;
canvas.drawCircle(x, y, 7, pointPaint);//画坐标
//重绘
invalidate();
}
public void setLine(boolean isLine){
this.isLine = isLine;
if(!isLine){
touchImg = BitmapFactory.decodeResource(getResources(), R.mipmap.image2).copy(Bitmap.Config.ARGB_8888, true);
Canvas canvas = new Canvas(touchImg);
canvas.drawCircle(before_x, before_y, 7, pointPaint);//画坐标
invalidate();
}
}

/**
* 绘制路径
*/
private void drawLine(Canvas canvas,float x,float y){
canvas.drawLine(before_x,before_y,x,y,startRectPaint);
}

@Override
protected void onDraw(Canvas canvas) {
if (isfrist) {
float n = (float) getWidth() / (float) touchImg.getWidth();
float m = (float) getHeight() / (float) touchImg.getHeight();
if (n > m) n = m; //将图片适配至全屏
matrix.postScale(n, n, 0, 0);
isfrist = false;
zoomP = n;
}
canvas.drawBitmap(touchImg, matrix, null);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
// 多点触摸的时候 必须加上MotionEvent.ACTION_MASK
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
saveMatrix.set(matrix);
x_down = event.getX();
y_down = event.getY();
// 初始为drag模式
mode = DRAG;
break;

case MotionEvent.ACTION_POINTER_DOWN:
saveMatrix.set(matrix);
// 初始的两个触摸点间的距离
initDis = spacing(event);
// 设置为缩放模式
mode = ZOOM;
// 多点触摸的时候 计算出中间点的坐标
midPoint(mid, event);
break;

case MotionEvent.ACTION_MOVE:
// drag模式
if (mode == DRAG) {
isFollow = false;
// 设置当前的 matrix
matrix1.set(saveMatrix);
// 平移 当前坐标减去初始坐标 移动的距离
matrix1.postTranslate(event.getX() - x_down, event.getY() - y_down);// 平移
// 判断达到移动标准
flag = checkMatrix(matrix1);
if (flag) {
// 设置matrix
matrix.set(matrix1);
invalidate();
}
} else if (mode == ZOOM) {
isFollow = false;//缩放的时候,关闭坐标点跟随
matrix1.set(saveMatrix);
float newDis = spacing(event);
// 计算出缩放比例
float scale = newDis / initDis;
// 以mid为中心进行缩放
matrix1.postScale(scale, scale, mid.x, mid.y);
flag = checkMatrix(matrix1);
if (flag) {
matrix.set(matrix1);
invalidate();
}
}
break;

case MotionEvent.ACTION_UP:
centerImagepointF.x = event.getX();
centerImagepointF.y = event.getY();
mx = mx + (event.getX() - x_down);
my = my + (event.getY() - y_down);
case MotionEvent.ACTION_POINTER_UP:
mode = 0;
break;
}
return true;
}

//取两点的距离
private float spacing(MotionEvent event) {
try {
float x = event.getX(0) - event.getX(1);
float y = event.getY(0) - event.getY(1);
return (float) Math.sqrt(x * x + y * y);
} catch (IllegalArgumentException ex) {
Log.v("TAG", ex.getLocalizedMessage());
return 0;
}
}

//取两点的中点
private void midPoint(PointF point, MotionEvent event) {
try {
float x = event.getX(0) + event.getX(1);
float y = event.getY(0) + event.getY(1);
point.set(x / 2, y / 2);
} catch (IllegalArgumentException ex) {
Log.v("TAG", ex.getLocalizedMessage());
}
}

private boolean checkMatrix(Matrix m) {
GetFour(m);
// 出界判断
//view的右边缘x坐标小于屏幕宽度的1/3的时候,
// view左边缘大于屏幕款短的2/3的时候
//view的下边缘在屏幕1/3上的时候
//view的上边缘在屏幕2/3下的时候
if ((x[0] < screenWidth / 3 && x[1] < screenWidth / 3
&& x[2] < screenWidth / 3 && x[3] < screenWidth / 3)
|| (x[0] > screenWidth * 2 / 3 && x[1] > screenWidth * 2 / 3
&& x[2] > screenWidth * 2 / 3 && x[3] > screenWidth * 2 / 3)
|| (y[0] < screenHeight / 3 && y[1] < screenHeight / 3
&& y[2] < screenHeight / 3 && y[3] < screenHeight / 3)
|| (y[0] > screenHeight * 2 / 3 && y[1] > screenHeight * 2 / 3
&& y[2] > screenHeight * 2 / 3 && y[3] > screenHeight * 2 / 3)) {
return true;
}
// 图片现宽度
double width = Math.sqrt((x[0] - x[1]) * (x[0] - x[1]) + (y[0] - y[1])
* (y[0] - y[1]));
// 缩放比率判断 宽度打雨3倍屏宽,或者小于1/3屏宽
if (width < screenWidth / 3 || width > screenWidth * 3) {
return true;
}
return false;
}

private void GetFour(Matrix matrix) {
float[] f = new float[9];
matrix.getValues(f);
// 图片4个顶点的坐标
//矩阵 9 MSCALE_X 缩放的, MSKEW_X 倾斜的 。MTRANS_X 平移的
x[0] = f[Matrix.MSCALE_X] * 0 + f[Matrix.MSKEW_X] * 0
+ f[Matrix.MTRANS_X];
y[0] = f[Matrix.MSKEW_Y] * 0 + f[Matrix.MSCALE_Y] * 0
+ f[Matrix.MTRANS_Y];
x[1] = f[Matrix.MSCALE_X] * touchImg.getWidth() + f[Matrix.MSKEW_X] * 0
+ f[Matrix.MTRANS_X];
y[1] = f[Matrix.MSKEW_Y] * touchImg.getWidth() + f[Matrix.MSCALE_Y] * 0
+ f[Matrix.MTRANS_Y];
x[2] = f[Matrix.MSCALE_X] * 0 + f[Matrix.MSKEW_X]
* touchImg.getHeight() + f[Matrix.MTRANS_X];
y[2] = f[Matrix.MSKEW_Y] * 0 + f[Matrix.MSCALE_Y]
* touchImg.getHeight() + f[Matrix.MTRANS_Y];
x[3] = f[Matrix.MSCALE_X] * touchImg.getWidth() + f[Matrix.MSKEW_X]
* touchImg.getHeight() + f[Matrix.MTRANS_X];
y[3] = f[Matrix.MSKEW_Y] * touchImg.getWidth() + f[Matrix.MSCALE_Y]
* touchImg.getHeight() + f[Matrix.MTRANS_Y];
}
/**
* 设置镜头跟随
* @param follow
*/
public void setFollow(boolean follow) {
setAllImage();
isFollow = follow;
// 平移 当前坐标减去初始坐标 移动的距离
matrix.postScale(zoomLib, zoomLib, 0, 0);//放大两倍后,跟随
float xx = getWidth() / 2 - zoomLib * mx;
float yy = getHeight() / 2 - zoomLib * my;
// matrix.postTranslate(xx/zoomP, yy/zoomP);// 平移到屏幕中间
centerImagepointF.x = zoomLib * mx;
centerImagepointF.y = zoomLib * my;
invalidate();
setAnim(xx,yy);
}

private void setAnim(float xx,float yy){
Point s = new Point(0,0);
Point starP = new Point(0,0);
Point endP = new Point(xx,yy);
ValueAnimator anim = ValueAnimator.ofObject((TypeEvaluator<Point>) (fraction, startValue, endValue) -> {
// 根据fraction来计算当前动画的x和y的值
float x = fraction * (endValue.getX() - startValue.getX());
float y = fraction * (endValue.getY() - startValue.getY());
// 将计算后的坐标封装到一个新的Point对象中并返回
Point point = new Point(x, y);
return point;
}, starP, endP);
anim.setDuration(1000);
anim.addUpdateListener(animation -> {
matrix.postTranslate((((Point)animation.getAnimatedValue()).x-s.x), (((Point)animation.getAnimatedValue()).y-s.y));// 平移
invalidate();
s.x = ((Point)animation.getAnimatedValue()).x;
s.y = ((Point)animation.getAnimatedValue()).y;

});
anim.start();
}

//设置全图显示
public void setAllImage() {
isfrist = true;
isFollow = false;
matrix = new Matrix();//重新计算比例
invalidate();
}

public class Point {
// 设置两个变量用于记录坐标的位置
private float x;
private float y;

// 构造方法用于设置坐标
public Point(float x, float y) {
this.x = x;
this.y = y;
}
// get方法用于获取坐标
public float getX() {
return x;
}

public float getY() {
return y;
}
}
}

使用的话,直接在逻辑层调用该View的setPointLocal即可。