需求

昨天在网上找了好久都没有找到一个合适的可以下拉刷新的scrollView代码,不过github上有一个开源项目(Android-PullToRefresh)写得很不错,但好像必须是AbsListView的子类才能使用,当然自己写的这个和该项目的实现原理是一样的。想想自己学Android已经快一年了,总得努力努力自己搞搞,所以今天上午就查阅资料看博客开始写,下面就贴贴自己的思路和代码和下载地址。如果喜欢请star,如果觉得有纰漏或是有更好的点子请及时评论。

实现

咋们还是按思路一步一步往下走..

1.需要编写头部文件和布局,这个人们都知道,继承RelativeLayout,然后提供三个设置状态的方法,这里就不粘代码了,有需要的可以下载我的源码,下面开始干重要活…

2.extends ScrollView,将头部View,加到容器布局,设置头部的magrin值将头部headView隐藏

    public class RefreshScrollView extends ScrollView {
    private Context mContext;
    // 容器布局,因为scroll只允许嵌套一个子布局
    private LinearLayout mScrollContainer = null;
    // 头部刷新的View
    private ScrollViewHeader mRefreshHeaderView;
    // 头部的高度
    private int mHeaderViewHeight;
    private final static float OFFSET_RADIO = 2.2f; // support iOS like pull

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

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

    public CopyOfRefreshScrollView(Context context, AttributeSet attrs,
            int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.mContext = context;
        initView();  
    }
    /**
     * 初始化view
     */
    private void initView() {
        // 添加头部布局到容器
        mRefreshHeaderView = new ScrollViewHeader(mContext);
        LinearLayout.LayoutParams headerViewParams = new LinearLayout.LayoutParams(
                LinearLayout.LayoutParams.MATCH_PARENT,
                LinearLayout.LayoutParams.WRAP_CONTENT);
        mScrollContainer = new LinearLayout(mContext);
        mScrollContainer.addView(mRefreshHeaderView, headerViewParams);
        mScrollContainer.setOrientation(LinearLayout.VERTICAL);

        addView(mScrollContainer);
        // 必须要测量一下头部view,要不然getMeasuredHeight()是0,
        // 当然mRefreshHeaderView.getViewTreeObserver().addOnGlobalLayoutListener也可以
        measureView(mRefreshHeaderView);
        // 获取头部刷新headView的高度,设置margin让头部隐藏
        mHeaderViewHeight = mRefreshHeaderView.getMeasuredHeight();
        mRefreshHeaderView
                .updateMargin(-mRefreshHeaderView.getMeasuredHeight());
    }
    /**
     * 通知父布局,占用的宽,高
     * 
     * @param view
     */
    private void measureView(View view) {
        ViewGroup.LayoutParams p = view.getLayoutParams();
        if (p == null) {
            p = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.WRAP_CONTENT);
        }
        int width = ViewGroup.getChildMeasureSpec(0, 0, p.width);
        int height;
        int tempHeight = p.height;
        if (tempHeight > 0) {
            height = MeasureSpec.makeMeasureSpec(tempHeight,
                    MeasureSpec.EXACTLY);
        } else {
            height = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
        }
        view.measure(width, height);
    }

3.这个时候我们可以新建一个activity试试看效果了,但一运行起来果断报异常了:java.lang.IllegalStateException:ScrollView can host only one direct child,这个没办法了总得解决一下吧。那么就新建一个方法setContainerView(View child) 吧?那如果非要在布局文件里面添加呢怎么办呢?所以只能够阅读源码了,发现scrollView在加载仅有的一个childView的时候会调用addView(View child, android.view.ViewGroup.LayoutParams params)方法,这下好了只要override这个方法就可以了,再次运行爽了。

    /**
     * 其他的addView 的方法也要override,暂且放一边
     */
    @Override
    public void addView(View child, android.view.ViewGroup.LayoutParams params) {
        // 2.重载addView(View child, android.view.ViewGroup.LayoutParams params)方法
        // 解决 java.lang.IllegalStateException
        // 因为scrollView只许添加一个子布局,如果在xml中添加子布局,那么肯定会throw
        // java.lang.IllegalStateException:ScrollView can host only one direct child
        this.removeAllViews();
        mScrollContainer.addView(child, params);
        super.addView(mScrollContainer, mScrollContainer.getLayoutParams());
    }

