Android中View的知识体系——(2)View的滑动

吃可爱长大的小学妹 提交于 2020-02-21 11:33:40

Android中View的知识体系——(2)View的滑动

作者:黑衣侠客


一、前言

在Android设备上,滑动几乎是应用的标配,不论是下拉刷新还是SlidingMenu,它们的基础都是滑动。所谓滑动效果,归根结底它们都是由不同的滑动外加一些特效所组成的。因此,掌握滑动的方法是实现绚丽自定义控件的基础。通常通过三种方法来实现View的滑动:
  • 通过View本身提供的scrollTo/scrollBy方法来实现滑动
  • 通过动画给View施加平移效果来实现滑动
  • 通过改变View的LayoutParams使得View重新布局从而实现滑动
下面我们就来一一分析学习。

二、View的滑动

1.使用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的改变规则:

这两个属性了可以通过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为负值,反之,为正值。

在这里插入图片描述

2.使用动画

通过动画,我们能够让一个View进行平移,而平移就是一种滑动,使用动画来移动View,主要是操作View的translationX和translationY属性,既可以采用传统的View动画,也可以采用属性动画,如果采用属性动画的话,为了能够兼容3.0以下的版本,需要采用开源动画库nineoldandroids(http://nineoldandroids.com/)。
<?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的影像而已

3.改变布局参数

改变布局参数,即改变LayoutParams

比如:我们想把一个Button向右平移100px,我们只需将这个Button的LayoutParams里的marginLeft参数的值增加100px即可

	MarginLayoutParams params = (MarginLayoutparamsmButton1.getLayoutParams();
	params.width + = 100;
	params.leftMargin + = 100;
	mButton1.requestLayout();
	//或者 mButton1.setLayoutParams(params);

同时,还有一种方法:

  • 在将要移动的View前面,设置一个空的,默认宽度为0的View,若想要平均移动View,只需要设置空View的宽度即可
改变LayoutParams的方式去实现View的滑动是一种很灵活的方法,需要根据不同的情况去做不同的处理

4.各类滑动的区别

  • scrollTo/scrollBy:它是View提供的原生方法,其作用是专门用于View的滑动,它可以比较方便的实现滑动效果,并且不影响内部元素的单击事件。但是他的缺点在于:它只能滑动View的内容,并不能滑动View本身。
  • 动画:在Android3.0以上并采用属性动画,那么采用这种方式没有明显的缺点;如果是使用View动画或者在Android3.0以下使用属性动画,均不能改变View本身的属性。在实际使用中,如果动画元素不需要响应用户的交互,那么使用动画来做滑动是比较合适的,反之,就不大合适。但是动画有一个明显的优点,那就是,一些复杂的效果必须要通过动画才能实现
  • 改变布局:除了操作稍微复杂,也没有什么明显的缺点。它主要适用的对象是一些具有交互性的View,因为这些View需要和用户交互,直接通过动画实现会有很大的问题,这时,我们可以使用直接改变布局参数的方式去实现
总结:
  • scrollTo/scrollBy:操作简单,适合对View内容的滑动;
  • 动画:操作简单,主要适用于没有交互的View和实现复杂的动画效果;
  • 改变布局参数:操作稍微复杂,适用于有交互的View;6

三、弹性滑动

所有滑动都有一个共同的思想:

将一次大的滑动分成若干次小的滑动并在一个时间段内完成,弹性滑动的具体实现方法有很多,比如通过Scroller、Handler#postDelayed以及Thread#sleep等。

1.使用Scroller

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的典型使用方法

startScroll方法:
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;
}
startX和startY表示的是滑动的起点,dx和dy表示的是要滑动的距离,而duration表示的是滑动的时间,即整个滑动过程完成所需要的时间,注意:这里的滑动指的是View内容的滑动,而非是View本身位置的改变
但是,仅仅调用startScroll方法是无法让View滑动的,因为它内部并没有做滑动相关的事。
接下来我们说一下Scroller究竟是如何让View滑动的:

原因在于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没有丝毫的引用,甚至在它内部连计时器都没有。

2.通过动画

动画本身就是一种渐进的过程,因此通过它来实现的滑动,天然就具有弹性效果,例如:
//让一个View的内容在100ms内向左移动100像素
ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();
拿scrollTo来说,我们也想模仿Scroller来实现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.getAnimatedFraction();
		mButton1.scrollTo(startX + (int) (deltaX * fraction),0);
	}
});
animator.start();
我们的动画本质上没有作用于任何对象上,它只是在1000ms内完成了整个动画过程。

利用这个特性,我们就可以在动画的每一帧到来时获取动画完成的比例,然后再根据这个比例计算出当前View所要滑动的距离。注意,这里的滑动针对的是View的内容而非是View本身。可以发现,这个方法的思想其实和Scroller比较类似,都是通过改变一个百分比配合scrollTo方法来完成View的滑动。

  • 需要说明的是:采用这种方法除了能够完成弹性滑动以外,还可以实现其他动画效果,我们完全可以在onAnimationUpdate方法中加上我们想要的其他操作。

3.延时策略

使用延时策略实现弹性滑动

核心思想:

通过发送一系列延时消息从而达到一种渐进式的效果,具体说可以使用Handler或View的postDelayed方法,也可以使用线程的sleep方法。对于postDelayed方法来说,我们可以通过它来延时发送一个消息,然后在消息中来进行View的滑动,如果接连不断地发送这种延时消息,那么就可以实现弹性滑动的效果,对于sleep方法来说,通过在while循环中不断地滑动View和sleep,就可以实现弹性滑动的效果。

下面采用Handler做个示例:

在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;
        }
    };
};

感谢收看

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!