回收视图中 ListView.setEmptyView 的等价物

RecyclerView中,我想设置一个空视图,以便在适配器为空时显示。有相当于 ListView.setEmptyView()的东西吗?

43117 次浏览
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();
}
}

something like this might help

Here's a class similar to @dragon born's, but more complete. Based on this gist.

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

My version, based on 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();
}
}

I would prefer to implement this functionality in Recycler.Adapter

On your overridden getItemCount method, inject empty check codes there:

@Override
public int getItemCount() {
if(data.size() == 0) listIsEmtpy();
return data.size();
}

Solution provided in this link seems perfect. It uses viewType to identify when to show emptyView. No need to create custom RecyclerView

Adding code from the above link:

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


}

I think this is more complete with both ErrorView & EmptyView https://gist.github.com/henrytao-me/2f7f113fb5f2a59987e7

With the new data binding feature you can also achieve this in your layout directly:

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

In that case you just need to add a variable and an import to the data section of your XML:

<data>
<import type="android.view.View"/>
<variable
name="dataset"
type="java.util.List&lt;java.lang.String&gt;"
/>
</data>

If you want to support more states such as loading state, error state then you can checkout https://github.com/rockerhieu/rv-adapter-states. Otherwise supporting empty view can be implemented easily using RecyclerViewAdapterWrapper from (https://github.com/rockerhieu/rv-adapter). The main advantage of this approach is you can easily support empty view without changing the logic of the existing adapter:

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

Usage:

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

I would simply prefer a simple solution like,

have your RecyclerView inside a FrameLayout or RelativeLayout with a TextView or other view with showing empty data message with visibility GONE by default and then in the adapter class, apply the logic

Here, I have one TextView with message no data

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

You can just paint the text on the RecyclerView when it's empty. The following custom subclass supports empty, failed, loading, and offline modes. For successful compilation add recyclerView_stateText color to your resources.

/**
* {@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;
}
}

From my point of view the easiest way how to do an empty View is to create new empty RecyclerView with layout you want to inflate as a background. And this empty Adapter is set when you check your dataset size.

I have fixed this:
Created layout layout_recyclerview_with_emptytext.xml file.
Created EmptyViewRecyclerView.java
---------

EmptyViewRecyclerView emptyRecyclerView = (EmptyViewRecyclerView) findViewById(R.id.emptyRecyclerViewLayout);
emptyRecyclerView.addAdapter(mPrayerCollectionRecyclerViewAdapter, "There is no prayer for selected category.");

layout_recyclerview_with_emptytext.xml file

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


}

Try RVEmptyObserver:

It's an implementation of an AdapterDataObserver that allows you to simply set a View as the default empty layout for your RecylerView. This way, instead of using a custom RecyclerView and making your life harder, you can easily use it with your existing code:


Example Usage:

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

You can see the code and example usage in an actual app here.


Class:

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

A more easy to use, and more resource save way to set Empty Label of RecyclerView.

In short, a new View named RecyclerViewEmpty is introduced. In its onDraw method, if the adapter is empty, it just paint an Empty Label on its center, otherwise, continue super.onDraw();

class RecyclerViewEmpty extends RecyclerView {
....
@Override
public void onDraw(Canvas canvas) {
Adapter a = this.getAdapter();
if(a==null || a.getItemCount()<1) {
int x= (this.getWidth()-strWidth)>>1;
int y = this.getHeight()>>1 ;
canvas.drawText(this.emptyLabel, x, y, labelPaint);
}
else {
super.onDraw(canvas);
}
}
....
}

For details, please refer to code: https://github.com/stzdzyhs/recyclerview-demo