forked from tyhjh/ZomMapView
-
Notifications
You must be signed in to change notification settings - Fork 0
/
ZomMapView.java
1806 lines (1647 loc) · 60.8 KB
/
ZomMapView.java
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
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package com.yorhp.mlratingstudio.mvp.view.myview;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.PointF;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
public class ZomMapView extends android.support.v7.widget.AppCompatImageView {
////////////////////////////////配置参数////////////////////////////////
/**
* 图片缩放动画时间
*/
public static final int SCALE_ANIMATOR_DURATION = 200;
/**
* 惯性动画衰减参数
*/
public static final float FLING_DAMPING_FACTOR = 0.9f;
/**
* 图片最大放大比例
*/
private static final float MAX_SCALE = 5f;
////////////////////////////////监听器////////////////////////////////
/**
* 外界点击事件
*
* @see #setOnClickListener(OnClickListener)
*/
private OnClickListener mOnClickListener;
/**
* 外界长按事件
*
* @see #setOnLongClickListener(OnLongClickListener)
*/
private OnLongClickListener mOnLongClickListener;
@Override
public void setOnClickListener(OnClickListener l) {
//默认的click会在任何点击情况下都会触发,所以搞成自己的
mOnClickListener = l;
}
@Override
public void setOnLongClickListener(OnLongClickListener l) {
//默认的long click会在任何长按情况下都会触发,所以搞成自己的
mOnLongClickListener = l;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int viewWidth = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
}
////////////////////////////////公共状态获取////////////////////////////////
/**
* 手势状态:自由状态
*
* @see #getPinchMode()
*/
public static final int PINCH_MODE_FREE = 0;
/**
* 手势状态:单指滚动状态
*
* @see #getPinchMode()
*/
public static final int PINCH_MODE_SCROLL = 1;
/**
* 手势状态:双指缩放状态
*
* @see #getPinchMode()
*/
public static final int PINCH_MODE_SCALE = 2;
/**
* 外层变换矩阵,如果是单位矩阵,那么图片是fit center状态
*
* @see #getOuterMatrix(Matrix)
* @see #outerMatrixTo(Matrix, long)
*/
private Matrix mOuterMatrix = new Matrix();
/**
* 矩形遮罩
*
* @see #getMask()
* @see #zoomMaskTo(RectF, long)
*/
private RectF mMask;
/**
* 当前手势状态
*
* @see #getPinchMode()
* @see #PINCH_MODE_FREE
* @see #PINCH_MODE_SCROLL
* @see #PINCH_MODE_SCALE
*/
private int mPinchMode = PINCH_MODE_FREE;
/**
* 获取外部变换矩阵.
* <p>
* 外部变换矩阵记录了图片手势操作的最终结果,是相对于图片fit center状态的变换.
* 默认值为单位矩阵,此时图片为fit center状态.
*
* @param matrix 用于填充结果的对象
* @return 如果传了matrix参数则将matrix填充后返回, 否则new一个填充返回
*/
public Matrix getOuterMatrix(Matrix matrix) {
if (matrix == null) {
matrix = new Matrix(mOuterMatrix);
} else {
matrix.set(mOuterMatrix);
}
return matrix;
}
/**
* 获取内部变换矩阵.
* <p>
* 内部变换矩阵是原图到fit center状态的变换,当原图尺寸变化或者控件大小变化都会发生改变
* 当尚未布局或者原图不存在时,其值无意义.所以在调用前需要确保前置条件有效,否则将影响计算结果.
*
* @param matrix 用于填充结果的对象
* @return 如果传了matrix参数则将matrix填充后返回, 否则new一个填充返回
*/
public Matrix getInnerMatrix(Matrix matrix) {
if (matrix == null) {
matrix = new Matrix();
} else {
matrix.reset();
}
if (isReady()) {
//原图大小
RectF tempSrc = MathUtils.rectFTake(0, 0, getDrawable().getIntrinsicWidth(), getDrawable().getIntrinsicHeight());
//控件大小
RectF tempDst = MathUtils.rectFTake(0, 0, getWidth(), getHeight());
//计算fit center矩阵
matrix.setRectToRect(tempSrc, tempDst, Matrix.ScaleToFit.CENTER);
//释放临时对象
MathUtils.rectFGiven(tempDst);
MathUtils.rectFGiven(tempSrc);
}
return matrix;
}
/**
* 获取图片总变换矩阵.
* <p>
* 总变换矩阵为内部变换矩阵x外部变换矩阵,决定了原图到所见最终状态的变换
* 当尚未布局或者原图不存在时,其值无意义.所以在调用前需要确保前置条件有效,否则将影响计算结果.
*
* @param matrix 用于填充结果的对象
* @return 如果传了matrix参数则将matrix填充后返回, 否则new一个填充返回
* @see #getOuterMatrix(Matrix)
* @see #getInnerMatrix(Matrix)
*/
public Matrix getCurrentImageMatrix(Matrix matrix) {
//获取内部变换矩阵
matrix = getInnerMatrix(matrix);
//乘上外部变换矩阵
matrix.postConcat(mOuterMatrix);
return matrix;
}
//标记************************************************************************************************
/**
* 获取当前变换后的图片位置和尺寸
* <p>
* 当尚未布局或者原图不存在时,其值无意义.所以在调用前需要确保前置条件有效,否则将影响计算结果.
*
* @param rectF 用于填充结果的对象
* @return 如果传了rectF参数则将rectF填充后返回, 否则new一个填充返回
* @see #getCurrentImageMatrix(Matrix)
*/
public RectF getImageBound(RectF rectF) {
if (rectF == null) {
rectF = new RectF();
} else {
rectF.setEmpty();
}
if (!isReady()) {
return rectF;
} else {
//申请一个空matrix
Matrix matrix = MathUtils.matrixTake();
//获取当前总变换矩阵
getCurrentImageMatrix(matrix);
//对原图矩形进行变换得到当前显示矩形
rectF.set(0, 0, getDrawable().getIntrinsicWidth(), getDrawable().getIntrinsicHeight());
matrix.mapRect(rectF);
//释放临时matrix
MathUtils.matrixGiven(matrix);
return rectF;
}
}
/**
* 获取当前设置的mask
*
* @return 返回当前的mask对象副本, 如果当前没有设置mask则返回null
*/
public RectF getMask() {
if (mMask != null) {
return new RectF(mMask);
} else {
return null;
}
}
/**
* 获取当前手势状态
*
* @see #PINCH_MODE_FREE
* @see #PINCH_MODE_SCROLL
* @see #PINCH_MODE_SCALE
*/
public int getPinchMode() {
return mPinchMode;
}
/**
* 与ViewPager结合的时候使用
*
* @param direction
* @return
*/
@Override
public boolean canScrollHorizontally(int direction) {
if (mPinchMode == ZomMapView.PINCH_MODE_SCALE) {
return true;
}
RectF bound = getImageBound(null);
if (bound == null) {
return false;
}
if (bound.isEmpty()) {
return false;
}
if (direction > 0) {
return bound.right > getWidth();
} else {
return bound.left < 0;
}
}
/**
* 与ViewPager结合的时候使用
*
* @param direction
* @return
*/
@Override
public boolean canScrollVertically(int direction) {
if (mPinchMode == ZomMapView.PINCH_MODE_SCALE) {
return true;
}
RectF bound = getImageBound(null);
if (bound == null) {
return false;
}
if (bound.isEmpty()) {
return false;
}
if (direction > 0) {
return bound.bottom > getHeight();
} else {
return bound.top < 0;
}
}
////////////////////////////////公共状态设置////////////////////////////////
/**
* 执行当前outerMatrix到指定outerMatrix渐变的动画
* <p>
* 调用此方法会停止正在进行中的手势以及手势动画.
* 当duration为0时,outerMatrix值会被立即设置而不会启动动画.
*
* @param endMatrix 动画目标矩阵
* @param duration 动画持续时间
* @see #getOuterMatrix(Matrix)
*/
public void outerMatrixTo(Matrix endMatrix, long duration) {
if (endMatrix == null) {
return;
}
//将手势设置为PINCH_MODE_FREE将停止后续手势的触发
mPinchMode = PINCH_MODE_FREE;
//停止所有正在进行的动画
cancelAllAnimator();
//如果时间不合法立即执行结果
if (duration <= 0) {
mOuterMatrix.set(endMatrix);
dispatchOuterMatrixChanged();
invalidate();
} else {
//创建矩阵变化动画
mScaleAnimator = new ScaleAnimator(mOuterMatrix, endMatrix, duration);
mScaleAnimator.start();
}
}
/**
* 执行当前mask到指定mask的变化动画
* <p>
* 调用此方法不会停止手势以及手势相关动画,但会停止正在进行的mask动画.
* 当前mask为null时,则不执行动画立即设置为目标mask.
* 当duration为0时,立即将当前mask设置为目标mask,不会执行动画.
*
* @param mask 动画目标mask
* @param duration 动画持续时间
* @see #getMask()
*/
public void zoomMaskTo(RectF mask, long duration) {
if (mask == null) {
return;
}
//停止mask动画
if (mMaskAnimator != null) {
mMaskAnimator.cancel();
mMaskAnimator = null;
}
//如果duration为0或者之前没有设置过mask,不执行动画,立即设置
if (duration <= 0 || mMask == null) {
if (mMask == null) {
mMask = new RectF();
}
mMask.set(mask);
invalidate();
} else {
//执行mask动画
mMaskAnimator = new MaskAnimator(mMask, mask, duration);
mMaskAnimator.start();
}
}
/**
* 重置所有状态
* <p>
* 重置位置到fit center状态,清空mask,停止所有手势,停止所有动画.
* 但不清空drawable,以及事件绑定相关数据.
*/
public void reset() {
//重置位置到fit
mOuterMatrix.reset();
dispatchOuterMatrixChanged();
//清空mask
mMask = null;
//停止所有手势
mPinchMode = PINCH_MODE_FREE;
mLastMovePoint.set(0, 0);
mScaleCenter.set(0, 0);
mScaleBase = 0;
//停止所有动画
if (mMaskAnimator != null) {
mMaskAnimator.cancel();
mMaskAnimator = null;
}
cancelAllAnimator();
//重绘
invalidate();
}
////////////////////////////////对外广播事件////////////////////////////////
/**
* 外部矩阵变化事件通知监听器
*/
public interface OuterMatrixChangedListener {
/**
* 外部矩阵变化回调
* <p>
* 外部矩阵的任何变化后都收到此回调.
* 外部矩阵变化后,总变化矩阵,图片的展示位置都将发生变化.
*
* @param pinchImageView
* @see #getOuterMatrix(Matrix)
* @see #getCurrentImageMatrix(Matrix)
* @see #getImageBound(RectF)
*/
void onOuterMatrixChanged(ZomMapView pinchImageView);
}
/**
* 所有OuterMatrixChangedListener监听列表
*
* @see #addOuterMatrixChangedListener(OuterMatrixChangedListener)
* @see #removeOuterMatrixChangedListener(OuterMatrixChangedListener)
*/
private List<OuterMatrixChangedListener> mOuterMatrixChangedListeners;
/**
* 当mOuterMatrixChangedListeners被锁定不允许修改时,临时将修改写到这个副本中
*
* @see #mOuterMatrixChangedListeners
*/
private List<OuterMatrixChangedListener> mOuterMatrixChangedListenersCopy;
/**
* mOuterMatrixChangedListeners的修改锁定
* <p>
* 当进入dispatchOuterMatrixChanged方法时,被加1,退出前被减1
*
* @see #dispatchOuterMatrixChanged()
* @see #addOuterMatrixChangedListener(OuterMatrixChangedListener)
* @see #removeOuterMatrixChangedListener(OuterMatrixChangedListener)
*/
private int mDispatchOuterMatrixChangedLock;
/**
* 添加外部矩阵变化监听
*
* @param listener
*/
public void addOuterMatrixChangedListener(OuterMatrixChangedListener listener) {
if (listener == null) {
return;
}
//如果监听列表没有被修改锁定直接将监听添加到监听列表
if (mDispatchOuterMatrixChangedLock == 0) {
if (mOuterMatrixChangedListeners == null) {
mOuterMatrixChangedListeners = new ArrayList<OuterMatrixChangedListener>();
}
mOuterMatrixChangedListeners.add(listener);
} else {
//如果监听列表修改被锁定,那么尝试在监听列表副本上添加
//监听列表副本将会在锁定被解除时替换到监听列表里
if (mOuterMatrixChangedListenersCopy == null) {
if (mOuterMatrixChangedListeners != null) {
mOuterMatrixChangedListenersCopy = new ArrayList<OuterMatrixChangedListener>(mOuterMatrixChangedListeners);
} else {
mOuterMatrixChangedListenersCopy = new ArrayList<OuterMatrixChangedListener>();
}
}
mOuterMatrixChangedListenersCopy.add(listener);
}
}
/**
* 删除外部矩阵变化监听
*
* @param listener
*/
public void removeOuterMatrixChangedListener(OuterMatrixChangedListener listener) {
if (listener == null) {
return;
}
//如果监听列表没有被修改锁定直接在监听列表数据结构上修改
if (mDispatchOuterMatrixChangedLock == 0) {
if (mOuterMatrixChangedListeners != null) {
mOuterMatrixChangedListeners.remove(listener);
}
} else {
//如果监听列表被修改锁定,那么就在其副本上修改
//其副本将会在锁定解除时替换回监听列表
if (mOuterMatrixChangedListenersCopy == null) {
if (mOuterMatrixChangedListeners != null) {
mOuterMatrixChangedListenersCopy = new ArrayList<OuterMatrixChangedListener>(mOuterMatrixChangedListeners);
}
}
if (mOuterMatrixChangedListenersCopy != null) {
mOuterMatrixChangedListenersCopy.remove(listener);
}
}
}
/**
* 触发外部矩阵修改事件
* <p>
* 需要在每次给外部矩阵设置值时都调用此方法.
*
* @see #mOuterMatrix
*/
private void dispatchOuterMatrixChanged() {
if (mOuterMatrixChangedListeners == null) {
return;
}
//增加锁
//这里之所以用计数器做锁定是因为可能在锁定期间又间接调用了此方法产生递归
//使用boolean无法判断递归结束
mDispatchOuterMatrixChangedLock++;
//在列表循环过程中不允许修改列表,否则将引发崩溃
for (OuterMatrixChangedListener listener : mOuterMatrixChangedListeners) {
listener.onOuterMatrixChanged(this);
}
//减锁
mDispatchOuterMatrixChangedLock--;
//如果是递归的情况,mDispatchOuterMatrixChangedLock可能大于1,只有减到0才能算列表的锁定解除
if (mDispatchOuterMatrixChangedLock == 0) {
//如果期间有修改列表,那么副本将不为null
if (mOuterMatrixChangedListenersCopy != null) {
//将副本替换掉正式的列表
mOuterMatrixChangedListeners = mOuterMatrixChangedListenersCopy;
//清空副本
mOuterMatrixChangedListenersCopy = null;
}
}
}
////////////////////////////////用于重载定制////////////////////////////////
/**
* 获取图片最大可放大的比例
* <p>
* 如果放大大于这个比例则不被允许.
* 在双手缩放过程中如果图片放大比例大于这个值,手指释放将回弹到这个比例.
* 在双击放大过程中不允许放大比例大于这个值.
* 覆盖此方法可以定制不同情况使用不同的最大可放大比例.
*
* @return 缩放比例
* @see #scaleEnd()
* @see #doubleTap(float, float)
*/
protected float getMaxScale() {
return MAX_SCALE;
}
/**
* 计算双击之后图片接下来应该被缩放的比例
* <p>
* 如果值大于getMaxScale或者小于fit center尺寸,则实际使用取边界值.
* 通过覆盖此方法可以定制不同的图片被双击时使用不同的放大策略.
*
* @param innerScale 当前内部矩阵的缩放值
* @param outerScale 当前外部矩阵的缩放值
* @return 接下来的缩放比例
* @see #doubleTap(float, float)
* @see #getMaxScale()
*/
protected float calculateNextScale(float innerScale, float outerScale) {
float currentScale = innerScale * outerScale;
if (currentScale < MAX_SCALE) {
return MAX_SCALE;
} else {
return innerScale;
}
}
////////////////////////////////初始化////////////////////////////////
public ZomMapView(Context context) {
super(context);
initView();
}
public ZomMapView(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
public ZomMapView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initView();
}
private void initView() {
//强制设置图片scaleType为matrix
super.setScaleType(ScaleType.MATRIX);
}
//不允许设置scaleType,只能用内部设置的matrix
@Override
public void setScaleType(ScaleType scaleType) {
}
////////////////////////////////绘制////////////////////////////////
@Override
protected void onDraw(Canvas canvas) {
//在绘制前设置变换矩阵
if (isReady()) {
Matrix matrix = MathUtils.matrixTake();
setImageMatrix(getCurrentImageMatrix(matrix));
MathUtils.matrixGiven(matrix);
}
//对图像做遮罩处理
if (mMask != null) {
canvas.save();
canvas.clipRect(mMask);
super.onDraw(canvas);
canvas.restore();
} else {
super.onDraw(canvas);
}
}
////////////////////////////////有效性判断////////////////////////////////
/**
* 判断当前情况是否能执行手势相关计算
* <p>
* 包括:是否有图片,图片是否有尺寸,控件是否有尺寸.
*
* @return 是否能执行手势相关计算
*/
private boolean isReady() {
return getDrawable() != null && getDrawable().getIntrinsicWidth() > 0 && getDrawable().getIntrinsicHeight() > 0
&& getWidth() > 0 && getHeight() > 0;
}
////////////////////////////////mask动画处理////////////////////////////////
/**
* mask修改的动画
* <p>
* 和图片的动画相互独立.
*
* @see #zoomMaskTo(RectF, long)
*/
private MaskAnimator mMaskAnimator;
/**
* mask变换动画
* <p>
* 将mask从一个rect动画到另外一个rect
*/
private class MaskAnimator extends ValueAnimator implements ValueAnimator.AnimatorUpdateListener {
/**
* 开始mask
*/
private float[] mStart = new float[4];
/**
* 结束mask
*/
private float[] mEnd = new float[4];
/**
* 中间结果mask
*/
private float[] mResult = new float[4];
/**
* 创建mask变换动画
*
* @param start 动画起始状态
* @param end 动画终点状态
* @param duration 动画持续时间
*/
public MaskAnimator(RectF start, RectF end, long duration) {
super();
setFloatValues(0, 1f);
setDuration(duration);
addUpdateListener(this);
//将起点终点拷贝到数组方便计算
mStart[0] = start.left;
mStart[1] = start.top;
mStart[2] = start.right;
mStart[3] = start.bottom;
mEnd[0] = end.left;
mEnd[1] = end.top;
mEnd[2] = end.right;
mEnd[3] = end.bottom;
}
@Override
public void onAnimationUpdate(ValueAnimator animation) {
//获取动画进度,0-1范围
float value = (Float) animation.getAnimatedValue();
//根据进度对起点终点之间做插值
for (int i = 0; i < 4; i++) {
mResult[i] = mStart[i] + (mEnd[i] - mStart[i]) * value;
}
//期间mask有可能被置空了,所以判断一下
if (mMask == null) {
mMask = new RectF();
}
//设置新的mask并绘制
mMask.set(mResult[0], mResult[1], mResult[2], mResult[3]);
invalidate();
}
}
////////////////////////////////手势动画处理////////////////////////////////
/**
* 在单指模式下:
* 记录上一次手指的位置,用于计算新的位置和上一次位置的差值.
* <p>
* 双指模式下:
* 记录两个手指的中点,作为和mScaleCenter绑定的点.
* 这个绑定可以保证mScaleCenter无论如何都会跟随这个中点.
*
* @see #mScaleCenter
* @see #scale(PointF, float, float, PointF)
* @see #scaleEnd()
*/
private PointF mLastMovePoint = new PointF();
/**
* 缩放模式下图片的缩放中点.
* <p>
* 为其指代的点经过innerMatrix变换之后的值.
* 其指代的点在手势过程中始终跟随mLastMovePoint.
* 通过双指缩放时,其为缩放中心点.
*
* @see #saveScaleContext(float, float, float, float)
* @see #mLastMovePoint
* @see #scale(PointF, float, float, PointF)
*/
private PointF mScaleCenter = new PointF();
/**
* 缩放模式下的基础缩放比例
* <p>
* 为外层缩放值除以开始缩放时两指距离.
* 其值乘上最新的两指之间距离为最新的图片缩放比例.
*
* @see #saveScaleContext(float, float, float, float)
* @see #scale(PointF, float, float, PointF)
*/
private float mScaleBase = 0;
/**
* 图片缩放动画
* <p>
* 缩放模式把图片的位置大小超出限制之后触发.
* 双击图片放大或缩小时触发.
* 手动调用outerMatrixTo触发.
*
* @see #scaleEnd()
* @see #doubleTap(float, float)
* @see #outerMatrixTo(Matrix, long)
*/
private ScaleAnimator mScaleAnimator;
/**
* 滑动产生的惯性动画
*
* @see #fling(float, float)
*/
private FlingAnimator mFlingAnimator;
/**
* 常用手势处理
* <p>
* 在onTouchEvent末尾被执行.
*/
private GestureDetector mGestureDetector = new GestureDetector(ZomMapView.this.getContext(), new GestureDetector.SimpleOnGestureListener() {
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
//只有在单指模式结束之后才允许执行fling
if (mPinchMode == PINCH_MODE_FREE && !(mScaleAnimator != null && mScaleAnimator.isRunning())) {
fling(velocityX, velocityY);
}
return true;
}
public void onLongPress(MotionEvent e) {
//触发长按
if (mOnLongClickListener != null) {
mOnLongClickListener.onLongClick(ZomMapView.this);
}
}
public boolean onDoubleTap(MotionEvent e) {
//当手指快速第二次按下触发,此时必须是单指模式才允许执行doubleTap
if (mPinchMode == PINCH_MODE_SCROLL && !(mScaleAnimator != null && mScaleAnimator.isRunning())) {
doubleTap(e.getX(), e.getY());
}
return true;
}
//触发点击触发点击触发点击触发点击触发点击触发点击触发点击触发点击触发点击触发点击触发点击触发点击触发点击触发点击触发点击触发点击触发点击触发点击触发点击触发点击触发点击触发点击触发点击触发点击触发点击触发点击触发点击
public boolean onSingleTapConfirmed(MotionEvent e) {
//触发点击
if (mOnClickListener != null) {
mOnClickListener.onClick(ZomMapView.this);
} else {
onMapClick();
}
return true;
}
});
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
pointX = (int) event.getX();
pointY = (int) event.getY();
int action = event.getAction() & MotionEvent.ACTION_MASK;
//最后一个点抬起或者取消,结束所有模式
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
//如果之前是缩放模式,还需要触发一下缩放结束动画
if (mPinchMode == PINCH_MODE_SCALE) {
scaleEnd();
}
mPinchMode = PINCH_MODE_FREE;
} else if (action == MotionEvent.ACTION_POINTER_UP) {
//多个手指情况下抬起一个手指,此时需要是缩放模式才触发
if (mPinchMode == PINCH_MODE_SCALE) {
//抬起的点如果大于2,那么缩放模式还有效,但是有可能初始点变了,重新测量初始点
if (event.getPointerCount() > 2) {
//如果还没结束缩放模式,但是第一个点抬起了,那么让第二个点和第三个点作为缩放控制点
if (event.getAction() >> 8 == 0) {
saveScaleContext(event.getX(1), event.getY(1), event.getX(2), event.getY(2));
//如果还没结束缩放模式,但是第二个点抬起了,那么让第一个点和第三个点作为缩放控制点
} else if (event.getAction() >> 8 == 1) {
saveScaleContext(event.getX(0), event.getY(0), event.getX(2), event.getY(2));
}
}
//如果抬起的点等于2,那么此时只剩下一个点,也不允许进入单指模式,因为此时可能图片没有在正确的位置上
}
//第一个点按下,开启滚动模式,记录开始滚动的点
} else if (action == MotionEvent.ACTION_DOWN) {
//在矩阵动画过程中不允许启动滚动模式
if (!(mScaleAnimator != null && mScaleAnimator.isRunning())) {
//停止所有动画
cancelAllAnimator();
//切换到滚动模式
mPinchMode = PINCH_MODE_SCROLL;
//保存触发点用于move计算差值
mLastMovePoint.set(event.getX(), event.getY());
}
//非第一个点按下,关闭滚动模式,开启缩放模式,记录缩放模式的一些初始数据
} else if (action == MotionEvent.ACTION_POINTER_DOWN) {
//停止所有动画
cancelAllAnimator();
//切换到缩放模式
mPinchMode = PINCH_MODE_SCALE;
//保存缩放的两个手指
saveScaleContext(event.getX(0), event.getY(0), event.getX(1), event.getY(1));
} else if (action == MotionEvent.ACTION_MOVE) {
if (!(mScaleAnimator != null && mScaleAnimator.isRunning())) {
//在滚动模式下移动
if (mPinchMode == PINCH_MODE_SCROLL) {
//每次移动产生一个差值累积到图片位置上
scrollBy(event.getX() - mLastMovePoint.x, event.getY() - mLastMovePoint.y);
//记录新的移动点
mLastMovePoint.set(event.getX(), event.getY());
//在缩放模式下移动
} else if (mPinchMode == PINCH_MODE_SCALE && event.getPointerCount() > 1) {
//两个缩放点间的距离
float distance = MathUtils.getDistance(event.getX(0), event.getY(0), event.getX(1), event.getY(1));
//保存缩放点中点
float[] lineCenter = MathUtils.getCenterPoint(event.getX(0), event.getY(0), event.getX(1), event.getY(1));
mLastMovePoint.set(lineCenter[0], lineCenter[1]);
//处理缩放
scale(mScaleCenter, mScaleBase, distance, mLastMovePoint);
}
}
}
//无论如何都处理各种外部手势
mGestureDetector.onTouchEvent(event);
return true;
}
/**
* 让图片移动一段距离
* <p>
* 不能移动超过可移动范围,超过了就到可移动范围边界为止.
*
* @param xDiff 移动距离
* @param yDiff 移动距离
* @return 是否改变了位置
*/
private boolean scrollBy(float xDiff, float yDiff) {
if (!isReady()) {
return false;
}
//原图方框
RectF bound = MathUtils.rectFTake();
getImageBound(bound);
//控件大小
float displayWidth = getWidth();
float displayHeight = getHeight();
//如果当前图片宽度小于控件宽度,则不能移动
if (bound.right - bound.left < displayWidth) {
xDiff = 0;
//如果图片左边在移动后超出控件左边
} else if (bound.left + xDiff > 0) {
//如果在移动之前是没超出的,计算应该移动的距离
if (bound.left < 0) {
xDiff = -bound.left;
//否则无法移动
} else {
xDiff = 0;
}
//如果图片右边在移动后超出控件右边
} else if (bound.right + xDiff < displayWidth) {
//如果在移动之前是没超出的,计算应该移动的距离
if (bound.right > displayWidth) {
xDiff = displayWidth - bound.right;
//否则无法移动
} else {
xDiff = 0;
}
}
//以下同理
if (bound.bottom - bound.top < displayHeight) {
yDiff = 0;
} else if (bound.top + yDiff > 0) {
if (bound.top < 0) {
yDiff = -bound.top;
} else {
yDiff = 0;
}
} else if (bound.bottom + yDiff < displayHeight) {
if (bound.bottom > displayHeight) {
yDiff = displayHeight - bound.bottom;
} else {
yDiff = 0;
}
}
MathUtils.rectFGiven(bound);
//应用移动变换
mOuterMatrix.postTranslate(xDiff, yDiff);
dispatchOuterMatrixChanged();
//触发重绘
invalidate();
//检查是否有变化
if (xDiff != 0 || yDiff != 0) {
return true;
} else {
return false;
}
}
/**
* 记录缩放前的一些信息
* <p>
* 保存基础缩放值.
* 保存图片缩放中点.
*
* @param x1 缩放第一个手指
* @param y1 缩放第一个手指
* @param x2 缩放第二个手指
* @param y2 缩放第二个手指
*/
private void saveScaleContext(float x1, float y1, float x2, float y2) {
//记录基础缩放值,其中图片缩放比例按照x方向来计算
//理论上图片应该是等比的,x和y方向比例相同
//但是有可能外部设定了不规范的值.
//但是后续的scale操作会将xy不等的缩放值纠正,改成和x方向相同
mScaleBase = MathUtils.getMatrixScale(mOuterMatrix)[0] / MathUtils.getDistance(x1, y1, x2, y2);
//两手指的中点在屏幕上落在了图片的某个点上,图片上的这个点在经过总矩阵变换后和手指中点相同
//现在我们需要得到图片上这个点在图片是fit center状态下在屏幕上的位置
//因为后续的计算都是基于图片是fit center状态下进行变换
//所以需要把两手指中点除以外层变换矩阵得到mScaleCenter
float[] center = MathUtils.inverseMatrixPoint(MathUtils.getCenterPoint(x1, y1, x2, y2), mOuterMatrix);
mScaleCenter.set(center[0], center[1]);
}
/**
* 对图片按照一些手势信息进行缩放
*
* @param scaleCenter mScaleCenter
* @param scaleBase mScaleBase
* @param distance 手指两点之间距离
* @param lineCenter 手指两点之间中点
* @see #mScaleCenter
* @see #mScaleBase
*/
private void scale(PointF scaleCenter, float scaleBase, float distance, PointF lineCenter) {
if (!isReady()) {
return;
}
//计算图片从fit center状态到目标状态的缩放比例
float scale = scaleBase * distance;