SwipeRefreshLayout进阶

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的区别

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容