码迷,mamicode.com
首页 > 其他好文 > 详细

PullScrollView源码解析

时间:2015-07-30 11:18:23      阅读:95      评论:0      收藏:0      [点我收藏+]

标签:

PullScrollView是Github上面的一个开源项目,主要用于实现下拉时头部伸缩的效果,项目地址见https://github.com/MarkMjw/PullScrollView

大家先来看一下实现的效果图(图侵删)

技术分享

但是由于这个项目的代码比较复杂,而实际上不这样做也可实现效果,我在网上找了另外一篇介绍pullScrollView的文章,写得非常好,本文也只是基于文章http://blog.csdn.net/harvic880925/article/details/46728247 谈谈自己的理解和收获,大家如果想知道更详细,可以拜访这篇文章。


下面我先来说明一下实现的思路,整体的布局是一个ScrollView,意味着我们要继承ScrollView来实现自己的控件pullScrollView,对于下拉时会有拉升效果的图片,其实是ScrollView以外的一个布局,pullScrollView提供一个setHeader()方法来为自己设置下拉头部。

我们可以为任意一个View设置为pullScrollView的头部,只要pullScrollView被拖动的时候,改变这个View的布局就可以了,我们称这个View为HeaderView

那么具体是怎么改变的呢?通过layout()方法来改变HeaderView的位置,拖动的时候,不断修改HeaderView位置使之下移,但是这样只是实现了拖动的效果,要有类似拉伸的效果的话,需要我们将HeaderView的marginTop改成负数,这样位置移动的时候,感觉HeaderView就会被拉长。

更好的效果是,底部也会拉长,这样看起来才像是两边一起拉造成的。在图片中我们看到HeaderView下面的主体布局,我们称为ContentView,我们让ContentView遮蔽HeaderView的一部分,然后让ContentView和HeaderView的下拉增长速度不一样,这样就可以造成底部也拉长的效果了。


现在来思考怎么使用layout()?显然我们要通过监听手指的down和move事件,down的时候记录下位置,move的时候用当前位置减去down的位置,这个差值就是HeaderView位置的增长距离,至于ContentView我们可以将之乘以一个小数使之小一些。

理论上我们实现了下拉的效果,但是有很多的缺陷,现在我们先看源码。

首先是属性和构造方法

public class PullScrollView extends ScrollView {
    //底部图片View
    private View mHeaderView;
    //头部图片的初始化位置
    private Rect mHeadInitRect = new Rect();
    //底部View
    private View mContentView;
    //ScrollView的contentView的初始化位置
    private Rect mContentInitRect = new Rect();

    //初始点击位置
    private Point mTouchPoint = new Point();


    //标识当前view是否移动
    boolean mIsMoving = false;

    //是否禁止控件本身的的移动
    boolean mEnableMoving = false;

    //是否使用layout函数移动布局
    boolean mIsLayout = false;
    /**
     * 阻尼系数,越小阻力就越大.
     */
    private static final float SCROLL_RATIO = 0.5f;

    private int mContentTop, mContentBottom;

    private int mHeaderCurTop, mHeaderCurBottom;

    //用户定义的headview高度
    private int mHeaderHeight = 0;

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

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

    private void init(Context context, AttributeSet attrs) {
        // set scroll mode
        setOverScrollMode(OVER_SCROLL_NEVER);

        if (null != attrs) {
            TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.PullScrollView);

            if (ta != null) {
                mHeaderHeight = (int) ta.getDimension(R.styleable.PullScrollView_headerHeight, -1);
                ta.recycle();

            }
        }
    }
对于成员属性大家可以看注释,我们初始化了两个Rect,和一个Point。对于init()方法,其实就是获取了两个我们自定义的属性。

注意,在这里ScrollView的位置使一直不变化的,变化的是ScrollView里面的contentView,所以我们使用setOverScrollMode(OVER_SCROLL_NEVER)来限制它的移动。

然后根据上面所说,我们要有一个setHeader()方法

public void setmHeaderView(View view) {
        mHeaderView = view;
    }

另外我们还有获取contentView以便于修改它的位置,而这个获取必须在布局渲染完毕以后

