Equivalente de ListView.setEmptyView en RecyclerView

Respuestas:

69

Con la nueva función de enlace de datos , también puede lograr esto en su diseño directamente:

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

En ese caso, solo necesita agregar una variable y una importación a la sección de datos de su XML:

<data>
<import type="android.view.View"/>
<variable
    name="dataset"
    type="java.util.List&lt;java.lang.String&gt;"
    />
</data>
André Diermann
fuente
6
El ejemplo anterior está simplificado para enfatizar el enfoque de enlace de datos. El enlace de datos es muy flexible. Por supuesto, puede importar el Adapterconjunto de datos en lugar del conjunto de datos y usarlo getItemCount()o envolver todo dentro de un ViewModely establecer android:visibilityen viewModel.getEmptyViewVisibility().
André Diermann
4
Esto debería votarse más alto, es un excelente ejemplo de las capacidades de enlace de datos
Ed George
1
@javmarina No, para mí el diseño no continuó actualizándose. Si mi adaptador comienza con el tamaño 0 y luego crece el conjunto de datos, el diseño no se actualizará como se desea. Parece que la vinculación de datos no funcionará para mí. :-(
meisteg
3
¿Se actualizará incluso si el adaptador crece o se reduce dinámicamente a cero? Dudo que.
David
1
@ a11n No actualiza el diseño cuando la lista se reduce a 0 o obtiene datos, tenemos que establecer un valor para el enlace de la clase cada vez que hacemos algún cambio en la lista, ¿hay alguna forma de actualizar el diseño por sí mismo?
Om Infowave Developers
114

Aquí hay una clase similar a la de @dragon born, pero más completa. Basado en esta esencia .

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
fuente
¿Puedes explicar cómo puedo usar esta clase?
Ololoking
Bueno, exactamente como lo haría con un RecyclerView, simplemente agrega el setEmptyViewmétodo, que puede llamar siempre que desee definir la vista vacía. Consulte la ListView.setEmptyViewdocumentación si no está claro, es la misma idea.
Marc Plano-Lesay
5
Una implementación similar de Google Samples: github.com/googlesamples/android-XYZTouristAttractions/blob/…
jase
2
Solución genial pero el nombre de una clase es extraño =)
Шах
1
@AJW Supongo que es principalmente una cuestión de lo que quieres lograr. La diferencia entre las dos implementaciones es realmente menor y no queda ninguna tan pronto como se configura un adaptador. Si no cambia el adaptador (que probablemente sea el caso), no hay diferencia.
Marc Plano-Lesay
26

La solución proporcionada en este enlace parece perfecta. Utiliza viewType para identificar cuándo mostrar emptyView. No es necesario crear un RecyclerView personalizado

Añadiendo código desde el enlace anterior:

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
fuente
6
Creo que extender RecyclerView es una solución más apropiada que esta porque, en general, tengo muchos adaptadores de reciclado y quiero evitar agregar este tipo de lógica a cada uno de ellos.
Gunhan
Eso tiene sentido @Gunhan, cuando se utilizan muchos adaptadores de reciclado. También puede intentar extender un solo BaseAdapter personalizado para cosas comunes en todos
Sudhasri
2
Incluso si solo tiene un adaptador y una vista de reciclador, no es responsabilidad del adaptador. El adaptador está aquí para presentar elementos, no para ausencia de elementos.
Marc Plano-Lesay
@Kernald Depende de su caso de uso. Personalmente, creo que es mucho más limpio como lo hizo Sudhasri. Especialmente si desea mostrar una vista diferente en caso de que no se presenten artículos como: "¡No hay artículos aquí, vaya de compras!" o cosas así
AgentKnopf
@Zainodis Como dijiste, es una vista diferente. Es no responsabilidad del adaptador, que es para mostrar los elementos de la recyclerview, nada más. Estoy de acuerdo en que técnicamente hablando, ambas soluciones funcionan y son prácticamente iguales. Pero los elementos del adaptador no están hechos para mostrar vistas como esta.
Marc Plano-Lesay
10

Simplemente preferiría una solución simple como,

tenga su RecyclerView dentro de un FrameLayout o RelativeLayout con un TextView u otra vista que muestre un mensaje de datos vacío con visibilidad GONE por defecto y luego en la clase de adaptador, aplique la lógica

