ScrollView,看到这个字眼相信大家都很熟悉,但是,对于ScrollView的滑动机制,一些特殊的处理,可能不是很了解。
最近把玩一些优秀的app,发现部分在ScrollView上做了一些不一样的处理,优化了体验。
那么,接下来就来分析一下Scrollview的一些特殊处理和处理方法吧。
(以下Scrollview为HorizontalScrollView)

一 . 掌控ScrollView的滑动停止

很多事件,是需要判断ScrollView是否停止滚动来进行的,但是很可惜的是,在ScrollView里面并没有一个固定的方法去监听ScrollView的滚动停止。
那么,在部分应用中,是有进行处理的。那么,这个是怎么做的呢? 其实方法很简单,我们可以在自定义Scrollview,然后进行如下处理:

@Override
public boolean onTouch(View v, MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_UP:
            handler.sendMessageDelayed(handler.obtainMessage(-10086), SpaceTime);
            break;
        case MotionEvent.ACTION_DOWN:
            lastX = 0;
            break;
        case MotionEvent.ACTION_MOVE:
            break;
    }
    return false;
}

我们可以用一个Handler监听ACTION_UP动作,发出一条message
然后,在hangler中通过对scrollview的滑动距离来判断是否停止。代码如下:

int SpaceTime=10;//这里的速度不能太大
      Handler handler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
        switch (msg.what) {
            case -10086:
                if (lastX == getScrollX()) {
                    //表示已经停止
                } else {//不一样表示未停止
                   handler.sendMessageDelayed(handler.obtainMessage(-10086),SpaceTime);
                   lastX = getScrollX();

                }
                break;

        }
    }
};

如此,便可以判断scrollview是否停止滚动了。

二.掌控scrollview的速度

相信用过scrollview的都知道,scrollview的滑动速度是有一套自己的算法的,他可以自动的根据我们滑动的速度来进行计算。并且,这个速度是没有提供api来控制的。
当然,我们可以去控制滑动的总体速率变化,这一点是可以通过重写系统的一个方法来实现的。如下:

@Override
public void fling(int velocityX) {
    super.fling(velocityX / 2);
}
通过对参数velocityX的修改,可以控制scrollview的整体速度

但是,仅仅这一点可能无法满足我们的需求,比如说,我想让ScrollView滚动到某个速度的时候,便不再减速了,而是匀速滑动,直到停止。又或者,我希望scrollview的加速减速是受到我们的控制的。

这些情况下,那么,仅仅修改整体速率可能是不够的了。那么,这该怎么做呢。

那么,我们有如下几点需要解决

第一点 自动滚动

在scrollview停止之后,我们需要通过代码控制再让他滚动起来。
这里有两个方法,一个是通过系统自带的smoothScrollTo()来实现,但是,这种情况下,滚动的速度是不受我们控制的。

第二种方法,就是通过scrollTo()来控制。
通过对系统源码的判断我们可以发现,其实smoothScrollTo()最终也是调用scrollTo()来实现滑动的。所不同的是,scrollTo()滑动是瞬间的,并且没有滑动轨迹的。
那么,为了达到和smoothScrollTo一样的滑动效果,我们可以通过handle不停的发送消息来通知scrollview进行scrollTo(),一段段微小的变动,就变成了smoothscrollto了。

那么这里代码如下;

//自动滚动
private void autoScrollView() {

    handler.sendEmptyMessageDelayed(autoScroll, SpaceTime);
}
int distance =10086;//滚动距离
int speed=20;//滚动速度
int autoScroll=-12345;
 Handler handler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
        switch (msg.what) {

            case -12345://自动滚动
                int add = distance > 0 ? 1 : -1;//左边右边滚动的判断
                //每次滚动speed的距离
                if (Math.abs(distance) > speed) {
                    scrollTo(speed * add + getScrollX(), 0);
                    sendEmptyMessageDelayed(autoScroll, SpaceTime);
                    distance -= speed * add;
                } else {
                    scrollTo(distance * add + getScrollX(), 0);
                    distance = 0; 
                }
                break;
        }
    }
};

第二点 掌控速度

解决了自动滚动之后,便是重点之处了,掌控scrollview的速度。

首先我们思考一下,scrollview的速度是怎么获取的?

这一点比较容易得知,我们开始有对scrollview是否停止滚动进行判断,这个判断是根据,两次handler直接,滚动的距离来判断的。而这个滚动距离,不正是我们需要的速度么!

ok,到这里,那么我们便获取到了scrollview的速度。

