Equivalente a ListView.setEmptyView em RecyclerView

Respostas:

68

Com o novo recurso de vinculação de dados, você também pode conseguir isso diretamente em seu layout:

<TextView
   android:text="No data to display."
   android:visibility="@{dataset.size() > 0 ? View.GONE : View.VISIBLE}" />

Nesse caso, você só precisa adicionar uma variável e uma importação à seção de dados do seu XML:

<data>
<import type="android.view.View"/>
<variable
    name="dataset"
    type="java.util.List&lt;java.lang.String&gt;"
    />
</data>
André Diermann
fonte
6
O exemplo acima é simplificado para enfatizar a abordagem de vinculação de dados. A vinculação de dados é muito flexível. Você pode, é claro, importar o em Adaptervez do conjunto de dados e usá-lo getItemCount()ou agrupar tudo dentro de ViewModele definir android:visibilitycomo viewModel.getEmptyViewVisibility().
André Diermann
4
Este deve ser votado mais alto, é um excelente exemplo dos recursos de vinculação de dados
Ed George
1
@javmarina Não, para mim o layout não continuou a ser atualizado. Se meu adaptador começar com tamanho 0 e depois o conjunto de dados crescer, o layout não será atualizado conforme desejado. Parece que a vinculação de dados não funciona para mim. :-(
meisteg
3
Isso vai ser atualizado mesmo se o adaptador estiver crescendo ou diminuindo dinamicamente para zero? Eu duvido disso.
David
1
@ a11n Ele não atualiza o layout quando a lista encolhe para 0 ou obtém dados, temos que definir o valor para o vínculo da classe toda vez que fazemos qualquer alteração na lista, há alguma maneira de atualizar o layout por si só?
Om Infowave Developers
114

Aqui está uma classe semelhante a @dragon born, mas mais completa. Com base nesta essência .

public class EmptyRecyclerView extends RecyclerView {
    private View emptyView;
    final private AdapterDataObserver observer = new AdapterDataObserver() {
        @Override
        public void onChanged() {
            checkIfEmpty();
        }

        @Override
        public void onItemRangeInserted(int positionStart, int itemCount) {
            checkIfEmpty();
        }

        @Override
        public void onItemRangeRemoved(int positionStart, int itemCount) {
            checkIfEmpty();
        }
    };

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

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

    public EmptyRecyclerView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    void checkIfEmpty() {
        if (emptyView != null && getAdapter() != null) {
            final boolean emptyViewVisible = getAdapter().getItemCount() == 0;
            emptyView.setVisibility(emptyViewVisible ? VISIBLE : GONE);
            setVisibility(emptyViewVisible ? GONE : VISIBLE);
        }
    }

    @Override
    public void setAdapter(Adapter adapter) {
        final Adapter oldAdapter = getAdapter();
        if (oldAdapter != null) {
            oldAdapter.unregisterAdapterDataObserver(observer);
        }
        super.setAdapter(adapter);
        if (adapter != null) {
            adapter.registerAdapterDataObserver(observer);
        }

        checkIfEmpty();
    }

    public void setEmptyView(View emptyView) {
        this.emptyView = emptyView;
        checkIfEmpty();
    }
}
Marc Plano-Lesay
fonte
Você pode explicar como posso usar esta classe?
Ololoking,
Bem, exatamente como você faria com um RecyclerView, ele apenas adiciona o setEmptyViewmétodo, que você pode chamar sempre que quiser definir a visualização vazia. Veja a ListView.setEmptyViewdocumentação se não estiver claro, é a mesma ideia.
Marc Plano-Lesay
5
Uma implementação semelhante do Google Samples: github.com/googlesamples/android-XYZTouristAttractions/blob/…
jase
2
Solução legal, mas um nome de classe é estranho =)
Шах
1
@AJW Acho que é principalmente uma questão de o que você deseja alcançar. A diferença entre as duas implementações é muito pequena e não sobra nenhuma assim que um adaptador é configurado. Se você não trocar o adaptador (o que provavelmente é o caso), não há diferença.
Marc Plano-Lesay
26

A solução fornecida neste link parece perfeita. Ele usa viewType para identificar quando mostrar emptyView. Não há necessidade de criar RecyclerView personalizado

Adicionando código do link acima:

package com.example.androidsampleproject;
import java.util.ArrayList;
import java.util.List;
import android.app.Activity;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

public class RecyclerViewActivity extends Activity {

RecyclerView recyclerView;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_recycler_view);
    recyclerView = (RecyclerView) findViewById(R.id.myList);
    recyclerView.setLayoutManager(new LinearLayoutManager(this));
    recyclerView.setAdapter(new MyAdapter());
}


