SwipeRefreshLayout进阶

SwipeRefreshLayout

SwipeRefreshLayout 是一个下拉刷新控件,几乎可以包裹一个任何可以滚动的内容(ListView GridView ScrollView RecyclerView),可以自动识别垂直滚动手势。使用起来非常方便。

但是如果直接采用原生的SwipeRefreshLayout,那么它的第一个子View必须是AdapterView(可以滚动的View)。现在有一种情况,当ListView没有数据时,我们通常会用一个EmptyView来提示用户。此时在SwipeRefreshLayout中需要有一个VIewGroup来包含ListView和一个EmptyView。

监听失败

布局文件:


    
        
            
            
        
    

可以看到SwipeRefreshLayout的第一个直接子View并不是ListView,这样就会导致不好使用效果:ListView无法下拉,也就是当ListView没有在最顶部时,无法显示上面被屏幕遮挡的数据,下拉只会出发刷新。

代码:

        mHandler = new Handler();
        mListView = (ListView) findViewById(R.id.id_list_view_child_test);
        mData = new ArrayList<>();
        for(int i = 20; i > 0; i--) {
            mData.add("This is item " + i);
        }
        mAdapter = new ArrayAdapter(
                this,
                android.R.layout.simple_list_item_1,
                mData
        );
        mListView.setAdapter(mAdapter);

        mSwipeRefresh = (SwipeRefreshLayout) findViewById(R.id.id_swipe_refresh_child_test);
        mSwipeRefresh.setColorSchemeResources(
                android.R.color.holo_blue_light,
                android.R.color.holo_green_light,
                android.R.color.holo_orange_light,
                android.R.color.holo_red_light
        );
        mSwipeRefresh.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
            @Override
            public void onRefresh() {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        double k = Math.random();
                        int index = (int) (k * 100);
                        mData.add(0, "This is item " + index);
                        mHandler.postDelayed(new Runnable() {
                            @Override
                            public void run() {
                                mAdapter.notifyDataSetChanged();
                                mSwipeRefresh.setRefreshing(false);
                            }
                        }, 3000);
                    }
                }).start();
            }
        });

上述代码是SwipeRefreshLayout基础用法。
效果:

下拉刷新无效果.gif

自定义SwipeRefreshLayout

解决上述问题的办法只有自定义SwipeRefreshLayout。

想法

查看文档发现,SwipeRefreshLayout继承ViewGroup。所以时间拦截一定在onInterceptTouchEvent()方法中。

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        ensureTarget();
        final int action = MotionEventCompat.getActionMasked(ev);
        if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
            mReturningToStart = false;
        }
        if (!isEnabled() || mReturningToStart || canChildScrollUp()
                || mRefreshing || mNestedScrollInProgress) {
            // Fail fast if we're not in a state where a swipe is possible
            return false;
        }
        ...
    }

这里复制了一些关键代码,可以看到首先通过ensureTarget()方法给变量mTarget赋值。

private void ensureTarget() {
        // Don't bother getting the parent height if the parent hasn't been laid
        // out yet.
        if (mTarget == null) {
            for (int i = 0; i < getChildCount(); i++) {
                View child = getChildAt(i);
                if (!child.equals(mCircleView)) {
                    mTarget = child;
                    break;
                }
            }
        }
    }

这里默认的认为SwipeRefreshLayout的第一个直接子View就是需要监听的View,而没有判断到底是否属于可滑动控件。所以有个想法直接覆写该方法,改变默认的方式,用自己的方法来赋值给mTarget变量。但是该方法是私有保护的,所以无法改变。再往下看onInterceptTouchEvent()方法,注意到if (!isEnabled() || mReturningToStart || canChildScrollUp() || mRefreshing || mNestedScrollInProgress)要让事件不被拦截,onInterceptTouchEvent必须返回false,所以这里观察到一个很关键的方法canChildScrollUp()

public boolean canChildScrollUp() {
        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 ViewCompat.canScrollVertically(mTarget, -1) || mTarget.getScrollY() > 0;
            }
        } else {
            return ViewCompat.canScrollVertically(mTarget, -1);
        }
    }

注意ViewCompat.canScrollVertically()就是用来判断mTarget是否还可以垂直滚动。所以最终的方案就是重新声明一个变量,作为自定义SwipeRefreshLayout的监听对象,然后创建该变量的setter方法,并且利用ViewCompat.canScrollVertically()覆写canChildScrollUp()

