Adding views to RelativeLayout inside ListView yields repeated items - android

I have a list view which uses a custom ArrayAdapter. The items of the ListView are RelativeLayouts. The "Light" views which are stored in "lightsOnThisTrack" list of a "Track" object are added afterward to its corresponding RelativeLayouts.
The thing is that if I add more items to the ListView, the views that were previously added to the relativeLayouts start to repeat on the newly added items. On the other hand, the TextView "trackText" is not being repeated, as can be seen in the example. As I've read on other posts, I know that it's a problem related to the way the ViewHolder pattern is implemented, but I cannot spot where the problem is.
public class TrackListAdapter extends ArrayAdapter<Track> {
private static final String TAG = "TrackListAdapter";
private LayoutInflater layoutInflater;
public ArrayList<Track> trackArrayList;
Context mContext;
RelativeLayout relativeLayout;
public TrackListAdapter(Context context, ArrayList<Track> trackArrayList) {
super(context, 0, trackArrayList);
layoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
this.mContext = context;
this.trackArrayList = trackArrayList;
}
#Override
public View getView(final int position, View convertView, ViewGroup parent) {
View rowView = convertView;
ViewHolder viewHolder;
if (rowView == null) {
rowView = layoutInflater.inflate(R.layout.track_list_item, null);
viewHolder = new ViewHolder();
viewHolder.relativeLayout = (RelativeLayout) rowView.findViewById(R.id.relativeLayout);
viewHolder.trackText = new TextView(mContext);
viewHolder.trackText.setTextColor(Color.GRAY);
viewHolder.trackText.setX(100);
viewHolder.trackText.setY(20);
viewHolder.trackText.setTextSize(18);
viewHolder.relativeLayout.addView(viewHolder.trackText);
rowView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) rowView.getTag();
}
viewHolder.track = trackArrayList.get(position);
if (viewHolder.track.getName() == null)
viewHolder.trackText.setText(" NUMBER " + position);
else
viewHolder.trackText.setText(viewHolder.track.getName());
for (int i = 0; i < viewHolder.track.getNumberOfLights(); i++) {
Light light = viewHolder.track.lightsOnThisTrackList.get(i);
if (light.getParent() != null) {
if (!light.getParent().equals(viewHolder.relativeLayout)) {
ViewGroup viewGroup = (ViewGroup) light.getParent();
if (viewGroup != null) viewGroup.removeView(light);
viewHolder.relativeLayout.addView(light);
}
} else {
viewHolder.relativeLayout.addView(light);
}
}
notifyDataSetInvalidated();
notifyDataSetChanged();
return rowView;
}
public static class ViewHolder {
Track track;
TextView trackText;
RelativeLayout relativeLayout;
}
public View getViewByPosition(int pos, ListView listView) {
final int firstListItemPosition = listView.getFirstVisiblePosition();
final int lastListItemPosition = firstListItemPosition + listView.getChildCount() - 1;
if (pos < firstListItemPosition || pos > lastListItemPosition) {
return listView.getAdapter().getView(pos, null, listView);
} else {
final int childIndex = pos - firstListItemPosition;
return listView.getChildAt(childIndex);
}
}
}

