头图

首先看View#scrollTo(int x,int y),以x为例。当传入的x>0View内容发生了滚动,并且沿着x轴的负方向滚动。why???

scrollTo滚动的是View的内容。如果想通过scrollTo使某个view发生滚动,应当调用其父View的scrollTo方法。为什么滚动的是view的内容,后面会说这个。

来看源码。

先看View#scrollTo(int x,int y)源码。x直接被赋值给mScrollX

    #View.java
   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();
            }
        }
    }
对于一个View,当调用其父view的scrollTo方法使其从A点滚动到B点,再滚动到C点,此时view的mScrollX是其总(A->C)的滚动距离,而不是阶段(A->B或者B->C)滚动距离

在看View# invalidate(int l, int t, int r, int b) 这里的scrollX就是上一个方法传入的参数xl-scrollX是绘制区域的左边,r-scrollX是绘制区域的右边。x>0绘制区域就会左移;x<0绘制区域就会右移。

    #View.java
   public void invalidate(int l, int t, int r, int b) {
        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        invalidateInternal(l - scrollX, t - scrollY, r - scrollX, b - scrollY, true, false);
    }

从上面可以看到View#scrollTo调用会触发View#invalidate,从而导致绘制区域发生了改变,也就是我们看到View显示内容的区域发生了改变。

要使一个View的位置发生改变,应当改变ViewmLeftmTop两个属性值。很明显,View#scrollTo并没有改变这两个属性的值。因此View#scrollTo不会引起View的位置滚动,而是滚动了View的内容。

那如何自定义一个可滚动的View呢?

继承自ViewGroup。当然如有必要需要重写onInterceptTouchEvent,拦截MotionEvent.ACTION_MOVE事件,重写onTouchEvent,然后调用scrollTo或者scrollBy实现滚动。



private float lastDownX,lastDownY;

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    switch(event.getAction()){
        case MotionEvent.ACTION_DOWN:
            lastDownX = event.getX();
            lastDownY = event.getY();
        break;
        case MotionEvent.ACTION_MOVE:
        if (满足一定条件){
            //请求父View不要拦截
            getParent().requestDisallowInterceptTouchEvent(true);
            //返回true。将ACTION_MOVE事件交给自身处理。
            return true;
        }
        break;
    }
    return super.onInterceptTouchEvent(ev);
}


@Override
public boolean onTouchEvent(MotionEvent event) {
   switch(event.getAction()){
        case MotionEvent.ACTION_DOWN:
            lastDownX = event.getX();
            lastDownY = event.getY();
        break;

        case MotionEvent.ACTION_MOVE:
            float moveX = event.getX();
            float moveY = event.getY();
            float diffX = moveX - lastDownX;
            float diffY = moveY - lastDownY;
            /**
            * 从左向右滑动,diffX<0;getScrollX()>0;
            * scrollTo的参数x>0
            * x = getScrollX() - diffX
            *
            * 从右向左滑动,diffX>0,getScrollX()<0;
            * scrollTo的参数x<0
            * x = getScrollX() - diffX
            *
            * 参数y同理
            */
            scrollTo((int)(getScrollX()-diffX),(int)(getScrollY()-diffY));
            //scrollBy(-(int)diffX,-(int)diffY);
            return true;
        case MotionEvent.ACTION_UP:
        lastDownX = 0;
        lastDownY = 0;
        break;
    } 
    return super.onTouchEvent(event);
}

滚动的时候还要考虑边界约束的问题,以QQ消息列表侧滑删除为例。
从左向右侧滑要对左边界约束,从右向左侧滑要对右边界约束。
在ACTION_MOVE事件中做边界约束。使用上面的代码。

    //最大滑动距离,根据自己的业务情况来确定这个值。以QQ侧滑删除为例,这个值是删除按钮的宽度。
    int maxScrollDistance;
    case MotionEvent.ACTION_MOVE:
        //某一时刻要滑动到的位置,这个位置到达边界时,要做约束。
        float targetX = getScrollX() - (event.x - lastDownX);
        //从左向右滑动,当滑动到左边界时,getScrollX() = 0;继续滑动,getScrollX()<0;对这个情况要做个约束,避免滑动越过左边界。
        if (targetX<0) targetX = 0;
        //从右向左滑动,当滑动到右边界时,getScrollX() = maxScrollDistance;继续滑动,getScrollX()>maxScrollDistance;对这个情况要做个约束,避免滑动越过右边界。
        if (targetX>maxScrollDistance) targetX = maxScrollDistance;
        scrollTo(targetX,0);

idealcn
27 声望4 粉丝