web-dev-qa-db-fra.com

Android - SwipeRefreshLayout avec textview vide

J'ai implémenté SwipeRefreshLayout dans mon application, mais elle ne peut contenir qu'un seul enfant direct, qui devrait être la liste. J'essaie de comprendre comment ajouter une vue texte vide au fichier XML de travail suivant:

<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>

Le placer dans une disposition linéaire/relative le rend bogué, car la vue liste sera toujours mise à jour lorsque vous souhaitez faire défiler la liste. Je peux penser à cela par programmation, mais je suppose que ce n’est pas la meilleure option.

Vous pouvez apprendre à l'implémenter en utilisant ce tutoriel: Balayez pour actualiser GUIDE

Donc, fondamentalement, tout fonctionne bien, mais j'aimerais ajouter une vue vide qui affiche un message lorsque la liste est vide.

30
Niels

La limitation à un seul enfant ne me plaisait pas ... De plus, l'implémentation actuelle de SwipeRefreshLayout a une gestion "magique" codée en dur pour ScrollView, ListView et GridView qui ne se déclenche que si la vue est l'enfant direct de votre propre vue.

Cela dit, la bonne nouvelle, c’est qu’il est open source, vous pouvez donc copier le code et vous adapter à vos besoins ou bien faire ce que j’ai fait:

Utilisez deux DIFFERENT SwipeRefreshLayout, un pour la vue vide et un pour le 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>

Indiquez ensuite à votre liste que la vue liste vide est la disposition d'actualisation par balayage de la vue vide.

Désormais, la présentation d'actualisation vide sera automatiquement masquée par votre vue Liste lorsque vous aurez des données et sera affichée lorsque la liste sera vide.

La présentation d'actualisation par balayage de la liste ne doit pas recevoir d'événements tactiles car la liste est masquée.

Bonne chance.

48
Daniele Segato

Il n'y a pas besoin de solution de contournement.

Vous pouvez simplement utiliser cette hiérarchie de vues:

    <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>

Ensuite, dans le code, vous appelez simplement:

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

C'est tout.


ÉDITER: Si vous souhaitez pouvoir effectuer un balayage pour actualiser même lorsque la vue vide est affichée, vous devrez en quelque sorte éviter de masquer le ListView. Vous pourrez donc utiliser un ListView personnalisé contenant cette fonction:

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

Avec la solution que j'ai écrite, le glissement-d'actualisation est affiché quel que soit le nombre d'éléments que vous affichez.

Bien sûr, si vous voulez vraiment cacher le ListView, vous devez changer le code. Peut-être ajouter "setVisibilityForReal (...)" :)

31

Voici ce que j'ai fait:.

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);
        }
    });

Mon 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>

Fonctionne comme un charme.

8
Stanislasdrg

J'ai rencontré le même problème. Il est gênant que SwipeRefreshLayout ne puisse avoir qu'un AdpaterView child. 

Si une vue vide est définie pour un AdpaterView , elle sera masquée si aucune donnée n'est disponible. Cependant, SwipeRefreshLayout a besoin d'un AdpaterView pour fonctionner. Donc, j'étends AdpaterView pour qu'il reste affiché, même s'il est vide.

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

Peut-être devez-vous également définir l’arrière-plan de la vue adaptateur comme transparent. Mais dans mon cas, ce n'est pas nécessaire, car la vue vide est un simple TextView .

6
hufeng03

Sur la base de certaines réponses ici et du code source de SwipeRefreshLayout, j'ai sous-classé la vue pour gérer spécifiquement le fait d'avoir un RecyclerView (ou ListView) ainsi qu'une vue "vide" à l'intérieur d'un conteneur qui est l'enfant.

Il attend une mise en page telle que

<SwipeRefreshLayoutWithEmpty ...>
  <FrameLayout ...>
    <TextView Android:text="List is Empty" ...>
    <RecyclerView ...>
  </FrameLayout>
</SwipeRefreshLayoutWithEmpty>

Le code est:

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;
    }
}

Full Gist: https://Gist.github.com/grennis/16cb2b0c7f798418284dd2d754499b43

4
Greg Ennis

J'ai essayé de suivre quelque chose. Dans les deux vues vides, et dans le cas d’une liste, balayer actualisera les données, fastscroll fonctionnera également sans qu'aucun changement de code ne soit requis dans l’activité . et listview est placé après celui-ci dans le bloc SwipeToRefresh . Sample Code - 

<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>
2
Gaurav Karwa

Pourquoi ne pas tout envelopper à l'intérieur de 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>
1
inmyth

Placez SwipeRefreshLayout dans un FrameLayout et d’autres vues derrière ce dernier.

<FrameLayout xmlns:Android="http://schemas.Android.com/apk/res/Android"
    Android:layout_width="match_parent"
    Android:layout_height="match_parent" >

    <LinearLayout
        Android:id="@+id/your_message_layout"
        Android:layout_width="match_parent"
        Android:layout_height="match_parent"
        Android:orientation="vertical" >

            <TextView
                Android:layout_width="match_parent"
                Android:layout_height="match_parent"
                Android:text="No result"/>
        </LinearLayout>

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

        <ListView
            Android:id="@+id/your_list_view"
            Android:layout_width="match_parent"
            Android:layout_height="wrap_content">
        </ListView>
    </Android.support.v4.widget.SwipeRefreshLayout>
</FrameLayout>
1
Lee Chun Hoe

Probablement tard, mais si vous faites toujours face à ce problème, voici la solution propre! Il n'est pas nécessaire de créer une autre SwipeRefreshLayout.

