作者:黑衣侠客
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);
}
这两个属性了可以通过getScrollX和getScrollY方法分别得到,在滑动过程中,mScrollX的值总是等于View左边缘和View内容左边缘在水平方向的距离,而mScrollY的值总是等于View上边缘和View内容上边缘在竖直方向的距离
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:fillAfter="true"
android:zAdjustment="normal">
<translate
android:duration="100"
android:fromXDelta="0"
android:fromYDelta="0"
android:interpolator="@android:anim/linear_interpolator"
android:toXDelta="100"
android:toYDelta="100" />
</set>
属性动画:
ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();
作用:将一个View在100ms内从原始位置向右平移100像素
关于动画的详细介绍请参考:
View动画是对View的影像做操作,它并不能真正改变View的位置参数,包括宽/高,并且如果希望动画后的状态保留下来,还必须将fillAfter的属性设置为true,否则,动画效果会消失
比如:
- fillAfter为false,那么在动画完成的一刹那,View会瞬间恢复到动画前的状态
- fillAfter为true,在动画完成后,View会停留在距原始位置100像素的右边
使用属性动画并不会出现上述问题,但是在Android3.0以下无法使用属性动画,这时我们可以使用动画兼容库nineoldandroids来实现属性动画,尽管如此,Android3.0以下的手机上通过nineoldandroids来实现的属性动画本质上仍是View动画。
假如我们通过View动画将一个Button向右移动100px,并且这个View设置有点击事件,然后你会发现,点击新位置无法触发onClick事件,而点击原位置仍然可以触发onClick事件,尽管Button已经不再原位置了。在系统眼里,这个Button没有发生任何变化,而新位置只是Button的影像而已
比如:我们想把一个Button向右平移100px,我们只需将这个Button的LayoutParams里的marginLeft参数的值增加100px即可
MarginLayoutParams params = (MarginLayoutparamsmButton1.getLayoutParams();
params.width + = 100;
params.leftMargin + = 100;
mButton1.requestLayout();
//或者 mButton1.setLayoutParams(params);
同时,还有一种方法:
- scrollTo/scrollBy:操作简单,适合对View内容的滑动;
- 动画:操作简单,主要适用于没有交互的View和实现复杂的动画效果;
- 改变布局参数:操作稍微复杂,适用于有交互的View;6
将一次大的滑动分成若干次小的滑动并在一个时间段内完成,弹性滑动的具体实现方法有很多,比如通过Scroller、Handler#postDelayed以及Thread#sleep等。
Scroller源码:
Scroller scroller = new Scroller(mContext);
//缓慢滚动到指定位置
private void smoothScrollTo(int destX,int destY){
int scrollerX = getScrollX();
int deltaX = destX - scrollX;
//1000ms内滑向destX,效果就是慢慢滑动
mScroller.startScroll(scrollX,0,deltaX,0,1000);
invalidate();
}
@Override
public void computeScroll(){
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
postInvalidate();
}
}
上面是Scroller的典型使用方法
public void startScroller(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;
}
原因在于startScroll方法下的invalidate方法。
invalidate方法会导致View重绘,在View的draw方法中又会去调用computeScroll方法,computeScroll方法在View中是一个空实现,因此需要我们自己去实现,上面的代码已经实现了computeScroll方法。也正是这个computeScroll方法,View才能实现弹性滑动。
当View重绘后,会在draw方法中调用computeScroll,而computeScroll又会去向Scroller获取当前的scrollX和scrollY;然后通过scrollTo方法实现滑动;接着又调用postInvalidate方法被调用;然后继续向Scroller获取当前的scrollX和scrollY,并通过scrollTo方法滑动到新位置,如此反复,直到整个滑动过程结束。
Scroller的computeScrollOffset方法:
public boolean computeScrollOffset(){
...
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;
...
}
}
return true;
}
这个方法会根据时间的流逝来计算出当前的scrollX和 scrollY的值。
根据时间流逝的百分比来算出scrollX和scrollY改变的百分比并计算出当前的值,这个过程类似于动画中的插值器的概念。方法的返回值也很重要,它返回true时,表示滑动还未结束,返回false则表示滑动已经结束,因此,当方法返回true时,我们还要继续进行View的滑动。
Scroller本身并不能实现View的滑动,它需要配合View的computeScroll方法才能完成弹性滑动的效果,它不断地让View重绘,而每一次重绘距滑动起始时间有一个时间间隔,通过这个时间间隔Scroller就可以得出View当前的滑动位置,知道了滑动位置就可以通过scrollTo方法来完成View的滑动,就这样,View的每一次重绘都会导致View进行小幅度的滑动,而多次的小幅度滑动就组成了弹性滑动,这就是Scroller的工作机制。
- 由此可见,Scroller的设计思想是多么值得称赞,整个过程中它对View没有丝毫的引用,甚至在它内部连计时器都没有。
//让一个View的内容在100ms内向左移动100像素
ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();
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.getAnimatedFraction();
mButton1.scrollTo(startX + (int) (deltaX * fraction),0);
}
});
animator.start();
利用这个特性,我们就可以在动画的每一帧到来时获取动画完成的比例,然后再根据这个比例计算出当前View所要滑动的距离。注意,这里的滑动针对的是View的内容而非是View本身。可以发现,这个方法的思想其实和Scroller比较类似,都是通过改变一个百分比配合scrollTo方法来完成View的滑动。
- 需要说明的是:采用这种方法除了能够完成弹性滑动以外,还可以实现其他动画效果,我们完全可以在onAnimationUpdate方法中加上我们想要的其他操作。
通过发送一系列延时消息从而达到一种渐进式的效果,具体说可以使用Handler或View的postDelayed方法,也可以使用线程的sleep方法。对于postDelayed方法来说,我们可以通过它来延时发送一个消息,然后在消息中来进行View的滑动,如果接连不断地发送这种延时消息,那么就可以实现弹性滑动的效果,对于sleep方法来说,通过在while循环中不断地滑动View和sleep,就可以实现弹性滑动的效果。
在1000ms内将View的内容向左移动了100个像素
之所以说1000ms内,是因为采用这种方式无法精确地定时,原因是系统的消息调度也是需要时间的,并且所需时间不定。
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;
@SuppressLint("HandlerLeak")
private Handler mHandler = new Handler() {
public void handleMessage(Message msg){
switch(msg.what){
case MESSAGE_SCROLL_TO: {
mCount++;
if(mCount <= FRAME_COUNT){
float fraction = mCount / (float) FRAME_COUNT;
int scrollX = (int) (fraction * 100);
mButton1.scrollTo(scrollX,0);
mHandler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO,DELAYED_TIME);
}
break;
}
default:
break;
}
};
};
感谢收看