美文网首页Android 入门进阶
SwipeRefreshLayout进阶

SwipeRefreshLayout进阶

作者: 狮_子歌歌 | 来源:发表于2016-11-05 17:51 被阅读661次

    SwipeRefreshLayout

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

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

    监听失败

    布局文件:

    <LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <android.support.v4.widget.SwipeRefreshLayout
            android:id="@+id/id_swipe_refresh_child_test"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
            <FrameLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent">
                <ListView
                    android:id="@+id/id_list_view_child_test"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"/>
                <ImageView
                    android:id="@+id/id_img_empty_view"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"/>
            </FrameLayout>
        </android.support.v4.widget.SwipeRefreshLayout>
    </LinearLayout>
    

    可以看到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<String>(
                    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进阶

        本文链接:https://www.haomeiwen.com/subject/kwlguttx.html