实践

public class SwipeRefreshLayout extends android.support.v4.widget.SwipeRefreshLayout{
    private View mView;
    public SwipeRefreshLayout(Context context) {
        super(context);
    }

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


    /**
     * 设定监听View,必须是AdapterView
     * @param view
     */
    public void setTarget(View view) {
        mView = view;
    }
    @Override
    public boolean canChildScrollUp() {
        //判断监听的View是否是可滑动View,
        //如果为true,那么根据ViewCompat.canScrollVertically返回的值来决定是否拦截时间
        if(mView instanceof AbsListView)
            return canChildScrollUp(mView);
        //否则返回true,拦截事件,开启刷新动画
        else
            return true;
    }

    /**
    * 判断垂直方向是否能滚动
    **/
    public boolean canChildScrollUp(View view) {
        return ViewCompat.canScrollVertically(view, -1);
    }
}

布局文件和上面一样,只不过用了自定义的SwipeRefreshLayout控件。在Activity.java中,除了SwipeRefreshLayout的基础用法外,还要调用定义的setter方法,给自定义的SwipeRefreshLayout设置监听对象。

mRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.id_swipe_refresh_custom);
    mRefreshLayout.setColorSchemeResources(
            android.R.color.holo_blue_light,
            android.R.color.holo_green_light,
            android.R.color.holo_orange_light,
            android.R.color.holo_red_light
    );
    mListView = (ListView) findViewById(R.id.id_list_view_custom);
    mRefreshLayout.setTarget(mListView);

效果:

SwipeRefreshLayout自定义动画效果消失.gif

问题

从上面的效果看到,当ListView的Adapter没有数据时,正常显示了布局文件中的EmptyView。但是当下拉刷新时,SwipeRefreshLayout的动画效果非常不好,貌似被隐藏了一样。没办法只有通过谷歌来解决。
发现一个帖子Android - SwipeRefreshLayout with empty textview

上面回答者讲到,SwipeRefreshLayout必须有一个AdapterView才可以正常工作。这就联想到了AdapterView.setEmptyView()方法当给定的参数不是null时,会把自己Visibility属性设置为gone

setEmptyView()

public void setEmptyView(View emptyView) {
        mEmptyView = emptyView;
        // If not explicitly specified this view is important for accessibility.
        if (emptyView != null
                && emptyView.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
            emptyView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
        }
        final T adapter = getAdapter();
        final boolean empty = ((adapter == null) || adapter.isEmpty());
        updateEmptyStatus(empty);
    }

可以看到决定ListView的Visibility属性有两个关键条件,一个是adapter不能为null,另外adapter不能没有数据源。
updateEmptyStatus

private void updateEmptyStatus(boolean empty) {
        if (isInFilterMode()) {
            empty = false;
        }
        if (empty) {
            if (mEmptyView != null) {
                mEmptyView.setVisibility(View.VISIBLE);
                setVisibility(View.GONE);
            } else {
                // If the caller just removed our empty view, make sure the list view is visible
                setVisibility(View.VISIBLE);
            }

updateEmptyStatus()方法根据setEmptyView()给定的参数empty来设置ListView的Visibility属性。

由此可以推断,解决该现象的方法就是在调用setEmptyView()方法后设定ListView的Visibility属性为View.VISIBLE。或者将空数据源的adapter设置给ListView时也初始化ListView的Visibility属性为View.VISIBLE。

最终效果.gif

INVISIBLE和GONE区别

大部分控件都有visibility这个属性,其属性有3个分别为“visible ”、“invisible”、“gone”。主要用来设置控制控件的显示和隐藏。

  • visible,设置View可见
  • invisible,设置View不可见
  • gone,隐藏View

而INVISIBLE和GONE的主要区别是:当控件visibility属性为INVISIBLE时,界面保留了view控件所占有的空间;而控件属性为GONE时,界面则不保留view控件所占有的空间。也就是说当一个ViewGroup的ChildView的visibility被设置成gone时,该ChildView不在ViewGroup的ViewTree中。

参考

SwipeRefreshLayout与RecyclerView的巧夺天工

SwipeRefreshLayout的学习

分析SwipeRefreshLayout源码

SwipeRefreshLayout

解决SwipeRefreshLayout结合ListView EmptyView使用不起作用的问题

Android中visibility属性VISIBLE、INVISIBLE、GONE的区别

你可能感兴趣的:(SwipeRefreshLayout进阶)