用Scroller实现简单viewpager滑动

来源:互联网 发布:什么软件找工作好 编辑:程序博客网 时间:2024/06/06 10:49

用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+diff<leftBorder){                    scrollTo(leftBorder,0);                    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<count;i++){            child = getChildAt(i);                          child.measure(widthMeasureSpec,heightMeasureSpec);        }    }    @Override    protected void onLayout(boolean changed, int l, int t, int r, int b) {        int count = getChildCount();        View child = null;        for (int i=0;i<count;i++){            child = getChildAt(i);           child.layout(child.getMeasuredWidth()*i,0,child.getMeasuredWidth()*(i+1),child.getMeasuredHeight());        }        leftBorder = getChildAt(0).getLeft();        rightBorder = getChildAt(count-1).getRight();    }}

上面的代码就可以实现滑动了,看图:
演示图

如果只使用了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"?>   <com.luo.usedemo.ScollerContainer xmlns:android="http://schemas.android.com/apk/res/android"       android:id="@+id/scrollerContainer"       android:layout_height="match_parent"       android:layout_width="match_parent">       <Button           android:layout_width="match_parent"           android:layout_height="100dp"           android:text="第一个view"/>       <Button           android:layout_width="match_parent"           android:layout_height="100dp"           android:text="第二个view"/>       <Button           android:layout_width="match_parent"           android:layout_height="100dp"           android:text="第三个view"/>   </com.luo.usedemo.ScollerContainer>public class MainActivity extends AppCompatActivity {    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);    }}
1 0