@Override
    protected void onFinishInflate() {
        if (getChildCount() > 0) {
            mContentView = getChildAt(0);
        }
        super.onFinishInflate();
    }


接下来就是监听事件了

@Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            //保存原始位置
            mTouchPoint.set((int) event.getX(), (int) event.getY());
            mHeadInitRect.set(mHeaderView.getLeft(), mHeaderView.getTop(), mHeaderView.getRight(), mHeaderView.getBottom());
            mContentInitRect.set(mContentView.getLeft(), mContentView.getTop(), mContentView.getRight(), mContentView.getBottom());
            mIsMoving = false;
            //如果当前不是从初始化位置开始滚动的话,就不让用户拖拽
            if (getScrollY() == 0){
                mIsLayout = true;
            }
        } else if (event.getAction() == MotionEvent.ACTION_MOVE) {
            //如果当前的事件是我们要处理的事件时,比如现在的下拉,这时候,我们就不能让子控件来处理这个事件
            //这里就需要把它截获,不传给子控件,更不能让子控件消费这个事件
            //不然子控件的行为就可能与我们的相冲突
            int deltaY = (int) event.getY() - mTouchPoint.y;
            deltaY = deltaY < 0 ? 0 : (deltaY > mHeaderHeight ? mHeaderHeight : deltaY);
            if (deltaY > 0 && deltaY >= getScrollY()&& getScrollY() == 0) {
                onTouchEvent(event);
                return true;
            }
        }
        return super.onInterceptTouchEvent(event);
    }
有人会奇怪,为什么是在onInterceptTouchEvent(MotionEvent event)方法里面监听,而不是onTouchEvent(MotionEvent event)里面呢?

这里其实涉及一个事件拦截和传递机制的问题,首先我们要明白,对于ScrollView这个GroupView,当我们手指按下的时候,首先调用的是dispatchTouchEvent()方法,这方法用于分发Touch事件,接下来默认会调用onInterceptTouchEvent(MotionEvent event)。这个方法是用于事件拦截的,如果返回true,则事件不再向下传递,也就是说不再分发给子控件。

如果返回false,则会分发给子控件,由子控件的onTouchEvent()方法去处理,如果子控件的onTouchEvent()方法返回false,则再将事件分发给ScrollView的onTouchEvent()方法处理,返回true则不分发给ScrollView的onTouchEvent()方法。

也就说Touch事件分发顺序如下:

父控件的onInterceptTouchEvent(MotionEvent event) ---》子控件的onTouchEvent() --》父控件的nTouchEvent()

在上面的传递过程中,有任意一个返回true,则不向下继续分发。


OK,现在我们去理解上面的写法,我们在onInterceptTouchEvent(MotionEvent event) 方法里面获得down的位置,是为了避免子控件消费掉事件(作为pullScrollView的作者,我们不能保证使用者的子控件会不会消费掉)。


另外,在down的时候,我们记录下了手指位置,这个使用mTouchPoint来记录

记录下了headerView的初始位置,这个使用mHeadInitRect来记录

记录下了contentView的初始位置,这个使用mContentInitRect来记录

其他的设置,我们暂时不管。


接下来我们看move的时候,

int deltaY = (int) event.getY() - mTouchPoint.y;
deltaY = deltaY < 0 ? 0 : (deltaY > mHeaderHeight ? mHeaderHeight : deltaY);
这里注意,我们对于移动的距离进行了处理,我们保证deltaY的最小值为0,最大值为一个mHeaderHeight,我们自定义的一个属性。

这样是为了限制下拉的距离,也就是说我们不能无限下拉,下拉到一定距离就不能继续拉了。


另外大家可能会想到上滑的情况,上滑有两种,一种是在初始位置就上滑。

这时headerView不应该动,而是交由ScrollView去处理这个事件(因为这个是ScrollView本身的滑动效果啊!)

所以我们要用getScrollY()==0来判断是不是在初始位置就开始上滑,如果是,则不实现拖拽

所以我们可以看到在down里面的判断,使用mIsLayout来做这个标记

//如果当前不是从初始化位置开始滚动的话,就不让用户拖拽
            if (getScrollY() == 0){
                mIsLayout = true;
            }

另外还有,就是下拉一段距离,再上滑,这时跟下拉的处理是一样的,只要调用layout修改位置就好了

可以看到,如果

if (deltaY > 0 && deltaY >= getScrollY()&& getScrollY() == 0) {
                onTouchEvent(event);
                return true;
            }
控件都在初始位置,并且下拉了,我们就直接调用ScrollView的onTouchEvent()方法,并且拦截事件


我再来看onTouchEvent()方法做了什么,先看move的情况

@Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE: {
                int deltaY = (int) event.getY() - mTouchPoint.y;
                deltaY = deltaY < 0 ? 0 : (deltaY > mHeaderHeight ? mHeaderHeight : deltaY);
                if (deltaY > 0 && deltaY >= getScrollY() && mIsLayout) {
                    float headerMoveHeight = deltaY * 0.5f * SCROLL_RATIO;
                    mHeaderCurTop = (int) (mHeadInitRect.top + headerMoveHeight);
                    mHeaderCurBottom = (int) (mHeadInitRect.bottom + headerMoveHeight);

                    float contentMoveHeight = deltaY * SCROLL_RATIO;
                    mContentTop = (int) (mContentInitRect.top + contentMoveHeight);
                    mContentBottom = (int) (mContentInitRect.bottom + contentMoveHeight);

                    if (mContentTop <= mHeaderCurBottom) {
                        mHeaderView.layout(mHeadInitRect.left, mHeaderCurTop, mHeadInitRect.right, mHeaderCurBottom);
                        mContentView.layout(mContentInitRect.left, mContentTop, mContentInitRect.right, mContentBottom);
                        mIsMoving = true;
                        mEnableMoving = true;
                    }
                }
            }
            break;

可以看到,我们又获取了移动的距离,进行限制以后,根据headerView,contentView的初始坐标,计算新位置,然后调用layout()方法。

这里有一个SCROLL_RATIO我们称之为阻尼系数,目的是提供下拉困难的效果(也就是说不能我们下拉多少就移动多少)

另外还要一个判断

if (mContentTop <= mHeaderCurBottom) {<span style="font-family:Arial, Helvetica, sans-serif;">...}</span>
这就是说contentView的顶部,不能超过headerView的底部,否则两者就分离了!因为contentView移动得比较慢,所以headerView不动以后,contentView还是可以继续下拉,所以这时就不能超过headerView的底部。


Ok,到此为止,下拉效果基本实现。

下拉来看回弹效果,这显然要在up事件中处理

@Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE: {
                ......
            }
            break;
            case MotionEvent.ACTION_UP: {
                //反弹
                if (mIsMoving) {
                    mHeaderView.layout(mHeadInitRect.left, mHeadInitRect.top, mHeadInitRect.right, mHeadInitRect.bottom);
                    TranslateAnimation headAnim = new TranslateAnimation(0, 0, mHeaderCurTop - mHeadInitRect.top, 0);
                    headAnim.setDuration(200);
                    mHeaderView.startAnimation(headAnim);

                    mContentView.layout(mContentInitRect.left, mContentInitRect.top, mContentInitRect.right, mContentInitRect.bottom);
                    TranslateAnimation contentAnim = new TranslateAnimation(0, 0, mContentTop - mContentInitRect.top, 0);
                    contentAnim.setDuration(200);
                    mContentView.startAnimation(contentAnim);
                    mIsMoving = false;
                }
                mEnableMoving = false;
                mIsLayout = false;
            }
            break;
        }
        // 禁止控件本身的滑动.
        //这句厉害,如果mEnableMoving返回TRUE,那么就不会执行super.onTouchEvent(event)
        //只有返回FALSE的时候,才会执行super.onTouchEvent(event)
        //禁止控件本身的滑动,就会让它,本来应有的滑动就不会滑动了,比如向上滚动
        return mEnableMoving || super.onTouchEvent(event);
    }

我还记得在下拉的时候,将mIsMoving这个标志设置true了,说明我们已经下拉了,只有下拉了我们才调用动画,在其他任何的action_up中,我们都不应该调用动画。

没错,回弹是使用动画TranslateAnimation来实现的

比较让人疑惑的是,

mHeaderView.layout(mHeadInitRect.left, mHeadInitRect.top, mHeadInitRect.right, mHeadInitRect.bottom);
这句目的是什么。我将headerView还原到了初始位置,然后再调用动画。

对于动画的坐标计算,我们要知道,原点是View当前的位置,也就是说即使View被下拉了,使用动画时,也是按照当前位置为起点计算的。

现在我们先把View的位置复原,那么我们在TranslateAnimation设置目的Y坐标就是0,其实Y坐标,就是当前位置减去初始位置。

这样才能合理地计算动画的移动距离。

有人会说,这样不会闪屏吗?答案是不会的,因为onlayout是在一瞬间完成的,所以人眼是感觉不出来这个变化的。

所谓回弹,就是让在一定时间内让控件不断地向初始位置复原。


最后我们来看一下onTouchEvent(MotionEvent event)的返回值,

// 禁止控件本身的滑动.
        //这句厉害,如果mEnableMoving返回TRUE,那么就不会执行super.onTouchEvent(event)
        //只有返回FALSE的时候,才会执行super.onTouchEvent(event)
        //禁止控件本身的滑动,就会让它,本来应有的滑动就不会滑动了,比如向上滚动
        return mEnableMoving || super.onTouchEvent(event);
注释说明得很清楚了,就是用于禁止ScrollView本身的滑动,在拖拽效果时,将mEnableMoving设置为true,这样super.onTouchEvent(event);就不会被调用,也就是ScrollView的效果被禁止。

另外在Action_up我们可以看到,要将mEnableMoving设置为false,恢复其滑动功能。

最后,希望大家看懂了我的讲解,如果不明白,大家可以看原作者的博客。

至于github的pullScrollView的具体实现,大家可以看一下我写的注释,其实思路是一样的,但是实现起来更为繁杂。

资源地址是:http://download.csdn.net/detail/kangaroo835127729/8945515

最后贴出本文pullScrollView的完整源代码

/**
 * Created by harvic on 2015/6/17
 * @adress blog.csdn.net/harvic880925
 */
public class PullScrollView extends ScrollView {
    //底部图片View
    private View mHeaderView;
    //头部图片的初始化位置
    private Rect mHeadInitRect = new Rect();
    //底部View
    private View mContentView;
    //ScrollView的contentView的初始化位置
    private Rect mContentInitRect = new Rect();

    //初始点击位置
    private Point mTouchPoint = new Point();


    //标识当前view是否移动
    boolean mIsMoving = false;

    //是否禁止控件本身的的移动
    boolean mEnableMoving = false;

    //是否使用layout函数移动布局
    boolean mIsLayout = false;
    /**
     * 阻尼系数,越小阻力就越大.
     */
    private static final float SCROLL_RATIO = 0.5f;

    private int mContentTop, mContentBottom;

    private int mHeaderCurTop, mHeaderCurBottom;

    //用户定义的headview高度
    private int mHeaderHeight = 0;

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

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