The problem is not the ViewHolder. The problem is that you are not taking into account what happens when your view is recycled.
Suppose for position 0 you add two Lights to the Relativelayout. Then the user scrolls and the view gets recycled to another position (let's say position 10). The RelativeLayout you are given already has two Lights in it before you do anything.
You either need to remove all the previous Lights first, or you need to be able to re-use ones that are there (and still you might have to remove some in case the row you're creating has fewer Lights than are already present).
The TextView is not repeated because you are not creating a TextView every time the view is recycled; you are only creating it when a new row is being inflated.
A few other suggestions:
There should be no reason to call notifyDataSetInvalidated() and notifyDataSetChanged() inside of getView().
I discourage the use of holding lists of Views (in this case, Lights) in your data model. You don't have a clear separation between data and presentation, and I think it will only complicate your code. It would be easier to just store how many lights a Track needs and handle the actual Views separately.
I would also try to avoid creating, adding, and removing Views inside of getView(). For instance, if you know there's a small, limited number of lights a Track can have (suppose it's five), then it's easy enough to have that many corresponding views in the row layout already and just toggle their visibility appropriately. Or, you can make a custom View that knows how to draw up to that number of lights and you just change the number inside of getView().

Related

Avoiding having to instantiate and add child views during getView() method of android adapter

The scenario
I'm trying the create something akin to the featured page on Google Play Store. But instead of showing three items for a category I'm allowing it show any number of items in a two column staggered grid view fashion.
So each list item has a header that has a title and a description followed by a custom view (lets call this SVG, as in Staggered View Group) that shows some number of children views in a staggered grid view fashion.
I have a class called FeaturedItems that hold the data for a row in the featured list. Here is an extract:
public class FeaturedItems {
private String mName;
private String mDescription;
private ArrayList<Object> mList;
public FeaturedItems(String name, String description, Object... items) {
mName = name;
mDescription = description;
mList = new ArrayList<Object>();
for (int i = 0; i < items.length; i++) {
mList.add(items[i]);
}
}
public int getItemCount() {
return mList.size();
}
public Object getItem(int position) {
return mList.get(position);
}
public String getFeatureName() {
return mName;
}
public String getFeatureDescription() {
return mDescription;
}
}
The FeaturedListAdapter binds the data with the views in the getView() method of the adapter. The getView() method is as follows:
#Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
FeaturedItems items = getItem(position);
if (convertView == null) {
LayoutInflater infalInflater = (LayoutInflater) this.mContext
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = infalInflater.inflate(mResource, null);
holder = new ViewHolder();
holder.title = (TextView) convertView.findViewById(R.id.list_item_shop_featured_title);
holder.description = (TextView) convertView.findViewById(R.id.list_item_shop_featured_description);
holder.svg = (StaggeredViewGroup) convertView.findViewById(R.id.list_item_shop_featured_staggered_view);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
holder.title.setText(items.getFeatureName());
holder.description.setText(items.getFeatureDescription());
// HELP NEEDED HERE
// THE FOLLOWING PART IS VERY INEFFICIENT
holder.svg.removeAllViews();
for (int i = 0; i < items.getItemCount(); i++) {
FeaturedItem item = new FeaturedItem(mContext, items.getItem(i));
item.setOnShopActionsListener((ShopActions) mContext);
holder.svg.addView(item);
}
return convertView;
}
The problem
In the getView() method, each time a view is returned, it removes all the child views in the SVG and instantiates new views called FeaturedItem that are then added to the SVG. Even if the SVG in a particular row, say first row, was populated, when the user scrolls back to it from the bottom, the getView() method will remove all the children views in the SVG and instantiates new views to be populated with.
The inefficiency here is very obvious, and the list view animation skips frames when scrolled quite often.
I can't just reuse the convertView here because it shows the wrong featured items in the StaggeredViewGroup. Therefore I have to remove all children from the StaggeredViewGroup and instantiate and add the views that are relevant to the current position.
The question
Is there a way around this problem? Or are there some alternative approaches to creating a page similar to the Google Play Store featured page, but with each row having different number of featured items thus having its unique height?
There should be an easy way to improve this solution. Just reuse the svg children that are already present, add new ones if they are not enough, and then remove any surplus ones.
For example (in semi-pseudocode, method names may not be exact):
for (int i = 0; i < items.getItemCount(); i++)
{
if (i < svg.getChildCount())
{
FeaturedItem item = i.getChildAt(i);
// This item might've been set to invisible the previous time
// (see below). Ensure it's visible.
item.setVisibility(View.VISIBLE);
// reuse the featuredItem view here, e.g.
item.setItem(items.getItem(i));
}
else
{
// Add one more item
FeaturedItem item = new FeaturedItem(mContext, items.getItem(i));
...
holder.svg.addView(item);
}
}
// hide surplus item views.
for (int i = items.getItemCount(); i < svg.getChildCount(); i++)
svg.getChildAt(i).setVisibility(View.GONE);
/**
as an alternative to this last part, you could delete these surplus views
instead of hiding them -- but it's probably wasteful, since they may need
to be recreated later
while (svg.getChildCount() > items.getItemCount())
svg.removeChildView(svg.getChildCount() - 1);
**/

Add a different element to ArrayAdapter/ListView

I have a ListView that's being populated by an ArrayAdapter:
someListView.setAdapter(adapter);
Each element in the adapter is inflated using the same layout.xml. Now I want to add an element of a different type (inflated using a different layout file) to the beginning of the ListView.
What I want to achieve is, to have a special element on top of all other elements in the list view, but also scrolls with the list (exits the screen from top if the user scrolls down).
I've tried to add the new element to the array but it's a different type so that won't work.
I've tried to insert a dummy element to the array at position 0, and modify the adapter's getView() so that if (position == 0) return myUniqueView, but that screwed up the entire list view somehow: items not showing, stuff jumping all over the place, huge gaps between elements, etc.
I start to think the best practice of achieving what I want, is not through editing the array adapter. But I don't know how to do it properly.
You don't need anything special to do what you ask. Android already provides that behavior built in to every ListView. Just call:
mListView.addHeaderView(viewToAdd);
That's it.
ListView Headers API
Tutorial
Do't know exactly but it might usefull
https://github.com/chrisjenx/ParallaxScrollView
In your adapter add a check on the position
private static final int LAYOUT_CONFIG_HEADER = 0;
private static final int LAYOUT_CONFIG_ITEMS = 1;
int layoutType;
#Override
public int getItemViewType(int position) {
if (position== 0){
layoutType = LAYOUT_CONFIG_HEADER;
} else {
layoutType = LAYOUT_CONFIG_ITEMS;
}
return layoutType;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View row = convertView;
LayoutInflater inflater = null;
int layoutType = getItemViewType(position);
if (row == null) {
if (layoutType == LAYOUT_CONFIG_HEADER) {
//inflate layout header
}
} else {
//inflate layout of others rows
}
}

How can display an AdView at every n-th position in a listView

I have the code below which works except it always hides atleast one real item in the listview because the ad displays at that position.
Example of the problem: I have a list of 4 times, and the adView is displaying at position 3. on the listview I can only see 3 times and the AdView, the 4th item does not get displayed
I played around with increasing the size of the adapter everytime I return an ad but it didn't work very well.
Any ideas?
public View getView(final int position, View row, ViewGroup parent) {
MyHolder holder = null;
boolean showAd = proVersion == false && (position % 8 == k);
if (showAd) {
AdView adView = adList.get(position);
if (adView == null) {
AdView adViewNew = new AdView((Activity) context, AdSize.BANNER, context.getResources().getString(
R.string.adId));
adViewNew.loadAd(Utils.getAdRequest("gps", lat, lng, keywords));
adList.add(position, adViewNew);
return adViewNew;
} else {
return adView;
}
} else if (row == null || row instanceof AdView) {
LayoutInflater inflater = ((SherlockActivity) context).getLayoutInflater();
row = inflater.inflate(viewResourceId, parent, false);
holder = new MyHolder();
holder.textName = (TextView) row.findViewById(R.id.name);
row.setTag(holder);
} else {
holder = (MyHolder) row.getTag();
}
holder.textName.setText(items.get(position).getName());
// more code
return row;
}
#Override
public int getCount() {
if (items != null) {
return items.size();
} else {
return 0;
}
}
There is more than one way of achieving this. The best way is relying upon the ability of the listView to recycle multiple item types.
In your adapter,
getViewTypeCount() - returns information how many types of rows you have in the listView
getItemViewType(int position) returns information on which layout type you should use based on the position. That's where your logic of determining the listView objects should go.
There is a good tutorial on how to use different item types in a list here:
Specifically, look at "Different list items’ layouts".
After that, you need minor arithmetic conversions for your indices so that you map the position to the right place in your data structure (or, you can merge your data structure to include both ad data and item data).
Basically, instead of using items.size() you need to use items.size() + Math.floor(items.size()/NTH_ITEM) and when you get the position, if it's an ad position (position % NTH_ITEM == 0) use a simple conversion like Math.floor(position/NTH_ITEM) to extract from the ad data structure and in similar fashion for your item's structure.
You should rely on the holder pattern of the ListView to reuse your different item view types, like in the tutorial above.
Different approaches, just for the notion, include using something like item wrappers that merge the data source into one and enables differentiating between items by using a type property, like an enum, or a "merging adapter" that merges two specific adapters (this might have a better modularity if you are to include these view types in different lists).
Let me know if you need any help implementing specific parts of this in your use case.
Ben Max's answer is cleaner.
It is recommended to use view types when dealing with different types of view.
Here's a good way to do it.
Create a wrapper for your different data types:
private class ItemWrapper {
public static final int TYPE_NORMAL = 0;
public static final int TYPE_AD = 1;
public static final int TYPE_COUNT = 2;
public ListObject item;
public AdObject adItem;
public int type;
public ItemWrapper(ListObject item) {
this.type = TYPE_NORMAL;
this.item = item
}
public ItemWrapper(AdObject adItem) {
this.type = TYPE_AD;
this.adItem = adItem;
}
}
Now come some of the changes to your adapter:
Let's assume you get you initialize your adapter in the constructor
public class MyAdapter extends BaseAdapter{
ArrayList<ItemWrapper> mWrappedItems = new ArrayList<ItemWrapper>();
public void MyAdapter(List<ListObject> items,AdItem adItem){
for(ListObject item:items){
mWrappedItems.add(new ItemWrapper(item));
}
//you can insert your ad item wherever you want!
mWrappedItems.add(2,new ItemWrapper(adItem));
}
}
Then a few more changes to your adapter:
#Override
public ItemWrapper getItem(int position) {
return mWrappedItems == null ? null : mWrappedItems.get(position);
}
#Override
public int getItemViewType(int position) {
return getItem(position).type;
}
#Override
public int getViewTypeCount() {
//tells the adapter how many different view types it will need to recycle
return ItemWrapper.TYPE_COUNT;
}
Now for your getView....
You'll see this is nicer because you can control how your views are inflated
public View getView(final int position, View row, ViewGroup parent) {
final ItemWrapper item = getItem(position);
final int type = item.type;
if (row == null) {
if (type == ItemWrapper.TYPE_NORMAL) {
//inflate your normal view layouts
} else if (type == ItemWrapper.TYPE_AD) {
//inflate your ad layout
}
}
//now you can easily populate your views based on the type
if (type == ItemWrapper.TYPE_NORMAL) {
//get your item data
final ListObject data = item.item;
//populate as you would normally
} else if (type == ItemWrapper.TYPE_AD) {
final AdItem adItem = item.adItem;
//populate your ad as needed
}
return row;
}
You'll notice that wrapping your data like this gives you great control on how/where your item is display by just manipulating the list instead of doing any weird logic inside your getView(); it also makes it simple to let you inflate layouts according to type and populate them easily.
I hope this helps!
P.S i wrote a blog entry about this topic before as well:
http://qtcstation.com/2012/04/a-limitation-with-the-viewholder-pattern-on-listview-adapters/
I'd go in a different way, instead of adding the ad layout dynamically. Assuming your layout (which is accessed via the View row parameter of your getView() method) is customized, simply add a LinearLayout in it which will be used for the AdView.
As you don't want it to be visible on all the rows, mark its visibility to gone by default, but defining it that way will allow you to control your layout and define it how it should be shown.
Assuming this is your layout file:
<LinearLayout
...>
<!-- Put here the AdView layout -->
<LinearLayout
android:id="#+id/adViewLL"
...
android:visibility="gone" />
<!-- Add whatever else you need in your default layout -->
...
</LinearLayout>
Now simply change your getView() method to set the visibility to VISIBLE whenever you need and put there the View for your AdView.
public View getView(final int position, View row, ViewGroup parent) {
MyHolder holder = null;
boolean showAd = proVersion == false && (position % 8 == k);
if (row == null) {
LayoutInflater inflater = ((SherlockActivity) context).getLayoutInflater();
row = inflater.inflate(viewResourceId, parent, false);
holder = new MyHolder();
holder.textName = (TextView) row.findViewById(R.id.name);
row.setTag(holder);
if (showAd) {
LinearLayout myAd = (LinearLayout) row.findViewByid(R.id.adViewLL);
myAd.setVisibility(View.VISIBLE);
AdView adViewNew = new AdView((Activity) context, AdSize.BANNER, context.getResources().getString(R.string.adId));
...
myAd.addView(adViewNew);
}
} else {
holder = (MyHolder) row.getTag();
}
holder.textName.setText(items.get(position).getName());
// more code
return row;
}
I would add to nKn's answer my following experience:
Encapsulate the Ad view with a ViewSwitcher
Show a message where the Ad goes, something like: "a message from our lovely sponsors…"
When the Ad callback hits you (onAdLoaded), do a holder.switcher.setDisplayedChild(1); // if 1 is where your AdView is.
Have a Flag where you can disable Ads so always do if (showAd && adsEnabled) {}, you never know when you might need to disable ads, if you have an API try to retrieve that value from there.
#4 can also be used for an else clause: else {holder.switcher.setDisplayedChild(0);//your generic message}
Have an "AdController" where you determine how often (every what ## of items you want to insert an ad). and have your list ask if (YourAdController.isAd(position) && adsEnabled) { do the add thing }
This is slightly a logical error.
Here's an explanation on whats happening.
Going by your description, every 8x position is an ad
Why go on to mess with views or maintaining multiple layouts
In your dataset(i.e the list that you are passing to the adapter) just add an item that indicates its an ad item at every 8x position
So, if its a pro version there's no ad item entry in the list
Finally, in your getview
if(ad item)
populate adView
else
populate regularItemView
Hope it simplifies
I have the same problem on one of the applications that i worked on. here's the approach that worked with me.
First,you need to create a class that contains your ad details.
public class AdUnitDetails {
private String mUnitId;
private int mIndex;
public AdUnitDetails(String mUnitId,int mIndex) {
super();
this.mUnitId = mUnitId;
this.mIndex = mIndex;
}
public String getUnitId() {
return mUnitId;
}
public void setUnitId(String mUnitId) {
this.mUnitId = mUnitId;
}
public int getIndex() {
return mIndex;
}
public void setIndex(int mIndex) {
this.mIndex = mIndex;
}
}
Then, you create a Sparsearray containing adviews with their positions
SparseArray<AdView> mAdViewsMap = new SparseArray<AdView>();
Then you need to modify your adapter to receive an arraylist of objects and then you add the ads in their corresponding positions when you fill this list. example(item, item , item , addetails, item, item, etc).
Then, in your adapter. add the following methods:
private static final int TYPE_ITEM= 0;
private static final int TYPE_AD = 1;
private static final int TYPES_COUNT = TYPE_AD + 1;
#Override
public int getViewTypeCount() {
return TYPES_COUNT;
}
#Override
public int getItemViewType(int position) {
if (mItems.get(position) instanceof YourItemClass)
return TYPE_ITEM;
else if (mItems.get(position) instanceof AdUnitDetails)
return TYPE_AD;
return super.getItemViewType(position);
}
Then, in your getView() method,add the following
View view = convertView;
if (mItems.get(position) instanceof YourItemClass) {
// Handle your listview item view
} else if (mItems.get(position) instanceof AdUnitDetails) {
return getAdView(position);
}
return view;
your getAdView() method
private AdView getAdView(final int position) {
if (mAdViewsMap.get(position) == null) {
AdSize adSize = getAdSize(AdSize.BANNER);
AdView mAdView = new AdView(mContext, adSize, ((AdUnitDetails) mItems.get(position)).getUnitId());
mAdView.loadAd(new AdRequest());
mAdViewsMap.put(position, mAdView);
return mAdView;
} else {
return mAdViewsMap.get(position);
}
}
It might be too late but here is how i solved mine.
Converted the current position from int to string ( optional)
and then every time view is inflated check if position contains (3 || 6 || 9) for that every 3rd visible layout shows an advertisement banner. hope it was helpful will post code if needed :)
oh and || means "or" if anyone is wondering
here is the code Ashish Kumar Gupta
RelativeLayout rl = (RelativeLayout) convertView.findViewById(R.id.adviewlayout); //putting the advertisement banner inside a relativelayout/linearlayout if you want. so that you can make it visibile whenever you want.
String pos = String.valueOf(position); // converting listview item position from int to string//
if (pos.endsWith("3")||pos.endsWith("6")||pos.endsWith("9")) {
// every fourth position will show an advertisement banner inside a listview. here is the proof (0,1,2,advertisement,4,5,advertisement,7,8,advertisement,10,11,12,advertisement,14,15,advertisement,17,18,advertisement,20). did not put advertisements in 0 position cuz its rude to put advertisements in like the first layout in my POV//
rl.setVisibility(View.VISIBLE);
}
else
{
rl.setVisibility(View.GONE);
//if its not a 3rd position advertisements will not be shown//
}
you are welcome :)

