用Scroller实现简单viewpager滑动

用Scroller实现简单viewpager滑动

看了guolin大神的一篇博客,介绍的很详细,不适合小白。
viewpager可以左右滑动,如何做的呢,viepager的实现代码太多了3千多行,不做深究了。我们实现简单的滑动即可。说到滑动大家一定会想到scrollTo(x,y)和scrollBy(x,y)。现在来看一下他们的起源,从View控件中可以找到。

public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {


 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();
            }
        }
    }

}

       代码可以看到这两个函数已经实现,不是抽象方法,而且我查了ViewGroup,LinearLayout,TextView里面都没有这两个方法的复写,只在TextView中使用过,所以可以这样认为,scrollTo(x,y)和scrollBy(x,y)在View类中就是最后的实现。所以查看它们到View中看就OK了。另一方面说明了,其他所有继承View控件都存在这两个方法,并且控件内容都可以移动。

       说了这么多废话,现在进入正题,scrollTo(x,y)和scrollBy(x,y)有什么区别呢。从scrollBy(x,y)的实现上可以看到,scrollBy(x,y)其实内部调用的就是scrollTo(x,y),唯一的区别就是在原有移动距离上加上新的移动距离。假设现在x轴已经移动了sx,y轴移动sy,如果在次调用scrollBy(x,y),在x轴上的移动距离变成x+sx,y轴上的一定距离y+sy。如果是scrollTo(x,y)无论调用多少次,只会在第一次调用时移动,除非改变x,y值。

       说道现在,大家可能已经明白了,viewpager的滑动与scrollTo(x,y)和scrollBy(x,y)有关。是的,就是他们实现了viewpager的滑动。下面是我写的滑动容器:

public class ScollerContainer extends ViewGroup {

    private Scroller scroller;
    private float XDown;
    private float XMove;
    private float XLastMove;
    private int leftBorder;
    private int rightBorder;
    private int touchSlop;


    public ScollerContainer(Context context) {
        super(context);
        init();
    }

    public ScollerContainer(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public ScollerContainer(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init(){
        scroller = new Scroller(getContext());
        ViewConfiguration viewConfiguration = ViewConfiguration.get(getContext());
        touchSlop = viewConfiguration.getScaledTouchSlop();
    }


    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {

        float diff = 0;
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                XDown = event.getRawX();
                XLastMove = XDown;
                break;
            case MotionEvent.ACTION_MOVE:
                XMove = event.getRawX();
                diff = Math.abs(XMove-XDown);
                XLastMove = XMove;
                if (diff>touchSlop){
                    return true;
                }
                break;
        }

        return super.onInterceptHoverEvent(event);
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {

        float scrollerX ;
        float diff;
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                scrollerX = getScrollX();
                XMove = event.getRawX();
                diff = XLastMove-XMove;
                if (scrollerX+diff0);
                    return true;
                }else if (scrollerX+diff+getWidth()>rightBorder){
                    scrollTo(rightBorder -getWidth(),0);
                    return true;
                }

                scrollBy((int) diff,0);
                XLastMove = XMove;
                break;
            case MotionEvent.ACTION_UP:

                // 当手指抬起时,根据当前的滚动值来判定应该滚动到哪个子控件的界面
                int targetIndex = (getScrollX() + getWidth() / 2) / getWidth();
                int dx = targetIndex * getWidth() - getScrollX();
                // 第二步,调用startScroll()方法来初始化滚动数据并刷新界面
                Log.d("moveX:","scrollX="+getScrollX()+"  dx="+dx);
                scroller.startScroll(getScrollX(), 0, dx, 0);
                invalidate();

                break;
        }


        return super.onTouchEvent(event);
    }


