昨天完成了一个支持设置margin,gravity,水平或者垂直排列的简单的自定义ViewGroup。但是它并不支持滑动,所以无法展现较多的内容。现在我们重写一下onTouchEvent(),来支持滑动。
重写onTouchEvent()以支持滑动:
要使View滑动,我们可以通过调用scrollTo()和scrollBy()来实现,这里需要注意的是:要使页面向左移动,需要增加mScrollX(就是向scrollBy传递一个正数),同样的,要使页面向上移动,需要增加mScrollY。
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
|
@Override
public
boolean
onTouchEvent(MotionEvent event) {
final
int
action = event.getAction();
if
(BuildConfig.DEBUG)
Log.d(
"onTouchEvent"
,
"action: "
+ action);
switch
(action) {
case
MotionEvent.ACTION_DOWN:
x = event.getX();
y = event.getY();
break
;
case
MotionEvent.ACTION_MOVE:
float
mx = event.getX();
float
my = event.getY();
//此处的moveBy是根据水平或是垂直排放的方向,
//来选择是水平移动还是垂直移动
moveBy((
int
) (x - mx), (
int
) (y - my));
x = mx;
y = my;
break
;
}
return
true
;
}
//此处的moveBy是根据水平或是垂直排放的方向,
//来选择是水平移动还是垂直移动
public
void
moveBy(
int
deltaX,
int
deltaY) {
if
(BuildConfig.DEBUG)
Log.d(
"moveBy"
,
"deltaX: "
+ deltaX +
" deltaY: "
+ deltaY);
if
(orientation == Orientation.HORIZONTAL) {
if
(Math.abs(deltaX) >= Math.abs(deltaY))
scrollBy(deltaX,
0
);
}
else
{
if
(Math.abs(deltaY) >= Math.abs(deltaX))
scrollBy(
0
, deltaY);
}
}
|
借助Scroller,并且处理ACTION_UP事件
Scroller是一个用于计算位置的工具类,它负责计算下一个位置的坐标(根据时长,最小以最大移动距离,以及阻尼算法(可以使用自定义的Interpolator))。
Scroller有两种模式:scroll和fling。
- scroll用于已知目标位置的情况(例如:Viewpager中向左滑动,就是要展示右边的一页,那么我们就可以准确计算出滑动的目标位置,此时就可以使用Scroller.startScroll()方法)
- fling用于不能准确得知目标位置的情况(例如:ListView,每一次的滑动,我们事先都不知道滑动距离,而是根据手指抬起是的速度来判断是滑远一点还是近一点,这时就可以使用Scroller.fling()方法)
现在我们改一下上面的onTouchEvent()方法,增加对ACTION_UP事件的处理,以及初速度的计算。
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
|
@Override
public
boolean
onTouchEvent(MotionEvent event) {
final
int
action = event.getAction();
if
(BuildConfig.DEBUG)
Log.d(
"onTouchEvent"
,
"action: "
+ action);
//将事件加入到VelocityTracker中,用于计算手指抬起时的初速度
if
(velocityTracker ==
null
) {
velocityTracker = VelocityTracker.obtain();
}
velocityTracker.addMovement(event);
switch
(action) {
case
MotionEvent.ACTION_DOWN:
x = event.getX();
y = event.getY();
if
(!mScroller.isFinished())
mScroller.abortAnimation();
break
;
case
MotionEvent.ACTION_MOVE:
float
mx = event.getX();
float
my = event.getY();
moveBy((
int
) (x - mx), (
int
) (y - my));
x = mx;
y = my;
break
;
case
MotionEvent.ACTION_UP:
//maxFlingVelocity是通过ViewConfiguration来获取的初速度的上限
//这个值可能会因为屏幕的不同而不同
velocityTracker.computeCurrentVelocity(
1000
, maxFlingVelocity);
float
velocityX = velocityTracker.getXVelocity();
float
velocityY = velocityTracker.getYVelocity();
//用来处理实际的移动
completeMove(-velocityX, -velocityY);
if
(velocityTracker !=
null
) {
velocityTracker.recycle();
velocityTracker =
null
;
}
break
;
return
true
;
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
private
void
completeMove(
float
velocityX,
float
velocityY) {
if
(orientation == Orientation.HORIZONTAL) {
int
mScrollX = getScrollX();
int
maxX = desireWidth - getWidth();
// - Math.abs(mScrollX);
if
(Math.abs(velocityX) >= minFlingVelocity && maxX >
0
) {
mScroller.fling(mScrollX,
0
, (
int
) velocityX,
0
,
0
, maxX,
0
,
0
);
invalidate();
}
}
else
{
int
mScrollY = getScrollY();
int
maxY = desireHeight - getHeight();
// - Math.abs(mScrollY);
if
(Math.abs(velocityY) >= minFlingVelocity && maxY >
0
) {
mScroller.fling(
0
, mScrollY,
0
, (
int
) velocityY,
0
,
0
,
0
, maxY);
invalidate();
}
}
}
|
其实原因就是上面所说的,Scroller只是帮助我们计算位置的,并不处理View的滑动。我们要想实现连续的滑动效果,那就要在View绘制完成后,再通过Scroller获得新位置,然后再重绘,如此反复,直至停止。
重写computeScroll(),实现View的连续绘制
1
2
3
4
5
6
7
8
9
10
11
12
|
@Override
public
void
computeScroll() {
if
(mScroller.computeScrollOffset()) {
if
(orientation == Orientation.HORIZONTAL) {
scrollTo(mScroller.getCurrX(),
0
);
postInvalidate();
}
else
{
scrollTo(
0
, mScroller.getCurrY());
postInvalidate();
}
}
}
|
computeScroll()是在ViewGroup的drawChild()中调用的,上面的代码中,我们通过调用computeScrollOffset()来判断滑动是否已停止,如果没有,那么我们可以通过getCurrX()和getCurrY()来获得新位置,然后通过调用scrollTo()来实现滑动,这里需要注意的是postInvalidate()的调用,它会将重绘的这个Event加入UI线程的消息队列,等scrollTo()执行完成后,就会处理这个事件,然后再次调用ViewGroup的draw()-->drawChild()-->computeScroll()-->scrollTo()如此就实现了连续绘制的效果。
现在我们再重新运行一下app,终于可以持续滑动了:),不过,当我们缓慢地拖动View,慢慢抬起手指,我们会发现通过这样的方式,可以使得所有的子View滑到屏幕之外,(所有的子View都消失了:()。
问题主要是出在completeMove()中,我们只是判断了初始速度是否大于最小阈值,如果小于这个最小阈值的话就什么都不做,缺少了边界的判断,因此修改computeMove()如下:
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
|
private
void
completeMove(
float
velocityX,
float
velocityY) {
if
(orientation == Orientation.HORIZONTAL) {
int
mScrollX = getScrollX();
int
maxX = desireWidth - getWidth();
if
(mScrollX > maxX) {
// 超出了右边界,弹回
mScroller.startScroll(mScrollX,
0
, maxX - mScrollX,
0
);
invalidate();
}
else
if
(mScrollX <
0
) {
// 超出了左边界,弹回
mScroller.startScroll(mScrollX,
0
, -mScrollX,
0
);
invalidate();
}
else
if
(Math.abs(velocityX) >= minFlingVelocity && maxX >
0
) {
mScroller.fling(mScrollX,
0
, (
int
) velocityX,
0
,
0
, maxX,
0
,
0
);
invalidate();
}
}
else
{
int
mScrollY = getScrollY();
int
maxY = desireHeight - getHeight();
if
(mScrollY > maxY) {
// 超出了下边界,弹回
mScroller.startScroll(
0
, mScrollY,
0
, maxY - mScrollY);
invalidate();
}
else
if
(mScrollY <
0
) {
// 超出了上边界,弹回
mScroller.startScroll(
0
, mScrollY,
0
, -mScrollY);
invalidate();
}
else
if
(Math.abs(velocityY) >= minFlingVelocity && maxY >
0
) {
mScroller.fling(
0
, mScrollY,
0
, (
int
) velocityY,
0
,
0
,
0
, maxY);
invalidate();
}
}
}
|
ok,现在当我们滑出边界,松手后,会自动弹回。
处理ACTION_POINTER_UP事件,解决多指交替滑动跳动的问题
现在ViewGroup可以灵活的滑动了,但是当我们使用多个指头交替滑动时,就会产生跳动的现象。原因是这样的:
我们实现onTouchEvent()的时候,是通过event.getX(),以及event.getY()来获取触摸坐标的,实际上是获取的手指索引为0的位置坐标,当我们放上第二个手指后,这第二个手指的索引为1,此时我们同时滑动这两个手指,会发现没有问题,因为我们追踪的是手指索引为0的手指位置。但是当我们抬起第一个手指后,问题就出现了, 因为这个时候原本索引为1的第二个手指的索引变为了0,所以我们追踪的轨迹就出现了错误。
简单来说,跳动就是因为追踪的手指的改变,而这两个手指之间原本存在间隙,而这个间隙的距离就是我们跳动的距离。
其实问题产生的根本原因就是手指的索引会变化,因此我们需要记录被追踪手指的id,然后当有手指离开屏幕时,判断离开的手指是否是我们正在追踪的手指:
- 如果不是,忽略
- 如果是,则选择一个新的手指作为被追踪手指,并且调整位置记录。
还有一点就是,要处理ACTION_POINTER_UP事件,就需要给action与上一个掩码:event.getAction()&MotionEvent.ACTION_MASK 或者使用 event.getActionMasked()方法。
更改后的onTouchEvent()的实现如下:
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
|
@Override
public
boolean
onTouchEvent(MotionEvent event) {
final
int
action = event.getActionMasked();
if
(velocityTracker ==
null
) {
velocityTracker = VelocityTracker.obtain();
}
velocityTracker.addMovement(event);
switch
(action) {
case
MotionEvent.ACTION_DOWN:
// 获取索引为0的手指id
mPointerId = event.getPointerId(
0
);
x = event.getX();
y = event.getY();
if
(!mScroller.isFinished())
mScroller.abortAnimation();
break
;
case
MotionEvent.ACTION_MOVE:
// 获取当前手指id所对应的索引,虽然在ACTION_DOWN的时候,我们默认选取索引为0
// 的手指,但当有第二个手指触摸,并且先前有效的手指up之后,我们会调整有效手指
// 屏幕上可能有多个手指,我们需要保证使用的是同一个手指的移动轨迹,
// 因此此处不能使用event.getActionIndex()来获得索引
final
int
pointerIndex = event.findPointerIndex(mPointerId);
float
mx = event.getX(pointerIndex);
float
my = event.getY(pointerIndex);
moveBy((
int
) (x - mx), (
int
) (y - my));
x = mx;
y = my;
break
;
case
MotionEvent.ACTION_UP:
velocityTracker.computeCurrentVelocity(
1000
, maxFlingVelocity);
float
velocityX = velocityTracker.getXVelocity(mPointerId);
float
velocityY = velocityTracker.getYVelocity(mPointerId);
completeMove(-velocityX, -velocityY);
if
(velocityTracker !=
null
) {
velocityTracker.recycle();
velocityTracker =
null
;
}
break
;
case
MotionEvent.ACTION_POINTER_UP:
// 获取离开屏幕的手指的索引
int
pointerIndexLeave = event.getActionIndex();
int
pointerIdLeave = event.getPointerId(pointerIndexLeave);
if
(mPointerId == pointerIdLeave) {
// 离开屏幕的正是目前的有效手指,此处需要重新调整,并且需要重置VelocityTracker
int
reIndex = pointerIndexLeave ==
0
?
1
:
0
;
mPointerId = event.getPointerId(reIndex);
// 调整触摸位置,防止出现跳动
x = event.getX(reIndex);
y = event.getY(reIndex);
if
(velocityTracker !=
null
)
velocityTracker.clear();
}
break
;
}
return
true
;
}
|
不过当我们想为咱们的自定义的ViewGroup设置onClick和onLongClick事件时,发现并不支持。更奇怪的是当我们为子View设置了事件之后(例如click事件),我们的ViewGroup居然不能正常滑动了。
上面第一个问题,我们需要在ACTION_UP中加一些处理,而第二个问题就需要重写onInterceptTouchEvent()方法,关于onInterceptTouchEvent()和onTouchEvent()之间的事件传递流程,就在明天的博客中再写吧:)