I've a custom listview in my chat application. It shows some people I've already chatted with. I want the first element in the listview to be an imageview that, when clicked, gives the user the opportunity to type a new person he wants to chat with. I've already implemented that (I don't want to use ListView header), by adding one more element to the listview and then inflate the imageview or the textview according to the position...
View v = convertView;
if (v == null) {
LayoutInflater vi;
vi = LayoutInflater.from(getContext());
if (position == 0)
v = vi.inflate(R.layout.list_item_chat_list_header, parent, false);
else
v = vi.inflate(R.layout.list_item_chat_list, parent, false);
}
TextView tvName = ((TextView) v.findViewById(R.id.id_chat_list_name));
TextView tvPreview = ((TextView) v.findViewById(R.id.id_chat_list_preview));
if ( tvName != null && tvPreview != null ) {
ChatListElement p = getItem(position);
tvName.setText(p.name);
String preview = p.lastMessage;
if (preview.length() > 30)
preview = preview.substring(0, 30) + "...";
tvPreview.setText(preview);
}
return v;
But, when I run it, I get some of the views in the middle were also turned into imageviews... I think it has something to do with recycled views in listview, but I don't understand how to make it work.
Thank you very much
Step #1: Override getViewTypeCount() in your adapter to return 2
Step #2: Override getItemViewType() in your adapter to return 0 for position 0 (your ImageView) and 1 for everything else (your normal rows)
Step #3: Use getLayoutInflater(), called on your Activity, not LayoutInflater.from() (not technically related to your problem, but you'll thank me later for helping ensure that your custom themes work as expected)
Steps #1 and #2 will teach the ListView to maintain two object pools, one for each type of row, and it ensures that the row you get back for recycling is of the right type for the requested position.
Related
I have a custom adapter that list my items. in each Item I check database and draw some circles with colors.
As you see in code I check if convertView==null defines new viewHolder and draw my items. but when I scroll listview very fast every drawn data ( not title and texts) show wrongs!
How I can manage dynamic View creation without showing wrong data?!
UPDATE
This is my attempts:
I used ui-thread to update my list but the result is same and data drawing go wrong.
in second I try to load all data with my object so that there is no need to check db in adapter. but it problem is still remains...
finally I create the HashMap<key,LinearLayout> and cache every drawn layout with id of its item. So if it's drawn before I just load its view from my HashMap and every dynamic layout will create just once. But it still shows wrong data on fast scrolling! Really I don't know what to do next!
#Override
public View getView(final int position, View convertView, ViewGroup parent) {
final ViewHolder viewHolder;
final MenuStructureCase item = getItem(position);
if (convertView == null) {
viewHolder = new ViewHolder();
convertView = this.mInflater.inflate(R.layout.fragment_menu_item, null);
viewHolder.menu_title = (TextView) convertView.findViewById(R.id.menu_title);
viewHolder.tag_list_in_menu_linear_layout = (LinearLayout) convertView.findViewById(R.id.tag_list_in_menu_linear_layout);
viewHolder.menu_delete = (ImageButton) convertView.findViewById(R.id.image_button_delete);
importMenuTags(viewHolder, getItem(position), viewHolder.tag_list_in_menu_linear_layout);
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) convertView.getTag();
}
viewHolder.menu_title.setText(item.getTitle());
}
return convertView;
}
and this is importMenuTags():
private void importMenuTags(ViewHolder viewHolder, MenuStructureCase item, LinearLayout layout) {
List<String> tags = db.getMenuTags(item.getTitle()); //this present list of string that contain my tags
for (String tag : tags) {
Drawable drawable = getContext().getResources().getDrawable(R.drawable.color_shape);
drawable.setColorFilter(Color.parseColor(each_tag_color), PorterDuff.Mode.SRC_ATOP);
RelativeLayout rl = new RelativeLayout(getContext());
LinearLayout.LayoutParams lparams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
lparams.setMargins(15, 15, 15, 15);
lparams.width = 50;
lparams.height = 50;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
rl.setBackground(drawable);
} else {
rl.setBackgroundDrawable(drawable);
}
rl.setLayoutParams(lparams);
layout.addView(rl);
}
}
You have to select data from db before adapter initialization. So that
getItem(position)
will return already a "ready" item-object.
You shouldn't set the values to Views inside
if (convertView == null) {
...
}
This code is only for a viewHolder initialization. You create a new one, if convertView is null or read it as tag.
Setting of values you have to do after viewHolder initialization, actually where you set the title.
But in order to increase a performance, you shouldn't select the values from db on each step of getView. You have to have everything prepared (already selected).
You can do this way:
First of all create method inside adapter class:
public void updateNewData(List<MenuStructureCase> newList){
this.currentList = newList;
notifyDataSetChanged();
}
Now call above method whenever you want to update ListView.
How to call with object of CustomAdapter:
mAdapter.updateNewData(YourNewListHere);
Hope this will help you.
Rendering of data takes times and may be that's causing the issue when you are scrolling fast.
You can restirct the scrolling ( like Gmail : use a pull to refresh ) so that a less amount to data is processed in a list view at single time .
use RecyclerView instead of listview for better performance
ListView recreates the view on scrolling .
May be you can explain more about your problem , then we can provide the inputs accordingly.
I am using ListView to show TextViews in some rows and not others. I don't know how many items there will be, it runs fine but when I scroll down the display changed.
For example:
If I have 10 items in ListView, and only 5 of them are currently visible. Adapter returns positions from 0 to 4 correctly...thats ok. When I scroll down position of item 10 and return to the top, I get all my items looking like the first item on the list, and if I scroll many times I get the first item repeated 10 times.
Im using ArrayAdapter.
Here's some code:
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View row = convertView;
PlanningHolder holder = null;
if (row == null) {
row = mInflater.inflate(layoutResourceId, parent, false);
holder = new PlanningHolder();
holder.numberOfJour = (TextView) row.findViewById(R.id.numberDate);
holder.lay = (LinearLayout) row.findViewById(R.id.layPlage);
row.setTag(holder);
} else {
holder = (PlanningHolder) row.getTag();
}
if(position%2 == 1){
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
params.setMargins(50, 5, 30, 5);
//MarginLayoutParams marginLayoutParams = (MarginLayoutParams) params.getLayoutParams();
//marginLayoutParams.setMargins(marginleft, 5, display.getWidth() - marginright, 5);
TextView newplage = new TextView(context);
newplage.setLayoutParams(params);
newplage.setBackgroundColor(context.getResources().getColor(R.color.darkorrange));
holder.lay.addView(newplage);
}
holder.numberOfJour.setText(myPlannig.getCurrent().get(Calendar.DAY_OF_MONTH) + "");
row.setId(position);
return row;
}
and this is my listView declaration:
<ListView
android:id="#+id/list_planning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#drawable/top_shadow"
android:scrollingCache="true"
android:dividerHeight="1dp">
</ListView>
The problem is caused by reusing convertView. Either disable faulty optimization
View row = null;
or repair optimization by implementing else branch of if and set row views numberDate, layPlage appropriately for this case.
Notice that you are adding views to holder.lay but you never remove them. They accumulate when you are scrolling and reusing convertView.
This is a common case in list view. You must handle if and else case correctly.
For example, if you set text in if case and not in else case, then the old value gets retained. The views in list view are getting recycled.
When you first open the list view, it is getting created.
When you scroll back and forth, You are loading data over an already existing view which already had values.
Whatever you handle in the if case within the getView, you must probably undo it in the else. In other words, you must handle the views in every case or path the code flows in the getView method.
Put else condition like this
else
{
lay.addView(newplage);
}
I'm working on a ListView based app and I have a very weird problem, my ListItems are reappering and the correct item is not shown in the correct spot. For the sake of making this easy to understand I've set the text on each ListItem to be the same as it's position. I'm doing this in my adapters getView() call. If I have my Nexus 7 4 ListItems are visible. If I have a total of 10 ListItems then it will go like 0, 1, 3, 4, 0, 1, 2, 3, 4. This goes for all devices meaining that the number of items initially on screen + 1 will be correct while all other ListItems are rearrenged.
In which part of my code do you guys think my problem lies because right now I've been trying to fix this for hours and I'm clueless. All help is very much appreciated.
EDIT:
Here's my getView():
#Override
public View getView(int position, View convertView, ViewGroup parent) {
CountdownItem ci = mTitle.get(position);
if (convertView == null) {
convertView = mInflater.inflate(R.layout.list_item, parent, false);
holder = new CountdownViewHolder();
holder.mTitle = (TextView) convertView.findViewById(R.id.textPrim);
holder.mSubtitle = (TextView) convertView
.findViewById(R.id.textSec);
holder.mDayProgress = (ProgressBar) convertView
.findViewById(R.id.day_progress);
holder.mMonthProgress = (ProgressBar) convertView
.findViewById(R.id.month_progress);
holder.mYearText = (TextView) convertView
.findViewById(R.id.year_text);
holder.day_help = (TextView) convertView
.findViewById(R.id.day_help);
holder.month_help = (TextView) convertView
.findViewById(R.id.month_help);
holder.setTitle(Integer.toString(position) + " Title");
holder.setSubtitle(ci.getSubtitle());
holder.fixImageAndText(position);
convertView.setTag(holder);
} else {
holder = (CountdownViewHolder) convertView.getTag();
}
return convertView;
}
You aren't using the ViewHolder pattern correctly. The following code needs to be moved outside the if/else clause and before return convertView:
holder.setTitle(Integer.toString(position) + " Title");
holder.setSubtitle(ci.getSubtitle());
holder.fixImageAndText(position);
This is the correct behaviour for the listview when it is reusing cells, the problem is that you only set the values when the cell is first created.
When convertView == null the listview has no cell to recycle. However, once it has created a few it can reuse them to display as you scroll.
What you need to do is set the title and subtitle even when convertView is not null. That way you're setting them for each new list position.
Yes, this is because android reuses views in lists, to increment performance and rendering speed.
The holder pattern is used to store views ids. After you retrieve them, you have to set the text you want to see inside.
For example, you retrieve your data (e.g. myDataArray[position]), and if it's all ok, you proceed setting title, subtitle, dayprogress, etc. with TextView's setText().
I have a GridView in which I want to always show 7 icons, and sometimes an additional icon depending on a request. In the beginning the additional icon is never shown. This is the structure:
0 1 2
3 4 5
6 [7]
All the icons fit into the screen so I don't need/have scroll. Each icon is composed by an image and a text.
For this, I have a CustomAdapter which extends BaseAdapter. I have overriden the getView method in which I set the text and the image for each icon.
public View getView(int position, View convertView, ViewGroup parent) {
View v = null;
if (convertView == null) {
LayoutInflater li = ((Activity) context).getLayoutInflater();
v = li.inflate(R.layout.icon, null);
} else {
v = convertView;
}
TextView tv = (TextView) v.findViewById(R.id.icon_textView);
tv.setText(position);
ImageView iv = (ImageView) v.findViewById(R.id.icon_ImageView);
iv.setImageResource(imageResourcesArray[position]);
if ((position == ADDITIONAL_ICON)) && !showAdditionalIcon) {
v.setVisibility(View.INVISIBLE);
}
return v;
}
The imageResourcesArray[] is an array of integers with the image resources.
The other functions and variables in the CustomAdapter are:
public static final int ADDITIONAL_ICON = 7;
private boolean showAdditionalIcon = false;
public showAdditionalIcon(){
this.showAdditionalIcon = true;
notifyDataSetChanged();
// notifyDataSetInvalidated();
}
public hideAdditionalIcon(){
this.showAdditionalIcon = false;
notifyDataSetChanged();
// notifyDataSetInvalidated();
}
Later on, I create and set the CustomAdapter to the GridView from a class which extends Activity (say ClassA):
GridView grid = (GridView) findViewById(R.id.main_gridView);
customAdapter = new CustomAdapter(this);
grid.setAdapter(customAdapter);
My problem appears when after some calculations and requests to a server, I have to show the additional icon (number 7). So I call (from ClassA):
customAdapter.showAdditionalIcon();
Now, the additional icon appears, but the first icon disappears... I have tried to use notifyDataSetInvalidated() and notifyDataSetChanged() but both had the same result.
Of course, I could generate a new CustomAdapter with the additional icon allowed, but I would preffer not to do it...
Thanks in advance.
I'm not sure if this counts as an answer for you. Root of the problem seems to be the convertView we are using. I did not dig so deep into Android source, but I think there is no guarantee on how views are reused even it is obvious that all views are already visible and there should be no reuse behind the scenes.
What this means is that the view we linked to position 7 as we visualize this whole scenario is actually reused later at position 0. Since your code does not explicitly reset a view to be visible, the view will be reused with visibility set to INVISIBLE, thus the mystery of the disappearing first item.
Simplest solution should be as #Vinay suggest above, by explicitly setting to View.VISIBLE.
if ((position == ADDITIONAL_ICON))) {
if (!showAdditionalIcon)
v.setVisibility(View.INVISIBLE);
else
v.setVisibility(View.VISIBLE);
}
Hope this helps, but I'm really hoping some Android expert pops by to tell us more about how this whole thing of reusing old views actually works.
I have a ListView with a custom list adapter. In the getView() method, am using the ViewHolder 'pattern' as shown in the API Demos for ListView14.java. When i first render the list it seems to load correctly. However, the issue i'm running into is that when i scroll the list, i'm seeing the data for the list show up in the wrong rows (i.e. a TextView that should be in row 10 is showing up in row 2 for example). However, when I do not use the viewholder, and instead call findViewById() every time, then the list view renders correctly.
However, the issue i'm running into is
that when i scroll the list, i'm
seeing the data for the list show up
in the wrong rows (i.e. a TextView
that should be in row 10 is showing up
in row 2 for example).
Most likely, you are improperly recycling your rows, such that the ViewHolders you are manipulating are not the right ones for the row you are returning.
Here is a free excerpt from one of my books that goes into more about row recycling -- perhaps it will help you identify where things are going wrong.
I faced same problem
Solved using below techninc
Reason : Adapter not Loaded Frequentlyy.
in Your Custom Adapter class add ViewHolder using Access Specifiers
private static class ViewHolder {
protected TextView itemName;
}
In Get View method
#Override
public View getView(int position, View view, ViewGroup viewGroup) {
// create a ViewHolder reference
ViewHolder holder;
//check to see if the reused view is null or not, if is not null then reuse it
if (view == null) {
holder = new ViewHolder();
view = mLayoutInflater.inflate(R.layout.list_item, null);
holder.itemName = (TextView) view.findViewById(R.id.list_item_text_view);
// the setTag is used to store the data within this view
view.setTag(holder);
} else {
// the getTag returns the viewHolder object set as a tag to the view
holder = (ViewHolder)view.getTag();
}
// now Use Holder object toget Idss
holder.itemName.setText(" sample text based on position ");
}
Important : And We should not set Any Tag for view object except Viewholder Object
so i think i discovered the real issue here. when you set layout parameters on the fly for each row, you need to make sure you do it for all conditions. my problem was that if it was the first row, i set a layout param (like padding or margins etc), but then if it was a middle row, i didn't explicitly set those params thinking that it would just use what was inflated by the view inflater. This explains why it worked when i inflated the view each time. Here is a before & after:
BEFORE:
if (position == 0) {
layoutParams.topMargin = uiHelper.getDip(15.0f);
layoutParams.addRule(RelativeLayout.ALIGN_PARENT_TOP,
RelativeLayout.TRUE);
holder.actionMenu.setLayoutParams(layoutParams);
holder.contentLayout.setBackgroundResource(R.drawable.top_row);
} else if (position == posts.size() - 1) {
holder.contentLayout
.setBackgroundResource(R.drawable.bottom_row);
holder.contentLayout.setPadding(holder.contentLayout
.getPaddingLeft(),
holder.contentLayout.getPaddingTop(),
holder.contentLayout.getPaddingRight(),
holder.contentLayout.getPaddingBottom() + uiHelper.getDip(10.0f));
} else {
holder.contentLayout
.setBackgroundResource(R.drawable.inner_row);
}
AFTER:`
layoutParams.topMargin = uiHelper.getDip(10.0f);
holder.contentLayout.setPadding(holder.contentLayout
.getPaddingLeft(),
holder.contentLayout.getPaddingTop(),
holder.contentLayout.getPaddingRight(),
uiHelper.getDip(10.0f));
if (position == 0) {
layoutParams.topMargin = uiHelper.getDip(15.0f);
layoutParams.addRule(RelativeLayout.ALIGN_PARENT_TOP,
RelativeLayout.TRUE);
holder.contentLayout.setBackgroundResource(R.drawable.top_row);
} else if (position == posts.size() - 1) {
holder.contentLayout
.setBackgroundResource(R.drawable.bottom_row);
holder.contentLayout.setPadding(holder.contentLayout
.getPaddingLeft(),
holder.contentLayout.getPaddingTop(),
holder.contentLayout.getPaddingRight(),
uiHelper.getDip(20.0f));
} else {
holder.contentLayout
.setBackgroundResource(R.drawable.inner_row);
}
holder.actionMenu.setLayoutParams(layoutParams);