View的滑动
参考 Android 8.0 源码
View的滑动有三种:
1 . 通过View本身提供的scrollTo()/scrollBy()方法来实现滑动;
2 . 通过动画给View施加平移效果来实现滑动;
3 . 通过改变View的LayoutParams使得View重新布局从而实现滑动;
使用scrollTo()/scrollBy()
scrollBy()实际上调用的是scrollTo()方法,scrollBy()方法实现的是基于当前位置的相对滑动,而scrollTo()实现的是基于传递参数的绝对滑动。
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + 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内部的两个属性mScrollX和mScrollY来进行滑动的。这两个属性,我们可以通过getScrollX()和getScrollY()方法来获取的。在滑动过程中,mScrollX的值总是等于View左边缘和View内容左边缘在水平方向的距离,而mScrollY的值总是等于View上边缘和View内容上边缘再竖直方向的距离。View边缘是指View的位置,由四个顶点组成,而View内容边缘是指View中的内容的边缘。scrollTo()和scrollBy()只能改变View内容的位置而不能改变View在布局中的位置。mScrollX和mScrollY的单位为像素,并且当View左边缘在View内容左边缘的右边时,mScrollX为正值,反之为负值。当View上边缘在View内容上边缘的下边时,mScrollY为正值,反之为负值。换句话说,如果从左向右滑动,那么mScrollX为负值,反之为正值;如果从上往下滑动,那么mScrollY为负值,反之为正值。
使用动画
通过动画我们能够让一个View进行平移,而平移就是一种滑动。使用动画来移动View,主要是操作View的两个属性translationX和translationY属性,既可以采用传统的View动画,也可以采用属性动画。
使用传统View动画时,View动画是对View的影像做操作,并不能真正改变View的位置参数,包括宽、高。并且如果希望动画后的状态得以保留,还必须将fillAfter属性设置为true,否则动画完成后其动画结果会消失。但是注意,在Android 3.0 以下,无法使用属性动画,可以使用一个动画兼容库nineoldandroids来实现属性动画。尽管如此,在Android 3.0 以下的手机上通过nineoldandroids来实现的属性动画本质上仍然是View动画。比如,我们使用传统的View动画使View滑动以后,即使我们设置View停留在动画结束的位置,但是View的实际位置仍然是原位置,我们无法在View的动画结束的位置触发单击等事件。
从Android 3.0 开始,使用属性动画可以解决View滑动后实际位置依旧是原位置而无法在新位置触发点击等事件的问题。
改变布局参数
改变布局参数,其实就是改变LayoutParams。比如,我们可以设置LayoutParams的marginLeft参数的值增加100px,就可以让View向右平移100px。
MarginLayoutParams params = (MarginLayoutParams)mButton.getLayoutParams();
params.width += 100;
params.leftMargin += 100;
mButton.requestLayout();
//使用下面这一行也可以;
mButton.setLayoutPatams(params);
三种滑动方式的比较
1 . scrollTo()和scrollBy()进行滑动:这两个是View提供的原生方法,其作用是专门用于View的滑动,使用这两个方法可以比较方便地实现滑动效果并且不影响内部元素的单击事件。缺点是:只能滑动View的内容,并不能滑动View本身。
操作简单,适合对View内容的滑动;
2 . 使用动画进行滑动:如果是在Android 3.0 以上并采用属性动画,那么采用动画滑动没有什么缺点。如果是使用View动画或者在Android 3.0 以下使用属性动画,均不能改变View本身的属性。在实际使用中,如果动画元素不需要响应用户的交互,那么使用动画来进行滑动是比较合适的。否则,如果需要进行交互,比如点击事件,那么采用动画来进行滑动就会有上面所说的实际位置与动画结束后位置不一致的问题。
操作简单,主要适用于没有交互的View和实现复杂的动画效果;
3 . 使用改变布局进行滑动:这种方法除了代码写起来麻烦些,没有其他明显的缺点。使用于一些具有交互性的View。
操作稍微复杂,适用于有交互的View;
弹性滑动
使用上面三种方式,滑动过程是比较生硬的。因此,弹性滑动就会有比较好的用户体验了。弹性滑动的思路就是:将一次大的滑动分成若干次小的滑动,并在一个时间段内完成。弹性滑动的实现方式有很多,比如通过Scroller、使用Handler机制的postDelayed()方法、使用Thread的sleep()方法等。
使用Scroller
Scroller的使用方法如下:
Scroller scroller = new Scroller(context);
//缓慢滚动到指定位置;
private void smoothScrollTo(int destX,int destY){
int scrollX = getScrollX();
int scrollY = getScrollY();
//1000ms内慢慢滑向destX位置;
scroller.startScroll(scrollX,0,delta,0,1000);
invalidate();
}
@Override
public void computeScroll(){
if(scroller.computeScrollOffset()){
scrollTo(scroller.getCurrX(),scroller.getCurrY());
postInvalidate();
}
}
Scroller的工作原理:构造一个Scroller对象并且调用它的startScroll()方法,Scroller内部其实什么也没做,只是保存了我们传递的几个参数,这几个参数从startScroll()方法中可以得知。
public void startScroll(int startX, int startY, int dx, int dy) {
startScroll(startX, startY, dx, dy, DEFAULT_DURATION);
}
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;
mDurationReciprocal = 1.0f / (float) mDuration;
}
上面方法中参数中,startX和startY表示的是滑动的起点,dx和dy表示的是要滑动的距离,duration表示的是滑动时间,也就是整个滑动过程完成所需要的时间,注意这里的滑动是指View内容的滑动而非View本身位置的改变。仅仅调用startScroll()方法是无法让View滑动的。从最上面的代码中,我们在使用了startScroll()方法以后,调用了invalidate()方法。invalidate()方法会导致View重绘,在View的draw()方法中又会去调用computeScroll()方法,computeScroll()方法在View中是一个空实现,因此我们需要去重写这个方法。因此,View实现了弹性滑动。
当View重绘后会在draw()方法中调用computeScroll()方法,而computeScroll()方法又会向Scroller获取当前的scrollX和scrollY;然后通过scrollTo()方法实现滑动;接着又调用postInvalidate()方法来进行第二次重绘,这一次重绘的过程和第一次重绘一样,还是会导致computeScroll()方法被调用;然后继续向Scroller获取当前的scrollX和scrollY,并通过scrollTo()方法滑动到新的位置,如此反复,直到整个滑动过程结束。
上面还使用computeScrollOffset()方法,源码如下:
public boolean computeScrollOffset() {
if (mFinished) {
return false;
}
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
if (timePassed < mDuration) {
switch (mMode) {
case SCROLL_MODE:
final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(x * mDeltaY);
break;
case FLING_MODE:
final float t = (float) timePassed / mDuration;
final int index = (int) (NB_SAMPLES * t);
float distanceCoef = 1.f;
float velocityCoef = 0.f;
if (index < NB_SAMPLES) {
final float t_inf = (float) index / NB_SAMPLES;
final float t_sup = (float) (index + 1) / NB_SAMPLES;
final float d_inf = SPLINE_POSITION[index];
final float d_sup = SPLINE_POSITION[index + 1];
velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
distanceCoef = d_inf + (t - t_inf) * velocityCoef;
}
mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;
mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
mCurrX = Math.min(mCurrX, mMaxX);
mCurrX = Math.max(mCurrX, mMinX);
mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
mCurrY = Math.min(mCurrY, mMaxY);
mCurrY = Math.max(mCurrY, mMinY);
if (mCurrX == mFinalX && mCurrY == mFinalY) {
mFinished = true;
}
break;
}
}else {
mCurrX = mFinalX;
mCurrY = mFinalY;
mFinished = true;
}
return true;
}
在这个方法中,会根据时间的流逝来计算出当前的scrollX和scrollY的值,这个过程类似于动画中的插值器。这个方法返回ture,表示滑动还未结束;返回false则表示滑动已经结束。因此,当这个方法返回ture时,会继续进行View的滑动。
Scroller的工作原理如下:Scroller本身并不能实现View的滑动,它需要配合View的computeScroll()方法才能完成弹性滑动的效果,它不断地让View重绘,而每一次重绘距滑动起始时间会有一个时间间隔,通过这个时间间隔,Scroller就可以得出View当前的滑动位置,知道了滑动位置就可以通过scrollTo()方法来完成View的滑动。就这样,View的每一次重绘都会导致View进行小幅度的滑动,而多次小幅度滑动就组成了弹性滑动。
通过动画
动画本身就是一种渐进的过程,因此通过它来实现的滑动自然就具有弹性效果。
final int startX = 0;
final int deltaX = 100;
ValueAnimator animator = ValueAnimator.ofInt(0,1).setDuration(1000);
animator.addUpdateListener(new AnimatorUpdateListener(){
@override
public void onAnimationUpdate(ValueAnimator animator){
float fraction = animator.getAnimatorFraction();
mButton.scrollTo(startX + (int)(deltaX * fraction),0);
}
});
animator.start();
在上面的代码中,我们可以在动画的每一帧到来时获取动画完成的比例,然后再根据这个比例计算出当前View所要滑动的距离。这里的滑动针对的是View的内容而非本身。我们还可以在onAnimationUpdate()方法中加上我们需要的业务逻辑。
使用延时策略
延迟策略的思路:通过发送一系列延时消息从而达到一种渐近式的效果,具体来说可以使用Handler或View的postDelayed()方法,也可以使用线程的sleep()方法。
对于postDelayed()方法,我们可以通过它来延时发送一个消息,然后在消息中来进行View的滑动,如果接连不断地发送这种延时消息,那么就实现了弹性滑动的效果;
对于sleep()方法,通过在while循环中不断地滑动View和执行sleep()方法,就可以实现弹性滑动的效果。
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-----------------last line for now------------------