private class MyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    private List<String> dataList = new ArrayList<String>();

    public class EmptyViewHolder extends RecyclerView.ViewHolder {
        public EmptyViewHolder(View itemView) {
            super(itemView);
        }
    }

    public class ViewHolder extends RecyclerView.ViewHolder {
        TextView data;

        public ViewHolder(View v) {
            super(v);
            data = (TextView) v.findViewById(R.id.data_view);
        }
    }

    @Override
    public int getItemCount() {
        return dataList.size() > 0 ? dataList.size() : 1;
    }

    @Override
    public int getItemViewType(int position) {
        if (dataList.size() == 0) {
            return EMPTY_VIEW;
        }
        return super.getItemViewType(position);
    }


    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder vho, final int pos) {
        if (vho instanceof ViewHolder) {
            ViewHolder vh = (ViewHolder) vho;
            String pi = dataList.get(pos);
        }
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View v;

        if (viewType == EMPTY_VIEW) {
            v = LayoutInflater.from(parent.getContext()).inflate(R.layout.empty_view, parent, false);
            EmptyViewHolder evh = new EmptyViewHolder(v);
            return evh;
        }

        v = LayoutInflater.from(parent.getContext()).inflate(R.layout.data_row, parent, false);
        ViewHolder vh = new ViewHolder(v);
        return vh;
    }

    private static final int EMPTY_VIEW = 10;
}

}
Sudhasri
fonte
6
Acho que estender o RecyclerView é uma solução mais apropriada do que esta porque geralmente tenho muitos adaptadores de reciclador e quero evitar adicionar esse tipo de lógica a cada um deles.
Gunhan
Isso faz sentido @Gunhan, ao usar muitos adaptadores de reciclagem. Você também pode tentar estender um único BaseAdapter personalizado para coisas comuns em todos
Sudhasri
2
Mesmo se você tiver apenas um adaptador e uma visualização de reciclador, não é responsabilidade do adaptador. O adaptador está aqui para apresentar itens, não ausência de itens.
Marc Plano-Lesay
@Kernald Depende do seu caso de uso. Pessoalmente, acho que é muito mais limpo o jeito que Sudhasri fez. Especialmente se você quiser mostrar uma visão diferente no caso de nenhum item estar presente, como: "Nenhum item aqui, vá às compras!" ou coisas assim
AgentKnopf
@Zainodis Como você disse, é uma visão diferente. É não responsabilidade do adaptador, que é para exibir itens na recyclerview, nada mais. Eu concordo que tecnicamente falando, ambas as soluções funcionam e são praticamente iguais. Mas os itens do adaptador não são feitos para exibir visualizações como esta.
Marc Plano-Lesay
10

Eu simplesmente preferiria uma solução simples como,

tenha seu RecyclerView dentro de um FrameLayout ou RelativeLayout com um TextView ou outra visualização mostrando mensagem de dados vazios com visibilidade DEVIDO por padrão e então na classe do adaptador, aplique a lógica

Aqui, eu tenho um TextView com mensagem sem dados

@Override
public int getItemCount() {
    textViewNoData.setVisibility(data.size() > 0 ? View.GONE : View.VISIBLE);
    return data.size();
}
Lalit Poptani
fonte
3

Experimente RVEmptyObserver:

É uma implementação de um AdapterDataObserverque permite que você simplesmente defina um Viewcomo o layout vazio padrão para o seu RecylerView. Dessa forma, em vez de usar um personalizado RecyclerViewe tornar sua vida mais difícil, você pode usá-lo facilmente com seu código existente:


Exemplo de uso:

RVEmptyObserver observer = new RVEmptyObserver(recyclerView, emptyView)
rvAdapter.registerAdapterDataObserver(observer);

Você pode ver o código e o uso de exemplo em um aplicativo real aqui.


Classe:

public class RVEmptyObserver extends RecyclerView.AdapterDataObserver {
    private View emptyView;
    private RecyclerView recyclerView;

    public RVEmptyObserver(RecyclerView rv, View ev) {
        this.recyclerView = rv;
        this.emptyView    = ev;
        checkIfEmpty();
    }

    private void checkIfEmpty() {
        if (emptyView != null && recyclerView.getAdapter() != null) {
            boolean emptyViewVisible = recyclerView.getAdapter().getItemCount() == 0;
            emptyView.setVisibility(emptyViewVisible ? View.VISIBLE : View.GONE);
            recyclerView.setVisibility(emptyViewVisible ? View.GONE : View.VISIBLE);
        }
    }

