目的
这篇文章主要对 View 的滑动进行学习,主要包括滑动的实现方式和弹性滑动提高用户体验
(一)View 的滑动
滑动的方式基本思想都是类似的:
当触摸事件传到View时,系统记下触摸点的坐标,手指移动时系统记下移动后的触摸的坐标并算出偏移量,并通过偏移量来修改View的坐标。
View 实现滑动的几种方式:
- 第一种,layout()方法
- 第二种,offsetLeftAndRight() 与 offsetTopAndBottom()
- 第三种,通过改变 View 的 LayoutParams 使得 View 重新布局从而实现滑动
- 第四种,通过动画给 View 施加平移效果来实现滑动
- 第五种,通过 View 本身提供的 scrollTo/scrollBy 方法来实现滑动
1. layout() 方法
View 进行绘制的时候会调用 onLayout() 方法来设置显示的位置,因此我们同样也可以通过修改 View 的属性来控制 View 的坐标
public class RedView extends View {
int lastX, lastY;
public RedView(Context context) {
super(context);
}
public RedView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public RedView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@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;
break;
case MotionEvent.ACTION_MOVE:
//计算移动的距离
int offsetX = x - lastX;
int offsetY = y - lastY;
//调用layout方法重新放置它的位置
layout(offsetX + getLeft(), offsetY + getTop(), offsetX + getRight(), offsetY + getBottom());
break;
}
return true;
}
}
2. offsetLeftAndRight() 与 offsetTopAndBottom()
将 ACTION_MOVE 中的代码替换
case MotionEvent.ACTION_MOVE:
//计算移动的距离
int offsetX = x - lastX;
int offsetY = y - lastY;
//对 left 和 right 进行偏移
offsetLeftAndRight(offsetX);
//对 top 和 bottom 进行偏移
offsetTopAndBottom(offsetY);
break;
3. LaytouParams(改变布局参数)
LayoutParams 主要保存了一个 View 的布局参数,因此我们可以通过改变参数从而达到改变 View 位置的效果
case MotionEvent.ACTION_MOVE:
//计算偏移量
int offsetX = x - lastX;
int offsetY = y - lastY;
LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);
break;
需要注意的是 LinearLayout 是 View 的父容器,如果父容器是 RelativeLayout,则要使用 RelativeLayout.LayoutParams
当然我们可以使用 ViewGroup.MarginLayoutParams 来实现,一劳永逸
case MotionEvent.ACTION_MOVE:
//计算偏移量
int offsetX = x - lastX;
int offsetY = y - lastY;
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);
break;
4. 动画
可以采用 View 动画来移动,在 res 目录新建 anim 文件夹并创建 translate.xml
<?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>
- android:fillAfter="true" View 平移后停留在最后的位置,如果为 false 则会返回起始位置
- android:duration="1000" 动画的时间为 1000 ms
- android:toXDelta="300" 平移的距离,300 像素
mView.setAnimation(AnimationUtils.loadAnimation(this, R.anim.translate));
在 Java 代码中执行动画,这里所用的是补间动画,虽然 View 位置发生改变,但是对系统来说,View 并没有发生改变,点击事件并不会发生。Android3.0 之后,可以通过属性动画来解决这个问题
ObjectAnimator.ofFloat(mView, "translationX", 0, 300).setDuration(1000).start();
5. scrollTo 与 scrollBy
scollTo(x, y) 表示移动到一个具体的坐标点,而 scollBy(dx, dy) 则表示移动的增量为 dx、dy。
两个方法区别: scrollBy() 相对移动,scrollTo() 绝对移动
其中 scollBy 最终也是要调用 scollTo 的。scollTo、scollBy 移动的是View的内容,如果在 ViewGroup 中使用则是移动他所有的子 View。我们将 ACTION_MOVE 中的代码替换成如下代码:
((View) getParent()).scrollBy(-offsetX, -offsetY);
这里要实现 mView 随着我们手指移动的效果的话,我们就需要将偏移量设置为负值。
在View内部有两个属性 mScrllX 和 mScrollY,分别可以通过 getScrollX() 和 getScrollY() 方法得到
在滑动过程中,mScrollX 总是等于 View 左边缘和 View 内容左边缘在水平方方向的距离;mScrollY 总是等于 View 上边缘和 View 中内容上边缘在竖直方向的距离。View 边缘是指 View 的位置也就是 View 的四个顶点到父容器的距离,View 内边缘是内容距离 View 四边的距离。
无论是 scrollTo() 还是 scrollBy() 都只能改变 View 内容的位置而不能改变 View 在布局中的位置
mScrollX/Y 单位为像素 px。当 View 左边缘在 View 内容左边缘右边时,mScrollX 为正值,反之为负值;同理,当 View 上边缘在 View 内容上边缘下边时,mScrollX 为正值,反之为负值。也就是说,View 从左向右滑动,mScrollX 为负值,反之为正值;从上往下滑动,mScrollY 为负值,反之为正值
各种滑动方式的对比
- 改变布局参数:操作复杂,适用于有交互的 View
- 属性动画:操作简单,适用于没交互 View 和实现复杂的动画效果
- scrollTo/By:操作简单,适合对 View 内容的滑动
(二)弹性滑动
在我们进行滑动时,这个过程是瞬间完成的,所以用户体验不大好,这里我们可以添加有过渡的滑动
View 弹性滑动的几种方式
- 第一种:Scroller
- 第二种:通过动画
这几种的方式有一个共同的思想:将一次大的滑动分成若干次小的滑动并在一个时间段内完成
1. Scroller
Scroller 本身是不能实现 View 的滑动的,它需要与 View 的 computeScroll() 方法配合才能实现弹性滑动的效果
- 初始化Scroller对象
- 重写View的computeScroll()方法
- 调用mScroller.startScroll()方法
public RedView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mScroller = new ScrollView(context);
}
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()) { //判断Scroller是否执行完毕
((View) getParent()).scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
//缓慢的滑到指定位置
public void smoothScrollTo(int destX, int destY) {
int deltaX = destX - getScrollX();
int deltaY = destY - getScrollY();
//2000ms 内滑到向(deltaY,deltaY)的位置,效果就是慢慢滑动
mScroller.startScroll(0, 0, deltaX, deltaY, 2000);
invalidate();
}
在Activity中调用:
mView.smoothScrollTo(-400, -400);
Scroller 本身并不能实现 View 的滑动,它需要配合 View 的 computeScroll 方法才能完成弹性滑动的效果,它通过不断地让 View 重绘,而每一次重绘距滑动起始时间间隔,通过这个时间间隔 Scroller 就可以得出 View 当前的滑动位置,知道了滑动位置就可以通过 scrollTo 方法来完成 View 的滑动。就这样,View 的每一次重绘都会导致 View 进行小幅度的滑动(弹性滑动的核心思想:将一次大的滑动分成若干次小的滑动并在一个时间段内完成),而多次的小幅度滑动就组成了弹性滑动,这就是 Scroller 的工作机制。
2. 通过动画
动画本身就是一种渐进的过程,因此通过它来实现的滑动天然就是具有弹性效果:
ObjectAnimator.ofFloat(mView, "translationX", 0, 100).setDuration(1000).start();
总结
滑动做为 View 最基本的行为,一切的华丽的、绚丽的 UI,归根结底都是建立在其基础上
最后
学习资料主要来源于《Android开发艺术探索》和《Android进阶之光》,对进阶学习的一个记录与总结,如果有幸能对大家有所帮助,那将荣幸之至。