    @Override
    public void computeScroll() {
        super.computeScroll();
        if (scroller.computeScrollOffset()){
            scrollTo(scroller.getCurrX(),scroller.getCurrY());
            invalidate();
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int count = getChildCount();
        View child=null;
        for (int i=0;i@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        int count = getChildCount();
        View child = null;
        for (int i=0;i0,child.getMeasuredWidth()*(i+1),child.getMeasuredHeight());
        }

        leftBorder = getChildAt(0).getLeft();
        rightBorder = getChildAt(count-1).getRight();
    }
}

上面的代码就可以实现滑动了,看图:
用Scroller实现简单viewpager滑动_第1张图片

如果只使用了scrollTo(x,y),scrollBy(x,y),虽然可以实现滑动,但是不会出现粘性滑动,就是手指离开后,控件慢慢回到原位。这是怎么做到的呢?下面开始讲解。

要做到粘性滑动,就要使用Scroller,可以看下Scroller源码,他是存粹的类,它的主要作用就是计算时间段滑动多少距离。看一段Scroller中的代码,

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

上述两个函数都传入了距离,时间。手指离开手机后,控件“粘性还原”要用到“时间”(没有传入时间,使用默认值DEFAULT_DURATION)和“移动的距离”。依据”时间“和“移动距离”计算出每秒移动的距离(这句话不严格,真正实现算法很复杂,还有加速,减速情况,只不过这样说容易理解),然后通过scroller实例中的scroller.getCurrX()和scroller.getCurrY()方法取得计算后要移动的距离值。如此看来,Scroller就是个计算“移动距离”的工具类。看代码,

  public void computeScroll() {
        super.computeScroll();
        if (scroller.computeScrollOffset()){
//取得计算后要移动的距离值            scrollTo(scroller.getCurrX(),scroller.getCurrY());
            invalidate();
        }
    }

scroller.computeScrollOffset()判断scroller内部处理有没有结束(内部处理结束也就意味着,控件已经粘性复原了,因为内部处理依赖传入的距离和时间吗),如果结束,则scroller.computeScrollOffset()返回false

有人会问,computeScroll()为什么会循环调用呢?看到 invalidate()了吗,这个充当循环角色, invalidate()被执行后,ui界面会被重新绘制,这样的话,draw()函数就会被调用,我们看一下它的源码:

boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
...
if (!drawingWithRenderNode) {
            computeScroll();
            sx = mScrollX;
            sy = mScrollY;
        }
...
}

draw()执行了computeScroll(),而computeScroll()中又存在invalidate()方法,所以构成了循环,不是吗。这只是粘性滑动实现的一部分,另一部分开代码(截取ScollerContainer中的代码),

            case MotionEvent.ACTION_UP:
                // 当手指抬起时,根据当前的滚动值来判定应该滚动到哪个子控件的界面
                int targetIndex = (getScrollX() + getWidth() / 2) / getWidth();
                int dx = targetIndex * getWidth() - getScrollX();
                // 第二步,调用startScroll()方法来初始化滚动数据并刷新界面
                Log.d("moveX:","scrollX="+getScrollX()+"  dx="+dx);
                scroller.startScroll(getScrollX(), 0, dx, 0);
                invalidate();
                break;

int targetIndex = (getScrollX() + getWidth() / 2) / getWidth();
这段代码如何解释,getScrollX() 已经滑动的距离, getWidth() / 2不滑动控件的宽的1/2。没有画图工具,大家自己画图思考,我用文字描述。
用viewpager解释,大家方便想象。假设viewpager中有10项,可以被滑动,分别标志0,1,2,3,4,5,6,7,8,9。假如当前滑动到第4项和第5项之间,手指不离开,脑洞打开想一下。当手指离开时,是让第4项显示还是第5项显示在手机屏幕上。在4,5之间,此时,getScrollX() >4*getWidth(),这里分两种情况,
第一种情况,如果4滑动过半了,getScrollX() + getWidth() / 2>5*getWidth(),那么,(getScrollX() + getWidth() / 2) / getWidth()值是不是5.xxx,取整后=5,再看, targetIndex * getWidth() - getScrollX()不就是4没有过半的距离值(targetIndex * getWidth()是第5项距离值),最后粘性结果手机屏幕上显示第5项。
第二种情况,如果4没有过半,getScrollX() + getWidth() / 2<5*getWidth(),同样,(getScrollX() + getWidth() / 2) / getWidth()值是不是4.xxx,取整后=4,在看targetIndex * getWidth() - getScrollX()不就是4移动的没过半的距离值。
在执行 ,scroller.startScroll(getScrollX(), 0, dx, 0);
invalidate();后,手指离开后,粘性滑动并复位了吗。

剩余代码:

xml version="1.0" encoding="utf-8"?>
   "http://schemas.android.com/apk/res/android"
       android:id="@+id/scrollerContainer"
       android:layout_height="match_parent"
       android:layout_width="match_parent">

       

你可能感兴趣的:(Android开发记录)