View 的滑动是 Android 实现自定义控件的基础,同时在开发中我们也难免会遇到 View 的滑动处理。其实不管是哪种滑动方式,其基本思想都是类似的:当点击事件传到 View 时,系统记下触摸点的坐标,手指移动时系统记下移动后触摸的坐标并算出偏移量,并通过偏移量来修改View的坐标。
实现 View 滑动有很多种方法,在这里主要讲解6种滑动方法,分别是 layout()、offsetLeftAndRight() 与 offsetTopAndBottom()、LayoutParams、Animation、scollTo() 与 scollBy(),以及 Scroller。
一、layout() 方法
View 进行绘制的时候会调用 onLayout() 方法来设置显示的位置,因此我们同样也可以通过修改 View 的 left、top、right、bottom 这4种属性来控制 View 的坐标。首先我们要自定义一个 View,在 onTouchEvent() 方法中获取触摸点的坐标,代码如下所示:
override fun onTouchEvent(event: MotionEvent?): Boolean {
// 获取手指触摸点的横坐标和纵坐标
val x = event?.x?.toInt()
val y = event?.y?.toInt()
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
lastX = x ?: 0
lastY = y ?: 0
}
...
}
...
}
接下来我们在 ACTION_MOVE 事件中计算偏移量,再调用 layout() 方法重新放置这个自定义 View 的位置即可。
override fun onTouchEvent(event: MotionEvent?): Boolean {
...
when (event?.action) {
...
MotionEvent.ACTION_MOVE -> {
// 计算移动的距离
val offsetX = x ?: (0 - lastX)
val offsetY = y ?: (0 - lastY)
// 调用 layout 方法来重新放置它的位置
layout(left + offsetX, top + offsetY, right + offsetX, bottom + offsetY)
}
}
...
}
在每次移动时都会调用 layout() 方法对屏幕重新布局,从而达到移动 View 的效果。自定义 View 的全部代码如下所示:
class CustomView(context: Context, attributeSet: AttributeSet) : View(context, attributeSet) {
private var lastX = 0
private var lastY = 0
override fun onTouchEvent(event: MotionEvent?): Boolean {
// 获取手指触摸点的横坐标和纵坐标
val x = event?.x?.toInt()
val y = event?.y?.toInt()
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
lastX = x ?: 0
lastY = y ?: 0
}
MotionEvent.ACTION_MOVE -> {
// 计算移动的距离
val offsetX = x ?: (0 - lastX)
val offsetY = y ?: (0 - lastY)
// 调用 layout 方法来重新放置它的位置
layout(left + offsetX, top + offsetY, right + offsetX, bottom + offsetY)
}
}
return true
}
}
随后,我们在布局中引用自定义 View 就可以了:
<com.tyhoo.android.demo.CustomView
android:id="@+id/test_view"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@android:color/holo_red_light"
... />
运行程序,效果如图1所示:
图1中的方块就是我们自定义的 View,它会随着我们手指的滑动改变自己的位置。
二、offsetLeftAndRight() 与 offsetTopAndBottom()
这两种方法和 layout() 方法的效果差不多,其使用方式也差不多。我们将 ACTION_MOVE 中的代码替换成如下代码:
override fun onTouchEvent(event: MotionEvent?): Boolean {
...
when (event?.action) {
...
MotionEvent.ACTION_MOVE -> {
// 计算移动的距离
val offsetX = x ?: (0 - lastX)
val offsetY = y ?: (0 - lastY)
// 对 left 和 right 进行偏移
offsetLeftAndRight(offsetX)
// 对 top 和 bottom 进行偏移
offsetTopAndBottom(offsetY)
}
}
...
}
三、LayoutParams
LayoutParams 主要保存了一个 View 的布局参数,因此我们可以通过 LayoutParams 来改变 View 的布局参数从而达到改变 View 位置的效果。同样,我们将 ACTION_MOVE 中的代码替换成如下代码:
override fun onTouchEvent(event: MotionEvent?): Boolean {
...
when (event?.action) {
...
MotionEvent.ACTION_MOVE -> {
// 计算移动的距离
val offsetX = x ?: (0 - lastX)
val offsetY = y ?: (0 - lastY)
val layoutParams = layoutParams as ConstraintLayout.LayoutParams
layoutParams.leftMargin = left + offsetX
layoutParams.topMargin = top + offsetY
setLayoutParams(layoutParams)
}
}
...
}
因为父控件是 ConstraintLayout,所以我们用了 ConstraintLayout.LayoutParams。如果父控件是 RelativeLayout,则要使用RelativeLayout.LayoutParams。除了使用布局的 LayoutParams 外,我们还可以用 ViewGroup.MarginLayoutParams 来实现:
override fun onTouchEvent(event: MotionEvent?): Boolean {
...
when (event?.action) {
...
MotionEvent.ACTION_MOVE -> {
// 计算移动的距离
val offsetX = x ?: (0 - lastX)
val offsetY = y ?: (0 - lastY)
val layoutParams = layoutParams as ViewGroup.MarginLayoutParams
layoutParams.leftMargin = left + offsetX
layoutParams.topMargin = top + offsetY
setLayoutParams(layoutParams)
}
}
...
}
四、Animation
可以采用 View 动画来移动,在 res 目录新建 anim 文件夹并创建 translate.xml:
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="1000"
android:fromXDelta="0"
android:toXDelta="300" />
</set>
接下来在 Kotlin 代码中调用就好了,代码如下所示:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val testView = findViewById<CustomView>(R.id.test_view)
testView.animation = AnimationUtils.loadAnimation(this, R.anim.translate)
}
}
运行程序,效果如图2所示:
运行程序,我们设置的方块会向右平移300像素,然后又会回到原来的位置。为了解决这个问题,我们需要在 translate.xml 中加上 fillAfter=“true”,代码如下所示:
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:fillAfter="true">
<translate
android:duration="1000"
android:fromXDelta="0"
android:toXDelta="300" />
</set>
运行程序,效果如图3所示:
运行代码后会发现,方块向右平移300像素后就停留在当前位置了。
需要注意的是,View 动画并不能改变 View 的位置参数。如果对一个 View 进行如上的平移动画操作,当 View 平移300像素停留在当前位置时,我们点击这个 View 并不会触发点击事件,但在我们点击这个 View 的原始位置时却触发了点击事件。对于系统来说这个 View 并没有改变原有的位置,所以我们点击其他位置当然不会触发这个 View 的点击事件。
五、scrollTo() 与 scollBy()
scrollTo(x, y) 表示移动到一个具体的坐标点,而 scrollBy(dx, dy) 则表示移动的增量为 dx、dy。其中,scollBy 最终也是要调用 scollTo 的。View 的 scollTo 和 scollBy 的源码如下所示:
public void scrollTo(int x, int y) {
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}
}
}
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
scollTo、scollBy 移动的是 View 的内容,如果在 ViewGroup 中使用,则是移动其所有的子 View。我们将 ACTION_MOVE 中的代码替换成如下代码:
override fun onTouchEvent(event: MotionEvent?): Boolean {
...
when (event?.action) {
...
MotionEvent.ACTION_MOVE -> {
// 计算移动的距离
val offsetX = x ?: (0 - lastX)
val offsetY = y ?: (0 - lastY)
(parent as View).scrollBy(-offsetX, -offsetY)
}
}
return true
}
这里若要实现自定义 View 随手指移动的效果,就需要将偏移量设置为负值。为什么要设置为负值呢?这是参考对象不同导致的差异。所以我们用 scrollBy 方法的时候要设置负数才会达到自己想要的效果。
六、Scroller
我们在用 scollTo/scollBy 方法进行滑动时,这个过程是瞬间完成的,所以用户体验不大好。这里我们可以使用 Scroller 来实现有过渡效果的滑动,这个过程不是瞬间完成的,而是在一定的时间间隔内完成的。Scroller 本身是不能实现 View 的滑动的,它需要与 View 的 computeScroll() 方法配合才能实现弹性滑动的效果。在这里我们实现自定义 View 平滑地向右移动。首先我们要初始化 Scroller,代码如下所示:
class CustomView(context: Context, attributeSet: AttributeSet) : View(context, attributeSet) {
...
private var scroller: Scroller? = null
init {
scroller = Scroller(context)
}
...
}
接下来重写 computeScroll() 方法,系统会在绘制 View 的时候在 draw() 方法中调用该方法。在这个方法中,我们调用父类的 scrollTo() 方法并通过 Scroller 来不断获取当前的滚动值,每滑动一小段距离我们就调用invalidate() 方法不断地进行重绘,重绘就会调用 computeScroll() 方法,这样我们通过不断地移动一个小的距离并连贯起来就实现了平滑移动的效果。
override fun computeScroll() {
super.computeScroll()
scroller?.let {
if (it.computeScrollOffset()) {
(parent as View).scrollTo(it.currX, it.currY)
invalidate()
}
}
}
我们在自定义 View 中写一个 smoothScrollTo 方法,调用 Scroller 的 startScroll() 方法,在 2000ms 内沿 X 轴平移 delta 像素,代码如下所示:
fun smoothScrollTo(destX: Int, destY: Int) {
val scrollX = scrollX
val delta = destX - scrollX
scroller?.startScroll(scrollX, 0, delta, 0, 2000)
invalidate()
}
最后我们再调用自定义 View 的 smoothScrollTo() 方法。这里我们设定自定义 View 沿着 X 轴向右平移 400 像素。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val testView = findViewById<CustomView>(R.id.test_view)
testView.smoothScrollTo(-400, 0)
}
}
运行程序,效果如图4所示: