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

SuperSwipeRefreshLayout源码分析

时间:2015-09-06 01:12:24      阅读:1403      评论:0      收藏:0      [点我收藏+]

标签:github   源码   

SuperSwipeRefreshLayout源码分析

源码及DEMO

特性

  • 支持下拉刷新和上拉加载更多
  • 非侵入式,对原来的ListView、RecyclerView没有任何影响,用法和SwipeRefreshLayout类似。
  • 可自定义头部View的样式,调用setHeaderView方法即可
  • 可自定义页尾View的样式,调用setFooterView方法即可
  • 支持RecyclerView,ListView,ScrollView,GridView等等。
  • 被包含的View(RecyclerView,ListView etc.)可跟随手指的滑动而滑动
    默认是跟随手指的滑动而滑动,也可以设置为不跟随:setTargetScrollWithLayout(false)
  • 回调方法更多
    比如:onRefresh() onPullDistance(int distance)和onPullEnable(boolean enable)
    开发人员可以根据下拉过程中distance的值做一系列动画。

思路

自定义一个ViewGroup,往其中添加headerView和footerView,然后在onMeasure中确定它们的大小,在onLayout中确定它们的位置。当子View滑动到最上方的时候,或者最下方的时候,拦截事件,自己处理onTouchEvent事件;其他情况,交给子View自己处理onTouchEvent事件。
基于以上分析:需要重点关注的方法有:

  • addView
  • onMeasure
  • getChildDrawingOrder
  • onLayout
  • isChildScrollToTop
  • isChildScrollToBottom
  • onInterceptTouchEvent
  • onTouchEvent

addView

主要是添加headerView和footerView,这是第一步。

addView(mHeadViewContainer);
...
addView(mFooterViewContainer);

onMeasure

重写ViewGroup的onMeasure方法:
注意:onMeasure方法中只决定子View的大小,并不决定View的位置

@Override
    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (mTarget == null) {
            ensureTarget();
        }
        if (mTarget == null) {
            return;
        }
        mTarget.measure(MeasureSpec.makeMeasureSpec(getMeasuredWidth()
                - getPaddingLeft() - getPaddingRight(), MeasureSpec.EXACTLY),
                MeasureSpec.makeMeasureSpec(getMeasuredHeight()
                        - getPaddingTop() - getPaddingBottom(),
                        MeasureSpec.EXACTLY));
        mHeadViewContainer.measure(MeasureSpec.makeMeasureSpec(
                mHeaderViewWidth, MeasureSpec.EXACTLY), MeasureSpec
                .makeMeasureSpec(mHeaderViewHeight, MeasureSpec.EXACTLY));
        mFooterViewContainer.measure(MeasureSpec.makeMeasureSpec(
                mFooterViewWidth, MeasureSpec.EXACTLY), MeasureSpec
                .makeMeasureSpec(mFooterViewHeight, MeasureSpec.EXACTLY));
        ...
        //查找在ViewGroup的index
        mHeaderViewIndex = -1;
        for (int index = 0; index < getChildCount(); index++) {
            if (getChildAt(index) == mHeadViewContainer) {
                mHeaderViewIndex = index;
                break;
            }
        }
        mFooterViewIndex = -1;
        for (int index = 0; index < getChildCount(); index++) {
            if (getChildAt(index) == mFooterViewContainer) {
                mFooterViewIndex = index;
                break;
            }
        }
    }

getChildDrawingOrder

重写ViewGroup的getChildDrawingOrder,由于HeaderView和FooterView刚开始是不显示的,分别隐藏在屏幕的上方和下方,因此,需要将它们两个的绘制顺序调整到最后。getChildDrawingOrder方法的含义是第i次应该绘制哪一个childView

    /**
     * 孩子节点绘制的顺序
     * 
     * @param childCount
     * @param i
     * @return
     */
    @Override
    protected int getChildDrawingOrder(int childCount, int i) {
        // 将新添加的View,放到最后绘制
        if (mHeaderViewIndex < 0 && mFooterViewIndex < 0) {
            return i;
        }
        if (i == childCount - 2) {
            return mHeaderViewIndex;
        }
        if (i == childCount - 1) {
            return mFooterViewIndex;
        }
        int bigIndex = mFooterViewIndex > mHeaderViewIndex ? mFooterViewIndex
                : mHeaderViewIndex;
        int smallIndex = mFooterViewIndex < mHeaderViewIndex ? mFooterViewIndex
                : mHeaderViewIndex;
        if (i >= smallIndex && i < bigIndex - 1) {
            return i + 1;
        }
        if (i >= bigIndex || (i == bigIndex - 1)) {
            return i + 2;
        }
        return i;
    }

onLayout

onMeasure方法中决定了子View的大小,onLayout决定了View的位置和大小
因此,我们可以通过重写onLayout方法。当拦截到事件后,我们可以根据滑动的距离,动态更新View的位置。所以,HeaderView和FooterView的layout跟滑动距离有关系。

    @Override
    protected void onLayout(boolean changed, int left, int top, int right,
            int bottom) {
        final int width = getMeasuredWidth();
        final int height = getMeasuredHeight();
        if (getChildCount() == 0) {
            return;
        }
        if (mTarget == null) {
            ensureTarget();
        }
        if (mTarget == null) {
            return;
        }
        int distance = mCurrentTargetOffsetTop + mHeadViewContainer.getHeight();
        if (!targetScrollWithLayout) {
            // 判断标志位,如果目标View不跟随手指的滑动而滑动,将下拉偏移量设置为0
            distance = 0;
        }
        final View child = mTarget;
        final int childLeft = getPaddingLeft();
        final int childTop = getPaddingTop() + distance - pushDistance;// 根据偏移量distance更新
        final int childWidth = width - getPaddingLeft() - getPaddingRight();
        final int childHeight = height - getPaddingTop() - getPaddingBottom();
        Log.d(LOG_TAG, "debug:onLayout childHeight = " + childHeight);
        child.layout(childLeft, childTop, childLeft + childWidth, childTop
                + childHeight);// 更新目标View的位置
        int headViewWidth = mHeadViewContainer.getMeasuredWidth();
        int headViewHeight = mHeadViewContainer.getMeasuredHeight();
        mHeadViewContainer.layout((width / 2 - headViewWidth / 2),
                mCurrentTargetOffsetTop, (width / 2 + headViewWidth / 2),
                mCurrentTargetOffsetTop + headViewHeight);// 更新头布局的位置
        int footViewWidth = mFooterViewContainer.getMeasuredWidth();
        int footViewHeight = mFooterViewContainer.getMeasuredHeight();
        mFooterViewContainer.layout((width / 2 - footViewWidth / 2), height
                - pushDistance, (width / 2 + footViewWidth / 2), height
                + footViewHeight - pushDistance);
    }

isChildScrollToTop

该方法可谓是下拉刷新的核心方法,如何判断目标View是否滑到顶点了呢?ListView和GridView的判断类似,而RecyclerView和ScrollView则与其不同。代码如下:

/**
     * 判断目标View是否滑动到顶部-还能否继续滑动
     * 
     * @return
     */
    public boolean isChildScrollToTop() {
        if (android.os.Build.VERSION.SDK_INT < 14) {
            if (mTarget instanceof AbsListView) {
                final AbsListView absListView = (AbsListView) mTarget;
                return !(absListView.getChildCount() > 0 && (absListView
                        .getFirstVisiblePosition() > 0 || absListView
                        .getChildAt(0).getTop() < absListView.getPaddingTop()));
            } else {
                return !(mTarget.getScrollY() > 0);
            }
        } else {
            return !ViewCompat.canScrollVertically(mTarget, -1);
        }
    }

isChildScrollToBottom

改方法是上拉加载更多的核心方法,要判断子View是否已经滑到底部,RecyclerView不同的LayoutManager的判断情况都不同。具体代码如下:

    /**
     * 是否滑动到底部
     * 
     * @return
     */
    public boolean isChildScrollToBottom() {
        if (isChildScrollToTop()) {
            return false;
        }
        if (mTarget instanceof RecyclerView) {//RecyclerView
            RecyclerView recyclerView = (RecyclerView) mTarget;
            LayoutManager layoutManager = recyclerView.getLayoutManager();
            int count = recyclerView.getAdapter().getItemCount();
            if (layoutManager instanceof LinearLayoutManager && count > 0) {
                LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager;
                if (linearLayoutManager.findLastCompletelyVisibleItemPosition() == count - 1) {
                    return true;
                }
            } else if (layoutManager instanceof StaggeredGridLayoutManager) {
                StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager) layoutManager;
                int[] lastItems = new int[2];
                staggeredGridLayoutManager
                        .findLastCompletelyVisibleItemPositions(lastItems);
                int lastItem = Math.max(lastItems[0], lastItems[1]);
                if (lastItem == count - 1) {
                    return true;
                }
            }
            return false;
        } else if (mTarget instanceof AbsListView) {
            final AbsListView absListView = (AbsListView) mTarget;
            int count = absListView.getAdapter().getCount();
            int fristPos = absListView.getFirstVisiblePosition();
            if (fristPos == 0
                    && absListView.getChildAt(0).getTop() >= absListView
                            .getPaddingTop()) {
                return false;
            }
            int lastPos = absListView.getLastVisiblePosition();
            if (lastPos > 0 && count > 0 && lastPos == count - 1) {
                return true;
            }
            return false;
        } else if (mTarget instanceof ScrollView) {
            ScrollView scrollView = (ScrollView) mTarget;
            View view = (View) scrollView
                    .getChildAt(scrollView.getChildCount() - 1);
            if (view != null) {
                int diff = (view.getBottom() - (scrollView.getHeight() + scrollView
                        .getScrollY()));
                if (diff == 0) {
                    return true;
                }
            }
        }
        return false;
    }

onInterceptTouchEvent

重写该方法,用于事件拦截的判断,该方法决定了事件到底交给谁处理
1.当return true时,表示ViewGroup自己来处理onTouchEvent事件,子View接收不到onTouchEvent事件
2.当return false时,表示ViewGroup不拦截事件,直接交给子View处理

我们不仅需要在点击时判断是否拦截事件,还需要在滑动过程中判断是否需要拦截事件,比如滑动的距离过小,则不需要拦截。

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        ensureTarget();

        final int action = MotionEventCompat.getActionMasked(ev);

        if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
            mReturningToStart = false;
        }
        if (!isEnabled() || mReturningToStart || mRefreshing || mLoadMore
                || (!isChildScrollToTop() && !isChildScrollToBottom())) {
            // 如果子View可以滑动,不拦截事件,交给子View处理-下拉刷新
            // 或者子View没有滑动到底部不拦截事件-上拉加载更多
            return false;
        }

        // 下拉刷新判断
        switch (action) {
        case MotionEvent.ACTION_DOWN:
            setTargetOffsetTopAndBottom(
                    mOriginalOffsetTop - mHeadViewContainer.getTop(), true);// 恢复HeaderView的初始位置
            mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
            mIsBeingDragged = false;
            final float initialMotionY = getMotionEventY(ev, mActivePointerId);
            if (initialMotionY == -1) {
                return false;
            }
            mInitialMotionY = initialMotionY;// 记录按下的位置

        case MotionEvent.ACTION_MOVE:
            if (mActivePointerId == INVALID_POINTER) {
                Log.e(LOG_TAG,
                        "Got ACTION_MOVE event but don‘t have an active pointer id.");
                return false;
            }

            final float y = getMotionEventY(ev, mActivePointerId);
            if (y == -1) {
                return false;
            }
            float yDiff = 0;
            if (isChildScrollToBottom()) {
                yDiff = mInitialMotionY - y;// 计算上拉距离
                if (yDiff > mTouchSlop && !mIsBeingDragged) {// 判断是否下拉的距离足够
                    mIsBeingDragged = true;// 正在上拉
                }
            } else {
                yDiff = y - mInitialMotionY;// 计算下拉距离
                if (yDiff > mTouchSlop && !mIsBeingDragged) {// 判断是否下拉的距离足够
                    mIsBeingDragged = true;// 正在下拉
                }
            }
            break;

        case MotionEventCompat.ACTION_POINTER_UP:
            onSecondaryPointerUp(ev);
            break;

        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            mIsBeingDragged = false;
            mActivePointerId = INVALID_POINTER;
            break;
        }

        return mIsBeingDragged;// 如果正在拖动,则拦截子View的事件
    }

onTouchEvent

在SuperSwipeRefreshLayout中,onTouchEvent只有当onInterceptTouchEvent返回true的时候才执行。它根据下拉或者上拉的距离,动态的修改headerView或footerView的位置,通过调用offsetTopAndBottom刷新onLayout方法。


    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        final int action = MotionEventCompat.getActionMasked(ev);

        if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
            mReturningToStart = false;
        }
        if (!isEnabled() || mReturningToStart
                || (!isChildScrollToTop() && !isChildScrollToBottom())) {
            // 如果子View可以滑动,不拦截事件,交给子View处理
            return false;
        }

        if (isChildScrollToBottom()) {// 上拉加载更多
            return handlerPushTouchEvent(ev, action);
        } else {// 下拉刷新
            return handlerPullTouchEvent(ev, action);
        }
    }

   private boolean handlerPullTouchEvent(MotionEvent ev, int action){
      ...
   }

   ...

More

你可能不注意的细节,但是有肯能引起奇怪的BUG

  • 不起眼的requestDisallowInterceptTouchEvent
    @Override
    public void requestDisallowInterceptTouchEvent(boolean b) {
        // Nope.
    }
  • MotionEventCompat.ACTION_POINTER_UP

总结

分析SuperSwipeRefreshLayout的源码,可以让我们明白一些事情:

  • 1.写自定义ViewGroup需要注意的onMeasure和onLayout
  • 2.事件拦截onInterceptTouchEvent的使用,到底什么时候该拦截,什么时候该交给子View处理。
    很多复杂的交互需求,也许能从onInterceptTouchEvent中找到你要的解决方案。
  • 3.如何判断一个ListView或RecyclerView或ScrollView是否已经滑动到底部或顶部。

    提示一句,SuperSwipeRefreshLayout是在读懂SwipeRefreshLayout源码的基础上写出来的~

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

SuperSwipeRefreshLayout源码分析

标签:github   源码   

原文地址:http://blog.csdn.net/nupt123456789/article/details/48225139

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