I'm trying to implement an EmptyView on my RecyclerView Adapter but I'm not getting any result.
I've followed this tutorial and this tip, but noone worked for me.
I've implemented:
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;
But it doesn't let me compile because they are differents ViewHolder, because I've created two ViewHolder classes but they extends Recycler.ViewHolder so I don't get it...
I'm trying to do this because I've got a SearchView and I want when the list is empty it shows an EmptyView, I've got it doing it programmatically but I prefer to add like a layout because I don't know that much how to put TextViews and Buttons programmatically.
Also if I put
return dataList.size() > 0 ? dataList.size() : 1;
It gives to me error because index is 0.
I've debugged the viewType and always is 1, then it won't join the if condition...
Deep on Android I found this :
/**
* Return the view type of the item at <code>position</code> for the purposes
* of view recycling.
*
* <p>The default implementation of this method returns 0, making the assumption of
* a single view type for the adapter. Unlike ListView adapters, types need not
* be contiguous. Consider using id resources to uniquely identify item view types.
*
* #param position position to query
* #return integer value identifying the type of the view needed to represent the item at
* <code>position</code>. Type codes need not be contiguous.
*/
public int getItemViewType(int position) {
return 0;
}
But the thing is that no changes the value.
EDIT
I almost done it, I did this :
#Override
public int getItemViewType(int position) {
return list.size() > 0 ? list.size() : 1;
}
But sometimes it returns 0 when the size() is 0... I don't get it, I'm using this SearchView, and sometimes when I type a letter that doesn't matches with any item of the list it doesn't show and sometimes it does...
Also other thing that happens is that when the layout popups it shows on the left of the screen when I put that is on center, but I think it's problem with RecyclerView because the layout puts inside of it.
RecyclerView layout :
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="#dimen/activity_vertical_margin"
android:paddingLeft="#dimen/activity_horizontal_margin"
android:paddingRight="#dimen/activity_horizontal_margin"
android:paddingTop="#dimen/activity_vertical_margin"
android:id="#+id/rtpew"
android:layout_centerInParent="true"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
>
<LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="#+id/linearpew">
<android.support.v7.widget.RecyclerView
android:id="#+id/rv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
</LinearLayout>
</RelativeLayout>
And this is my emptylayout :
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/ImageViewSearchFail"
android:src="#drawable/sadface"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:layout_gravity="center"
android:textSize="#dimen/15dp"
android:layout_marginTop="4dp"
android:text="foo"
android:layout_below="#+id/ImageViewSearchFail"
android:layout_centerHorizontal="true" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/ButtonAddEntity"
android:text="foo"
android:background="?android:selectableItemBackground"
android:layout_centerVertical="true"
android:layout_centerHorizontal="true" />
</RelativeLayout>
The other way that I thought is to implement it programmatically as follow :
#Override
public boolean onQueryTextChange(String query) {
final ArrayList<List> filteredModelList = filter(mModel, query);
mAdapter.animateTo(filteredModelList);
rv.scrollToPosition(0);
if(query.isEmpty()){
//Here
}
return true;
}
And :
private ArrayList<List> filter(ArrayList<List> models, String query) {
query = query.toLowerCase();
final ArrayList<List> filteredModelList = new ArrayList<List>();
for (List model : models) {
final String text = model.getRedName().toLowerCase();
if (text.contains(query)) {
filteredModelList.add(model);
}
}
if (filteredModelList.size()<0) {
//HERE
}
else{
//Delete the views added
}
return filteredModelList;
}
PROBLEMS
-I only add the view using the #Jimeux answer but I'd like to do this on the Adapter, I got it, but not always shows the view even if the list is empty.
-At the time to put the emptyview.xml it puts inside of the RecyclerView then since I've put all of this xml at the center it shows on the right. I've tried to add the xml programmatically but it's like a chaos....
Since you need to handle two different kind of views, it would be easier to use an intermediate list of business object for more easily binding them with views. Idea is to have a kind of placeholder in your list for representing empty state. Defining an intermediate layer is extremely useful in this sense for allowing you to consider eventual changes to be applied to your list in future (e.g. adding you element types). Moreover in this way you can more clearly separate your business model from ui representation (for example you can implement methods returning ui settings based on internal status of model objects).
You can proceed as follows:
Define a dedicated abstract type for List items (e.g. ListItem) to wrap your business objects. Its implementation could be something like this:
public abstract class ListItem {
public static final int TYPE_EMPTY = 0;
public static final int TYPE_MY_OBJ = 1;
abstract public int getType();
}
Define a class for each of your List element type:
public class EmptyItem extends ListItem {
#Override
public int getType() {
return TYPE_EMPTY;
}
}
public class MyObjItem extends ListItem {
private MyObj obj;
public ContactItem(MyObj obj) {
this.obj = obj;
}
public MyObj getMyObj() {
return obj;
}
// here you can also add methods for simplify
// objects rendering (e.g. get background color
// based on your object internal status)
#Override
public int getType() {
return TYPE_MY_OBJ;
}
}
Create your list.
List<ListItem> mItems = new ArrayList<>();
if (dataList != null && dataList.size() > 0) {
for (MyObj obj : dataList) {
mItems.add(new MyObjItem(obj));
}
} else {
mItems.add(new EmptyItem());
}
This is the most important part of code. You have many options for creating this list. You can do it inside your RecyclerView Adapter or outside, but it's extremely important to properly handle eventual modifications to it. This is essential for exploiting Adapter notify methods. For example, if you create list within the Adapter, it should probably provide also methods for adding or removing your model items. For example:
public void addObj(MyObj obj) {
if (mItems.size() == 1 && mItems.get(0).getType() == ListItem.EMPTY_TYPE) {
mItems.clear();
}
mItems.add(new MyObjItem(obj));
notifyDataSetChanged();
}
Define an adapter for your RecyclerView, working on List defined at point 3. Here what is important is to override getItemViewType method as follows:
#Override
public int getItemViewType(int position) {
return mItems.get(position).getType();
}
Moreover, type of ViewHolder should be RecyclerView.ViewHolder (unless you decide to create an intermediate class even in this case).
Then you need to have two layouts and ViewHolder for empty and business obj items. Adapter methods should take care of this accordingly:
#Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == ListItem.TYPE_EMPTY) {
View itemView = mLayoutInflater.inflate(R.layout.empty_layout, parent, false);
return new EmptyViewHolder(itemView);
} else {
View itemView = mLayoutInflater.inflate(R.layout.myobj_layout, parent, false);
return new MyObjViewHolder(itemView);
}
}
#Override
public void onBindViewHolder(final RecyclerView.ViewHolder viewHolder, final int position) {
int type = getItemViewType(position);
if (type == ListItem.TYPE_EMPTY) {
EmptyItem header = (EmptyItem) mItems.get(position);
EmptyViewHolder holder = (EmptyViewHolder) viewHolder;
// your logic here... probably nothing to do since it's empty
} else {
MyObjItem event = (MyObjItem) mItems.get(position);
MyObjViewHolder holder = (MyObjViewHolder) viewHolder;
// your logic here
}
}
Of course, as I wrote at the beginning you don't need to strictly define intermediate types for ui representation (EmptyItem and MyObjItem). You can even just use MyObj type and create a specific configuration for it that represent an empty placeholder. This approach is probably not the best in case in future you need to make your logic more complex by including for example new list item types.
Follow the below steps one by one
1). Since you have two types of views for your RecyclerView item, your adapter declaration should look like this a generic one
public class YourAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
and your ViewHolders for both listview item and empty view should extend RecyclerView.ViewHolder like this
static class ListItemViewHolder extends RecyclerView.ViewHolder {
public ListItemViewHolder(View itemView) {
super(itemView);
// initialize your views here for list items
}
}
static class EmptyViewViewHolder extends RecyclerView.ViewHolder {
public EmptyViewViewHolder(View itemView) {
super(itemView);
// initialize your views here for empty list
}
}
2). You have to Override getItemCount() and getItemViewType()
#Override
public int getItemCount() {
return yourList.size() > 0 ? yourList.size() : 1;// if size of your list is greater than 0, you will return your size of list otherwise 1 for the empty view.
}
#Override
public int getItemViewType(int position) {
if (yourList.size() == 0) {
return VIEW_TYPE_EMPTY;
}
return position;
}
3). Your onCreateViewHolder() will look alike this now
#Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == VIEW_TYPE_EMPTY) {
return new EmptyViewViewHolder(mLayoutInflater
.inflate(R.layout.empty_view_layout, parent, false));
} else {
return new ListItemViewHolder(mLayoutInflater
.inflate(R.layout.row_list_item, parent, false));
}
}
4). Same check you have to apply in your onBindViewHolder() as well
#Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (getItemViewType(position) == VIEW_TYPE_EMPTY) {
EmptyViewViewHolder emptyViewViewHolder = (EmptyViewViewHolder) holder;
// set values for your empty views
} else {
ListItemViewHolder listItemViewHolder = (ListItemViewHolder) holder;
// set values for your list items
}
}
5). At last Override your SearcView.setOnQueryTextListener()
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
#Override
public boolean onQueryTextSubmit(String query) {
return false;
}
#Override
public boolean onQueryTextChange(String newText) {
currentSearchKeyword = newText.trim();
if(currentSearchKeyword.iseEmpty()){
yourList.clear();
yourAdapter.notifyDataSetChanged();
}else{
// there will two cases again 1). If your currentSearchKeyword matchces with list results, add that items to your list and notify your adapter. 2) If the currentSearchKeyword doesn't matched with list results, clear your list and notify your adapter;
}
return false;
}
});
Hope it will help you, let me know if any issues.
The compilation error probably results because of you extending RecyclerView.Adapter with your main ViewHolder as the generic argument.
You should make it like
YourAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
And then cast your ViewHolders appropriately (you can reuse getViewType(position) here). Be sure to switch the ViewHolder type in your methods as well.
If I were you, I wouldn't put the empty view in the adapter at all. Put it under your linearpew layout that's holding the RecyclerView and hide/show it as your data changes. You can easily add a loading view, error view, etc. with this setup too.
Here's a bit of simplified code from one of my apps to give you some ideas. #Bind comes from Butter Knife if you're not familiar with it. You may also want to check out Jake Wharton's u2020 project for more RecyclerView ideas.
//fragment_layout.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:id="#+id/content">
</FrameLayout>
<include layout="#layout/status_views" />
</RelativeLayout>
//status_views.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<LinearLayout style="#style/ListStatusView"
android:id="#+id/empty_view"/>
<LinearLayout style="#style/ListStatusView"
android:id="#+id/error_view"/>
<LinearLayout style="#style/ListStatusView"
android:id="#+id/loading_view"
android:padding="30dp"/>
</LinearLayout>
//MyFragment.java
#Bind(R.id.content) protected ViewGroup contentView;
#Bind(R.id.loading_view) protected ViewGroup loadingView;
#Bind(R.id.empty_view) protected ViewGroup emptyView;
#Bind(R.id.error_view) protected ViewGroup errorView;
#Bind({R.id.loading_view, R.id.error_view, R.id.empty_view, R.id.content})
protected List<ViewGroup> stateViews;
protected void activateView(View view) {
for (ViewGroup vg : stateViews)
vg.setVisibility(View.GONE);
view.setVisibility(VISIBLE);
}
#Override
public void onActivityCreated(#Nullable Bundle state) {
super.onActivityCreated(state);
if (state == null) {
activateView(loadingView);
loadData();
} else if (data.isEmpty())
activateView(emptyView);
else
activateView(contentView);
}
Edit: Here's a simplified version without Butter Knife.
private ViewGroup contentView;
private ViewGroup emptyView;
#Override
protected void onCreate(#Nullable Bundle state) {
super.onCreate(state);
setContentView(R.layout.activity_main);
contentView = (ViewGroup) findViewById(R.id.content_view);
emptyView = (ViewGroup) findViewById(R.id.empty_view);
}
#Override
public boolean onQueryTextChange(String query) {
final ArrayList<List> filteredModelList = filter(mModel, query);
mAdapter.animateTo(filteredModelList);
rv.scrollToPosition(0);
if(query.isEmpty()){
contentView.setVisibility(View.GONE);
emptyView.setVisibility(View.VISIBLE);
} else {
contentView.setVisibility(View.VISIBLE);
emptyView.setVisibility(View.GONE);
}
return true;
}
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/rtpew"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true">
<LinearLayout
android:id="#+id/content_view"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.RecyclerView
android:id="#+id/rv"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
<RelativeLayout android:id="#+id/empty_view">
<ImageView android:src="#drawable/sadface"/>
<TextView android:text="foo"/>
<Button android:id="#+id/ButtonAddEntity"/>
</RelativeLayout>
</RelativeLayout>
Here is what you can try:
1. Replace
EmptyViewHolder evh = new EmptyViewHolder(v);
with
RecyclerView.ViewHolder evh = new EmptyViewHolder(v);
This is probably why the compilation fails.
2. Replace
#Override
public int getItemViewType(int position) {
return list.size() > 0 ? list.size() : 1;
}
with
#Override
public int getItemViewType(int position) {
return list.get(position) != null ? 1 : 0;
}
For this to work, you must insert a null object whenever you want to show an EmptyView:
int progressPosition = list.size();
list.add(null);
adapter.notifyItemInserted(progressPosition);
and remove the null object when you want to hide the EmptyView:
int progressPosition = existingList.size() - 1;
existingList.remove(progressPosition);
adapter.notifyItemRemoved(progressPosition);
Also, you must modify your onCreateViewHolder() method as follows:
#Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == 1) {
// inflate your default ViewHolder here ...
} else {
// inflate the EmptyViewHolder here
}
}
I believe we have discussed this before ... see this question for a detailed discussion on this.
3. Instead of using a SearchView, consider using an AutoCompleteTextView with a Filter. This may be easier to integrate with your RecyclerView's Adapter. See this answer for an example of this.
I will update this answer as I understand your question better ... do try this and update me.
Related
I am trying to achieve a chatting app type layout in RecylcerView. Now the only problem is that even though I have set width of View to wrap around text. Inside RecyclerView sometime small words take whole line and sometime the word is fine.
Green highlight is expected behavior and red is unexpected behavior.
This is the code of cardView
<androidx.cardview.widget.CardView
android:id="#+id/cardView3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
app:cardBackgroundColor="#00BCD4"
app:cardCornerRadius="12dp"
app:cardPreventCornerOverlap="false"
app:cardUseCompatPadding="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.123">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="#+id/textMessageSent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxWidth="260dp"
android:paddingBottom="8dp"
android:paddingLeft="12dp"
android:paddingRight="12dp"
android:paddingTop="8dp"
android:text="oh noo"
android:textColor="#000000"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:maxLines="10" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
Here is Adapter code
public class ChatAdapter extends ListAdapter {
String reciever,sender;
public ChatAdapter(String from,String to)
{
super(DIFF_CALLBACK);
this.reciever = from;
this.sender = to;
}
private static final DiffUtil.ItemCallback<Chat> DIFF_CALLBACK = new DiffUtil.ItemCallback<Chat>() {
#Override
public boolean areItemsTheSame(#NonNull Chat oldItem, #NonNull Chat newItem) {
return oldItem.getId() == newItem.getId();
}
#Override
public boolean areContentsTheSame(#NonNull Chat oldItem, #NonNull Chat newItem) {
return oldItem.getSender().equals(newItem.getSender()) &&
oldItem.getReciever().equals(newItem.getReciever()) &&
oldItem.getDate().equals(newItem.getDate()) &&
oldItem.getMessageIdSent() == newItem.getMessageIdSent() &&
oldItem.getMessageRecieved().equals(newItem.getMessageRecieved()) &&
oldItem.getTime().equals(newItem.getTime());
}
};
private final int TEXT_MESSAGE_SENT = 0;
private final int TEXT_MESSAGE_RECIEVED = 1;
#Override
public int getItemViewType(int position) {
Chat currentChats = (Chat) getItem(position);
if(!currentChats.getMessageRecieved().equals(""))
{
return TEXT_MESSAGE_RECIEVED;
}
else if(!currentChats.getMessageSent().equals(""))
{
return TEXT_MESSAGE_SENT;
}
return -1;
}
#NonNull
#Override
public RecyclerView.ViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
View view;
if(viewType == TEXT_MESSAGE_SENT)
{
view = layoutInflater.inflate(R.layout.me_text_message,parent,false);
return new TextChatSenderViewHolder(view);
}
//if reciever is text
view = layoutInflater.inflate(R.layout.other_text_message,parent,false);
return new TextChatRecieverViewHolder(view);
}
#Override
public void onBindViewHolder(#NonNull RecyclerView.ViewHolder holder, int position) {
Chat currentChat = (Chat) getItem(position);
switch (holder.getItemViewType())
{
case TEXT_MESSAGE_RECIEVED:
//bind REcievedText viewholder
TextChatRecieverViewHolder viewHolder = (TextChatRecieverViewHolder) holder;
if(!currentChat.getMessageRecieved().equals(""))
{
viewHolder.textMessageRecieved.setText(currentChat.getMessageRecieved());
viewHolder.textTimeMessageRecieved.setText(currentChat.getTime());
}
break;
case TEXT_MESSAGE_SENT:
TextChatSenderViewHolder viewHolder2 = (TextChatSenderViewHolder) holder;
if(!currentChat.getMessageSent().equals("")) {
viewHolder2.textMessageSent.setText(currentChat.getMessageSent());
viewHolder2.textTimeMessageSent.setText(currentChat.getTime());
}
break;
}
}
class TextChatSenderViewHolder extends RecyclerView.ViewHolder
{
TextView textMessageSent,textTimeMessageSent;
public TextChatSenderViewHolder(#NonNull View itemView) {
super(itemView);
textTimeMessageSent = itemView.findViewById(R.id.textTimeMessageSent);
textMessageSent = itemView.findViewById(R.id.textMessageSent);
}
}
class TextChatRecieverViewHolder extends RecyclerView.ViewHolder
{
TextView textMessageRecieved,textTimeMessageRecieved;
public TextChatRecieverViewHolder(#NonNull View itemView) {
super(itemView);
textTimeMessageRecieved = itemView.findViewById(R.id.textTimeMessageRecieved);
textMessageRecieved = itemView.findViewById(R.id.textMessageRecieved);
}
}
}
NOTE: I have noticed that layout changes when the message goes out of screen and you scroll back up.
You need to change your onCreateViewHolder, keep below lines in else
view = layoutInflater.inflate(R.layout.other_text_message,parent,false);
return new TextChatRecieverViewHolder(view);
Also keep layout width to
wrap_content
rather than
match_parent.
you've messed up recycling pattern (link for ListView but it describes pattern better than official doc imho) by overriding getItemViewType method and letting this method return -1. thats not any of view types, but onCreateViewHolder MUST return ViewHolder, so you are returning R.layout.other_text_message. if any message would have 0-length then this empty layout will show up on list
another problem related to wrong recycling implementation is that you are not always calling setText inside onBindViewHolder, because of inconsistent view types and if check (probably unnecessary, as itemType is then -1). when you understand how recycling works (above linked) you will notice that when you won't ALWAYS call setText in onBindViewHolder then recycled item may contain text from previous iteration (before scroll) and this duplicated text will show at this position (wrongly recycler view)
now your list item view have android:layout_width="match_parent" which may "remember" length before recycling, showing set after recycling, something is messing up in here. if you want text wrapping then TextView should have set android:layout_width="wrap_content". keeping android:layout_width="match_parent" (ContraintLayout) inside android:layout_width="wrap_content" parent (CardView) is very unefficient and may cause multiple redrawings/remeasurements, parent probably should also have android:layout_width="match_parent"
my advise to you is to getting rid of empty messages BEFORE setting them to adapter - drawing mechanism isn't a place for filtering inproper items (empty messages in your case) not intended to be drawn. return always some known view type and always fully handle it (onCreate... and onBind... methods), use at least else setText(""); for clearing text after previous item/text set for this view
I know that I should measure children in onMeasure() and layout them in onLayout(). The question is in what of these methods should I add/recycle views so I could measure all children together with an eye on how they are mutually positioned (i.e. grid, list or whatever)?
My first approach was to add/recycle views in onLayout() but from that point I can't measure my children because they aren't added to AdapterView yet and getChildCount() returns 0 in onMeasure(). And I can't measure AdapterView itself without children being already layouted because it really depends upon their mutual positions, right?
I'm really confused with android layouting process in AdapterView when childrens are added/removed dynamically.
I can't post a comment because I'm a new user, but can you describe WHAT you're trying to do, as opposed to HOW you're trying to do it? Frequently, you will find that this is an issue of design as opposed to coding. Especially if you're coming from a different platform (example, iOS). From experience, I found that measuring and manual layouts in Android is mostly unnecessary if you design your layout properly in light of your business need.
EDIT:
As I mentioned this can be solved using some design decisions. I will use your Nodes/List example (hoping that this is your actual use case, but the solution can be expanded for a more general problem).
So if we think about your Header as a comment in a forum, and the List as replies to your comment, we can make the following assumption:
One list is enough, not two. Each item in the list can either be a header (comment) or a list item (reply). Each reply is a comment, but not all comments are replies.
For item n, I know if it's a comment or a reply (i.e. is it a header or an item in your list).
For item n, I have a boolean member isVisible (default false; View.GONE).
Now, you can use the following components:
One extended adapter class
Two Layout XMLs: One for your Comment, one for your reply. You can have unlimited comments and each comment can have unlimited replies. Both those satisfy your requirements.
Your fragment or activity container class that implements OnItemClickListener to show/hide your list.
So let's look at some code, shall we?
First, your XML files:
Comment row (your header)
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/overall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true">
<TextView
android:id="#+id/comment_row_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</RelativeLayout>
Now your reply row (an element in your list)
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/overall"
android:layout_width="match_parent"
android:layout_height="wrap_content"> <!-- this is important -->
<TextView
android:id="#+id/reply_row_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"/> <!-- important -->
</RelativeLayout>
Ok, now your adapter class
public class CommentsListAdapter extends BaseAdapter implements OnClickListener
{
public static String TAG = "CommentsListAdapter";
private final int NORMAL_COMMENT_TYPE = 0;
private final int REPLY_COMMENT_TYPE = 1;
private Context context = null;
private List<Comment> commentEntries = null;
private LayoutInflater inflater = null;
//All replies are comments, but not all comments are replies. The commentsList includes all your data. (Remember that the refresh method allows you to add items to the list at runtime.
public CommentsListAdapter(Context context, List<Comment> commentsList)
{
super();
this.context = context;
this.inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
this.commentEntries = commentsList;
}
//For our first XML layout file
public static class CommentViewHolder
{
public RelativeLayout overall;
public TextView label;
}
//For our second XML
public static class ReplyViewHolder
{
public RelativeView replyOverall;
public TextView replyLabel;
}
#Override
public int getViewTypeCount()
{
return 2; //Important. We have two views, Comment and reply.
}
//Change the following method to determine if the current item is a header or a list item.
#Override
public int getItemViewType(int position)
{
int type = -1;
if(commentEntries.get(position).getParentKey() == null)
type = NORMAL_COMMENT_TYPE;
else if(commentEntries.get(position).getParentKey() == 0L)
type = NORMAL_COMMENT_TYPE;
else
type = REPLY_COMMENT_TYPE;
return type;
}
#Override
public int getCount()
{
return this.commentEntries.size(); //all data
}
#Override
public Object getItem(int position)
{
return this.commentEntries.get(position);
}
#Override
public long getItemId(int position)
{
return this.commentEntries.indexOf(this.commentEntries.get(position));
}
#Override
public View getView(int position, View convertView, ViewGroup parent)
{
CommentViewHolder holder = null;
ReplyViewHolder replyHolder = null;
int type = getItemViewType(position);
if(convertView == null)
{
if(type == NORMAL_COMMENT_TYPE)
{
convertView = inflater.inflate(R.layout.row_comment_entry, null);
holder = new CommentViewHolder();
holder.label =(TextView)convertView.findViewById(R.id.comment_row_label);
convertView.setTag(holder);
}
else if(type == REPLY_COMMENT_TYPE)
{
convertView = inflater.inflate(R.layout.row_comment_reply_entry, null);
replyHolder = new ReplyViewHolder();
replyHolder.replyLable = (TextView)convertView.findViewById(R.id.reply_row_label);
convertView.setTag(replyHolder);
}
}
else
{
if(type == NORMAL_COMMENT_TYPE)
{
holder = (CommentViewHolder)convertView.getTag();
}
else if(type == REPLY_COMMENT_TYPE)
{
replyHolder = (ReplyViewHolder)convertView.getTag();
}
}
//Now, set the values of your labels
if(type == NORMAL_COMMENT_TYPE)
{
holder.label.setTag((Integer)position); //Important for onClick handling
//your data model object
Comment entry = (Comment)getItem(position);
holder.label.setText(entry.getLabel());
}
else if(type == REPLY_COMMENT_TYPE)
{
replyHolder = (ReplyViewHolder)convertView.getTag(); //if you want to implement onClick for list items.
//Or another data model if you decide to use multiple Lists
Comment entry = (Comment)getItem(position);
replyHolder.replyLabel.setText(entry.getLabel()));
//This is the key
if(entry.getVisible() == true)
replyHolder.replyLabel.setVisibility(View.VISIBLE);
else
replyHolder.replyLabel.setVisibility(View.GONE);
}
return convertView;
}
//You can use this method to add items to your list. Remember that if you are using two data models, then you will have to send the correct model list here and create another refresh method for the other list.
public void refresh(List<Comment> commentsList)
{
try
{
this.commentEntries = commentsList;
notifyDataSetChanged();
}
catch(Exception e)
{
e.printStackTrace();
Log.d(TAG, "::Error refreshing comments list.");
}
}
//Utility method to show/hide your list items
public void changeVisibility(int position)
{
if(this.commentEntries == null || this.commentEntries.size() == 0)
return;
Comment parent = (Comment)getItem(position);
for(Comment entry : this.commentEntries)
{
if(entry.getParent().isEqual(parent))
entry.setVisible(!entry.getVisible()); //if it's shown, hide it. Show it otherwise.
}
notifyDataSetChanged(); //redraw
}
}
Ok great, now we have a list of headers with hidden children (remember, we set the default visibility of children to 'gone'). Not what we wanted, so let's fix that.
Your container class (fragment or activity) you will have the following XML definition
<!-- the #null divider means transparent -->
<ListView
android:id="#+id/comments_entries_list"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:divider="#null"
android:dividerHeight="5dp" />
And your onCreateView will implement OnItemClickListener and have the following
private ListView commentsListView = null;
private List<Comment>comments = null;
private static CommentsListAdapter adapter = null;
....
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
...
//comments list can be null here, and you can use adapter.refresh(data) to set the data
adapter = new CommentsListAdapter(getActivity(), comments);
this.commentsListView.setAdapter(adapter);
this.commentsListView.setOnClickListener(this); //to show your list
}
Now to show your list when you click a header
#Override
public void onItemClick(AdapterView<?> parent, View view, int position,
long id)
{
adapter.changeVisibility(position);
}
Now, if an item is clicked and that item has a parent (i.e. list item), it will be shown/hidden according to its current state.
Some comments about the code:
I wrote this on WordPad as I don't have a dev environment handy. Sorry for any compilation errors.
This code can be optimized: If you have a very large data set, this code would be slow since you're redrawing the entire list on every call to changeVisibility(). You can maintain two lists (one for headers, one for list items) and in changeVisibility you can query over the list items only).
I re-enforce that idea that some design decisions would make your life a lot easier. For example, if your list items were actually just a list of labels, then you can have one custom XML file (for your header) and a ListView view within it that you can set to View.GONE. This will make all other views pretend that it's not even there and your layout will work properly.
Hope this helps.
I am creating a list which contains, different types of views. Like facebook does in showing their feeds in Mobile Application.
Example Some times scrollview or some times list inside list.
To do this what will be a good choice.
What if i add a fragment in each item of recyclerview. Like
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
>
<fragment
android:layout_width="match_parent"
android:name="com.profile.EditProfileFragment"
android:layout_height="wrap_content"/>
and add this as a item in Reclycleview and keep different logic inside it.
Can any one suggest me how to go through this.
I don't think it's going to be possible using Fragment as a row. Fragment has its own lifecycle which I don't lies within what RecyclerView can control.
But you can do it with simple view.
Just override several methods in RecyclerView.Adapter
private ArrayList<Data> items = new ArrayList<>(); // data associated
// with each row
#Override
public int getItemViewType(int position) {
return items.get(position).getRowType();
// assuming this is the getter to get the type
}
#Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
switch (viewType) {
case Data.TYPE_A:
View convertView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.row_feed, parent, false);
return new ViewHolderTypeA(convertView);
case Data.TYPE_B:
...
default:
return null;
}
}
#Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
Data data = items.get(position);
if (holder instanceof ViewHolderTypeA) {
ViewHolderTypeA holderA= (ViewHolderTypeA) holder;
// manipulate the views in view holder for this type
...
}else if (holder instanceof ViewHolderTypeB){
}
}
Edit: You don't need different Adapter for each item. You just need item that includes all your logic. So if you want to have two row types, one for status, one for photo like Facebook feed then your item will be:
class Data {
String status, photoUrl;
}
Of course some members will be empty depending on the row.
I know this problem has being asked numerous times relating to ListViews, However I have never been able to understand the solution truly. I am aware that the position parameter of onBindViewHolder() in RecyclerViewAdapter is similar to the position parameter in listAdapter getView(). I also am aware that the position is only relative to visible views. So how can I go about loading an Image for each row of the RecyclerView? The image may be different depending on position. The RecyclerView has a known number of rows (57)
I intend to have an array list of 'favourites'for example int[] fav = {2,6,30,40}. So if position equals one of these elements then ImageView sets one drawble, else it sets another.Could anyone help me with this?
At the moment position repeats itself and many extra rows are setting the drawable reserved for row IDs in the fav array
Thank You,
public class RecyclerViewAdapter extends ...... {
private List<StockItemRow> stockItems;
private Context mContext;
private int[] favourites = {2,3};
RecyclerViewAdapter(Context context, List modelData) {
if (modelData == null) {
throw new IllegalArgumentException("modelData must not be null");
}
this.stockItems = modelData;
this.mContext = context;
}
#Override
public ListItemViewHolder onCreateViewHolder(...)
{
View itemView =
LayoutInflater.fromLayoutInflater.
from(viewGroup.getContext()).
inflate(R.layout.listitem_row, viewGroup, false);
return new ListItemViewHolder(itemView);
}
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
#Override
public void onBindViewHolder(ListItemViewHolder viewHolder, int position) {
StockItemRow stockModel = stockItems.get(position);
for(int i = 0;i<favourites.length;i++) {
if (position == favourites[i]) {
viewHolder.star.setImageResource(R.drawable.star_pressed);
}
}
#Override
public int getItemCount() {
return stockItems.size();
}
public final static class ListItemViewHolder extends....
ImageView star;
public ListItemViewHolder(final View itemView) {
super(itemView);
star= (ImageView) itemView.findViewById(R.id.star);
}
}
}
XML
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<android.support.v7.widget.RecyclerView
android:layout_height="match_parent"
android:id="#+id/recyclerView"
android:divider="#drawable/list_selector"
android:dividerHeight="1dip"
android:layout_width="match_parent">
</android.support.v7.widget.RecyclerView>
</LinearLayout>
#Deev is correct: the position is the position within the adapter, not on screen. The problem you are running into is because your onBindViewHolder() method is only handling the favorite case, but not the non-favorite case. The ViewHolder pattern is used for efficiency and it's re-using views. Change the code to be this:
int fav_res = R.drawable.star;
for(int i = 0;i<favourites.length;i++) {
if (position == favourites[i]) {
fav_res = R.drawable.star_presed;
break;
}
}
viewHolder.star.setImageResource(fav_res);
I am trying to determine the best way to have a single ListView that contains different layouts for each row. I know how to create a custom row + custom array adapter to support a custom row for the entire list view, but how can I implement many different row styles in the ListView?
Since you know how many types of layout you would have - it's possible to use those methods.
getViewTypeCount() - this methods returns information how many types of rows do you have in your list
getItemViewType(int position) - returns information which layout type you should use based on position
Then you inflate layout only if it's null and determine type using getItemViewType.
Look at this tutorial for further information.
To achieve some optimizations in structure that you've described in comment I would suggest:
Storing views in object called ViewHolder. It would increase speed because you won't have to call findViewById() every time in getView method. See List14 in API demos.
Create one generic layout that will conform all combinations of properties and hide some elements if current position doesn't have it.
I hope that will help you. If you could provide some XML stub with your data structure and information how exactly you want to map it into row, I would be able to give you more precise advise. By pixel.
I know how to create a custom row + custom array adapter to support a custom row for the entire list view. But how can one listview support many different row styles?
You already know the basics. You just need to get your custom adapter to return a different layout/view based on the row/cursor information being provided.
A ListView can support multiple row styles because it derives from AdapterView:
An AdapterView is a view whose children are determined by an Adapter.
If you look at the Adapter, you'll see methods that account for using row-specific views:
abstract int getViewTypeCount()
// Returns the number of types of Views that will be created ...
abstract int getItemViewType(int position)
// Get the type of View that will be created ...
abstract View getView(int position, View convertView, ViewGroup parent)
// Get a View that displays the data ...
The latter two methods provide the position so you can use that to determine the type of view you should use for that row.
Of course, you generally don't use AdapterView and Adapter directly, but rather use or derive from one of their subclasses. The subclasses of Adapter may add additional functionality that change how to get custom layouts for different rows. Since the view used for a given row is driven by the adapter, the trick is to get the adapter to return the desired view for a given row. How to do this differs depending on the specific adapter.
For example, to use ArrayAdapter,
override getView() to inflate, populate, and return the desired view for the given position. The getView() method includes an opportunity reuse views via the convertView parameter.
But to use derivatives of CursorAdapter,
override newView() to inflate, populate, and return the desired view for the current cursor state (i.e. the current "row") [you also need to override bindView so that widget can reuse views]
However, to use SimpleCursorAdapter,
define a SimpleCursorAdapter.ViewBinder with a setViewValue() method to inflate, populate, and return the desired view for a given row (current cursor state) and data "column". The method can define just the "special" views and defer to SimpleCursorAdapter's standard behavior for the "normal" bindings.
Look up the specific examples/tutorials for the kind of adapter you end up using.
Take a look in the code below.
First, we create custom layouts. In this case, four types.
even.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:background="#ff500000"
android:layout_height="match_parent">
<TextView
android:id="#+id/text"
android:textColor="#android:color/white"
android:layout_width="match_parent"
android:layout_gravity="center"
android:textSize="24sp"
android:layout_height="wrap_content" />
</LinearLayout>
odd.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:background="#ff001f50"
android:gravity="right"
android:layout_height="match_parent">
<TextView
android:id="#+id/text"
android:textColor="#android:color/white"
android:layout_width="wrap_content"
android:layout_gravity="center"
android:textSize="28sp"
android:layout_height="wrap_content" />
</LinearLayout>
white.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:background="#ffffffff"
android:gravity="right"
android:layout_height="match_parent">
<TextView
android:id="#+id/text"
android:textColor="#android:color/black"
android:layout_width="wrap_content"
android:layout_gravity="center"
android:textSize="28sp"
android:layout_height="wrap_content" />
</LinearLayout>
black.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:background="#ff000000"
android:layout_height="match_parent">
<TextView
android:id="#+id/text"
android:textColor="#android:color/white"
android:layout_width="wrap_content"
android:layout_gravity="center"
android:textSize="33sp"
android:layout_height="wrap_content" />
</LinearLayout>
Then, we create the listview item. In our case, with a string and a type.
public class ListViewItem {
private String text;
private int type;
public ListViewItem(String text, int type) {
this.text = text;
this.type = type;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public int getType() {
return type;
}
public void setType(int type) {
this.type = type;
}
}
After that, we create a view holder. It's strongly recommended because Android OS keeps the layout reference to reuse your item when it disappears and appears back on the screen. If you don't use this approach, every single time that your item appears on the screen Android OS will create a new one and causing your app to leak memory.
public class ViewHolder {
TextView text;
public ViewHolder(TextView text) {
this.text = text;
}
public TextView getText() {
return text;
}
public void setText(TextView text) {
this.text = text;
}
}
Finally, we create our custom adapter overriding getViewTypeCount() and getItemViewType(int position).
public class CustomAdapter extends ArrayAdapter {
public static final int TYPE_ODD = 0;
public static final int TYPE_EVEN = 1;
public static final int TYPE_WHITE = 2;
public static final int TYPE_BLACK = 3;
private ListViewItem[] objects;
#Override
public int getViewTypeCount() {
return 4;
}
#Override
public int getItemViewType(int position) {
return objects[position].getType();
}
public CustomAdapter(Context context, int resource, ListViewItem[] objects) {
super(context, resource, objects);
this.objects = objects;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder = null;
ListViewItem listViewItem = objects[position];
int listViewItemType = getItemViewType(position);
if (convertView == null) {
if (listViewItemType == TYPE_EVEN) {
convertView = LayoutInflater.from(getContext()).inflate(R.layout.type_even, null);
} else if (listViewItemType == TYPE_ODD) {
convertView = LayoutInflater.from(getContext()).inflate(R.layout.type_odd, null);
} else if (listViewItemType == TYPE_WHITE) {
convertView = LayoutInflater.from(getContext()).inflate(R.layout.type_white, null);
} else {
convertView = LayoutInflater.from(getContext()).inflate(R.layout.type_black, null);
}
TextView textView = (TextView) convertView.findViewById(R.id.text);
viewHolder = new ViewHolder(textView);
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) convertView.getTag();
}
viewHolder.getText().setText(listViewItem.getText());
return convertView;
}
}
And our activity is something like this:
private ListView listView;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); // here, you can create a single layout with a listview
listView = (ListView) findViewById(R.id.listview);
final ListViewItem[] items = new ListViewItem[40];
for (int i = 0; i < items.length; i++) {
if (i == 4) {
items[i] = new ListViewItem("White " + i, CustomAdapter.TYPE_WHITE);
} else if (i == 9) {
items[i] = new ListViewItem("Black " + i, CustomAdapter.TYPE_BLACK);
} else if (i % 2 == 0) {
items[i] = new ListViewItem("EVEN " + i, CustomAdapter.TYPE_EVEN);
} else {
items[i] = new ListViewItem("ODD " + i, CustomAdapter.TYPE_ODD);
}
}
CustomAdapter customAdapter = new CustomAdapter(this, R.id.text, items);
listView.setAdapter(customAdapter);
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
#Override
public void onItemClick(AdapterView adapterView, View view, int i, long l) {
Toast.makeText(getBaseContext(), items[i].getText(), Toast.LENGTH_SHORT).show();
}
});
}
}
now create a listview inside mainactivity.xml
like this
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context="com.example.shivnandan.gygy.MainActivity">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="#style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="#style/AppTheme.PopupOverlay" />
</android.support.design.widget.AppBarLayout>
<include layout="#layout/content_main" />
<ListView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/listView"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_marginTop="100dp" />
</android.support.design.widget.CoordinatorLayout>
In your custom array adapter, you override the getView() method, as you presumably familiar with. Then all you have to do is use a switch statement or an if statement to return a certain custom View depending on the position argument passed to the getView method. Android is clever in that it will only give you a convertView of the appropriate type for your position/row; you do not need to check it is of the correct type. You can help Android with this by overriding the getItemViewType() and getViewTypeCount() methods appropriately.
If we need to show different type of view in list-view then its good to use getViewTypeCount() and getItemViewType() in adapter instead of toggling a view VIEW.GONE and VIEW.VISIBLE can be very expensive task inside getView() which will affect the list scroll.
Please check this one for use of getViewTypeCount() and getItemViewType() in Adapter.
Link : the-use-of-getviewtypecount
ListView was intended for simple use cases like the same static view for all row items.
Since you have to create ViewHolders and make significant use of getItemViewType(), and dynamically show different row item layout xml's, you should try doing that using the RecyclerView, which is available in Android API 22. It offers better support and structure for multiple view types.
Check out this tutorial on how to use the RecyclerView to do what you are looking for.