    public void onChanged() { checkIfEmpty(); }
    public void onItemRangeInserted(int positionStart, int itemCount) { checkIfEmpty(); }
    public void onItemRangeRemoved(int positionStart, int itemCount) { checkIfEmpty(); }
}
Sheharyar
fonte
2

Minha versão, com base em https://gist.github.com/adelnizamutdinov/31c8f054d1af4588dc5c

public class EmptyRecyclerView extends RecyclerView {
    @Nullable
    private View emptyView;

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

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

    public EmptyRecyclerView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    private void checkIfEmpty() {
        if (emptyView != null && getAdapter() != null) {
            emptyView.setVisibility(getAdapter().getItemCount() > 0 ? GONE : VISIBLE);
        }
    }

    private final AdapterDataObserver observer = new AdapterDataObserver() {
        @Override
        public void onChanged() {
            checkIfEmpty();
        }

        @Override
        public void onItemRangeInserted(int positionStart, int itemCount) {
            checkIfEmpty();
        }

        @Override
        public void onItemRangeRemoved(int positionStart, int itemCount) {
            checkIfEmpty();
        }
    };

    @Override
    public void setAdapter(@Nullable Adapter adapter) {
        final Adapter oldAdapter = getAdapter();
        if (oldAdapter != null) {
            oldAdapter.unregisterAdapterDataObserver(observer);
        }
        super.setAdapter(adapter);
        if (adapter != null) {
            adapter.registerAdapterDataObserver(observer);
        }
        checkIfEmpty();
    }

    @Override
    public void setVisibility(int visibility) {
        super.setVisibility(visibility);
        if (null != emptyView && (visibility == GONE || visibility == INVISIBLE)) {
            emptyView.setVisibility(GONE);
        } else {
            checkIfEmpty();
        }
    }

    public void setEmptyView(@Nullable View emptyView) {
        this.emptyView = emptyView;
        checkIfEmpty();
    }
}
localhost
fonte
3
Boa ideia para reimplementar setVisibilitytambém.
Marc Plano-Lesay
2

Eu preferiria implementar essa funcionalidade no Recycler.Adapter

Em seu método getItemCount substituído, injete códigos de verificação vazios lá:

@Override
public int getItemCount() {
    if(data.size() == 0) listIsEmtpy();
    return data.size();
}
Bilal
fonte
3
Não é responsabilidade do adaptador. O adaptador está aqui para apresentar itens, não ausência de itens.
Marc Plano-Lesay
@Kernald É o nosso código e à nossa maneira, como o personalizamos e o usamos.
Lalit Poptani
@LalitPoptani com certeza. Mas é um site de perguntas e respostas, onde as pessoas procuram respostas, na maioria das vezes sem pensar mais do que "qual é o atalho de cópia mesmo?". Indicar que a solução está semanticamente errada (além disso, quando você também tem soluções de "direitos") não é realmente inútil ...
Marc Plano-Lesay
@Kernald bom acho que essa solução é a mais simples de todas e é uma boa solução também, pois toda vez que o adaptador for notificado ele será chamado e poderá ser usado para verificar o tamanho dos dados!
Lalit Poptani
1
@ MarcPlano-Lesay está correto. Esta resposta está incompleta porque não trata do caso em que a exibição vazia precisa ficar invisível depois que os itens são preenchidos. Depois de implementar essa parte, essa solução se torna ineficiente porque toda vez que o adaptador consulta a contagem de itens, setVisibility()é chamado. Claro que você pode adicionar alguns sinalizadores para compensar, mas é aí que fica mais complexo.
razzledazzle de
2

Se você deseja oferecer suporte a mais estados, como estado de carregamento, estado de erro, você pode verificar https://github.com/rockerhieu/rv-adapter-states . Caso contrário, o suporte para visualização vazia pode ser implementado facilmente RecyclerViewAdapterWrapperem ( https://github.com/rockerhieu/rv-adapter ). A principal vantagem dessa abordagem é que você pode facilmente suportar a visualização vazia sem alterar a lógica do adaptador existente:

public class StatesRecyclerViewAdapter extends RecyclerViewAdapterWrapper {
    private final View vEmptyView;

    @IntDef({STATE_NORMAL, STATE_EMPTY})
    @Retention(RetentionPolicy.SOURCE)
    public @interface State {
    }

    public static final int STATE_NORMAL = 0;
    public static final int STATE_EMPTY = 2;

    public static final int TYPE_EMPTY = 1001;

    @State
    private int state = STATE_NORMAL;