How can I force the position of an element in a ListView

I have an Activity, which simply consists in listing Pair<String, String> objects. I have a custom TextWithSubTextAdapter, which extends ArrayAdapter:
public View getView(int position, View convertView, ViewGroup parent)
{
View view;
if (convertView == null)
{
LayoutInflater li = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
view = li.inflate(R.layout.text_sub, null);
TextView tv = (TextView)view.findViewById(R.id.mainText);
tv.setText(mCategories.get(position).first);
TextView desc = (TextView)view.findViewById(R.id.subText);
desc.setText(Html.fromHtml(mCategories.get(position).second));
}
else
{
view = (View) convertView;
}
return view;
}
mCategories is an ArrayList<Pair<String, String>>
I then call lv.setAdapter(new TextSubTextAdapter(this, Common.physConstants));
As long as I have a limited set of elements, It works great, because I don't need to scroll. However, when I add enough elements, after scrolling, the items swap their positions, like this:
I suspect that this behavior is due to me calling mCategories.get(position). Because the Views are never kept in the background and Android regenerates them every time, I never get the same item, as position will rarely have the same value.
Is there a way to get a constant id, which could allow me to get items with fixed positions ? I tried to use getItemID, but I do not understand how to implement it.
Note: Every string comes from a strings.xml file. They are never compared, and instanciated once, at startup.
When you scroll your list Android dynamically reuses the Views which scroll out of the screen. These convertViews don't have the content which should be at this position yet. You have to set that manually.
View view;
if (convertView == null)
{
LayoutInflater li = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
view = li.inflate(R.layout.text_sub, null);
}
else
{
view = convertView;
}
TextView tv = (TextView)view.findViewById(R.id.mainText);
tv.setText(mCategories.get(position).first);
TextView desc = (TextView)view.findViewById(R.id.subText);
desc.setText(Html.fromHtml(mCategories.get(position).second));
return view;

