Jake Wharton's LinePageIndicator does not restart after a view recycles - android

I am having a problem with the ListView adapter. I am trying to develop a simple timeline view. I have decided to use ListView and BaseAdapter for it, in order to efficiently show items in the vertical row. Everything seems to be fine but there is a problem with the reuse of views used to visualize items in the list. Each item has three TextView (Title, Details and Story), ViewPager which I am using to display photos and Jake Wharton's LinePageIndicator to show how many photos are there and which of them is currently displayed.
There are always right pictures loaded in the ViewPager and they are swiped from left to the right, however the indicator does not work right. Sometimes is shows that there are two photos although there is only one (this happens because the previous view had two photos) and it does not show that the first photo in the ViewPager is currently presented. It shows the last current position in the ViewPager from the previous view.
Obviously there is a problem with the indicator and it should be somehow restarted every time when a view recycles. I have already looked in the LinePageIndicator source and seen that setViewPager(...) method has this
if (mViewPager != null) {
//Clear us from the old pager.
mViewPager.setOnPageChangeListener(null);
}
but obviously it does not help…
Finally here is my getView(...) method
...
#Override
public View getView(int position, View convertView, ViewGroup parent) {
TextView txtTimelineItemTitle;
TextView txtTimelineItemDetails;
TextView txtTimelineItemText;
ViewPager viewPager;
LinePageIndicator indicator;
if (convertView == null) {
convertView = LayoutInflater.from(context)
.inflate(R.layout.timeline_item, parent, false);
txtTimelineItemTitle = (TextView) convertView.findViewById(R.id.timeline_item_title);
txtTimelineItemDetails = (TextView) convertView.findViewById(R.id.timeline_item_details);
txtTimelineItemText = (TextView) convertView.findViewById(R.id.timeline_item_text);
viewPager = (ViewPager) convertView.findViewById(R.id.timeline_view_pager);
indicator = (LinePageIndicator) convertView.findViewById(R.id.indicator);
convertView.setTag(new ViewHolder(context, txtTimelineItemTitle, txtTimelineItemDetails, txtTimelineItemText, viewPager, indicator));
} else {
ViewHolder viewHolder = (ViewHolder) convertView.getTag();
txtTimelineItemTitle = viewHolder.txtTimelineItemTitle;
txtTimelineItemDetails = viewHolder.txtTimelineItemDetails;
txtTimelineItemText = viewHolder.txtTimelineItemText;
viewPager = viewHolder.viewPager;
indicator = viewHolder.indicator;
}
Story story = (Story) getItem(position);
txtTimelineItemTitle.setText(story.getTitle());
txtTimelineItemDetails.setText(Utils.dateFormat.format(stories.get(position).getDate()) + ", " + stories.get(position).getLocation());
txtTimelineItemText.setText(story.getText());
if (story.getImages().isEmpty()) {
viewPager.setVisibility(View.GONE);
indicator.setVisibility(View.GONE);
} else {
viewPager.setVisibility(View.VISIBLE);
indicator.setVisibility(View.VISIBLE);
ImageAdapter imageAdapter = new ImageAdapter(context);
imageAdapter.updateImages(story.getImages());
viewPager.setAdapter(imageAdapter);
indicator.setViewPager(viewPager);
}
return convertView;
}
...
Has anyone of you already encountered issue like this? How could it be fixed and if not, is there an alternative?
Thank you very much

Since this was very important for me, I have continued with exploring source code of the LinePageIndicator. I have seen that besides of the method setViewPager(ViewPager viewPager) there is also method setViewPager(ViewPager view, int initialPosition) which as you can see allows you to set the current item in the ViewPager. I have changed only one line in the code above
and it solved my issue.
indicator.setViewPager(viewPager, 0); //always start from the first item in the ViewPager

Related

Dynamic items in viewholder on RecyclerView