    public StatesRecyclerViewAdapter(@NonNull RecyclerView.Adapter wrapped, @Nullable View emptyView) {
        super(wrapped);
        this.vEmptyView = emptyView;
    }

    @State
    public int getState() {
        return state;
    }

    public void setState(@State int state) {
        this.state = state;
        getWrappedAdapter().notifyDataSetChanged();
        notifyDataSetChanged();
    }

    @Override
    public int getItemCount() {
        switch (state) {
            case STATE_EMPTY:
                return 1;
        }
        return super.getItemCount();
    }

    @Override
    public int getItemViewType(int position) {
        switch (state) {
            case STATE_EMPTY:
                return TYPE_EMPTY;
        }
        return super.getItemViewType(position);
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        switch (viewType) {
            case TYPE_EMPTY:
                return new SimpleViewHolder(vEmptyView);
        }
        return super.onCreateViewHolder(parent, viewType);
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        switch (state) {
            case STATE_EMPTY:
                onBindEmptyViewHolder(holder, position);
                break;
            default:
                super.onBindViewHolder(holder, position);
                break;
        }
    }

    public void onBindEmptyViewHolder(RecyclerView.ViewHolder holder, int position) {
    }

    public static class SimpleViewHolder extends RecyclerView.ViewHolder {
        public SimpleViewHolder(View itemView) {
            super(itemView);
        }
    }
}

Uso:

Adapter adapter = originalAdapter();
StatesRecyclerViewAdapter statesRecyclerViewAdapter = new StatesRecyclerViewAdapter(adapter, emptyView);
rv.setAdapter(endlessRecyclerViewAdapter);

// Change the states of the adapter
statesRecyclerViewAdapter.setState(StatesRecyclerViewAdapter.STATE_EMPTY);
statesRecyclerViewAdapter.setState(StatesRecyclerViewAdapter.STATE_NORMAL);
Hieu Rocker
fonte
Usei seu código como base para uma solução semelhante. Obrigado!
Albert Vila Calvo
2

Eu
consertei isso: Criado o arquivo layout_recyclerview_with_emptytext.xml.
EmptyViewRecyclerView.java criado
---------

EmptyViewRecyclerView emptyRecyclerView = (EmptyViewRecyclerView) findViewById (R.id.emptyRecyclerViewLayout);
emptyRecyclerView.addAdapter (mPrayerCollectionRecyclerViewAdapter, "Não há oração para a categoria selecionada.");

arquivo layout_recyclerview_with_emptytext.xml

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

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

<com.ninestars.views.CustomFontTextView android:id="@+id/recyclerViewEmptyTextView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:text="Empty Text"
    android:layout_gravity="center"
    android:gravity="center"
    android:textStyle="bold"
    />

    </merge>


EmptyViewRecyclerView.java

public class EmptyViewRecyclerView extends ViewSwitcher {
private RecyclerView mRecyclerView;
private CustomFontTextView mRecyclerViewExptyTextView;

public EmptyViewRecyclerView(Context context) {
    super(context);
    initView(context);
}

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


private void initView(Context context) {
    LayoutInflater.from(context).inflate(R.layout.layout_recyclerview_with_emptytext, this, true);
    mRecyclerViewExptyTextView = (CustomFontTextView) findViewById(R.id.recyclerViewEmptyTextView);
    mRecyclerView = (RecyclerView) findViewById(R.id.recyclerView);
    mRecyclerView.setLayoutManager(new LinearLayoutManager(context));
}

public void addAdapter(final RecyclerView.Adapter<?> adapter) {
    mRecyclerView.setAdapter(adapter);
    adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
        @Override
        public void onChanged() {
            super.onChanged();
            if(adapter.getItemCount() > 0) {
                if (R.id.recyclerView == getNextView().getId()) {
                    showNext();
                }
            } else {
                if (R.id.recyclerViewEmptyTextView == getNextView().getId()) {
                    showNext();
                }
            }
        }
    });
}

public void addAdapter(final RecyclerView.Adapter<?> adapter, String emptyTextMsg) {
    addAdapter(adapter);
    setEmptyText(emptyTextMsg);
}

public RecyclerView getRecyclerView() {
    return mRecyclerView;
}

public void setEmptyText(String emptyTextMsg) {
    mRecyclerViewExptyTextView.setText(emptyTextMsg);
}

}
Ashwani
fonte
1
public class EmptyRecyclerView extends RecyclerView {
  @Nullable View emptyView;

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

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