How to manage Views in a Listview correctly

I am wondering how to manage the views inside a ListView.
I have a custom Adapter that is set on the ListView, this Adapter overrides the getView method
public View getView(int position, View convertView, ViewGroup parent) {
View v = convertView;
if (v == null) {
v = mInflater_.inflate(R.layout.news_newsentry, null);
}
final NewsItem newsItem = getItem(position);
if (newsItem != null) {
// Do stuff
}
return v;
}
But the thing is that when the user clicks on an item, I slightly change the view to make it bigger. It works well, but when the item view is recycled, it keeps the "big" height to display another item.
To prevent that, I changed the code to create a new View each time
Change:
View v = convertView;
if (v == null) {
v = mInflater_.inflate(R.layout.news_newsentry, null);
}
By
View v = mInflater_.inflate(R.layout.news_newsentry, null);
The problem now is that when the item disappears from the list and reappears (the list is scrolled), the view is completely new and the height is set to "small".
My question then: how to manage the items views to keeps their properties, without messing with the other views and the view recycling?
I think you can get the result you want by using the ListView built in support for more than one view type in a list.
In your adapter you would implement additional methods similar to
#Override
public int getItemViewType(int position) {
int type = 0;
if (position == mySelectedPosition) {
type = 1;
}
return type;
}
#Override
public int getViewTypeCount() {
return 2;
}
Then your getView method will be handed a view of the correct type for the position of the item. Ie, the selected item will always be given a "big" view to re-use.
Creating a new View every time is not recommended for performance and memory reasons.

Categories

Resources