Scroller机制原理解析

首先,以下所述关于Scroller所讲的滚动均表示View内容的滚动,注意,是内容,不是位置!什么叫做内容呢?比如说一个TextView,如果使用scrollTo(),那么移动的是里面的文字,而不是位置。位置的移动基本都是通过改变View本身的LayoutParams中的margin值来实现,比如早期的PullToRefreshListView就是通过手指的滑动在onMove()函数中连续的修改指定View的marginTop的值来实现的平滑下拉效果。而Scroller提供了一种更高效简单的机制来实现平滑滚动。

在View类里面,有两个和滚动相关的方法,scrollTo()和scrollBy(),这两个方法可以实现View内容的移动,是移动,不是滚动!scrollBy()也是一样的。那么为什么是移动,不是滚动呢?这是因为这两个方法完成的都是瞬间完成,即瞬移,而不是带有平滑滚动过程的滚动。

实际上任何人眼看起来的连续平滑滚动其根本上都是由这种连续不断的瞬移完成的,只要频率超过人眼的分辨率看起来就是一种连续的平滑感动,原理同View的绘制频率(60Hz)一致。并且Scroller的平滑滚动实现也就是依赖View的绘制呈现出来的,在View中有2个成员变量mScrollX和mScrollY,表示View中内容的偏移量,如果每次绘制的时候都去递增或递减mScrollX和mScrollY的值,就能实现人眼看起来的平滑滚动效果,而Scroller类就是这个用来计算每次绘制mScrollX和mScrollY的该变量的辅助类

View的源码有一个空实现的computeScroll()函数,此函数在每次绘制的时候都会被调用,通过注释我们知道只需要复写该函数,在方法体中去定量的更新mScrollX和mScrollY 就能实现View的平滑滚动,借助Scroller类就能轻松完成

    /**
     * Called by a parent to request that a child update its values for mScrollX
     * and mScrollY if necessary. This will typically be done if the child is
     * animating a scroll using a {@link android.widget.Scroller Scroller}
     * object.
     */
    public void computeScroll() {
    }

Scroller的使用:

Scroller mScroller = new Scroller(context);//1:建Scroller对象

//2:开始滑动调用startScroll(),传入起点xy和需要滑动的距离,以下函数表示当前View需要返回到初始位置
mScroller.startScroll(getScrollX(),getScrollY(),-getScrollX(),-getScrollY());
invalidate();  //手动调用此方法来重绘页面 进而调用computeScroll方法

//3:重写computeScroll(),调用scrollTo()移动View内容,然后继续重绘连续移动实现平滑直至完成
@Override
public void computeScroll() {
     super.computeScroll();
     if(mScroller.computeScrollOffset()){ //表示滑动距离是否完成
         scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
         invalidate();//滑动距离未结束必须调用invalidate()来继续递归
     }
 }

对于Scroller来说,只需要知道某次滑动的起点坐标和终点坐标,将滑动距离拆分成若干段,记录下View的当前坐标和下次瞬移点的坐标即可。
mScroller.startScroll()函数就是只是记录了当前View的起点终点数据而已,然后调用invalidate()唤起重绘执行computeScroll(); computeScroll()中调用scrollTo()移动View内容,然后继续重绘连续移动实现平滑直至完成,本质上平滑滚动的实现是由重写View的computeScroll()来实现的,而把复杂的计算工作都封装在了Scroller类中。

在Scroller的另一个构造器可以看出Scroller也支持插值器(可以控制滑动的速率变化),
public Scroller(Context context, Interpolator interpolator) {
}
Scroller中默认使用的是ViscousFluidInterpolator来控制滑动的速率变化,貌似是一个持续减速的插值器,没看懂有机会深入研究。
另外官方组件ViewDragHelper可以帮你轻松实现需要手势拖拽来控制View滚动的需求,实现机制就是Scroller。使用如下

public class MyDragLayout extends FrameLayout{

    private ViewDragHelper mViewDragHelper;
    private View mMenuView,mMainView;
    private int mWidth;

    public MyDragLayout(Context context) {
        this(context,null);
    }

    public MyDragLayout(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

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

    private void init(){
        mViewDragHelper = ViewDragHelper.create(this, new ViewDragHelper.Callback() {

            //用户触摸到View后回调
            @Override
            public void onViewCaptured(View capturedChild, int activePointerId) {
                Log.d("zhangbo","onViewCaptured ");
            }

            //触摸时间的拖拽状态改变时回调
            @Override
            public void onViewDragStateChanged(int state) {
                Log.d("zhangbo","onViewDragStateChanged state=="+state);
            }

            //当View位置发生改变时回调
            @Override
            public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
                //Log.d("zhangbo","onViewPositionChanged "+changedView.getTag()+ left +"-"+ top);
            }

            //是否捕获当前触摸事件
            @Override
            public boolean tryCaptureView(View child, int pointerId) {
                return mMainView==child;
            }

            //处理水平滑动
            @Override
            public int clampViewPositionHorizontal(View child, int left, int dx) {
                return left;
            }

            //处理垂直滑动
            @Override
            public int clampViewPositionVertical(View child, int top, int dy) {
                return top/2;
            }

            //拖动结束后调用
            @Override
            public void onViewReleased(View releasedChild, float xvel, float yvel) {
                super.onViewReleased(releasedChild, xvel, yvel);
                if(mMainView.getLeft()<500){
                    mViewDragHelper.smoothSlideViewTo(mMainView,0,0);
                    ViewCompat.postInvalidateOnAnimation(MyDragLayout.this);
                }else{
                    mViewDragHelper.smoothSlideViewTo(mMainView,300,0);
                    ViewCompat.postInvalidateOnAnimation(MyDragLayout.this);
                }
            }

        });
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mMainView = getChildAt(0);
    }
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return mViewDragHelper.shouldInterceptTouchEvent(ev);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mViewDragHelper.processTouchEvent(event);
        return true;
    }
    @Override
    public void computeScroll() {
        if(mViewDragHelper.continueSettling(true)){
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }
}

猜你喜欢

转载自blog.csdn.net/zhangbo328/article/details/81132593