Android View 滑动
View 滑动
有三种方式可以实现View的滑动:
- 通过View本身提供的scrollTo/scrollBy方法来实现滑动
- 通过动画给View施加平移效果来实现滑动
- 通过改变Viev的LayoutParams使得View重新布局从而实现滑动
使用scrollTo/scrollBy
View提供了scrollTo/scrollBy来实现滑动,实现如下:
/**
* Set the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int, int, int, int)} and the view will be
* invalidated.
* @param x the x position to scroll to
* @param y the y position to scroll to
*/
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();
}
}
}
/**
* Move the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int, int, int, int)} and the view will be
* invalidated.
* @param x the amount of pixels to scroll by horizontally
* @param y the amount of pixels to scroll by vertically
*/
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
从上面的源码可以看出,scrollTo实现了基于所传递参数的绝对滑动,scrollBy实现了基于当前位置的相对滑动,实际上也是调用了scrollTo方法,这两个方法中,mScrollX和mScrollY这两个属性非常重要,这两个属性可以通过getScrollX和getScrollY方法分别得到。
关于这两个属性简单概述就是:mScrollX、mScrollY分时是view里的内容相对view位置的滑动距离距离,但比较混淆的一点是mScrollX与mScrollY取值的正负与滑动方向是相反的,如下图所示:
使用动画
滑动的另一种方式,主要是操作View的translationX,translationY属性,即可以采用传统的View动画,也可以采用属性动画,如果用属性动画的话,为了兼容3.0以下的版本需要使用开源库nineoldandroids
实现在100ms里让一个View从初始的位置向右下角移动100个像素,如下:
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(testButton,"translationX",0,100).setDuration(100).start();
View动画是对View的影像做操作,它并不能真正改变View的位置参数,包括高宽,并且如果希望动画后的状态得以保存还必须将fillAfter属性设置为true,否则动画完成之后就会消失,比如我们要把View向右移动100个像素,如果fillAfter为false,那么动画完成的一刹那,View就会恢复之前的状态,fillAfter为true的话就会停留在最终点,属性动画不会有这样的问题。
改变布局参数
通过改变布局参数,即改变LayoutParams来实现滑动,比如把一个Button向右平移100px,只需要将这个Button的LayoutParams里的marginLeft参数的值增加100px即可,或者在Button左边放置一个默认宽度为0的view,当需要向右移动Button时,只需要重新设置空View的宽度即可,就自动被挤向右边,即实现了向右平移的效果。设置View的LayoutParams的方式如下:
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) button.getLayoutParams();
params.width +=100;
params.leftMargin +=100;
button.requestLayout();
//或者button.setLayoutParams(params);
三种滑动方式对比
- scrollTo/scrollBy:操作简单,只能滑动View内容,不能滑动View本身
- 动画:操作简单,能实现复杂的动画效果
- 改变布局参数:操作稍微复杂,适用于有交互的View
下面通过动画来实现view跟手滑动的效果:
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getRawX();
int y = (int) event.getRawY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:{
int deltaX = x - mLastX;
int deltaY = y - mLastY;
int translationX = ViewHelper.getTranslationX(this) + deltaX;
int translationY = ViewHelper.getTranslationY(this) + deltaY;
ViewHelper.setTranslationX(this,translationX);
ViewHelper.setTranslationY(this,translationY);
break;
}
case MotionEvent.ACTION_UP:
break;
default:
break;
}
mLastX = x;
mLastY = y;
return true;
}
重写onTouchEvent方法并且处理ACTION_MOVE事件,根据两次滑动之间的距离就可以实现它的滑动,首先通过getRawX和getRawY方法来获取手指当前的坐标,因为需要获取当前点击事件在屏幕中的坐标而不是相对于View本身的坐标,其次,要得到两次滑动之间的位移,有了这个位移就可以移动当前的View,移动方法采用的是动画兼容库nineoldandroids中的ViewHelper类所提供的setTranslationX和setTranslationy,没有3.0以上版本才能使用的限制,与此类似的还有setX、setScaleX、setAlpha等方法。
弹性滑动
弹性滑动就是将一次大的滑动分成若干个小的滑动,并在一个时间段完成,实现方式很多,比如Scroller,Handler#postDelayed以及Thread#Sleep等。
使用Scroller
Scroller的使用方法:
Scroller mScroller = new Scroller(getContext());
private void smoothScrollTo(int destX,int destY){
int scrollX = 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对象并且调用它的startScroll方法,scroller内部其实并没有做什么,他只是保存了我们传递的参数,这几个参数从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;
mDurationReciprocal = 1.0f / (float) mDuration;
}
可以看到startScroll内部并没有做滑动相关的事,真正让View弹性滑动的是startScroll下面的invalidate方法,该方法会导致View重绘,当View重绘后会在draw方法中调用computeScroll,而computeScroll又会去向Scroller获取当前的scrollX和ScrollY,然后通过scrollTo方法实现滑动;接着又调用postInvalidate方法来进行第二次重绘,如此反复,直到整个滑动过程结束。
再来看下Scroller的computeScrollOffset方法的实现:
/**
* Call this when you want to know the new location. If it returns true,
* the animation is not yet finished.
*/
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的值,方法返回false表示结束,返回true表示滑动还未结束,就需要继续滑动View
最后概括一下Scroller工作原理,Scroller本身并不滑动,需要配合computeScroll方法才能完成弹性滑动的效果,通过不断的让View重绘,而每次都有一些时间间隔,通过这个时间间隔就能得到他的滑动位置,这样就可以用ScrollTo方法来完成View的滑动了,就这样,View的每一次重绘都会导致View进行小幅度的滑动,而多次的小幅度滑动形成了弹性滑动。
通过动画
动画本身就是一种渐进的过程,因此通过他来实现滑动天然就具有弹性效果,比如以下代码让一个view在100ms内左移100像素。
ObjectAnimator.ofFloat(testView, "translationX", 0, 100).setDuration(100).start();
不过这里想说的并不是这个问题,我们可用利用动画的特性来实现一些动画不能实现的效果,还拿scorllTo来说,我们想模仿scroller来实现View的弹性滑动,那么利用动画的特性我们可用这样做:
final int startX = 0;
final int deltaX = 100;
final ValueAnimator animator = ValueAnimator.ofInt(0,1).setDuration(1000);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animator) {
float fraction = animator.getAnimatedFraction();
button.scrollTo(startX + (int)(deltaX * fraction),0);
}
});
animator.start();
在动画的每一帧到来时获取动画完成的比例,然后再根据这个比例计算出当前View所要滑动的距离,最后通过scrolITo方法来完成View的滑动。
使用延时策略
通过发送一系列延时消息从而达到一种渐近式的效果,具体来说可以使用Handler或View的postDelayed方法,也可以使用线程的sleep方法。对于postDelayed方法来说,我们可以通过它来延时发送一个消息,然后在消息中来进行View的滑动,如果接连不断地发送这种延时消息,那么就可以实现弹性滑动的效果。对于sleep方法来说,通过在while循环中不断的滑动View和sleep,就可以实现弹性滑动的效果。
下面使用Handler来做个示例,在大约1000ms内将View的内容向左移动100像素:
private static final int MESSAGE_SCROLL_TO = 1;
private static final int FRAME_COUNT = 30;
private static final int DELAYED_TIME = 33;
private int count = 1;
private Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
switch (msg.what){
case MESSAGE_SCROLL_TO: {
count++;
if(count <= FRAME_COUNT) {
float fraction = count / (float)FRAME_COUNT;
int scrollX = (int)(fraction * 100);
button.scrollTo(scrollX,0);
handler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO,DELAYED_TIME);
}
break;
}
default:
break;
}
}
};