实现滑动的方法

了解滑动实现原理后,可以总结出实现滑动效果的基本思想:当View被触摸时,记录当前触摸点的坐标;当触摸点移动时,记录移动后触摸点的坐标;根据两次获取的坐标计算出触摸点的偏移量,通过偏移量修改View的坐标;不断重复,实现滑动效果。

接下来介绍实现滑动效果的一些方法。

layout方法

在View进行绘制时,会调用onLayout()方法设置显示的位置,因此,可以通过修改View的left,top,right,bottom属性来控制View的坐标。具体在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
@Override
public boolean onTouchEvent(MotionEvent event) {
// 获取触摸点坐标
int x = (int) event.getX();
int y = (int) event.getY();

switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 记录当前触摸点坐标
lastX = x;
lastY = y;
return true;
case MotionEvent.ACTION_MOVE:
// 计算偏移量
int offsetX = x - lastX;
int offsetY = y - lastY;

// 根据偏移量重新布局View,使View移动产生滑动效果
layout(getLeft() + offsetX, getTop() + offsetY,
getRight() + offsetX, getBottom() + offsetY);
return true;
}

return super.onTouchEvent(event);
}

面的示例代码是通过getX()和getY()方法获取触摸事件的坐标值,下面以getRawX()和getRawY()方法来获取坐标值实现滑动效果:

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
@Override
public boolean onTouchEvent(MotionEvent event) {
// 获取触摸点坐标
int rawX = (int) event.getRawX();
int rawY = (int) event.getRawY();

switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 记录当前触摸点坐标
lastX = rawX;
lastY = rawY;
return true;
case MotionEvent.ACTION_MOVE:
// 计算偏移量
int offsetX = rawX - lastX;
int offsetY = rawY - lastY;

// 根据偏移量重新布局View,使View移动产生滑动效果
layout(getLeft() + offsetX, getTop() + offsetY,
getRight() + offsetX, getBottom() + offsetY);

// 重新设置初始坐标
lastX = rawX;
lastY = rawY;
return true;
}

return super.onTouchEvent(event);
}

注意

使用绝对坐标系时,每次执行完ACTION_MOVE逻辑后,一定要重新设置初始坐标,这样才能准确获取连续移动时的偏移量。因为在相对坐标系中,ACTION_DOWN事件的坐标值对于View是不变的,而在绝对坐标系中,这个坐标值对于View是变化的。

offsetLeftAndRight和offsetTopAndBottom方法

这两个方法只需要偏移量就可以完成View的重新绘制,达到layout方法同样的效果,示例代码如下:

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
@Override
public boolean onTouchEvent(MotionEvent event) {
// 获取触摸点坐标
int x = (int) event.getX();
int y = (int) event.getY();

switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 记录当前触摸点坐标
lastX = x;
lastY = y;
return true;
case MotionEvent.ACTION_MOVE:
// 计算偏移量
int offsetX = x - lastX;
int offsetY = y - lastY;

// 根据偏移量重新布局View,使View移动产生滑动效果
offsetLeftAndRight(offsetX);
offsetTopAndBottom(offsetY);
return true;
}

return super.onTouchEvent(event);
}

LayoutParams

LayoutParams保存了View的布局参数,可以通过改变LayoutParams来动态修改View的位置参数,实现View的滑动效果。具体示例代码如下:

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
@Override
public boolean onTouchEvent(MotionEvent event) {
// 获取触摸点坐标
int x = (int) event.getX();
int y = (int) event.getY();

switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 记录当前触摸点坐标
lastX = x;
lastY = y;
return true;
case MotionEvent.ACTION_MOVE:
// 计算偏移量
int offsetX = x - lastX;
int offsetY = y - lastY;

// 根据偏移量重新布局View,使View移动产生滑动效果
ViewGroup.MarginLayoutParams layoutParams =
(ViewGroup.MarginLayoutParams) getLayoutParams();
layoutParams.leftMargin += offsetX;
layoutParams.topMargin += offsetY;
setLayoutParams(layoutParams);
return true;
}

return super.onTouchEvent(event);
}

scrollTo与scrollBy方法

View提供了scrollTo与scrollBy方法来改变View的位置,scrollTo(x, y)表示移动到坐标点(x, y),scrollBy(dx, dy)表示移动的增量为dx、dy。