If we have N categories (between 1 and 100 according to a REST API) and each with X items (between 1 and 50, depending on the category), what is the best way to do a category RecyclerView?
To add do this I am adding card views for each item item of a category in the method "onBindViewHolder" like this:
#Override
public void onBindViewHolder(MoviesViewHolder holder, int i) {
holder.title.setText(items.get(i).getTitle());
// I add a CardView (item_category.xml) for each item in the list
for (ObjectShort object: items.get(i).getObjects()) {
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View view = inflater.inflate(R.layout.item_category, holder.linearCategories, false);
ImageView img = (ImageView) view.findViewById(R.id.iv_wallpaper);
imageLoader.load(img, object.getImg().getPoster().getThumbnail());
holder.linearCategories.addView(view);
}
}
And when it is recycled I remove the ITEMS elements in "onViewRecycled" because if I do not do it when doing vertical scrolling and reloading a category, items are added again duplicating:
#Override
public void onViewRecycled(MoviesViewHolder holder) {
// delete all items
holder.linearCategories.removeAllViews();
}
Is there any better way to do it? This is not effective and the scroll is very slow
thanks to all
To answer your question, I'm going to go back to the old days of ListView. Let's imagine that I had a simple list where each row was just a title and an image. A really naive implementation of getView() to support that list might look like this:
#Override
public View getView(int position, View convertView, ViewGroup parent) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
View view = inflater.inflate(R.layout.itemview, parent, false);
TextView title = view.findViewById(R.id.title);
title.setText("hello world");
ImageView image = view.findViewById(R.id.image);
image.setImageResource(R.drawable.my_image);
return view;
}
There's two major sources of performance trouble in this implementation:
We're inflating a new view every time
We're calling findViewById() every time
Number 1 was solved by making use of the convertView parameter, and number 2 was solved by something called the "view holder pattern". When the Google dev team created the RecyclerView API to replace ListView, they made "view holders" a first-class citizen, and now everyone works with RecyclerView.ViewHolders by default.
It's important to remember that the performance gains of avoiding repeated view inflation and repeated view lookup were so valuable that Google baked them into the new system.
Now let's look at your code...
public void onBindViewHolder(MoviesViewHolder holder, int i) {
...
for (ObjectShort object: items.get(i).getObjects()) {
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View view = inflater.inflate(R.layout.item_category, holder.linearCategories, false);
ImageView img = (ImageView) view.findViewById(R.id.iv_wallpaper);
imageLoader.load(img, object.getImg().getPoster().getThumbnail());
holder.linearCategories.addView(view);
}
}
This for loop inside your bind method has all the same problems that my naive getView() implementation above had. Inflating a new view for each item in the category, and invoking findViewById() to get ahold of its ImageView is going to be expensive, especially when the user flings the list.
To fix your performance problems, just apply the same logic to your "inner" list of content as you have to your "outer" content: use a RecyclerView for items (inside each ViewHolder for categories).
Here is a really simple sample app that illustrates my solution: https://gist.github.com/zizibaloob/2eb64f63ba8d1468100a69997d525a54

Update adapter items with add dynamic drawable on getview() lead to show wrong data

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.

ViewPager inside RecyclerView: ImageView not showing in the first page