4.处理最重要的方法onTouchEvent的触摸事件

    // 4.处理触摸事件,override onTouchEvent
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if (mLastY == -1) {
            mLastY = ev.getRawY();
        }
        switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mLastY = ev.getRawY();
            break;
        case MotionEvent.ACTION_MOVE:
            final float deltaY = ev.getRawY() - mLastY;
            mLastY = ev.getRawY();
            if(deltaY < 0 && mRefreshing){
                // 如果往上滑并且是刷新的状态就不除阻力,要不然当头部出来的时候向上滑动怪怪的
                updateHeader(deltaY);
            }else if (getScrollY() == 0
                && (deltaY > 0 || mRefreshHeaderView.getTopMargin() > -mHeaderViewHeight)) {
                // 更新headerView的高度,同时更改状态 
                updateHeader(deltaY/OFFSET_RADIO);
                return true;
            }
            break;
        default:  
            //这里没有使用action_up的原因是,可能会受到viewpager的影响接收到action_cacel事件  
            if (getScrollY() == 0) {  
                if (mRefreshHeaderView.getTopMargin() > 0 && mEnableRefresh && !mRefreshing) 
                {  
                    mRefreshing = true;  
                    mRefreshHeaderView.setState(ScrollViewHeader.STATE_REFRESHING);
                    // 刷新加载中,给调用者监听  
                    if(mListener!=null){
                        mListener.onRefresh();
                    }
                } 
                //重置RefreshHeaderView的高度 
                resetHeaderView();  
            }  
            break;
        }
        return super.onTouchEvent(ev);
    }

    /** 
     * 更新headerview的高度,同时更改状态 
     * @param deltY 
     */  
    public void updateHeader(float deltY) {  
        int currentMargin = (int) (mRefreshHeaderView.getTopMargin() + deltY);  
        mRefreshHeaderView.updateMargin(currentMargin);  
        if(mEnableRefresh && !mRefreshing) {
            if (currentMargin > mHeaderViewHeight/5) { 
                // 头部全部出来了,就显示松开刷新
                mRefreshHeaderView.setState(ScrollViewHeader.STATE_READY);  
            } else { 
                // 否则显示下拉加载更多
                mRefreshHeaderView.setState(ScrollViewHeader.STATE_NORMAL);  
            }  
        }  
    }  

    /** 
     * 重置RefreshHeaderView的高度 
     */  
    public void resetHeaderView() {
        int margin = mRefreshHeaderView.getTopMargin(); 
        if(margin == -mHeaderViewHeight) {  
            return ;  
        }  
        if(margin < 0 && mRefreshing) {  
            // 当前已经在刷新,又重新进行拖动,但未拖满,不进行操作  
            return ;  
        }  
        int finalMargin = 0;  
        if(margin <= 0 && !mRefreshing) {  
            finalMargin = mHeaderViewHeight;  
        }  
        // 松开刷新,或者下拉刷新,又松手,没有触发刷新  
        // mAssistScroller辅助滚动 ,得要Override computeScroll()
        // 如果头部的状态已经是隐藏的就没必要再去滚动了
        if(this.getScrollY()<mHeaderViewHeight){
            mAssistScroller.startScroll(0, -margin, 0, finalMargin + margin, SCROLL_DURATION);  
            invalidate();  
        } 
    }

    @Override  
    public void computeScroll() {  
        if(mAssistScroller.computeScrollOffset()) {  
            mRefreshHeaderView.updateMargin(-mAssistScroller.getCurrY());  
            //继续重绘  
            postInvalidate();  
        }  
        super.computeScroll();  
    }

5.到第四步为止就基本大功告成了,最后就差几个提供给调用者的公共方法和刷新的监听。

     /**
     * 设置刷新的监听
     * @param listener
     */
    public void setOnRefreshScrollViewListener(OnRefreshScrollViewListener listener){
        this.mListener = listener;
    }
    public interface OnRefreshScrollViewListener {  
        public void onRefresh();  
    }
    /**
     * 加载完成
     */
    public void onLoadComplete(){
        if(mRefreshing) {  
            mRefreshing = false;  
            resetHeaderView();  
        } 
    }

    /** 
     * 设置scroll是否可以刷新 
     *  
     * @param enableRefresh 
     */  
    public void setEnableRefresh(boolean enableRefresh) {  
        this.mEnableRefresh = enableRefresh;  
    }