一、View的位置参数
在了解View滑动的实现之前,我们先要了解View的位置参数
View的位置主要由由四个顶点来决定,分别对应于四个属性:top、left、right、bottom
top:左上角纵坐标
left:左上角横坐标
right:右下角横坐标
bottom:右下角纵坐标
这些参数都是相对于View的父容器的坐标,是一种参数坐标
在Android中x轴和y轴的正方向为右和下
View位置坐标和父容器的关系如下图:

由此我们可以得到View宽高和坐标的关系
width=right-left
height=bottom-top
我们可以通过方法来获取位置参数
java
left=getLeft();
right=getRight();
top=getTop();
bottom=getBottom();
除此之外,还有几个额外参数
x,y:View左上角的坐标
translationX和translationY:左上角相对于父容器的偏移量
x=left+translationX
y=top+translationY
View在平移时top和left表示左上角原始信息,其值不会改变
x,y,translationX和translationY会发生改变
二、实现View的滑动
scrollTo/scrollBy
scrollTo和scrollBy是View自己提供的专门用于实现此功能的两个方法,以下是这两个方法的源码
java
// View.java 中的相关源码
/**
* 实现View内容的绝对滑动
* @param x 目标位置的水平滚动偏移量
* @param y 目标位置的垂直滚动偏移量
*/
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();
}
}
}
/**
* 实现View内容的相对滑动
* @param x 水平方向要滚动的距离
* @param y 垂直方向要滚动的距离
*/
public void scrollBy(int x, int y) {
// 调用scrollTo,在当前基础上累加偏移量
scrollTo(mScrollX + x, mScrollY + y);
}
由上我们发现,scrollBy实际是通过调用scrollTo实现的,scrollTo是基于所传递参数的绝对滑动,而scrollBy是基于当前位置的相对滑动。
我们重点了解一下scrollTo方法中的两个参数:
mScrollX:View左边缘和View内容左边缘在水平方向上的距离,单位为像素,可通过getScrollX方法得到
mScrollY:View上边缘和View内容上边缘在竖直方向上的距离,单位为像素,可通过getScrollY方法得到
根据上面的分析,我们可以知道,scrollTo/scrollBy只能将View的内容进行移动,不能改变View本身的位置。
在onTouchEvent中处理
1.layout方法
view进行绘制时会调用onLayout()方法设置显示的位置,因此我们可以通过改变View的left、top、right、bottom这四种属性实现移动
java
@Override
public boolean onTouchEvent(MotionEvent event) {
// 获取触摸点的x,y
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 moveX = x - lastX;
int moveY = y - lastY;
// 更改位置
layout(getLeft() + moveX, getTop() + moveY,
getRight() + moveX, getBottom() + moveY);
break;
}
return true;
}
2.offsetLeftAndRight()和offsetTopAndBottom
这两种方法分别可以实现对View左右、上下移动的操作,传入的参数是各个方向的偏移量
java
@Override
public boolean onTouchEvent(MotionEvent event) {
// 获取触摸点的x,y
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 moveX = x - lastX;
int moveY = y - lastY;
// 更改位置
offsetLeftAndRight(moveX);
offsetTopAndBottom(moveY);
break;
}
return true;
}
使用动画
通过改变View的translationX和translationY属性对View进行平移,这里使用属性动画完成,因为属性动画真正改变了View的位置属性。
属性动画主要有以下两种实现方式:
ObjectAnimator
ObjectAnimator 可以对 View 的 translationX 和 translationY 属性进行动画设置,从而实现水平方向和垂直方向的平移效果。translationX 和 translationY 是相对于 View 的初始位置的偏移量,单位是像素。
java
// 水平方向平移动画,向右平移100像素,动画时长500毫秒
ObjectAnimator animatorX = ObjectAnimator.ofFloat(view, "translationX", 0f, 100f).setDuration(500);;
animatorX.start();
// 垂直方向平移动画,向下平移100像素
ObjectAnimator animatorY = ObjectAnimator.ofFloat(view, "translationY", 0f, 100f).setDuration(500);
animatorY.start();
ValueAnimator
ValueAnimator最基本的属性动画类,通过值变化驱动动画
java
// 基本用法
ValueAnimator animator = ValueAnimator.ofFloat(0f, 360f);
animator.setDuration(1000);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (float) animation.getAnimatedValue();
// 使用计算出的值更新View属性
view.setRotation(value);
}
});
animator.start();
改变布局参数
LayoutParams 是 Android 中用于描述 View 在父容器中布局参数的类。它包含了 View 的大小、位置、边距等布局信息,我们可以通过改变View的布局参数实现滑动。
java
// 通过getLayoutParams()获取布局参数
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
//修改布局参数属性
params.leftMargin += dx;
params.topMargin += dy;
//设置新的LayoutParams
view.setLayoutParams(params);
//或view.requestLayout();请求重新布局
三、View的弹性滑动
简单的滑动实现效果比较生硬,用户的体验效果较差,因此我们要在此基础上实现渐进式滑动,以优化滑动效果。
使用Scroller
Scroller是弹性滑动对象,用于实现View的弹性滑动,即有过渡效果的滑动,需要和View的computeScroll方法配合使用。
使用方法
java
//初始化Scroller对象
Scroller scroller=new Scroller(mContext);
//调用startScroll,开始平滑滚动
private void smoothScrollTo(int destX,int destY){
int scrollX=getScrollX();
int delta=destX-scrollX;
//1000ms内滚动到指定位置,效果为缓慢滑动
mScroller.startScroll(scrollX,0,delta,0,1000);
invalidate();//触发重绘
}
//重写computeScroll()方法,绘制期间会不断调用computeScroll()实现滑动
public void computeScroll(){
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
postInvalidate();//再次触发重绘
}
}
原理:
startScroll方法本身不处理滑动,只保存我们传递的参数,真正实现弹性滑动的是invalidate()方法。
invalidate()会导致事件重绘,去调用View的computeScroll(),而computeScroll()本身为空实现,需要重写。我们在computeScroll()内部调用 scrollTo实现滑动,之后调用postInvalidate()再次重绘,又会调用computeScroll()方法,如此反复,直到滑动结束。
在computeScroll()方法中,我们还使用了computeScrollOffset(),这个方法用来判断滑动是否结束。
动画
动画实现的滑动天然具有弹性效果,我们可以通过上述的ObjectAnimator实现属性动画,通过设置滑动的时长获得想要的效果。
java
// 水平方向平移动画,向右平移100像素,动画时长500毫秒
ObjectAnimator animatorX = ObjectAnimator.ofFloat(view, "translationX", 0f, 100f).setDuration(500);;
animatorX.start();
延时策略
核心思想:通过发送一系列延时消息从而达到渐进式效果,可以使用Handler、View的postDelayed方法或线程的sleep方法实现。
java
//postDelayed方法实现
view.postDelayed(new Runnable() {
@Override
public void run() {
smoothScrollTo(100, 0);
}
}, 300);
java
//Handler实现,此种方法无法精确定时,因为系统调度需要时间
private static final int MESSAGE_SCROLL_TO = 1;
private static final int FRAME_COUNT = 30;
private static final int DELAYED_TIME = 33;
private int mCount=0;
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (msg.what == MESSAGE_SCROLL_TO) {
mCount++;
if (mCount <= FRAME_COUNT) {
// 使用弹性公式计算进度
float fraction = mCount / (float) FRAME_COUNT;
// 计算当前位置并滚动
int scrollX = (int) (fraction * 100);
mView.scrollTo(scrollX, 0);
// 继续下一帧
mHandler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO, DELAYED_TIME);
}
}
}
};