I have a ViewPager as the row of a RecyclerView. It is like a "featured products" row.
I set the adapter of the ViewPager in the onBindViewHolder of the RecyclerView. ViewPager contains a TextView and an ImageView. ImageView is loaded from an URL via Glide in instantiateItem. The list of items in the ViewPager is 4.
The problem is, the ImageViews of the first two items in the ViewPager are not loaded. If I swipe the ViewPager to the 3rd item and back, I see the first ImageView successfully.
TextViews work fine. The problem is only with the images.
If I debug the code, I observe that the code block that belongs to Glide is reached.
If I use the ViewPager as a standalone view in the fragment (not as a row of the RecyclerView) I observe no problems.
There is a similar question:
Image not loading in first page of ViewPager (PagerAdapter)
But that unfortunately does not apply to my case. I declare my variables locally and with the final modifier already.
PagerAdapter's instantiateItem is like follows:
#Override
public Object instantiateItem(ViewGroup container, int position) {
final LayoutInflater inflater = (LayoutInflater) container.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
final View v = inflater.inflate(viewpager_layout, container, false);
final ImageView imgProduct = (ImageView) v.findViewById(R.id.imgProduct);
final TextView lblName = (TextView) v.findViewById(R.id.lblName);
final Product product = data.get(position);
lblName.setText(product.name);
Glide
.with(ctx)
.load(product.url)
.asBitmap()
.thumbnail((float) 0.4)
.placeholder(R.drawable.placeholder)
.error(R.drawable.placeholder)
.animate(R.animator.fade_in)
.into(imgProduct);
}
RecyclerView's onBindViewHolder looks like:
#Override
public void onBindViewHolder(final DataObjectHolder holder, int position) {
int listType = getItemViewType(position);
final ProductList item = data.get(position);
if (listType == 0) {
final List<Product> lstProducts = GetProducts(item.products);
final MyPagerAdapter myAdapter = new MyPagerAdapter(ctx, lstProducts);
holder.viewPager.setAdapter(myAdapter);
myAdapter.notifyDataSetChanged(); // this changes nothing also..
}
else {
// removed..
}
}
I work with the AppCompat library by the way.
All suggestions are welcome. Thank you.
Glide sets images asynchronously and helps to avoid any lag in UI thread. If there are many images it does take some time for the image to set in, but it doesn't cause any lag.
It's a simple Asynchronous request issue, when you swipe to the third tab and come back, till the data would have come and come and then it's gets binded with the UI.
I had faced the similar issue in my project.
I am assuming that you are using a new fragment for each view pager
One thing you can do is...
1.While creating fragment in viewPager in **getItem()**, set a tag with fragment.
2. create a viewPager **addOnPageChangeListener** and on page selected, get that fragment by tag and check if image is loaded or not.
3. If not loaded, then show some loader, and wait for the response, after response hide the loader

The first item in my ListView will not update when I change the text of an already visible TextView

