Android - SwipeRefreshLayout与空文本视图

37

我已经在我的应用程序中实现了SwipeRefreshLayout,但它只能容纳一个直接子元素,这个子元素应该是listview。我正在尝试弄清如何将一个空的textview添加到下面工作的XML文件中:

<android.support.v4.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/swipe_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <ListView
        android:id="@+id/listViewConversation"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:dividerHeight="1dp" />

</android.support.v4.widget.SwipeRefreshLayout>

将其包裹在线性/相对布局中会导致错误,因为当你想向上滑动列表视图时,列表视图将总是更新。我能想到的一种方法是通过编程来实现,但我认为那不是最好的选择。

您可以使用此教程了解如何实现: 下拉刷新指南

所以基本上一切都正常工作,但我想添加一个空视图,以便在列表视图为空时显示消息。


1
尝试使用FrameLayout - CommonsWare
15个回答

59

我不喜欢只能有一个孩子的限制。此外,SwipeRefreshLayout当前的实现对于ScrollView、ListView和GridView有一个硬编码的“神奇”处理方式,仅在视图是您自己视图的直接子视图时触发。

话虽如此,好消息是它是开源的,因此您可以复制代码并根据需要进行适应,或者您可以像我一样:

使用两个不同的SwipeRefreshLayout,一个用于Empty视图,另一个用于ListView。

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.tobrun.example.swipetorefresh.MainActivity">

    <android.support.v4.widget.SwipeRefreshLayout
        android:id="@+id/swipeRefreshLayout_listView"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ListView
            android:id="@+id/listView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

    </android.support.v4.widget.SwipeRefreshLayout>

    <android.support.v4.widget.SwipeRefreshLayout
        android:id="@+id/swipeRefreshLayout_emptyView"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ScrollView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:fillViewport="true">

            <TextView
                android:id="@+id/emptyView"
                android:text="@string/empty"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:gravity="center" />

        </ScrollView>

    </android.support.v4.widget.SwipeRefreshLayout>


</FrameLayout>

然后告诉您的列表视图,空列表视图是空视图的滑动刷新布局。

现在当您有数据时,列表视图将自动隐藏空的刷新布局,并在列表为空时显示。

列表的滑动刷新布局不应该接收触摸事件,因为列表是隐藏的。

祝好运。


5
这是一些示例代码:https://github.com/AndroidExamples/SwipeRefreshLayout-ListViewExample。 - Tobrun
你的解决方案非常好(与所有其他建议相反),但我必须完全按照你的布局来操作(将滑动布局包装在相对布局中)。谢谢! - javaxian
@javaxian 不用谢。那个参考实现不是我的,我只是找到了它,因为懒得提供自己的实现,所以我链接了它。你不需要使用相对布局,可以使用帧布局或其他布局。重要的是SwipeRefreshLayout仅对其直接子项进行操作,这意味着您需要两个:一个用于listview,一个用于空视图。如果可以使用它们,FrameLayout在性能方面更有效率。 - Daniele Segato
我可以在这里问一个蠢问题吗:为什么ScrollView会使空视图的SwipeRefreshLayout正常工作,而没有它则不行? - passer
@passer 你所说的“work”是什么意思?你的问题没有意义。你可能是想用*listview代替scrollview。如果是这样,我可以回答:当你将一个空视图设置为listview时,当它没有任何项时,listview会自动隐藏/显示(setVisibility VISIBLE / GONE)空视图(反之亦然)。如果空视图是滑动,则它会在布局中变为可见,并因此对触摸做出反应。 - Daniele Segato
显示剩余6条评论

31

无需任何解决方法。

您可以直接使用此视图层次结构:

    <FrameLayout ...>

        <android.support.v4.widget.SwipeRefreshLayout ...>

            <ListView
                android:id="@android:id/list" ... />
        </android.support.v4.widget.SwipeRefreshLayout>

        <TextView
            android:id="@android:id/empty" ...
            android:text="@string/empty_list"/>
    </FrameLayout>

然后,在代码中,您只需要调用:

_listView.setEmptyView(findViewById(android.R.id.empty));

就这样。


编辑:如果您希望在显示空视图时仍然能够进行下拉刷新,请避免隐藏ListView,您可以使用具有此功能的自定义ListView:

@Override
  public void setVisibility(final int visibility)
    {
    if(visibility!=View.GONE||getCount()!=0)
      super.setVisibility(visibility);
    }

我写的解决方案可以让下拉刷新一直显示,无论你显示多少项。

当然,如果你真想隐藏ListView,就应该修改代码。也许加上"setVisibilityForReal(...)"就行了 :)


1
如果您在添加SwipeRefreshLayout之前使用LinearLayout,则需要切换到FrameLayout或RelativeLayout并不明显。结合上面关于使用第二个SwipeRefreshLayout的评论,可以解决这个问题(如果您还需要刷新空列表)。 - Travis Castillo
1
但我无法向下滑动这个空视图以刷新。我能得到解决方案吗?谢谢你提前。@androiddeveloper - Ashokchakravarthi Nagarajan
@androiddeveloper:很抱歉要说,但是在使用您的解决方案后,当列表中没有数据时,我无法进行滑动并再次刷新它。我的实现方式与您的完全相同。 - Bhavik Mehta
7
如果您希望能够通过下拉刷新空视图,您需要将包含ListView和EmptyView的FrameLayout放置在SwipeRefreshLayout内。此外,请在FrameLayout中设置android:clickable=”true”。 - antoinem
2
@androiddeveloper,恕我直言,当数据无法加载时,由于没有网络或服务器宕机,这确实是有意义的。因此,可以刷新空视图。非常感谢您提到@antoinem的解决方案。之前我也尝试过相同的方法,但android:clickable=true是关键,而且确实有效。但是,这在标准的SwipeRefreshLayout中不起作用!必须使用https://github.com/googlesamples/android-SwipeRefreshMultipleViews。 - Muhammad Babar
显示剩余11条评论

11

实际上,你所缺失的就是将空的TextView用可滚动的容器包裹起来 - 例如ScrollView。关于详情,请查看SwipeRefreshLayout.canChildScrollUp()方法及其使用。

无论如何,回到重点。这里有一个成功的实现:

activity_some.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/swipe_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:measureAllChildren="true">

        <WebView
            android:id="@+id/webview"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

        <include layout="@layout/empty" />

    </FrameLayout>

</android.support.v4.widget.SwipeRefreshLayout>

你的empty.xml基本上可以是任何你希望使用ScrollView包装的内容。

empty.xml

<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/empty"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fillViewport="true">

    <TextView
        android:text="Nothing here..."
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</ScrollView>

现在为了解决著名的SwipeRefreshLayout刷新仅在顶部时的问题,需要根据需要切换SwipeRefreshLayout(针对特定的Fragment):

private ViewTreeObserver.OnScrollChangedListener mOnScrollChangedListener;

@Override
public void onStart() {
    super.onStart();

    mOnScrollChangedListener = new ViewTreeObserver.OnScrollChangedListener() {
        @Override
        public void onScrollChanged() {
            int scrollY = mWebView.getScrollY();
            if (scrollY == 0)
                swipeLayout.setEnabled(true);
            else
                swipeLayout.setEnabled(false);

        }
    };
    swipeLayout.getViewTreeObserver().addOnScrollChangedListener(mOnScrollChangedListener);
}

@Override
public void onStop() {
    swipeLayout.getViewTreeObserver().removeOnScrollChangedListener(mOnScrollChangedListener);
    super.onStop();
}

就是这样了!希望能有所帮助! ;)

顺便问一下,为什么你要这样使用 SwipeRefreshLayoutFrameLayout?因为这样你可以做平滑的过渡动画,比如 交叉淡入淡出 效果,并且你的任何一个 状态 视图都可以被 可滑动的(如果你想要一个统一的 获取/刷新/重试 机制)。


1
谢谢,我最终采用了你的解决方案。不过我想补充一点观察:关键是ScrollView必须是SwipeRefreshLayout的子级。在我的情况下,我最初只尝试将空视图(与SwipeRefreshLayout类似的兄弟视图)包装在ScrollView中,但这并不足够。 - Bianca Daniciuc
你应该得到更多的赞。值得一提的是,大家不要忘记在ScrollView中使用android:fillViewport="true" - saiyancoder