Aquí, tengo un TextView con mensaje sin datos

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

Prueba RVEmptyObserver:

Es una implementación de un AdapterDataObserverque le permite simplemente establecer un Viewcomo el diseño vacío predeterminado para su RecylerView. De esta manera, en lugar de usar una costumbre RecyclerViewy hacer su vida más difícil, puede usarla fácilmente con su código existente:


Ejemplo de uso:

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

Puede ver el código y el uso de ejemplo en una aplicación real aquí.


Clase:

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

Mi versión, basada en 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
fuente
3
Buena idea para reimplementar setVisibilitytambién.
Marc Plano-Lesay
2

Preferiría implementar esta funcionalidad en Recycler.Adapter

En su método getItemCount anulado, inyecte códigos de verificación vacíos allí:

@Override
public int getItemCount() {
    if(data.size() == 0) listIsEmtpy();
    return data.size();
}
Bilal
fuente
3
No es responsabilidad del adaptador. El adaptador está aquí para presentar elementos, no para ausencia de elementos.
Marc Plano-Lesay
@Kernald Es nuestro código y es nuestra propia manera, cómo lo personalizamos y lo usamos.
Lalit Poptani
@LalitPoptani seguro. Pero es un sitio web de preguntas y respuestas, donde la gente busca respuestas, la mayoría de las veces sin pensar más que "¿cuál es el atajo de copia?". Indicar que la solución es semánticamente incorrecta (además, cuando también tiene soluciones de "derechos") no es realmente inútil ...
Marc Plano-Lesay
@Kernald bueno, creo que esta solución es la más simple de todas y también es una buena solución, porque cada vez que se notifica al adaptador, se llamará y se puede usar para verificar el tamaño de los datos.
Lalit Poptani
1
@ MarcPlano-Lesay tiene razón. Esta respuesta está incompleta porque no maneja el caso cuando la vista vacía necesita ser invisible una vez que se llenan los elementos. Después de implementar esa parte, esta solución se vuelve ineficaz porque cada vez que el adaptador consulta el recuento de elementos, setVisibility()se llama. Seguro que podrías agregar algunas banderas para compensar, pero ahí es cuando se vuelve más complejo.
razzledazzle
2

Si desea admitir más estados, como el estado de carga, el estado de error, puede consultar https://github.com/rockerhieu/rv-adapter-states . De lo contrario, el soporte de la vista vacía se puede implementar fácilmente usando RecyclerViewAdapterWrapperfrom ( https://github.com/rockerhieu/rv-adapter ). La principal ventaja de este enfoque es que puede admitir fácilmente la vista vacía sin cambiar la lógica del 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
fuente
Usé su código como base para una solución similar. ¡Gracias!
Albert Vila Calvo
2

He solucionado esto:
diseño creado layout_recyclerview_with_emptytext.xml archivo.
Creado EmptyViewRecyclerView.java
---------

EmptyViewRecyclerView emptyRecyclerView = (EmptyViewRecyclerView) findViewById (R.id.emptyRecyclerViewLayout);
emptyRecyclerView.addAdapter (mPrayerCollectionRecyclerViewAdapter, "No hay oración para la categoría seleccionada");

layout_recyclerview_with_emptytext.xml archivo

    <?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
fuente
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 como esto podría ayudar

Munawwar Hussain Shelia
fuente
2
Esto está incompleto. Probablemente necesitará ocultar el RecyclerViewcuando emptyViewesté visible (y lo contrario). También deberá llamar checkIfEmpty()a onItemRangeInserted()y onItemRangeRemoved(). Ah, y podría haber citado su fuente: gist.github.com/adelnizamutdinov/31c8f054d1af4588dc5c
Marc Plano-Lesay
1

Puedes pintar el texto RecyclerViewcuando esté vacío. Soporta los siguientes personalizados subclase empty, failed, loading, y offlinemodos. Para una compilación exitosa, agregue recyclerView_stateTextcolor a sus 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.
fuente
1

Desde mi punto de vista, la forma más fácil de hacer una Vista vacía es crear un nuevo RecyclerView vacío con el diseño que desea inflar como fondo. Y este adaptador vacío se configura cuando verifica el tamaño de su conjunto de datos.

usuario7108272
fuente