envelopper empty view dans une ScrollView. Coller AdapterView et ScrollView dans ViewGroup et le mettre dans SwipeRefreshLayout

Échantillon:

<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>

MODIFIER

Voici un moyen beaucoup plus simple.

vérifiez si votre liste est vide. Si oui, alors le rendre invisible. Échantillon:

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

cela fera mRecyclerView toujours là et tous les balayages fonctionneront bien maintenant même s'il n'y a pas de données!

1
Fajar Khan

Cela a fonctionné pour moi

<FrameLayout 
    xmlns:Android="http://schemas.Android.com/apk/res/Android"
    Android:layout_width="match_parent"
    Android:layout_height="match_parent"
    Android:background="@color/lighter_white">

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

        <ListView
            Android:id="@+id/list"
            Android:layout_width="match_parent"
            Android:layout_height="wrap_content"
            Android:layout_marginLeft="10dp"
            Android:layout_marginRight="10dp"
            Android:background="@color/silver"
            Android:layout_marginTop="10dp"
            Android:divider="@Android:color/transparent"
            Android:dividerHeight="8dp"
            Android:padding="4dp"/>

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

    <TextView
        Android:id="@+id/empty"
        Android:text="@string/strNoRecordsFound"
        Android:layout_width="match_parent"
        Android:layout_height="match_parent"
        Android:textColor="@Android:color/black"
        Android:alpha="0.5"
        Android:gravity="center">
    </TextView>
</FrameLayout>
0
Rahul Tiwari

Une autre option qui fonctionne bien dans le cas où votre vue vide ne nécessite aucune interaction. Par exemple, il s’agit d’un texte simple qui dit "Aucune donnée ici".

<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"
>

<TextView
    Android:id="@+id/emptyView"
    Android:layout_width="match_parent"
    Android:layout_height="match_parent"
    Android:gravity="center"
    Android:visibility="visible"
    Android:layout_centerInParent="true"
    Android:text="@string/no_earnable_available"
    Android:textSize="18dp"
    Android:textColor="@color/black"
    Android:background="@color/white"
    />

<some.app.RecyclerViewSwipeToRefreshLayout
    Android:id="@+id/swipe_refresh_layout"
    Android:layout_width="match_parent"
    Android:layout_height="match_parent"
    Android:paddingTop="4dp"
    Android:background="@Android:color/transparent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    >
    <Android.support.v7.widget.RecyclerView
        Android:id="@+id/recyclerView"
        Android:layout_width="match_parent"
        Android:layout_height="match_parent"
        Android:background="@Android:color/transparent"
        />

</some.app.RecyclerViewSwipeToRefreshLayout>
</RelativeLayout>

Cela place la vue vide derrière le SwipeToRefreshLayout qui est transparent et qui contient également un RecyclerView transparent. 

Ensuite, dans le code, à l'endroit où vous ajoutez les éléments à l'adaptateur de vue Recycler, vous vérifiez si l'adaptateur est vide et, le cas échéant, définissez la visibilité de la vue vide sur "visible". Et vice versa.

L'astuce est que la vue est derrière la vue transparente du recycleur, ce qui signifie que la vue du recycleur et son parent, SwipeToRefreshLayout, gèrent le défilement lorsqu'il n'y a aucun élément dans le recycleur. La vue vide derrière ne sera même pas touchée et l'événement tactile sera donc consommé par SwipeTorefreshLayout. 

La RecyclerSwipeTorefreshLayout personnalisée doit gérer la méthode canChildScrollUp de la manière suivante

@Override
public boolean canChildScrollUp() {
    if (recycler == null) throw new IllegalArgumentException("recycler not set!");
    else if (recycler.getAdapter().getItemCount() == 0){ // this check could be done in a more optimised way by setting a flag from the same place where you change the visibility of the empty view
        return super.canChildScrollUp();
    } else {
        RecyclerView.LayoutManager layoutManager = recycler.getLayoutManager();

        if (layoutManager instanceof LinearLayoutManager) {
            return ((LinearLayoutManager) recycler.getLayoutManager()).findFirstVisibleItemPosition() != 0 ||
                    (((LinearLayoutManager) recycler.getLayoutManager()).findFirstVisibleItemPosition() == 0
                            && recycler.getChildAt(0) != null && recycler.getChildAt(0).getY() < 0);
//...

Ça fera l'affaire.

UPDATE: Bien entendu, la vue recycleur n'a pas besoin d'être transparente en permanence. Vous pouvez mettre à jour la transparence pour n'être active que lorsque l'adaptateur est vide. 

À votre santé!

0
Vitongs

C'était une question très frustrante pour moi, mais après quelques heures d'essais et d'échecs, j'ai proposé cette solution.

Avec cela, je peux actualiser même avec une vue vide visible (et RecyclerView aussi bien sûr)

Dans mon fichier de mise en page, j'ai cette structure:

SwipeRefreshLayout
    FrameLayout
        RecyclerView
        my_empty_layout // Doesnt have to be ScrollView it can be whatever ViewGroup you want, I used LinearLayout with a single TextView child

Dans du code:

...
adapter.notifyDataSetChanged()

if (adapter.getItemCount() == 0) {
    recyclerView.setVisibility(View.GONE);
    emptyView.setVisibility(View.VISIBLE);
}
else {
    recyclerView.setVisibility(View.VISIBLE);
    emptyView.setVisibility(View.GONE);
}
0
Arbitur