Android自定义view-手势与滑动

前言

总所周知,自定义view在写对应的触控事件要重写onTouchEvent,根据不同的Action来进行我们的处理,Android也提供了一个GestureDetectorCompat来方便开发者快速的获取一些常用的手势事件。
处理滑动也有ScrollTo() ScrollBy()最原始的移动view内容的方法,而且都是瞬间移动没有平滑处理的,而Android同样提供了一个OverScroller来方便开发者进行view的平滑处理。

GestureDetectorCompat(触摸手势识别)

  1. 单击的情况
    点击时间很短:onDown —-> onSingleTapUp —-> onSingleTapConfirmed
    点击时间稍长:onDown —-> onShowPress —-> onSingleTapUp —-> onSingleTapConfirmed

2.长按的情况
onDown —-> onShowPress —-> onLongPress

3.滑动的情况
手指触动屏幕后,稍微滑动后立即松开:
onDown —-> onScroll —-> onScroll —-> onScroll —-> ……… —-> onFling

4双击的情况
双击 onDown —-> onSingleTapUp —-> onDoubleTap

示例代码

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
public class GestureDetectorCompatSimple extends View {
private GestureDetectorCompat detector;
private GestureListener gestureListener = new GestureListener();
public GestureDetectorCompatSimple(Context context) {
super(context);
}
public GestureDetectorCompatSimple(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
detector = new GestureDetectorCompat(context, gestureListener);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return detector.onTouchEvent(event);//返回是否消耗事件
}
class GestureListener extends GestureDetector.SimpleOnGestureListener {
//手指放下会触发
@Override
public boolean onDown(MotionEvent e) {//返回值决定下面的事件是否触发
return true;
}
//单击 时间稍长ViewConfiguration.TAP_TIMEOUT 按下100ms会触发
@Override
public void onShowPress(MotionEvent e) {
}
//在按下并抬起时发生,只要符合这个条件就触发该函数。
@Override
public boolean onSingleTapUp(MotionEvent e) {
return false;
}
//拖动时不断触发 distanceX distanceY 上一个坐标减当前触摸坐标的距离
@Override
public boolean onScroll(MotionEvent down, MotionEvent event, float distanceX, float distanceY) {
return false;
}
//长按触发 500ms ViewConfiguration.DEFAULT_LONG_PRESS_TIMEOUT
@Override
public void onLongPress(MotionEvent e) {
}
//拖动结束后触发 一般用于惯性滑动 XY方向的滑动的速度(意思为一秒时间内运动了多少个像素)
@Override
public boolean onFling(MotionEvent down, MotionEvent event, float velocityX, float velocityY) {
return false;
}
//真正意义上的单击事件 双击不会触发
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
return false;
}
// 双击
@Override
public boolean onDoubleTap(MotionEvent e) {
return false;
}
//双击后滑动事件监听
@Override
public boolean onDoubleTapEvent(MotionEvent e) {
return false;
}
}
}

OverScroller(滑动处理计算器)

OverScroller 这个类只负责计算坐标,移动要自己实现。

基础知识

移动view/ViewGroup的内容的位置

scrollTo scrollBy

注意scrollTo scrollBy 滑动的是view/ViewGroup的内容,而不是自己本身。
scrollTo是绝对位置移动,scrollBy是在当前位置叠加。以被移动的内容做参照物,它的坐标轴是和平时相反的,例如移动到(300,100)在scrollTo上表达的是scrollTo(-300,-100),例如当前坐标叠加(x+30,y+40)在scrollBy上表达的是scrollBy(-30,-40)

getScrollX getScrollY

原点和已滑动的坐标相减,表示滑动的偏移量

移动view/ViewGroup本身的位置

offsetLeftAndRight offsetTopAndBottom

在当前位置叠加偏移

layout

最直接修改view布局的位置

setLayoutParams

修改布局的参数来修改位置

VelocityTracker 计算滑动速度

直接上实例代码

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
public class VelocityTrackerSimple extends ViewGroup {
float minVelocity;
float maxVelocity;
ViewConfiguration viewConfiguration;
VelocityTracker velocityTracker = VelocityTracker.obtain();
public VelocityTrackerSimple(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
viewConfiguration = ViewConfiguration.get(context);
maxVelocity = viewConfiguration.getScaledMaximumFlingVelocity();//滑动的最大值
minVelocity = viewConfiguration.getScaledMinimumFlingVelocity();//开始滑动的最小值
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//布局处理
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
measureChildren(widthMeasureSpec, heightMeasureSpec);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
velocityTracker.clear();//清空计速器
}
velocityTracker.addMovement(event);
switch (event.getActionMasked()) {
case MotionEvent.ACTION_UP:
// 速度追踪器 设置units的值为1000,意思为一秒时间内运动了多少个像素 表示速率的最大值
velocityTracker.computeCurrentVelocity(1000, maxVelocity);
float vx = velocityTracker.getXVelocity();//横向速度
float vy = velocityTracker.getYVelocity();//纵向速度
if (Math.abs(vx) < minVelocity) {
//处理
} else {
//处理
}
if (Math.abs(vy) < minVelocity) {
//处理
} else {
//处理
}
break;
}
return true;
}
}