scrollTo与scrollBy方法的本质是移动View,从而使View中content产生相对移动。以下示意图以scrollBy方法为例进行说明:

理解scrollBy方法

从示意图可以看出,content移动的方向和ViewGroup移动的方向是相反的,所以,如果以子View的偏移量来移动ViewGroup来达到子View的滑动效果,那么,scrollBy中的偏移量要使用负值。

根据以上分析,使用scrollBy方法实现滑动效果的示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
public boolean onTouchEvent(MotionEvent event) {
// 获取触摸点坐标
int x = (int) event.getX();
int y = (int) event.getY();

switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 记录当前触摸点坐标
lastX = x;
lastY = y;
return true;
case MotionEvent.ACTION_MOVE:
// 计算偏移量
int offsetX = x - lastX;
int offsetY = y - lastY;

// 根据偏移量重新布局View,使View移动产生滑动效果
((View) getParent()).scrollBy(-offsetX, -offsetY);
return true;
}

return super.onTouchEvent(event);
}

类似的,在绝对坐标系中,可以使用scrollTo方法实现同样的滑动效果。

Scroller

在使用scrollTo与scrollBy方法移动时,View的移动都是瞬时完成的,为了产生平滑移动的效果,需要使用Scroller类。

Scroller的原理也是使用scrollTo与scrollBy方法来移动View,但是它会把移动的偏移量分为多个很小的偏移量,虽然在每个小的偏移量里面,移动是瞬时的,但是整体上会是一个平滑的移动效果。

使用Scroller类实现滑动效果需要以下几个步骤:

  • 初始化Scroller
1
mScroller = new Scroller(context);
  • 重写computeScroll()方法,实现平滑移动

系统绘制View的时候会在draw()方法中调用computeScroll()方法,可以在computeScroll()中使用scrollTo()方法进行小的偏移量的移动,接着调用invalidate()方法触发draw()方法,形成一个循环过程,最终实现平滑移动。示例代码如下:

1
2
3
4
5
6
7
8
9
10
@Override
public void computeScroll() {
super.computeScroll();
// 判断Scroller是否执行完毕
if (mScroller.computeScrollOffset()) {
((View) getParent()).scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
// 通过重绘不断调用computeScroll(),达到平滑移动的效果
invalidate();
}
}

Scroller类提供了computeScrollOffset()方法来判断是否完成了整个滑动过程,用时提供了getCurX()、getCurY()方法获取当前的滑动坐标。

  • 执行startScroll()方法,执行滑动过程
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
@Override
public boolean onTouchEvent(MotionEvent event) {
// 获取触摸点坐标
int x = (int) event.getX();
int y = (int) event.getY();

switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 记录当前触摸点坐标
lastX = x;
lastY = y;
return true;
case MotionEvent.ACTION_MOVE:
// 计算偏移量
int offsetX = x - lastX;
int offsetY = y - lastY;

// 执行滑动过程
View viewGroup = (View) getParent();
mScroller.startScroll(viewGroup.getScrollX(), viewGroup.getScrollY(),
-offsetX, -offsetY, 0);
// 通过重绘首次触发computeScroll()
invalidate();
return true;
}

return super.onTouchEvent(event);
}

通常使用getScrollX()和getScrollY()方法获取滑动的起始坐标,由于是父视图在滑动,因此,偏移值的正负情况与scrollTo、scrollBy方法相同。同时,需要注意调用invalidate()触发computeScroll()方法进行滑动过程。

位移动画

位移动画(Translate Animation)可以实现View的移动效果,其本质是改变View的translationX和translationY值。因此,可以使用位移动画实现View的滑动效果,具体实现可以参考动画机制相关的文章。

ViewDragHelper

Android的Support库中提供了DrawerLayout和SlidingPaneLayout方便实现侧滑菜单的效果,这两个布局就是通过ViewDragHelper类实现的。通过ViewDragHelper类,基本可以实现各种不同的Scroll、Drag需求。

ViewDragHelper功能比较强大,使用也相对比较复杂,具体使用可以搜索相关资料,这里就不详细介绍了。

坚持原创技术分享,您的支持将鼓励我继续创作!