I have a ListView that is getting populated by my custom BaseAdapter with views that contain a couple of TextViews and ImageButtons. If the user selects an item from a PopupMenu on one of the list items, it changes the text of an associated TextView on that list item. This is working great for all of the items in my ListView except the first one. The first item never updates the TextView for some reason. I suspect it has something to do with my convertView for that item not getting reset, but I'm not sure how to go about fixing it.
Here is my setup. I have a ListView that is getting populated by a custom BaseAdapter. In my BaseAdapter, I have this for my getView():
public View getView(int position, View convertView, ViewGroup parent) {
if(convertView == null) {
IMOModGroup modGroup = modGroups.get(position);
convertView = modGroup.getConvertView(parent);
}
return convertView;
}
IMOModGroup is a special data structure that holds all the states and data for each list item. So when the convertView comes in null, the BaseAdapter requests a new convertView from the IMOModGroup for that position. Here's the IMOModGroup method that creates the new convertView:
public View getConvertView(ViewGroup parent) {
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = inflater.inflate(R.layout.modifier_list_item, parent, false);
//...
TextView selectedModText = (TextView) convertView.findViewById(R.id.modName);
selectedModText.setText(R.string.no_modifier_selected);
selectedModText.setTextColor(context.getResources().getColor(R.color.grayed_out_text_color));
//...
return convertView;
}
The user can open a PopupMenu on each list item and make a selection. When they do this, the TextView in the above code is changed to reflect the selection with new text. That code looks like this:
public boolean onMenuItemClick(MenuItem item) {
String selectedMod = item.getTitle().toString();
TextView selectedModText = (TextView) convertView.findViewById(R.id.modName);
selectedModText.setText(selectedMod);
selectedModText.setTextColor(context.getResources().getColor(R.color.imo_blue_light));
//...
return false;
}
All of this is working great on every item in the ListView except for the very first one, which never updates to reflect the TextView change. I've tested to make sure the TextView text is actually getting changed and can be called and viewed at a later time, and it's being set correctly. So it seems like the data is correct, but the view is not getting updated.
I'm pretty sure the problem is that my convertView is not getting reset, so the ListView is just continuing to use the old view with the old text. I tried calling notifyDataSetChanged() on the BaseAdapter after I set the text, and that didn't work.
This question looks promising: First item in list view is not displaying correctly
But I'm not sure if that's my problem or not, and I'm not sure how I would apply that to my scenario.
Any ideas? I've read through a few similar questions on SO and Google, but none of them really seemed to be relevant to my specific scenario. I've been using ListViews and Adapters for quite a while now and this is the first time I've seen something like this. Thanks for your help.
EDITS IN RESPONSE TO COMMENTS
Here is the complete BaseAdapter code...
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import java.util.ArrayList;
public class IMOModListAdapter extends BaseAdapter {
private Context context;
private ArrayList<IMOModGroup> modGroups;
public IMOModListAdapter(Context context, ArrayList<IMOModGroup> modGroups) {
this.context = context;
this.modGroups = modGroups;
}
public View getView(int position, View convertView, ViewGroup parent) {
if(convertView == null) {
IMOModGroup modGroup = modGroups.get(position);
convertView = modGroup.getConvertView(parent);
}
return convertView;
}
public Object getItem(int position) {
return 0;
}
public long getItemId(int position) {
return 0;
}
public int getCount() {
return modGroups.size();
}
}
FURTHER TESTING
As I digged deeper into my debugging, I realized that when the TextView for element 1 in the list is changed, getView() is not even called at all. But when any of the other elements are changed, getView() is called as normal.
As a sanity check, I tried changing the text of the first list item from the outside, from the parent Activity itself. If I try to change the first item, it does nothing, but if I try to change any of the other list items, it works fine! This is so bizarre! This is what I tried:
//Set the text of the first item. This does NOTHING and the TextView
//in the item remains unchanged
IMOModGroup modGroup = modGroups.get(0);
modGroup.setTestText("This is some test text");
modList.notifyDataSetChanged();
//Set the text of the second item. This works completely fine!!!!!
IMOModGroup modGroup = modGroups.get(1);
modGroup.setTestText("This is some test text");
modList.notifyDataSetChanged();
Well I found a solution to this issue. Just researching random SO questions about BaseAdapters and getView(), I finally came across this post by Romain Guy in which he states that you should never use wrap_content for the layout_height of a ListView, since it causes the ListView to call the adapter's getView() tons of times just to create dummy views which it uses to measure the height of the content. It turns out this was causing my issue, so changing my layout_height to match_parent did the trick.
Unfortunately, now this causes a new problem since I can't use match_parent since it will hide other views in my parent layout. But at least the first list item is now updating correctly.
UPDATE:
I learned how to manually and dynamically set the height of my ListView after its adapter is populated. I used the method below, and hopefully this will help someone out running into similar issues. This made it so I didn't have to use wrap_content but I could still set the ListView height to only be as big as its content.
private void setModListHeight() {
int numberOfItems = modGroups.size(); //the ArrayList providing data for my ListView
int totalItemsHeight = 0;
for(int position = 0; position < numberOfItems; position++) {
View item = modListAdapter.getView(position, null, modList);
item.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
totalItemsHeight += item.getMeasuredHeight();
}
int totalDividersHeight = modList.getDividerHeight() * (numberOfItems - 1);
ViewGroup.LayoutParams params = modList.getLayoutParams();
params.height = totalItemsHeight + totalDividersHeight;
modList.setLayoutParams(params);
modList.requestLayout();
}

Setting visible to elements that were invisible before in GridView

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.

Categories

Resources