8
这是我的做法: 除非我的listView在顶部,否则我禁用了下拉刷新的功能。
mBookedProductsListView.setOnScrollListener(new AbsListView.OnScrollListener() {
        @Override
        public void onScrollStateChanged(AbsListView absListView, int i) {
        }
        @Override
        public void onScroll(AbsListView absListView, int firstVisibleItem, int visibleItemCount,     int totalItemCount) {
            if (firstVisibleItem == 0)
                mSwipeRefreshLayout.setEnabled(true);
            else
                mSwipeRefreshLayout.setEnabled(false);
        }
    });

我的XML:

<?xml version="1.0" encoding="utf-8"?>

<android.support.v4.widget.SwipeRefreshLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/swipeRefreshLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >

<RelativeLayout
        android:id="@+id/productListLL"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        >

    <EditText
            android:id="@+id/search"
            android:layout_width="match_parent"
            android:layout_height="40dp"
            android:layout_alignParentTop="true"
            android:background="#a4aeb8"
            android:drawableRight="@drawable/search_icon"
            android:paddingRight="15dp"
            android:textColor="@android:color/white"
            android:paddingLeft="15dp"
            android:hint="Rechercher"
            android:textColorHint="@android:color/white"
            android:inputType="textCapWords|textNoSuggestions"
            />

    <include android:layout_width="match_parent" android:layout_height="wrap_content" layout="@layout/filter_by_categories_buttons" android:id="@+id/categoriesTree" android:layout_below="@id/search" />

    <ListView
            android:id="@+id/productListLV"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:divider="@drawable/product_listview_divider"
            android:dividerHeight="1dp"
            android:scrollbars="none"
            android:overScrollMode="never"
            android:choiceMode="multipleChoiceModal"
            android:background="#e7eaef"
            android:visibility="gone"
            android:layout_below="@id/categoriesTree"
            />

</RelativeLayout>

</android.support.v4.widget.SwipeRefreshLayout>

运作得十分顺畅。

6
我遇到了相同的问题。很烦人的是,SwipeRefreshLayout 只能有一个 AdpaterView 子控件。
如果为 AdpaterView 设置了空视图,则在没有数据时它将被隐藏。然而,SwipeRefreshLayout 需要一个 AdpaterView 才能工作。因此,我扩展了AdpaterView,使其即使为空仍然显示出来。
@Override
public void setVisibility(int visibility) {
    if (visibility == View.GONE && getCount() == 0) {
        return;
    }
    super.setVisibility(visibility);
}

也许你需要将适配器视图的背景设置为透明。但在我的情况下,不需要这样做,因为空视图只是一个简单的文本视图。

只是为了澄清,我相信SwipeRefreshLayout可以包含ScrollView子元素以及AdapterViews。 - Justin Pollard

5
基于这里的一些答案和SwipeRefreshLayout的源代码,我已经创建了一个子类来专门处理在容器中同时拥有RecyclerView(或ListView)和“空”视图的情况。
它期望一个这样的布局:
<SwipeRefreshLayoutWithEmpty ...>
  <FrameLayout ...>
    <TextView android:text="List is Empty" ...>
    <RecyclerView ...>
  </FrameLayout>
</SwipeRefreshLayoutWithEmpty>

代码如下:
public class SwipeRefreshLayoutWithEmpty extends SwipeRefreshLayout {
    private ViewGroup container;

    public SwipeRefreshLayoutWithEmpty(Context context) {
        super(context);
    }

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

    @Override
    public boolean canChildScrollUp() {
        // The swipe refresh layout has 2 children; the circle refresh indicator
        // and the view container. The container is needed here
        ViewGroup container = getContainer();
        if (container == null) {
            return false;
        }

        // The container has 2 children; the empty view and the scrollable view.
        // Use whichever one is visible and test that it can scroll
        View view = container.getChildAt(0);
        if (view.getVisibility() != View.VISIBLE) {
            view = container.getChildAt(1);
        }

        return ViewCompat.canScrollVertically(view, -1);
    }

    private ViewGroup getContainer() {
        // Cache this view
        if (container != null) {
            return container;
        }

        // The container may not be the first view. Need to iterate to find it
        for (int i=0; i<getChildCount(); i++) {
            if (getChildAt(i) instanceof ViewGroup) {
                container = (ViewGroup) getChildAt(i);

                if (container.getChildCount() != 2) {
                    throw new RuntimeException("Container must have an empty view and content view");
                }

                break;
            }
        }

        if (container == null) {
            throw new RuntimeException("Container view not found");
        }

        return container;
    }
}

