View的滑动主要有3种:
(1)调用scrollTo/scrollBy方法: 滑动的是View内容,操作比较简单。
(2)动画: 滑动的View位置,其实改变的是View的translationX或者translationY。(参考我的博文 Android View - 位置参数)
(3)改变布局参数: 通过设置LayoutParams,使View改变位置,只要加上延时,不断得改变位置参数,就可以达到滑动效果。
scrollTo/scrollBy滑动
为了View的滑动,Android在View内实现了2个方法:scrollTo和scrollBy。
代码如下:
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);
}
其实scrollBy方法调用了scrollTo方法。scrollTo改变了mScrollX和mScrollY,然后重绘,调用onScrollChanged通知View(子类可以重写,监听滑动)。
我们说过,调用scrollTo移动的View的内容,比如我们调用某个View的scrollTo(100, 100),那么View的内容发生如下变化:
你会发现内容向x轴和y轴负方向都移动了100,为什么是负方向呢?因为调用scrollTo移动的是View的内容,你可以看做是View边界移动了100,所以View内容相对于View就移动了-100了。
其实调用scrollTo方法会瞬间完成View的内容移动,这个用户体验显然是不好的。Android还提供了一个Scroller类,配合View的滑动,实现在规定时间内,移动指定距离。
我们从源码分析Scroller类。先从构造函数看起:
public Scroller(Context context) {
this(context, null);
}
public Scroller(Context context, Interpolator interpolator) {
mFinished = true;
mInterpolator = interpolator;
float ppi = context.getResources().getDisplayMetrics().density * 160.0f;
mDeceleration = SensorManager.GRAVITY_EARTH // g (m/s^2)
* 39.37f // inch/meter
* ppi // pixels per inch
* ViewConfiguration.getScrollFriction();
}
我们使用Scroller时,可以定义插值器。插值器可以指定View滑动的变化速度,可以先快后慢,也可以先慢后快,也可以匀速。
还要介绍Scroller类的2个方法:
startScroll
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
mMode = SCROLL_MODE;
// 设置没有完成滑动
mFinished = false;
// 滑动的时间
mDuration = duration;
// 滑动的开始时间
mStartTime = AnimationUtils.currentAnimationTimeMillis();
// 开始滑动的坐标
mStartX = startX;
mStartY = startY;
// 结束滑动的坐标
mFinalX = startX + dx;
mFinalY = startY + dy;
// 滑动的距离
mDeltaX = dx;
mDeltaY = dy;
// 计算指定移动距离应该分几次完成,如20秒内完成,则每次移动为1/20
mDurationReciprocal = 1.0f / (float) mDuration;
// This controls the viscous fluid effect (how much of it)
mViscousFluidScale = 8.0f;
// must be set to 1.0 (used in viscousFluid())
mViscousFluidNormalize = 1.0f;
mViscousFluidNormalize = 1.0f / viscousFluid(1.0f);
}
可以看出,startScroll方法只是设置了滑动的基本信息,并没有滑动呢。所以如果我们只是调用startScroll,View的滑动并不会开始哦。
computeScrollOffset
public boolean computeScrollOffset() {
// 如果结束了,就返回false
if (mFinished) {
return false;
}
// 计算已经滑动的时间
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
// 如果已经移动的时间小于指定移动的时间
if (timePassed < mDuration) {
switch (mMode) {
case SCROLL_MODE:
// 计算当前时间应该滑动的距离
float x = (float)timePassed * mDurationReciprocal;
if (mInterpolator == null)
x = viscousFluid(x);
else
x = mInterpolator.getInterpolation(x);
// 如x=10/20,那么10秒钟应该移动 mDeltaX * 10/20 的距离
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(x * mDeltaY);
break;
case FLING_MODE:
float timePassedSeconds = timePassed / 1000.0f;
float distance = (mVelocity * timePassedSeconds)
- (mDeceleration * timePassedSeconds * timePassedSeconds / 2.0f);
mCurrX = mStartX + Math.round(distance * mCoeffX);
// Pin to mMinX <= mCurrX <= mMaxX
mCurrX = Math.min(mCurrX, mMaxX);
mCurrX = Math.max(mCurrX, mMinX);
mCurrY = mStartY + Math.round(distance * mCoeffY);
// Pin to mMinY <= mCurrY <= mMaxY
mCurrY = Math.min(mCurrY, mMaxY);
mCurrY = Math.max(mCurrY, mMinY);
break;
}
}
// 如果到时间了,就设置当前位置为滑动最终位置,并设置结束标志
else {
mCurrX = mFinalX;
mCurrY = mFinalY;
mFinished = true;
}
return true;
}
computeScrollOffset方法主要设置当前时间的位置,供View使用。
接下来我们看看View怎么使用Scroller,从View的绘制draw方法开始,只给出重要代码:
public void draw(Canvas canvas) {
...
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
if (!dirtyOpaque) onDraw(canvas);
dispatchDraw(canvas);
onDrawScrollBars(canvas);
return;
}
...
}
在draw方法会调用dispatchDraw方法,dispatchDraw会传入子View调用drawChild方法:
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
...
if (!concatMatrix && canvas.quickReject(cl, ct, cr, cb, Canvas.EdgeType.BW) &&
(child.mPrivateFlags & DRAW_ANIMATION) == 0) {
return more;
}
child.computeScroll();
final int sx = child.mScrollX;
final int sy = child.mScrollY;
boolean scalingRequired = false;
Bitmap cache = null;
...
}
此时每个View的computeScroll会被调用:
public void computeScroll() {
}
而computeScroll却是一个空方法,供子类重写。
所以我们可以在这个方法调用Scroller的computeScrollOffset方法,让Scroller帮我们计算当前滑动的位置,并判断滑动是否完成,然后我们获取Scroller帮我们计算的位置,调用View的scrollTo方法滑动View一部分。
整个流程下来,是这样的:
代码实现:
import android.content.Context;
import android.util.AttributeSet;
import android.widget.FrameLayout;
import android.widget.Scroller;
public class MyViewGroup extends FrameLayout {
private Scroller scroller;
public MyViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
scroller = new Scroller(context);
// 延时看的清楚一点
postDelayed(new Runnable() {
@Override
public void run() {
scroller.startScroll(0, 0, 100, 100, 2000);
invalidate();
}
}, 1000);
}
public MyViewGroup(Context context) {
super(context);
}
@Override
public void computeScroll() {
if (scroller.computeScrollOffset()) {
scrollTo(scroller.getCurrX(), scroller.getCurrY());
invalidate();
}
}
}
例子实现的效果果然是蓝色块向上和向左移动了100。
动画滑动
采用动画也可以实现View的滑动,主要操作translationX和translationY属性。简单介绍一下动画。
动画分两种:传统动画和属性动画。
传统动画分为帧动画(Frame Animation)和补间动画(Tweened Animation)。
帧动画是最容易实现的一种动画,这种动画更多的依赖于完善的UI资源,他的原理就是将一张张单独的图片连贯的进行播放,从而在视觉上产生一种动画的效果;有点类似于某些软件制作gif动画的方式。
补间动画又可以分为四种形式,分别是 alpha(淡入淡出),translate(位移),scale(缩放大小),rotate(旋转)。
补间动画的实现,一般会采用xml 文件的形式;代码会更容易书写和阅读,同时也更容易复用。
使用传统补间动画做View的滑动,滑动的是View的影像,并非真正改变View的位置。滑动结束瞬间,View便回到初始状态。如果设置了fillAfter属性为true,View会保留到动画后的状态,但是位置依然没有改变。如果你给这个View设置点击事件,此时点击这个View,会发现这个View不会响应点击事件,因为点击事件的范围还保留在原始状态的位置范围。所以给View设置了事件的,不适合使用传统动画。
属性动画是Android3.0之后才有的,不过有兼容包(nineoldandroids)。属性动画容易操作,代码简单,不会出现传统动画上面的负面影响,所以属性动画肯定是不二之选。
改变布局滑动
改变布局滑动,其实就是设置View的LayoutParams,设置View的padding或者margin来改变View的位置,达到View滑动的效果。和scrollTo/scrollBy不同的是,改变布局,其实是改变View的位置,而scrollTo/scrollBy改变的是View的内容,不过也是瞬间完成,体验效果不过,通常配合动画使用或者利用延时机制使用。
滑动冲突
分2中情况分析滑动冲突
(1)外部滑动和内部滑动不一致。
(2)外部滑动和内部滑动一致。
如果不懂View事件分发和拦截的,请参考Android View - 事件分发,拦截,处理机制,因为处理滑动冲突,需要理解View事件分发和拦截。
滑动不一致
这种情况比较容易解决,只要外部重写onInterceptTouchEvent拦截事件时,判断滑动方向,如果符合自己的滑动方向的,就拦截下来。如果不符合,则不拦截,交给内部View处理滑动。
滑动一致
这种情况稍微复杂,因为滑动方向一样。解决这种冲突的方案,一般是外部不拦截,先把事件交给内部,内部判断符合自己的条件,重写onTouchEvent处理事件,并返回true,事件到此结束。如果不符合自己的条件,重写onTouchEvent不处理事件,并返回false,交给外部处理。
处理滑动冲突还是比较复杂的,需要我们不断积累经验,才能处理好,慢慢体会吧。