    private void init(Context context, AttributeSet attrs) {
        // set scroll mode
        setOverScrollMode(OVER_SCROLL_NEVER);

        if (null != attrs) {
            TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.PullScrollView);

            if (ta != null) {
                mHeaderHeight = (int) ta.getDimension(R.styleable.PullScrollView_headerHeight, -1);
                ta.recycle();

            }
        }
    }

    public void setmHeaderView(View view) {
        mHeaderView = view;
    }

    @Override
    protected void onFinishInflate() {
        if (getChildCount() > 0) {
            mContentView = getChildAt(0);
        }
        super.onFinishInflate();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE: {
                int deltaY = (int) event.getY() - mTouchPoint.y;
//                deltaY = deltaY < 0 ? 0 : (deltaY > mHeaderView.getHeight() ? mHeaderView.getHeight() : deltaY);
                deltaY = deltaY < 0 ? 0 : (deltaY > mHeaderHeight ? mHeaderHeight : deltaY);
                if (deltaY > 0 && deltaY >= getScrollY() && mIsLayout) {
                    float headerMoveHeight = deltaY * 0.5f * SCROLL_RATIO;
                    mHeaderCurTop = (int) (mHeadInitRect.top + headerMoveHeight);
                    mHeaderCurBottom = (int) (mHeadInitRect.bottom + headerMoveHeight);

                    float contentMoveHeight = deltaY * SCROLL_RATIO;
                    mContentTop = (int) (mContentInitRect.top + contentMoveHeight);
                    mContentBottom = (int) (mContentInitRect.bottom + contentMoveHeight);

                    if (mContentTop <= mHeaderCurBottom) {
                        mHeaderView.layout(mHeadInitRect.left, mHeaderCurTop, mHeadInitRect.right, mHeaderCurBottom);
                        mContentView.layout(mContentInitRect.left, mContentTop, mContentInitRect.right, mContentBottom);
                        mIsMoving = true;
                        mEnableMoving = true;
                    }
                }
            }
            break;
            case MotionEvent.ACTION_UP: {
                //反弹
                if (mIsMoving) {
                    mHeaderView.layout(mHeadInitRect.left, mHeadInitRect.top, mHeadInitRect.right, mHeadInitRect.bottom);
                    TranslateAnimation headAnim = new TranslateAnimation(0, 0, mHeaderCurTop - mHeadInitRect.top, 0);
                    headAnim.setDuration(200);
                    mHeaderView.startAnimation(headAnim);

                    mContentView.layout(mContentInitRect.left, mContentInitRect.top, mContentInitRect.right, mContentInitRect.bottom);
                    TranslateAnimation contentAnim = new TranslateAnimation(0, 0, mContentTop - mContentInitRect.top, 0);
                    contentAnim.setDuration(200);
                    mContentView.startAnimation(contentAnim);
                    mIsMoving = false;
                }
                mEnableMoving = false;
                mIsLayout = false;
            }
            break;
        }
        // 禁止控件本身的滑动.
        //这句厉害,如果mEnableMoving返回TRUE,那么就不会执行super.onTouchEvent(event)
        //只有返回FALSE的时候,才会执行super.onTouchEvent(event)
        //禁止控件本身的滑动,就会让它,本来应有的滑动就不会滑动了,比如向上滚动
        return mEnableMoving || super.onTouchEvent(event);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            //保存原始位置
            mTouchPoint.set((int) event.getX(), (int) event.getY());
            mHeadInitRect.set(mHeaderView.getLeft(), mHeaderView.getTop(), mHeaderView.getRight(), mHeaderView.getBottom());
            mContentInitRect.set(mContentView.getLeft(), mContentView.getTop(), mContentView.getRight(), mContentView.getBottom());
            mIsMoving = false;
            //如果当前不是从初始化位置开始滚动的话,就不让用户拖拽
            if (getScrollY() == 0){
                mIsLayout = true;
            }
        } else if (event.getAction() == MotionEvent.ACTION_MOVE) {
            //如果当前的事件是我们要处理的事件时,比如现在的下拉,这时候,我们就不能让子控件来处理这个事件
            //这里就需要把它截获,不传给子控件,更不能让子控件消费这个事件
            //不然子控件的行为就可能与我们的相冲突
            int deltaY = (int) event.getY() - mTouchPoint.y;
//          deltaY = deltaY < 0 ? 0 : (deltaY > mHeaderView.getHeight() ? mHeaderView.getHeight() : deltaY);
            deltaY = deltaY < 0 ? 0 : (deltaY > mHeaderHeight ? mHeaderHeight : deltaY);
            if (deltaY > 0 && deltaY >= getScrollY()&& getScrollY() == 0) {
                onTouchEvent(event);
                return true;
            }
        }
        return super.onInterceptTouchEvent(event);
    }
}


版权声明:本文为博主原创文章,未经博主允许不得转载。

PullScrollView源码解析

标签:

原文地址:http://blog.csdn.net/crazy__chen/article/details/47143995

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!