完整代码片段:https://gist.github.com/grennis/16cb2b0c7f798418284dd2d754499b43

2

如果你还在遇到这个问题,这里有一个简洁的解决方案!不需要再创建另一个 SwipeRefreshLayout

empty view 包装到一个 ScrollView 中。将 AdapterViewScrollView 都粘合到一个 ViewGroup 中,并将其放入 SwipeRefreshLayout 中。

示例:

<android.support.v4.widget.SwipeRefreshLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ListView
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

        <ScrollView
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <TextView
                android:layout_width="250dp"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:gravity="center" />

        </ScrollView>

    </FrameLayout>

</android.support.v4.widget.SwipeRefreshLayout>

编辑

这里有一种更简单的方法。

检查您的列表是否为空。如果是,则使其不可见。示例:

if(lst.size() > 0) {
 mRecyclerView.setVisibility(View.VISIBLE);
//set adapter
}else
{
  mRecyclerView.setVisibility(View.INVISIBLE);
 findViewById(R.id.txtNodata).setVisibility(View.VISIBLE);
}

这将使得mRecyclerView仍然存在,即使没有数据,所有的滑动操作都可以正常工作!


2
你可以在SwipeRefreshLayout中使用NestedScrollView,但只能有一个容器。以下是一些错误的用法列表:mRecyclerView.setNestedScrollingEnabled(false);,这可能会导致问题。"最初的回答"。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    <!-- some toolbar -->

    <android.support.v4.widget.SwipeRefreshLayout
            android:id="@+id/conversationFragment_swipeRefresh"
            android:layout_alignParentBottom="true"
            android:layout_below="@+id/some_toolbar"
            android:layout_width="match_parent"
            android:layout_height="match_parent">

        <android.support.v4.widget.NestedScrollView
                android:layout_width="match_parent"
                android:layout_height="match_parent">

            <FrameLayout
                    android:layout_width="match_parent"
                    android:layout_height="match_parent">

                <TextView
                        android:id="@+id/conversationFragment_noResultText"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        android:text="@string/FragmentConversations_empty"
                        android:layout_centerHorizontal="true" />

                <android.support.v7.widget.RecyclerView
                        android:id="@+id/conversationFragment_recyclerView"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent" />

            </FrameLayout>

        </android.support.v4.widget.NestedScrollView>

    </android.support.v4.widget.SwipeRefreshLayout>

</RelativeLayout>

2

我已经尝试过以下方法。在空视图和列表的情况下,滑动将刷新数据,并且快速滚动也可以正常工作,不需要在活动中进行任何代码更改。 我已经在列表视图之前放置了空视图,并将其可见性标记为gone。 然后在SwipeToRefresh块内放置了列表视图。 示例代码 -

<FrameLayout 
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >

    <TextView
        android:id="@+id/tv_empty_text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="32dp"
        android:gravity="center_horizontal"
        android:text="No item found"
        android:visibility="gone"/>


    <android.support.v4.widget.SwipeRefreshLayout 
        android:id="@+id/swipe"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ListView
            android:id="@+id/lv_product"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

    </android.support.v4.widget.SwipeRefreshLayout>


</FrameLayout>

谢谢。这个可行。你必须使用RelativeLayout或者FrameLayout。如果你使用LinearLayout,它就不会起作用。 - Drew Szurko

1
为什么不将所有内容包装在SwipeRefreshLayout中?
<android.support.v4.widget.SwipeRefreshLayout 
    android:id="@+id/swipe"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <ListView
        android:id="@android:id/list"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="center_horizontal"/>

    <RelativeLayout
        android:id="@android:id/empty"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
        ...
    </RelativeLayout>
</LinearLayout>
</android.support.v4.widget.SwipeRefreshLayout>

2
是的,我也认为那样会起作用,但如果你这样做,就会出现一些问题。当向下滑动然后再向上滑动时,它会开始刷新。因此,我建议将ListView放在SwipeRefreshLayout的直接子级中。 - Kevin van Mierlo
这个配置对我也失败了。滚动和刷新没有按预期工作,有时我无法将ListView滚动回顶部,因为SwipeRefreshLayout想要控制。即使在ListView未滚动到顶部时添加了禁用SwipeRefreshLayout的代码,这种情况仍然会发生。 - David Rector

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接