接下来,我们拿这个速度干嘛呢?我们需要scrollview的滚动速度达到某个值的时候,用我们自己的滑动去替代系统的滑动。

比如说,当scrollview的滚动达到速度为50的时候,Ok,我不想让他的速度再降低了,我要让他一直保持在50.

那么,首先应该干嘛呢?

首先,我们应该停止系统的滑动,因为,scrollview的滑动是不受控制的,而如果系统滑动没有停止的话,我们的滑动时无法生效的。

ok,想到第一点,那么我们可想解决办法了。但是问题来了,系统并没有提供一个主动停止滑动的方法给我们。那么应该怎么做呢?

这时候,就要去看看系统的源码了。因为我们知道,滑动中的scrollview虽然没有提供api停止滚动,但是.我们再使用scrollview的过程中可以发现,在滑动中,如果,点击一下scrollview的话,那滑动是会停止的。既然scrollview自带这个事件,那么,我们是否可以找到这个方法呢?

这里,通过阅读源码,找到了一段代码:

case MotionEvent.ACTION_DOWN: {
            if (getChildCount() == 0) {
                return false;
            }
            if ((mIsBeingDragged = !mScroller.isFinished())) {
                final ViewParent parent = getParent();
                if (parent != null) {
                    parent.requestDisallowInterceptTouchEvent(true);
                }
            }

            /*
             * If being flinged and user touches, stop the fling. isFinished
             * will be false if being flinged.
             */
            if (!mScroller.isFinished()) {
                mScroller.abortAnimation();
            }

            // Remember where the motion event started
            mLastMotionX = (int) ev.getX();
            mActivePointerId = ev.getPointerId(0);
            break;
        }

在scrollview的ontouch事件的ACTION_DOWN里面有这么一段代码。我们可以看到这段

/*
         * If being flinged and user touches, stop the fling. isFinished
         * will be false if being flinged.
         */
        if (!mScroller.isFinished()) {
            mScroller.abortAnimation();
        }

从代码上我们就可以看到,scrollview是通过内部的Scroller来终止滑动的。

Ok,到这,我们找到了让scrollview停止的方法。那么我们是否可以通过调用mScroller.abortAnimation();来停止scrollview滚动了。

但是,我们发现,这个mScroller是私有的,也就是说,在自定义的scrollview中是无法正常获得mScroller的对象的。

对了,是无法正常获得,可不代表无法获得。

为什么?

因为安卓源码是java代码,也就是说,我们是可以通过反射来获取到父类的private对象的。

这里直接贴上方法。

public Object get(Object instance, String variableName) {
    Class targeClass = instance.getClass().getSuperclass();

    HorizontalScrollView superInst = (HorizontalScrollView) targetClass.cast(instance);
    Field field;
    try {
        field = targetClass.getDeclaredField(variableName);
        //修改访问限制
        field.setAccessible(true);
        // superInst 为 null 可以获取静态成员
        // 非 null 访问实例成员
        return field.get(superInst);
    } catch (Exception e) {
        e.printStackTrace();
        return null;
    }
}
 OverScroller mscroller;

 private void init() {
    mscroller = (OverScroller) get(this, "mScroller");
}

如此,便可获取到mScrollview对象了。

这两点解决,那么我们便可以顺利的接管scrollview的滑动了。

整体代码如下:

Handler handler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
        switch (msg.what) {
            case -10086:
                if (lastX == getScrollX()) {//
                       //停止滚动了
                } else {
                    if (Math.abs(getScrollX() - lastX) <=MINSPEED) {
                         mscroller.abortAnimation();
                            sendEmptyMessageDelayed(autoScroll, SpaceTime);
                         //当达到我们想要控制的速度,同时发送一条自动滚动的消息。
                    } else {
                                        handler.sendMessageDelayed(handler.obtainMessage(ViewScrollTag), SpaceTime);
                    }
                    lastX = getScrollX();

                }
                break;
            case -12345://自动滚动
                int add = distance > 0 ? 1 : -1;
                if (Math.abs(distance) > speed) {
                    scrollTo(speed * add + getScrollX(), 0);
                    sendEmptyMessageDelayed(autoScroll, SpaceTime);
                    distance -= speed * add;
                } else {
                    scrollTo(distance * add + getScrollX(), 0);
                    distance = 0;

                }
                break;
        }
    }
};

三 总结

经过以上的操作,我们便可以顺利的掌控scrollview的滑动了,一些优化的处理也能更好的进行了。为了方便大家更好的理解,特地写了一个小demo.有兴趣的可以下载看看。