坐标

getStartX() 滑动的起点x轴坐标
getStartY() 滑动的起点y轴坐标
getFinalX()滑动的终点x轴坐标
getFinalY()滑动的终点y轴坐标
getCurrX()滑动中途x轴坐标
getCurrY()滑动中途y轴坐标

startScroll(平滑移动)

startX startY 开始移动的坐标, dx dy水平和垂直方向滑动的距离偏移值,duration 移动时间/ms。

1
2
startScroll(int startX, int startY, int dx, int dy)
startScroll(int startX, int startY, int dx, int dy, int duration)

fling(带加速度的移动)

startX startY 开始移动坐标,velocityX velocityY 速度,minX minY 最小能移动到的xy坐标位置, maxX maxY最大能移动到的xy坐标位置 , overX overY允许溢出边界 回弹效果。

1
2
fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY)
fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY, int overX, int overY)

computeScrollOffset(判断移动是否完成)

用来判断是否滑动是否结束

1
2
3
4
5
6
7
@Override
public void computeScroll() {
if (scroller.computeScrollOffset()) {//true表示滑动未完成
offsetX = scroller.getCurrX();
offsetY = scroller.getCurrY();
}
}

示例代码与效果

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
public class GestureDetectorCompatSimple extends View {
private GestureDetectorCompat detector;
private OverScroller scroller;
private float offsetX;
private float offsetY;
private int rectWidth=300;
private int rectHeight=300;
private GestureListener gestureListener = new GestureListener();
private Paint mPaint=new Paint();
public GestureDetectorCompatSimple(Context context) {
super(context);
}
public GestureDetectorCompatSimple(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mPaint.setColor(Color.RED);
detector = new GestureDetectorCompat(context, gestureListener);
scroller = new OverScroller(context);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return detector.onTouchEvent(event);//返回是否消耗事件
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(offsetX,offsetY,offsetX+rectWidth,offsetY+rectHeight,mPaint);
}
@Override
public void computeScroll() {
if (scroller.computeScrollOffset()) {//滑动 刷新界面
offsetX = scroller.getCurrX();
offsetY = scroller.getCurrY();
ViewCompat.postInvalidateOnAnimation(this);
}
}
class GestureListener extends GestureDetector.SimpleOnGestureListener {
//手指放下会触发
@Override
public boolean onDown(MotionEvent e) {//返回值决定下面的事件是否触发
return true;
}
//单击 时间稍长ViewConfiguration.TAP_TIMEOUT 按下100ms会触发
@Override
public void onShowPress(MotionEvent e) {
}
//在按下并抬起时发生,只要符合这个条件就触发该函数。
@Override
public boolean onSingleTapUp(MotionEvent e) {
return false;
}
//拖动时不断触发 distanceX distanceY 上一个坐标减当前触摸坐标的距离
@Override
public boolean onScroll(MotionEvent down, MotionEvent event, float distanceX, float distanceY) {
offsetX -= distanceX;//上一个坐标减当前触摸坐标的距离 所以distance往右滑是负数 但画布向右移动是正数 所以用减号
offsetY -= distanceY;
offsetX=Math.max(0,offsetX);
offsetX=Math.min(getWidth()-rectWidth,offsetX);
offsetY=Math.max(0,offsetY);
offsetY=Math.min(getHeight()-rectHeight,offsetY);
invalidate();
return false;
}
//长按触发 500ms ViewConfiguration.DEFAULT_LONG_PRESS_TIMEOUT
@Override
public void onLongPress(MotionEvent e) {
}
//拖动结束后触发 一般用于惯性滑动 XY方向的滑动的速度(意思为一秒时间内运动了多少个像素)
@Override
public boolean onFling(MotionEvent down, MotionEvent event, float velocityX, float velocityY) {
scroller.fling((int) offsetX, (int) offsetY, (int) velocityX, (int) velocityY,
0,
(int)(getWidth()-rectWidth),
0,
(int)(getHeight()-rectHeight),rectWidth/2,rectHeight/2);
postInvalidateOnAnimation();
return false;
}
//真正意义上的单击事件 双击不会触发
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
return false;
}
// 双击
@Override
public boolean onDoubleTap(MotionEvent e) {
scroller.startScroll((int)offsetX,(int)offsetY,200,200,1000);
postInvalidateOnAnimation();
return false;
}
//双击后滑动事件监听
@Override
public boolean onDoubleTapEvent(MotionEvent e) {
return false;
}
}
}