I have successfully implemented a listview with a custom adapter which displays a picture and title. It also set as a multi-click view so multiple items can be selected and the data can pass to other activities. I also implemented a feature so that when I click an item its background changes color.
The only thing I cannot solve is when I click a second item the first items background color changes back. I would like all items clicked to remain changed in color.
// Assigns the listView variable to the listView in the xml file.
listView = findViewById(R.id.distortedListViewWorkout);
// Instantiate the Custom Adapter (currently not being used)
MyAdapter adapter = new MyAdapter(this, mTitle, images);
// Instantiates the Adatper with a premade android simple layout and the title array list.
listView.setAdapter(adapter);
//((new ArrayAdapter<String>(this, android.R.layout.simple_list_item_multiple_choice, mTitle)));
// Enables the user to select multiple items in the listview.
listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
// sets an onItemClickListener for the listView
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
/**
*
* #param parent
* #param view
* #param position
* #param id
*/
#Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
// variable which will capture the title of the item selected in the listView.
distortionsString = "";
int userInput = listView.getCount();
SparseBooleanArray sparseBooleanArray = listView.getCheckedItemPositions();
for(int loop = 0; loop < userInput; loop++){
if(sparseBooleanArray.get(loop)) {
distortionsString += listView.getItemAtPosition(loop).toString() + "\n";
view.setSelected(true);
}
}
}});
}
/**
* Custom Adapter class containing an icon and title.
*/
class MyAdapter extends ArrayAdapter<String> {
Context context;
String rTitle[];
int rImages[];
MyAdapter (Context c, String title[], int images[]){
super(c, R.layout.row, R.id.titleEtWorkout, title);
this.context = c;
this.rTitle = title;
this.rImages = images;
}
/**
*
* #param position
* #param convertView
* #param parent
* #return
*/
#NonNull
#Override
public View getView(int position, #Nullable View convertView, #NonNull ViewGroup parent) {
LayoutInflater layoutInflater = (LayoutInflater) getApplicationContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View row = layoutInflater.inflate(R.layout.row, parent, false);
ImageView images = row.findViewById(R.id.iconsWorkout);
TextView myTitle = row.findViewById(R.id.titleEtWorkout);
images.setImageResource(rImages[position]);
myTitle.setText(rTitle[position]);
return row;
}
}
}
I have a layout, default_label.xml, like so:
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/label"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
Then, I have this class, which basically allows me to set the default button text for a spinner:
public class NothingSelectedSpinnerAdapter implements SpinnerAdapter, ListAdapter {
protected static final int EXTRA = 1;
protected SpinnerAdapter adapter;
protected Context context;
protected int nothingSelectedLayout;
protected int nothingSelectedDropdownLayout;
protected LayoutInflater layoutInflater;
protected TextView label;
/**
* Use this constructor to have NO 'Select One...' item, instead use
* the standard prompt or nothing at all.
*
* #param spinnerAdapter wrapped Adapter.
* #param nothingSelectedLayout layout for nothing selected, perhaps
* you want text grayed out like a prompt...
* #param context Context
*/
public NothingSelectedSpinnerAdapter(
SpinnerAdapter spinnerAdapter,
int nothingSelectedLayout, Context context) {
this(spinnerAdapter, nothingSelectedLayout, -1, context);
}
/**
* Use this constructor to Define your 'Select One...' layout as the first
* row in the returned choices.
* If you do this, you probably don't want a prompt on your spinner or it'll
* have two 'Select' rows.
*
* #param spinnerAdapter wrapped Adapter. Should probably return false for isEnabled(0)
* #param nothingSelectedLayout layout for nothing selected, perhaps you want
* text grayed out like a prompt...
* #param nothingSelectedDropdownLayout layout for your 'Select an Item...' in
* the dropdown.
* #param context Context
*/
public NothingSelectedSpinnerAdapter(SpinnerAdapter spinnerAdapter,
int nothingSelectedLayout, int nothingSelectedDropdownLayout, Context context) {
this.adapter = spinnerAdapter;
this.context = context;
this.nothingSelectedLayout = nothingSelectedLayout;
this.nothingSelectedDropdownLayout = nothingSelectedDropdownLayout;
layoutInflater = LayoutInflater.from(context);
}
#Override
public final View getView(int position, View convertView, ViewGroup parent) {
// This provides the View for the Selected Item in the Spinner, not
// the dropdown (unless dropdownView is not set).
if (position == 0) {
return getNothingSelectedView(parent);
}
return adapter.getView(position - EXTRA, null, parent); // Could re-use
// the convertView if possible.
}
public int getPosition(String value) {
int index = 0;
for (int i = 1; i < getCount() + EXTRA; i++) {
if (getItem(i).equals(value)) {
index = i;
break;
}
}
return index;
}
public TextView getNothingSelectedView(ViewGroup parent) {
label = (TextView) layoutInflater.inflate(nothingSelectedLayout, parent, false);
return label;
}
#Override
public View getDropDownView(int position, View convertView, ViewGroup parent) {
// Android BUG! http://code.google.com/p/android/issues/detail?id=17128 -
// Spinner does not support multiple view types
if (position == 0) {
return new View(context);
}
if (adapter.getItem(position-EXTRA).toString().equals("")){
View view = nothingSelectedDropdownLayout == -1 ?
new View(context) :
getNothingSelectedDropdownView(parent);
view.setEnabled(false);
view.setOnClickListener(null);
return view;
}
// Could re-use the convertView if possible, use setTag...
return adapter.getDropDownView(position - EXTRA, null, parent);
}
protected View getNothingSelectedDropdownView(ViewGroup parent) {
return layoutInflater.inflate(nothingSelectedDropdownLayout, parent, false);
}
#Override
public int getCount() {
int count = adapter.getCount();
return count == 0 ? 0 : count + EXTRA;
}
#Override
public Object getItem(int position) {
return position == 0 ? null : adapter.getItem(position - EXTRA);
}
#Override
public int getItemViewType(int position) {
return 0;
}
#Override
public int getViewTypeCount() {
return 1;
}
#Override
public long getItemId(int position) {
return position >= EXTRA ? adapter.getItemId(position - EXTRA) : position - EXTRA;
}
#Override
public boolean hasStableIds() {
return adapter.hasStableIds();
}
#Override
public boolean isEmpty() {
return adapter.isEmpty();
}
#Override
public void registerDataSetObserver(DataSetObserver observer) {
adapter.registerDataSetObserver(observer);
}
#Override
public void unregisterDataSetObserver(DataSetObserver observer) {
adapter.unregisterDataSetObserver(observer);
}
#Override
public boolean areAllItemsEnabled() {
return false;
}
#Override
public boolean isEnabled(int position) {
return position != 0; // Don't allow the 'nothing selected'
// item to be picked.
}
}
What I want to be able to do is change the text of the TextView, something like:
NothingSelectedSpinnerAdapter myAdapter = new NothingSelectedSpinnerAdapter(adapter, R.layout.default_label, getContext());
myAdapter.setText("Custom Label");
mySpinner.setAdapter(myAdapter);
However, when I try adding the following method to the above NothingSelectedSpinnerAdapter class:
public void setText(String text) {
label.setText(text);
}
I get the following error:
java.lang.NullPointerException: Attempt to invoke virtual method 'void
android.widget.TextView.setText(java.lang.CharSequence)' on a null
object reference
What should I change?
Take a look at the following three lines:
NothingSelectedSpinnerAdapter myAdapter = new NothingSelectedSpinnerAdapter(adapter, R.layout.default_label, getContext());
The Adapter is instantiated, label is null
myAdapter.setText("Custom Label");
The TextView label is still null, that's why label.setText(); causes a NullPointerException
mySpinner.setAdapter(myAdapter);
Now the runtime will be able to draw the Spinner items. This involves repeated calling of getView() (at least once for every item which should be drawn). After getView() has been executed for position = 0, you will have assigned a value to label. From now on, you can safely call setText() on it.
If you want to be able to call myAdapter.setText("Custom Label"); whenever you like without having to bother about the inner workings of the Adapter, then you can introduce a field private String mLabelText and implement the method as follows
public void setText(String labelText){
mLabelText = labelText;
if (label != null){
label.setText(labelText);
}
}
One last step: don't forget to set the text right after initializing label
public TextView getNothingSelectedView(ViewGroup parent) {
label = (TextView) layoutInflater.inflate(nothingSelectedLayout, parent, false);
label.setText(mLabelText);
return label;
}
i would like to print a toast when i click on a recyclerview, i have an error in the variable "context" in the OnClick Method, i dont find how to fix this error.
private class MyOnClickListener implements View.OnClickListener{
#Override
public void onClick(View view) {
int itemPosition = recyclerView.getChildLayoutPosition(view);
String item = items.get(itemPosition);
Toast.makeText(context, item, Toast.LENGTH_LONG).show();
}
}
#Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// get inflater and get view by resource id itemLayout
View v = LayoutInflater.from(parent.getContext()).inflate(itemLayout, parent, false);
// return ViewHolder with View
v.setOnClickListener(mOnClickListener);
return new ViewHolder(v);
}
/**
* Get the size of items in adapter
* #return the size of items in adapter
*/
#Override
public int getItemCount() {
return items.size();
}
/**
* Bind View Holder with Items
* #param holder: the view holder
* #param position : the current position
*/
Change it as follows
Toast.makeText(view.getContext(), item, Toast.LENGTH_LONG).show();
I have a PopupWindow that contains a RecyclerView. The RecyclerView's last element is a button that adds a new item to the end of the adapter's list when clicked.
The problem:
During the first time my PopupWindow has been launched the button successfully adds new items to the RecyclerView with notifyItemInserted(dataSize - 1) when clicked, but the RecyclerView doesn't update and show them. If I close and re-open the PopupWindow however, the items previously added are properly shown in the RecyclerView and it properly updates and animates new items being added to its adapter.
The Question: I'm not sure why the RecyclerView doesn't refresh and show the newly added items on first run of the PopupWindow, but works perfectly from second run onward. How do I make it work during the first run of the PopupWindow?
P.S. Its worth noting that if I use notifyDataSetChanged() the RecyclerView works correctly (displays new items) even on first launch of the PopupWindow. I want to find a way to make notifyItemInserted() work however, because it has nice animations when new items are added.
UserChordsAdapter.java
public class UserChordsAdapter extends RecyclerView.Adapter<UserChordsAdapter.ChordViewHolder> {
private Context context;
private final ListItemClickListener mClickHandler;
private ArrayList<String> mChordData = new ArrayList<String>(); //contains all user created chords as comma delimited note #s
/**
* The interface that receives onClick messages.
*/
public interface ListItemClickListener {
void onListItemClick(int clickedItemIndex);
}
/**
*
* #param clickHandler The on-click handler for this adapter. This single handler is called
* when an item is clicked.
*/
public UserChordsAdapter(ListItemClickListener clickHandler) {
mClickHandler = clickHandler;
}
#Override
public ChordViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
context = parent.getContext();
int layoutIdForListItem = R.layout.user_chord_list_item;
int layoutIdForFooterItem = R.layout.user_chord_add_new;
LayoutInflater inflater = LayoutInflater.from(context);
boolean shouldAttachToParentImmediately = false;
View listItem;
ChordViewHolder viewHolder;
if (viewType == R.layout.user_chord_list_item) { //inflate chord item
listItem = inflater.inflate(layoutIdForListItem, parent, shouldAttachToParentImmediately);
viewHolder = new ChordViewHolder(listItem);
}
else { //inflate "+ Add new" button (last list item)
listItem = inflater.inflate(layoutIdForFooterItem, parent, shouldAttachToParentImmediately);
viewHolder = new ChordViewHolder(listItem);
}
return viewHolder;
}
#Override
public void onBindViewHolder(ChordViewHolder holder, int position) {
if (position == mChordData.size()){
holder.mAddChordButton.setOnClickListener(new View.OnClickListener(){
#Override
public void onClick(View v) {
mChordData.add("1,30,40");
notifyItemInserted(mChordData.size()-1);
}
});
}
else {
holder.mChordName.setText("Chord " + Integer.toString(position));
}
}
#Override
public int getItemCount() {
if (mChordData == null){
return 1;
}
return mChordData.size() + 1; // +1 is for footer button (add new)
}
class ChordViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
// Will display which ViewHolder is displaying this data
TextView mChordName;
Button mAddChordButton;
/**
* Constructor for our ViewHolder. Within this constructor, we get a reference to our
* TextViews and set an onClickListener to listen for clicks. Those will be handled in the
* onClick method below.
*/
public ChordViewHolder(View itemView) {
super(itemView);
mAddChordButton = (Button) itemView.findViewById(R.id.button_add_new);
mChordName = (TextView) itemView.findViewById(R.id.tv_view_holder_instance);
itemView.setOnClickListener(this);
}
/**
* Called whenever a user clicks on an item in the list.
* #param v The View that was clicked
*/
#Override
public void onClick(View v) {
int clickedPosition = getAdapterPosition();
String chordData = mChordData.get(clickedPosition);
mClickHandler.onListItemClick(clickedPosition);
}
}
/**
* Distinguishes if view is a Chord list item or the last item in the list (add new chord)
* #param position
* #return
*/
#Override
public int getItemViewType(int position) {
return (position == mChordData.size()) ? R.layout.user_chord_add_new : R.layout.user_chord_list_item;
}}
FragmentChordMenu.java
public class FragmentChordMenu extends Fragment implements UserChordsAdapter.ListItemClickListener{
private FloatingActionButton mFAB;
private View mPopupView;
private PopupWindow mUserChordMenu;
private RecyclerView mUserChordsList;
private UserChordsAdapter mRecyclerViewAdapter;
private int numItems = 0; //TODO: dynamically calculate this as # of saved chords + 1(add new)
#Override
public void onActivityCreated(#Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mRecyclerViewAdapter = new UserChordsAdapter(this);
}
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, #Nullable ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.menu_fragment_chord, container, false);
LayoutInflater layoutInflater = (LayoutInflater)getActivity().getBaseContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mPopupView = layoutInflater.inflate(R.layout.menu_popup_set_chords, null);
int menuWidth = (int)(MainActivity.getActualWidth()*.95);
int menuHeight = (int)(MainActivity.getActualHeight()*.90);
mUserChordMenu = new PopupWindow(mPopupView, menuWidth, menuHeight);
mUserChordMenu.setFocusable(true);
mFAB = (FloatingActionButton) v.findViewById(R.id.addChord);
mFAB.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
mUserChordMenu.showAtLocation(mPopupView, Gravity.CENTER, 10, 10);
mUserChordsList = (RecyclerView) mPopupView.findViewById(R.id.rv_userChords);
LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());
mUserChordsList.setLayoutManager(layoutManager);
mUserChordsList.setAdapter(mRecyclerViewAdapter);
}
});
return v;
}
/**
* Called from UserChordsAdapter's onClick. Only fires on list item clicks, not the add new button
*
* */
#Override
public void onListItemClick(int clickedItemIndex) {
}}
The problem lies with the logic you use to update your views. Currently what you are saying is this, only notify my data when a view is drawn on the screen(OnBind). That is why it always work for the second try, because whenever a view is being drawn(swipe etc). that onBind method will be triggered.What you need to do is to create a method in the Adapter class, that replaces this logic.
holder.mAddChordButton.setOnClickListener(new View.OnClickListener(){
#Override
public void onClick(View v) {
mChordData.add("1,30,40");
notifyItemInserted(mChordData.size()-1);
}
});
So create a method that adds item to the mChorData set object, then call notifyItemInserted(mChordData.size()-1); in that method. This will always update and notify the adapter of any changes, hence triggering redraw automatically.
First create a public method in UserChordsAdapter that accepts an mChordData for its paramter,then in that method call, notifyItemInserted(mChordData.size()-1);. Firstly you need to expose the clickListener outside of that adapter.
I spent a moment trying to figure out a way to add a header to a RecyclerView, unsuccessfully.
This is what I got so far:
#Override
protected void onCreate(Bundle savedInstanceState) {
...
layouManager = new LinearLayoutManager(getActivity());
recyclerView.setLayoutManager(layouManager);
LayoutInflater inflater = (LayoutInflater) getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
headerPlaceHolder = inflater.inflate(R.layout.view_header_holder_medium, null, false);
layouManager.addView(headerPlaceHolder, 0);
...
}
The LayoutManager seems to be the object handling the disposition of the RecyclerView items. As I couldn't find any addHeaderView(View view) method, I decided to go with the LayoutManager's addView(View view, int position) method and to add my header view in the first position to act as a header.
Aaand this is where things get uglier:
java.lang.NullPointerException: Attempt to read from field 'android.support.v7.widget.RecyclerView$ViewHolder android.support.v7.widget.RecyclerView$LayoutParams.mViewHolder' on a null object reference
at android.support.v7.widget.RecyclerView.getChildViewHolderInt(RecyclerView.java:2497)
at android.support.v7.widget.RecyclerView$LayoutManager.addViewInt(RecyclerView.java:4807)
at android.support.v7.widget.RecyclerView$LayoutManager.addView(RecyclerView.java:4803)
at com.mathieumaree.showz.fragments.CategoryFragment.setRecyclerView(CategoryFragment.java:231)
at com.mathieumaree.showz.fragments.CategoryFragment.access$200(CategoryFragment.java:47)
at com.mathieumaree.showz.fragments.CategoryFragment$2.success(CategoryFragment.java:201)
at com.mathieumaree.showz.fragments.CategoryFragment$2.success(CategoryFragment.java:196)
at retrofit.CallbackRunnable$1.run(CallbackRunnable.java:41)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:135)
at android.app.ActivityThread.main(ActivityThread.java:5221)
at java.lang.reflect.Method.invoke(Native Method)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:899)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:694)
After getting several NullPointerExceptions trying to call the addView(View view) at different moments of the Activity creation (also tried adding the view once everything is set up, even the Adapter's data), I realized I have no idea if this is the right way to do it (and it doesn't look to be).
PS: Also, a solution that could handle the GridLayoutManager in addition to the LinearLayoutManager would be really appreciated!
I had to add a footer to my RecyclerView and here I'm sharing my code snippet as I thought it might be useful. Please check the comments inside the code for better understanding of the overall flow.
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
public class RecyclerViewWithFooterAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final int FOOTER_VIEW = 1;
private ArrayList<String> data; // Take any list that matches your requirement.
private Context context;
// Define a constructor
public RecyclerViewWithFooterAdapter(Context context, ArrayList<String> data) {
this.context = context;
this.data = data;
}
// Define a ViewHolder for Footer view
public class FooterViewHolder extends ViewHolder {
public FooterViewHolder(View itemView) {
super(itemView);
itemView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
// Do whatever you want on clicking the item
}
});
}
}
// Now define the ViewHolder for Normal list item
public class NormalViewHolder extends ViewHolder {
public NormalViewHolder(View itemView) {
super(itemView);
itemView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
// Do whatever you want on clicking the normal items
}
});
}
}
// And now in onCreateViewHolder you have to pass the correct view
// while populating the list item.
#Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v;
if (viewType == FOOTER_VIEW) {
v = LayoutInflater.from(context).inflate(R.layout.list_item_footer, parent, false);
FooterViewHolder vh = new FooterViewHolder(v);
return vh;
}
v = LayoutInflater.from(context).inflate(R.layout.list_item_normal, parent, false);
NormalViewHolder vh = new NormalViewHolder(v);
return vh;
}
// Now bind the ViewHolder in onBindViewHolder
#Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
try {
if (holder instanceof NormalViewHolder) {
NormalViewHolder vh = (NormalViewHolder) holder;
vh.bindView(position);
} else if (holder instanceof FooterViewHolder) {
FooterViewHolder vh = (FooterViewHolder) holder;
}
} catch (Exception e) {
e.printStackTrace();
}
}
// Now the critical part. You have return the exact item count of your list
// I've only one footer. So I returned data.size() + 1
// If you've multiple headers and footers, you've to return total count
// like, headers.size() + data.size() + footers.size()
#Override
public int getItemCount() {
if (data == null) {
return 0;
}
if (data.size() == 0) {
//Return 1 here to show nothing
return 1;
}
// Add extra view to show the footer view
return data.size() + 1;
}
// Now define getItemViewType of your own.
#Override
public int getItemViewType(int position) {
if (position == data.size()) {
// This is where we'll add footer.
return FOOTER_VIEW;
}
return super.getItemViewType(position);
}
// So you're done with adding a footer and its action on onClick.
// Now set the default ViewHolder for NormalViewHolder
public class ViewHolder extends RecyclerView.ViewHolder {
// Define elements of a row here
public ViewHolder(View itemView) {
super(itemView);
// Find view by ID and initialize here
}
public void bindView(int position) {
// bindView() method to implement actions
}
}
}
The above code snippet adds a footer to the RecyclerView. You can check this GitHub repository for checking the implementation of adding both header and a footer.
Very simple to solve!!
I don't like an idea of having logic inside adapter as a different view type because every time it checks for the view type before returning the view. Below solution avoids extra checks.
Just add LinearLayout (vertical) header view + recyclerview + footer view inside android.support.v4.widget.NestedScrollView.
Check this out:
<android.support.v4.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<View
android:id="#+id/header"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<android.support.v7.widget.RecyclerView
android:id="#+id/list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layoutManager="LinearLayoutManager"/>
<View
android:id="#+id/footer"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
</android.support.v4.widget.NestedScrollView>
Add this line of code for smooth scrolling
RecyclerView v = (RecyclerView) findViewById(...);
v.setNestedScrollingEnabled(false);
This will lose all RV performance and RV will try to lay out all view holders regardless of the layout_height of RV
Recommended using for the small size list like Nav drawer or settings etc.
I had the same problem on Lollipop and created two approaches to wrap the Recyclerview adapter. One is pretty easy to use, but I'm not sure how it will behave with a changing dataset. Because it wraps your adapter and you need to make yourself sure to call methods like notifyDataSetChanged on the right adapter-object.
The other shouldn't have such problems. Just let your regular adapter extend the class, implement the abstract methods and you should be ready. And here they are:
gists
HeaderRecyclerViewAdapterV1.java usage new HeaderRecyclerViewAdapterV1(new RegularAdapter());
HeaderRecyclerViewAdapterV2.java usage RegularAdapter extends HeaderRecyclerViewAdapterV2
HeaderRecyclerViewAdapterV1
import android.support.v7.widget.RecyclerView;
import android.view.ViewGroup;
/**
* Created by sebnapi on 08.11.14.
* <p/>
* This is a Plug-and-Play Approach for adding a Header or Footer to
* a RecyclerView backed list
* <p/>
* Just wrap your regular adapter like this
* <p/>
* new HeaderRecyclerViewAdapterV1(new RegularAdapter())
* <p/>
* Let RegularAdapter implement HeaderRecyclerView, FooterRecyclerView or both
* and you are ready to go.
* <p/>
* I'm absolutely not sure how this will behave with changes in the dataset.
* You can always wrap a fresh adapter and make sure to not change the old one or
* use my other approach.
* <p/>
* With the other approach you need to let your Adapter extend HeaderRecyclerViewAdapterV2
* (and therefore change potentially more code) but possible omit these shortcomings.
* <p/>
* TOTALLY UNTESTED - USE WITH CARE - HAVE FUN :)
*/
public class HeaderRecyclerViewAdapterV1 extends RecyclerView.Adapter {
private static final int TYPE_HEADER = Integer.MIN_VALUE;
private static final int TYPE_FOOTER = Integer.MIN_VALUE + 1;
private static final int TYPE_ADAPTEE_OFFSET = 2;
private final RecyclerView.Adapter mAdaptee;
public HeaderRecyclerViewAdapterV1(RecyclerView.Adapter adaptee) {
mAdaptee = adaptee;
}
public RecyclerView.Adapter getAdaptee() {
return mAdaptee;
}
#Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == TYPE_HEADER && mAdaptee instanceof HeaderRecyclerView) {
return ((HeaderRecyclerView) mAdaptee).onCreateHeaderViewHolder(parent, viewType);
} else if (viewType == TYPE_FOOTER && mAdaptee instanceof FooterRecyclerView) {
return ((FooterRecyclerView) mAdaptee).onCreateFooterViewHolder(parent, viewType);
}
return mAdaptee.onCreateViewHolder(parent, viewType - TYPE_ADAPTEE_OFFSET);
}
#Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (position == 0 && holder.getItemViewType() == TYPE_HEADER && useHeader()) {
((HeaderRecyclerView) mAdaptee).onBindHeaderView(holder, position);
} else if (position == mAdaptee.getItemCount() && holder.getItemViewType() == TYPE_FOOTER && useFooter()) {
((FooterRecyclerView) mAdaptee).onBindFooterView(holder, position);
} else {
mAdaptee.onBindViewHolder(holder, position - (useHeader() ? 1 : 0));
}
}
#Override
public int getItemCount() {
int itemCount = mAdaptee.getItemCount();
if (useHeader()) {
itemCount += 1;
}
if (useFooter()) {
itemCount += 1;
}
return itemCount;
}
private boolean useHeader() {
if (mAdaptee instanceof HeaderRecyclerView) {
return true;
}
return false;
}
private boolean useFooter() {
if (mAdaptee instanceof FooterRecyclerView) {
return true;
}
return false;
}
#Override
public int getItemViewType(int position) {
if (position == 0 && useHeader()) {
return TYPE_HEADER;
}
if (position == mAdaptee.getItemCount() && useFooter()) {
return TYPE_FOOTER;
}
if (mAdaptee.getItemCount() >= Integer.MAX_VALUE - TYPE_ADAPTEE_OFFSET) {
new IllegalStateException("HeaderRecyclerViewAdapter offsets your BasicItemType by " + TYPE_ADAPTEE_OFFSET + ".");
}
return mAdaptee.getItemViewType(position) + TYPE_ADAPTEE_OFFSET;
}
public static interface HeaderRecyclerView {
public RecyclerView.ViewHolder onCreateHeaderViewHolder(ViewGroup parent, int viewType);
public void onBindHeaderView(RecyclerView.ViewHolder holder, int position);
}
public static interface FooterRecyclerView {
public RecyclerView.ViewHolder onCreateFooterViewHolder(ViewGroup parent, int viewType);
public void onBindFooterView(RecyclerView.ViewHolder holder, int position);
}
}
HeaderRecyclerViewAdapterV2
import android.support.v7.widget.RecyclerView;
import android.view.ViewGroup;
/**
* Created by sebnapi on 08.11.14.
* <p/>
* If you extend this Adapter you are able to add a Header, a Footer or both
* by a similar ViewHolder pattern as in RecyclerView.
* <p/>
* If you want to omit changes to your class hierarchy you can try the Plug-and-Play
* approach HeaderRecyclerViewAdapterV1.
* <p/>
* Don't override (Be careful while overriding)
* - onCreateViewHolder
* - onBindViewHolder
* - getItemCount
* - getItemViewType
* <p/>
* You need to override the abstract methods introduced by this class. This class
* is not using generics as RecyclerView.Adapter make yourself sure to cast right.
* <p/>
* TOTALLY UNTESTED - USE WITH CARE - HAVE FUN :)
*/
public abstract class HeaderRecyclerViewAdapterV2 extends RecyclerView.Adapter {
private static final int TYPE_HEADER = Integer.MIN_VALUE;
private static final int TYPE_FOOTER = Integer.MIN_VALUE + 1;
private static final int TYPE_ADAPTEE_OFFSET = 2;
#Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == TYPE_HEADER) {
return onCreateHeaderViewHolder(parent, viewType);
} else if (viewType == TYPE_FOOTER) {
return onCreateFooterViewHolder(parent, viewType);
}
return onCreateBasicItemViewHolder(parent, viewType - TYPE_ADAPTEE_OFFSET);
}
#Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (position == 0 && holder.getItemViewType() == TYPE_HEADER) {
onBindHeaderView(holder, position);
} else if (position == getBasicItemCount() && holder.getItemViewType() == TYPE_FOOTER) {
onBindFooterView(holder, position);
} else {
onBindBasicItemView(holder, position - (useHeader() ? 1 : 0));
}
}
#Override
public int getItemCount() {
int itemCount = getBasicItemCount();
if (useHeader()) {
itemCount += 1;
}
if (useFooter()) {
itemCount += 1;
}
return itemCount;
}
#Override
public int getItemViewType(int position) {
if (position == 0 && useHeader()) {
return TYPE_HEADER;
}
if (position == getBasicItemCount() && useFooter()) {
return TYPE_FOOTER;
}
if (getBasicItemType(position) >= Integer.MAX_VALUE - TYPE_ADAPTEE_OFFSET) {
new IllegalStateException("HeaderRecyclerViewAdapter offsets your BasicItemType by " + TYPE_ADAPTEE_OFFSET + ".");
}
return getBasicItemType(position) + TYPE_ADAPTEE_OFFSET;
}
public abstract boolean useHeader();
public abstract RecyclerView.ViewHolder onCreateHeaderViewHolder(ViewGroup parent, int viewType);
public abstract void onBindHeaderView(RecyclerView.ViewHolder holder, int position);
public abstract boolean useFooter();
public abstract RecyclerView.ViewHolder onCreateFooterViewHolder(ViewGroup parent, int viewType);
public abstract void onBindFooterView(RecyclerView.ViewHolder holder, int position);
public abstract RecyclerView.ViewHolder onCreateBasicItemViewHolder(ViewGroup parent, int viewType);
public abstract void onBindBasicItemView(RecyclerView.ViewHolder holder, int position);
public abstract int getBasicItemCount();
/**
* make sure you don't use [Integer.MAX_VALUE-1, Integer.MAX_VALUE] as BasicItemViewType
*
* #param position
* #return
*/
public abstract int getBasicItemType(int position);
}
Feedback and forks appreciated. I will use HeaderRecyclerViewAdapterV2 by my self and evolve, test and post the changes in the future.
EDIT: #OvidiuLatcu Yes I had some problems. Actually I stopped offsetting the Header implicitly by position - (useHeader() ? 1 : 0) and instead created a public method int offsetPosition(int position) for it. Because if you set an OnItemTouchListener on Recyclerview, you can intercept the touch, get the x,y coordinates of the touch, find the according child view and then call recyclerView.getChildPosition(...)and you will always get the non-offsetted position in the adapter! This is a shortcomming in the RecyclerView Code, I don't see an easy method to overcome this. This is why I now offset the positions explicit when I need to by my own code.
I haven't tried this, but I would simply add 1 (or 2, if you want both a header and footer) to the integer returned by getItemCount in your adapter. You can then override getItemViewType in your adapter to return a different integer when i==0: https://developer.android.com/reference/android/support/v7/widget/RecyclerView.Adapter.html#getItemViewType(int)
createViewHolder is then passed the integer you returned from getItemViewType, allowing you to create or configure the view holder differently for the header view: https://developer.android.com/reference/android/support/v7/widget/RecyclerView.Adapter.html#createViewHolder(android.view.ViewGroup, int)
Don't forget to subtract one from the position integer passed to bindViewHolder.
You can used this GitHub library allowing to add Header and/or Footer in your RecyclerView in the simplest way possible.
You need to add HFRecyclerView library in your project or you can also grab it from Gradle:
compile 'com.mikhaellopez:hfrecyclerview:1.0.0'
This is a result in image:
EDIT:
If you just want to add a margin at the top and/or bottom with this library: SimpleItemDecoration:
int offsetPx = 10;
recyclerView.addItemDecoration(new StartOffsetItemDecoration(offsetPx));
recyclerView.addItemDecoration(new EndOffsetItemDecoration(offsetPx));
I ended up implementing my own adapter to wrap any other adapter and provide methods to add header and footer views.
Created a gist here: HeaderViewRecyclerAdapter.java
The main feature I wanted was a similar interface to a ListView, so I wanted to be able to inflate the views in my Fragment and add them to the RecyclerView in onCreateView. This is done by creating a HeaderViewRecyclerAdapter passing the adapter to be wrapped, and calling addHeaderView and addFooterView passing your inflated views. Then set the HeaderViewRecyclerAdapter instance as the adapter on the RecyclerView.
An extra requirement was that I needed to be able to easily swap out adapters while keeping the headers and footers, I didn't want to have multiple adapters with multiple instances of these headers and footers. So you can call setAdapter to change the wrapped adapter leaving the headers and footers intact, with the RecyclerView being notified of the change.
recyclerview:1.2.0 introduces ConcatAdapter class which concatenates multiple adapters into a single one. So it allows to create separate header/footer adapters and reuse them across multiple lists.
myRecyclerView.adapter = ConcatAdapter(headerAdapter, listAdapter, footerAdapter)
Take a look at the announcement article. It contains a sample how to display a loading progress in header and footer using ConcatAdapter.
For the moment when I post this answer the version 1.2.0 of the library is in alpha stage, so the api might change. You can check the status here.
my "keep it simple stupid" way ...it waste some resources , i know , but i dont care as my code keep simple
so...
First, add a footer with visibility GONE to your item_layout
<LinearLayout
android:id="#+id/footer"
android:layout_width="match_parent"
android:layout_height="80dp"
android:orientation="vertical"
android:visibility="gone">
</LinearLayout>
Then, set it visible on the last item
public void onBindViewHolder(ChannelAdapter.MyViewHolder holder, int position) {
boolean last = position==data.size()-1;
//....
holder.footer.setVisibility(View.GONE);
if (last && showFooter){
holder.footer.setVisibility(View.VISIBLE);
}
}
do the opposite for header
Based on #seb's solution, I created a subclass of RecyclerView.Adapter that supports an arbitrary number of headers and footers.
https://gist.github.com/mheras/0908873267def75dc746
Although it seems to be a solution, I also think this thing should be managed by the LayoutManager. Unfortunately, I need it now and I don't have time to implement a StaggeredGridLayoutManager from scratch (nor even extend from it).
I'm still testing it, but you can try it out if you want. Please let me know if you find any issues with it.
You can use viewtype to solve this problem, here is my demo:
https://github.com/yefengfreedom/RecyclerViewWithHeaderFooterLoadingEmptyViewErrorView
you can define some recycler view display mode:
public static final int MODE_DATA = 0, MODE_LOADING = 1, MODE_ERROR = 2, MODE_EMPTY = 3, MODE_HEADER_VIEW = 4, MODE_FOOTER_VIEW = 5;
2.override the getItemViewType mothod
#Override
public int getItemViewType(int position) {
if (mMode == RecyclerViewMode.MODE_LOADING) {
return RecyclerViewMode.MODE_LOADING;
}
if (mMode == RecyclerViewMode.MODE_ERROR) {
return RecyclerViewMode.MODE_ERROR;
}
if (mMode == RecyclerViewMode.MODE_EMPTY) {
return RecyclerViewMode.MODE_EMPTY;
}
//check what type our position is, based on the assumption that the order is headers > items > footers
if (position < mHeaders.size()) {
return RecyclerViewMode.MODE_HEADER_VIEW;
} else if (position >= mHeaders.size() + mData.size()) {
return RecyclerViewMode.MODE_FOOTER_VIEW;
}
return RecyclerViewMode.MODE_DATA;
}
3.override the getItemCount method
#Override
public int getItemCount() {
if (mMode == RecyclerViewMode.MODE_DATA) {
return mData.size() + mHeaders.size() + mFooters.size();
} else {
return 1;
}
}
4.override the onCreateViewHolder method. create view holder by viewType
#Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == RecyclerViewMode.MODE_LOADING) {
RecyclerView.ViewHolder loadingViewHolder = onCreateLoadingViewHolder(parent);
loadingViewHolder.itemView.setLayoutParams(
new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, parent.getHeight() - mToolBarHeight)
);
return loadingViewHolder;
}
if (viewType == RecyclerViewMode.MODE_ERROR) {
RecyclerView.ViewHolder errorViewHolder = onCreateErrorViewHolder(parent);
errorViewHolder.itemView.setLayoutParams(
new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, parent.getHeight() - mToolBarHeight)
);
errorViewHolder.itemView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(final View v) {
if (null != mOnErrorViewClickListener) {
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
mOnErrorViewClickListener.onErrorViewClick(v);
}
}, 200);
}
}
});
return errorViewHolder;
}
if (viewType == RecyclerViewMode.MODE_EMPTY) {
RecyclerView.ViewHolder emptyViewHolder = onCreateEmptyViewHolder(parent);
emptyViewHolder.itemView.setLayoutParams(
new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, parent.getHeight() - mToolBarHeight)
);
emptyViewHolder.itemView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(final View v) {
if (null != mOnEmptyViewClickListener) {
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
mOnEmptyViewClickListener.onEmptyViewClick(v);
}
}, 200);
}
}
});
return emptyViewHolder;
}
if (viewType == RecyclerViewMode.MODE_HEADER_VIEW) {
RecyclerView.ViewHolder headerViewHolder = onCreateHeaderViewHolder(parent);
headerViewHolder.itemView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(final View v) {
if (null != mOnHeaderViewClickListener) {
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
mOnHeaderViewClickListener.onHeaderViewClick(v, v.getTag());
}
}, 200);
}
}
});
return headerViewHolder;
}
if (viewType == RecyclerViewMode.MODE_FOOTER_VIEW) {
RecyclerView.ViewHolder footerViewHolder = onCreateFooterViewHolder(parent);
footerViewHolder.itemView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(final View v) {
if (null != mOnFooterViewClickListener) {
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
mOnFooterViewClickListener.onFooterViewClick(v, v.getTag());
}
}, 200);
}
}
});
return footerViewHolder;
}
RecyclerView.ViewHolder dataViewHolder = onCreateDataViewHolder(parent);
dataViewHolder.itemView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(final View v) {
if (null != mOnItemClickListener) {
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
mOnItemClickListener.onItemClick(v, v.getTag());
}
}, 200);
}
}
});
dataViewHolder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
#Override
public boolean onLongClick(final View v) {
if (null != mOnItemLongClickListener) {
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
mOnItemLongClickListener.onItemLongClick(v, v.getTag());
}
}, 200);
return true;
}
return false;
}
});
return dataViewHolder;
}
5.Override the onBindViewHolder method. bind data by viewType
#Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (mMode == RecyclerViewMode.MODE_LOADING) {
onBindLoadingViewHolder(holder, position);
} else if (mMode == RecyclerViewMode.MODE_ERROR) {
onBindErrorViewHolder(holder, position);
} else if (mMode == RecyclerViewMode.MODE_EMPTY) {
onBindEmptyViewHolder(holder, position);
} else {
if (position < mHeaders.size()) {
if (mHeaders.size() > 0) {
onBindHeaderViewHolder(holder, position);
}
} else if (position >= mHeaders.size() + mData.size()) {
if (mFooters.size() > 0) {
onBindFooterViewHolder(holder, position - mHeaders.size() - mData.size());
}
} else {
onBindDataViewHolder(holder, position - mHeaders.size());
}
}
}
You can use the library SectionedRecyclerViewAdapter to group your items in sections and add a header to each section, like on the image below:
First you create your section class:
class MySection extends StatelessSection {
String title;
List<String> list;
public MySection(String title, List<String> list) {
// call constructor with layout resources for this Section header, footer and items
super(R.layout.section_header, R.layout.section_item);
this.title = title;
this.list = list;
}
#Override
public int getContentItemsTotal() {
return list.size(); // number of items of this section
}
#Override
public RecyclerView.ViewHolder getItemViewHolder(View view) {
// return a custom instance of ViewHolder for the items of this section
return new MyItemViewHolder(view);
}
#Override
public void onBindItemViewHolder(RecyclerView.ViewHolder holder, int position) {
MyItemViewHolder itemHolder = (MyItemViewHolder) holder;
// bind your view here
itemHolder.tvItem.setText(list.get(position));
}
#Override
public RecyclerView.ViewHolder getHeaderViewHolder(View view) {
return new SimpleHeaderViewHolder(view);
}
#Override
public void onBindHeaderViewHolder(RecyclerView.ViewHolder holder) {
MyHeaderViewHolder headerHolder = (MyHeaderViewHolder) holder;
// bind your header view here
headerHolder.tvItem.setText(title);
}
}
Then you set up the RecyclerView with your sections and change the SpanSize of the headers with a GridLayoutManager:
// Create an instance of SectionedRecyclerViewAdapter
SectionedRecyclerViewAdapter sectionAdapter = new SectionedRecyclerViewAdapter();
// Create your sections with the list of data
MySection section1 = new MySection("My Section 1 title", dataList1);
MySection section2 = new MySection("My Section 2 title", dataList2);
// Add your Sections to the adapter
sectionAdapter.addSection(section1);
sectionAdapter.addSection(section2);
// Set up a GridLayoutManager to change the SpanSize of the header
GridLayoutManager glm = new GridLayoutManager(getContext(), 2);
glm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
#Override
public int getSpanSize(int position) {
switch(sectionAdapter.getSectionItemViewType(position)) {
case SectionedRecyclerViewAdapter.VIEW_TYPE_HEADER:
return 2;
default:
return 1;
}
}
});
// Set up your RecyclerView with the SectionedRecyclerViewAdapter
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recyclerview);
recyclerView.setLayoutManager(glm);
recyclerView.setAdapter(sectionAdapter);
I would just add an alternative to all those HeaderRecyclerViewAdapter implementation. CompoundAdapter:
https://github.com/negusoft/CompoundAdapter-android
It is a more flexible approach, since you can create a AdapterGroup out of Adapters. For the header example, use your adapter as it is, along with an adapter containing one item for the header:
AdapterGroup adapterGroup = new AdapterGroup();
adapterGroup.addAdapter(SingleAdapter.create(R.layout.header));
adapterGroup.addAdapter(new MyAdapter(...));
recyclerView.setAdapter(adapterGroup);
It is fairly simple and readable. You can implement more complex adapter easily using the same principle.
Excellent answer by #reaz-murshed shared here . But I dont like the part where datasize is added with +1 and returning Footer View if the end is reached.
It tells that every last element is a Footer View and I had really hard time removing the footer view.
Instead did something like this for my case -
private List<RealResponse> addEmptyLoaderResponse(List<RealResponse> originalList){
if(originalList == null){
originalList= new ArrayList<>();
}
originalList.add(new EmptyRealResponse());
return originalList;
}
private class EmptyRealResponse extends RealResponse{
/**Just an Empty class as placeholder for loader at Footer View
*
*/
}
public void setItems(List<InconcurPostResponse> items) {
this.items = addEmptyLoaderResponse(items);
}
#Override
public int getItemCount() {
return items.size();
}
#Override
public int getItemViewType(int position){
if(this.items.get(position) instanceof EmptyRealResponse){
return ViewTypes.FOOTER_VIEW_TYPE.getViewType();
}
return super.getItemViewType(position);
}
This is way cleaner for me and It loads actual Object into Recycler View. Plus I did get the benefit of removing the Footer View when I dont need it Or If I want to add more Placeholder Footer View.
I know I come late, but only recently I was able to implement such "addHeader" to the Adapter. In my FlexibleAdapter project you can call setHeader on a Sectionable item, then you call showAllHeaders. If you need only 1 header then the first item should have the header. If you delete this item, then the header is automatically linked to the next one.
Unfortunately footers are not covered (yet).
The FlexibleAdapter allows you to do much more than create headers/sections.
You really should have a look: https://github.com/davideas/FlexibleAdapter.