  public EmptyRecyclerView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
  }

  void checkIfEmpty() {
    if (emptyView != null) {
      emptyView.setVisibility(getAdapter().getItemCount() > 0 ? GONE : VISIBLE);
    }
  }

  final @NotNull AdapterDataObserver observer = new AdapterDataObserver() {
    @Override public void onChanged() {
      super.onChanged();
      checkIfEmpty();
    }
  };

  @Override public void setAdapter(@Nullable Adapter adapter) {
    final Adapter oldAdapter = getAdapter();
    if (oldAdapter != null) {
      oldAdapter.unregisterAdapterDataObserver(observer);
    }
    super.setAdapter(adapter);
    if (adapter != null) {
      adapter.registerAdapterDataObserver(observer);
    }
  }

  public void setEmptyView(@Nullable View emptyView) {
    this.emptyView = emptyView;
    checkIfEmpty();
  }
}

algo assim pode ajudar

Munawwar Hussain Shelia
fonte
2
Isso está incompleto. Provavelmente, você precisará ocultar o RecyclerViewquando o emptyViewestiver visível (e o oposto). Você também precisará ligar checkIfEmpty()para onItemRangeInserted()e onItemRangeRemoved(). Ah, e você poderia ter citado sua fonte: gist.github.com/adelnizamutdinov/31c8f054d1af4588dc5c
Marc Plano-Lesay
1

Você pode simplesmente pintar o texto RecyclerViewquando estiver vazio. A seguir personalizados subclasse suportes empty, failed, loading, e offlinemodos. Para uma compilação bem-sucedida, adicione recyclerView_stateTextcor aos seus recursos.

/**
 * {@code RecyclerView} that supports loading and empty states.
 */
public final class SupportRecyclerView extends RecyclerView
{
    public enum State
    {
        NORMAL,
        LOADING,
        EMPTY,
        FAILED,
        OFFLINE
    }

    public SupportRecyclerView(@NonNull Context context)
    {
        super(context);

        setUp(context);
    }

    public SupportRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs)
    {
        super(context, attrs);

        setUp(context);
    }

    public SupportRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle)
    {
        super(context, attrs, defStyle);

        setUp(context);
    }

    private Paint textPaint;
    private Rect textBounds;
    private PointF textOrigin;

    private void setUp(Context c)
    {
        textPaint = new Paint();
        textPaint.setAntiAlias(true);
        textPaint.setColor(ContextCompat.getColor(c, R.color.recyclerView_stateText));

        textBounds = new Rect();
        textOrigin = new PointF();
    }

    private State state;

    public State state()
    {
        return state;
    }

    public void setState(State newState)
    {
        state = newState;
        calculateLayout(getWidth(), getHeight());
        invalidate();
    }

    private String loadingText = "Loading...";

    public void setLoadingText(@StringRes int resId)
    {
        loadingText = getResources().getString(resId);
    }

    private String emptyText = "Empty";

    public void setEmptyText(@StringRes int resId)
    {
        emptyText = getResources().getString(resId);
    }

    private String failedText = "Failed";

    public void setFailedText(@StringRes int resId)
    {
        failedText = getResources().getString(resId);
    }

    private String offlineText = "Offline";

    public void setOfflineText(@StringRes int resId)
    {
        offlineText = getResources().getString(resId);
    }

    @Override
    public void onDraw(Canvas canvas)
    {
        super.onDraw(canvas);

        String s = stringForCurrentState();
        if (s == null)
            return;

        canvas.drawText(s, textOrigin.x, textOrigin.y, textPaint);
    }

    private void calculateLayout(int w, int h)
    {
        String s = stringForCurrentState();
        if (s == null)
            return;

        textPaint.setTextSize(.1f * w);
        textPaint.getTextBounds(s, 0, s.length(), textBounds);

        textOrigin.set(
         w / 2f - textBounds.width() / 2f - textBounds.left,
         h / 2f - textBounds.height() / 2f - textBounds.top);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh)
    {
        super.onSizeChanged(w, h, oldw, oldh);

        calculateLayout(w, h);
    }

    private String stringForCurrentState()
    {
        if (state == State.EMPTY)
            return emptyText;
        else if (state == State.LOADING)
            return loadingText;
        else if (state == State.FAILED)
            return failedText;
        else if (state == State.OFFLINE)
            return offlineText;
        else
            return null;
    }
}
Aleks N.
fonte
1

Do meu ponto de vista, a maneira mais fácil de fazer uma View vazia é criar uma nova RecyclerView vazia com o layout que você deseja aumentar como plano de fundo. E este adaptador vazio é definido quando você verifica o tamanho do conjunto de dados.

user7108272
fonte