I have a ViewPager whose getCount(); changes based on a list of objects passed through the constructor. Using the adapter.notifyDataSetChanged(); I expect to see another page added. This seems to work correctly but when I remove an item from the mentioned list (used to calculate the getCount()) the ViewPager only removes the page successfully if its outside the offsetLimit.
Exemple: I have two pages and through a button I remove an item from the list. Then I call notifyDataSetChanged() and expect to have only one page left, but... the second "Page" is still there even thought I cannot swipe to it!!!!
How can I come around this? I want to avoid re-instantiating the whole ViewPager with the fragments.
CODE (simplification)
public class Adapter extends FragmentStatePagerAdapter {
private final List<String> list;
public Adapter(FragmentManager fragmentManager, List<String> list) {
super(fragmentManager);
this.list = list;
}
#Override
public int getCount() {
return list.size();
}
}
And this is inside the Activity:
adapter = new Adapter(getSupportFragmentManager(), items);
final ViewPager pager = findViewById(R.id.pager);
pager.setAdapter(adapter);
findViewById(R.id.add).setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
items.add("test");
adapter.notifyDataSetChanged();
}
});
findViewById(R.id.remove).setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
items.remove(items.size() - 1);
adapter.notifyDataSetChanged();
}
});
With this I can add items from the Activity and update de adapter (it works) and remove ... its doesn't work. What can I do?
You can override getItemPosition() & return POSITION_NONE in it.
getItemPosition() :
Called when the host view is attempting to determine if an item's position has changed. Returns POSITION_UNCHANGED if the position of the given item has not changed or POSITION_NONE if the item is no longer present in the adapter.
The default implementation assumes that items will never change position and always returns POSITION_UNCHANGED.
Related
In my Fragment, I have 2 views. A Textview and a RecyclerView. The Textview essentially displays the current size of the RecyclerView. So, when a row is removed in the adapter class, I need to update the TextView's value accordingly.
The row is removed successfully when removeBtn is clicked, but I need to update the TextView in the Fragment accordingly.
FRAGMENT
titleText.text = "SIZE (" + arrayStringList().size.toString() + ")"
//...
//...
//Set RecyclerView Adapter
mRecyclerView.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(context)
val adapter = MRecyclerView(context!!, arrayStringList)
mRecyclerView.adapter = adapter
RECYCLERVIEW
holder.removeBtn{
mData.removeAt(position)
notifyItemRemoved(position)
}
Is there some sort of listener that I can put in my fragment to detect when the data changes? Or is there a way to send data from the recyclerview back to the fragment when removeBtn is clicked?
Create a interface in recyclerView class , and implement that interface in fragment . Once you click the remove button , call this interface . In Fragment class , update the text view with adapter.getItemCount.
In Adapter
interface ItemCallback{
void updateTextView();
}
This interface will be implemented in fragment class ,where you can update your textView with itemCount .
public class Fragment implements Adapter.ItemCallback{
#Override
public void updateTextView() {
tvTextView.setText(adapter.getItemCount()); // This will return the current item count of adapter
}
}
You can do this via call back. Make an interface and implement that on Fragment. And pass the reference of the interface to the adapter. When you trigger the event to delete item from recycler view call the interface method (That you implemented on Activity). And in that call back method, you need to set the value to your text view. To set value on TextView, you can call size() method and set the value whatever is returned from it. For example the List you are passing to the adapter is mData. When you get call back the call size method on the reference and set the value as mentioned below code.
titleText.setText(String.valueOf(mData.size())).
Hope this will help you.
NOTE: You need to call String.valueOf method because size method returns integer value so it needs to be cast.
"Or is there a way to send data from the recyclerview back to the fragment when removeBtn is clicked?" the answer is yes
One approach is to pass your adapter your own click listener (an interface), this way you can control the click from the fragment something like this,
A1) create an interface
public interface ItemTouchListener {
void onCardClick(View view, int position);
boolean onCardLongClick(View view, int position);
}
A2) implement this interface in your fragment
public class YourFragment extends Fragment implements ItemTouchListener
A3) implement the methods you created in your fragment
#Override
public void onCardClick(View view, int position) {
if (view.getId() == R.id.removeBtn){
//update your text
}else{
//other buttons
}
}
#Override
public boolean onCardLongClick(View view, int position) {
if (view.getId() == R.id.removeBtn){
//update your text
} else {
//other buttons
}
return true;
}
A4) create this interface in your adapter and set it to your button
private ItemTouchListener onItemTouchListener;
public YourAdapter(List<Object> list, ItemTouchListener onItemTouchListener) {
this.onItemTouchListener = onItemTouchListener;
this.list = list;
}
removeBtn = view.findViewById(R.id.removeBtn);
removeBtn.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
onItemTouchListener.onCardClick(v, getAdapterPosition());
}
});
removeBtn.setOnLongClickListener(new View.OnLongClickListener() {
#Override
public boolean onLongClick(View view) {
onItemTouchListener.onCardLongClick(view, getAdapterPosition());
return true;
}
});
A5) and now when you create your adapter it will ask for the ItemTouchListener which you can give it by just passing 'this'
YourAdapter adapter = new YourAdapter(this);
A6) you may also want to give your adapter a method something like getCount which return the list size
public int getCount(){
list.size();
}
and then you can call this like myAdapter.getCount
I know there are lots of threads already on this topic, but none of the given solutions worked for me so far. I'm trying to add or update an item of a RecyclerView. Here's my code so far:
MainActivity
private MyListItemAdapter mAdapter;
private RecyclerView recyclerView;
// called on activity create
private void init() {
// initialize activity, load items, etc ...
mAdapter = new MyListItemAdapter(this, items);
recyclerView.setAdapter(mAdapter);
}
// called when I want to replace an item
private void updateItem(final Item newItem, final int pos) {
mAdapter.replaceItem(newItem, pos);
}
MyListItemAdapter
public class MyListItemAdapter extends RecyclerView.Adapter<MyListItemAdapter.MyListItemViewHolder> {
private List<Item> mItems;
public void replaceItem(final Item newItem, final int pos) {
mItems.remove(position);
mItems.add(position, newItem);
notifyItemChanged(position);
notifyDataSetChanged();
}
}
I tried to make this changes from the MainActivity aswell, but in every case I tried my list doesn't get updated. The only way it worked was when I reset the adapter to the recyclerView:
mAdapter.notifyDataSetChanged();
recyclerView.setAdapter(mAdapter);
which obviously is a bad idea. (aside from the bad side effects wouldn't even work when I'm using lazy loading on my lists).
So my question is, how can I make notifyDataSetChanged() work properly?
edit
I found a solution for replacing items. After mAdapter.replaceItem(newItem, pos); I had to call recyclerView.removeViewAt(position);
This works for replacing an item, but doesn't solve my problem when I want to add items (e.g. lazy loading) to my list
edit2
I found a working solution for adding items
Adapter:
public void addItem(final Item newItem) {
mItems.add(newItem);
notifyDataSetChanged();
}
Activity:
private void addItem(final Item newItem) {
mAdapter.addItem(newItem);
recyclerView.removeViewAt(0); // without this line nothing happens
}
For some reason this works (also: it doesn't remove the view at position 0), but I'm sure this isn't the correct way to add items to a recyclerView
This should work:
private ArrayList<Item> mItems;
public void replaceItem(final Item newItem, final int position) {
mItems.set(position, newItem);
notifyItemChanged(position);
}
ArrayList.set() is the way to go to replace items.
For adding items, just append them to mItems and then go notifyDatasetChanged(). Another way to go is to use notifyItemRangeInserted(). Depending on where/how are you adding new items and how many of them, it might be worth it.
Use
mItems.set(position, newItem);
instead of
mItems.add(position, newItem);
because .set method will replace your data to particular position.
In one of my apps, I need to add Fragments on both sides of the ViewPager. First of all, I will get a constant of 5 feeds, and my ViewPager will show feed at index 2 i.e. my current displayed Fragment will contain data present at index 2. So overall my ViewPager will show center of 5 feeds at start and that i have achieved by just setting the ViewPager current Item as 2 like this
viewPager.setCurrentItem(2);
Now user can swipe both sides, when he will swipe left from center position, I will look for next feed i.e fetch from server and add feed at zero index of my ViewPager like this
feedsList.add(0, modelClassObject); // feedsList will contain all feeds and has been used by adapter to show data in fragments.
adapter.notifyDataSetChanged();
and when i swipe right from center position, i will add feed at the last simply like this
feedsList.add(modelClassObject);
adapter.notifyDataSetChanged();
Now the problem is if i only add feeds at the right i.e at the end of the feedsList, everything works fine, but problem comes when i add feeds at zero index. My adapter is not showing that new feed that has been added to zero position instead it is repeating one of the existing feed and that too on the right side but not on the left. I Have tried everything, but nothing is going right way. Here is my adapter code.
private class HorizontalPagerAdapter extends FragmentStatePagerAdapter {
public HorizontalPagerAdapter(FragmentManager fm) {
super(fm);
}
#Override
public Fragment getItem(int arg0) {
return FeedUserProfileFragment.newInstance(feedsList.get(arg0),
arg0);
}
#Override
public int getCount() {
return feedsList.size();
}
}
I have also used this
#Override
public int getItemPosition(Object object) {
return POSITION_NONE;
}
but no results.. :(
So in severe need, If anyone had done that earlier and faced the same issue, please let me know what i am doing wrong. I only need to add fragment at zero index of my ViewPager.
I faced a similar problem before, and my solution was :
at first the list is declared in the adapter itself, so that when creating an instance of that adapter I can have it's own list then.
modified the method getItem(int arg0) in the adapter class so that it returns a specific item from the list depending on that item position.
when creating a new fragment, use instantiate method to create it, and after that add it to your fragments.
So, the complete solution would be :
adapter class:
private class HorizontalPagerAdapter extends FragmentStatePagerAdapter {
public List<Fragment> feedsList;
public HorizontalPagerAdapter(FragmentManager fm) {
super(fm);
}
#Override
public Fragment getItem(int position) {
return feedsList.get(position);
}
#Override
public int getCount() {
return feedsList.size();
}
}
and when creating the adapter:
public static YourPageAdapter adapter_obj; // make sure it's static object
adapter_obj = new YourPageAdapter(getSupportFragmentManager());
adapter_obj.feedsList = new ArrayList<Fragment>();
// then add the list of fixed fragments to it(the 5 in the beginning)
adapter_obj.feedsList = fragments_list; // an ArrayList contains the 5 fragments
and when want to create a new fragment:
adapter_obj.feedsList.add(0, Fragment.instantiate(mContext, ViewPager_Container_Class.class.getName(), page));
adapter_obj.notifyDataSetChanged();
FragmentStatePagerAdapter can't handle it right when you add a new fragment in front.
The workaround is this:
Add a new fragment at the end.
Call notifyDataSetChanged();
Bring the fragment to front.
Call notifyDataSetChanged();
BTW, getItemPosition() should return correct positions all along:
public int getItemPosition(Object object)
{
return feedsList.indexOf( object );
}
So, with your code, it should be:
newFrag = Fragment.instantiate(...);
feedsList.add( newFrag );
adapter_obj.notifyDataSetChanged();
feedsList.remove( feedsList.size() - 1 );
feedsList.add( 0, newFrag );
adapter_obj.notifyDataSetChanged();
I guess the implementation of FragmentStatePagerAdapter doesn't expect both adding a new fragment and changing position happen at the same time.
I have a ViewPager set up that is drawing the data for its pages (views) from data passed down from a server. On occasion, the server will send down new data that will re-order the views (and sometimes add new views) and I need to make that happen seamlessly on my android device while the user is currently viewing a particular fragment.
I have it set to call notifyDataSetChanged() when the new data is pulled down from the server, but that seems to keep a cached version of the slides to the left and right of the currently viewed slide which potentially will change with the reorder. I have looked at this topic here: ViewPager PagerAdapter not updating the View and implemented the first solution which works fine, other than it reloads the current slide that is being viewed which won't work for my purposes. I took a shallow-dive look into implementing the setTag() method proposed on that same page in the second answer and that would work for updating information on the slides, but it won't help me for reordering them.
is there some way I can reorder all of the slides behind the scenes w/o causing any burps in the currently viewed slide?
TIA
EDIT: adding code and asking for some further clarification
This is my Adapter class.
private class ScreenSlidePagerAdapter extends FragmentPagerAdapter {
ContestEntriesModel entries;
public ScreenSlidePagerAdapter(FragmentManager fm, ContestEntriesModel Entries) {
super(fm);
this.entries = Entries;
}
#Override
public long getItemId(int position) {
return entries.entries[position].ContestEntryId;
}
#Override
public int getItemPosition(Object object) {
android.support.v4.app.Fragment f = (android.support.v4.app.Fragment)object;
for(int i = 0; i < getCount(); i++){
android.support.v4.app.Fragment fragment = getItem(i);
if(f.equals(fragment)){
return i;
}
}
return POSITION_NONE;
}
#Override
public android.support.v4.app.Fragment getItem(int position) {
long entryId = getItemId(position);
//Log.d("EntryIds",Integer.toString(entryId));
if(mItems.get(entryId) != null) {
return mItems.get(entryId);
}
Fragment f = ScreenSlidePageFragment.create(position);
mItems.put(entryId, f);
return f;
}
#Override
public int getCount() {
return NUM_PAGES;
}
}
And here is what I'm using when a new payload comes down from the server:
#Override
protected void onPostExecute(ContestEntriesModel entries) {
Fragment currentFragment = ContestEntries.mItems.get(ContestEntries.CurrentEntryId);
Log.d("fromUpdate",Long.toString(ContestEntries.CurrentEntryId));
ContestEntries.Entries = entries;
ContestEntries.NUM_PAGES = ContestEntries.Entries.entries.length;
ContestEntries.mPagerAdapter.notifyDataSetChanged();
ContestEntries.mPager.setCurrentItem(ContestEntries.mPagerAdapter.getItemPosition(currentFragment));
}
Check my answer here. The key is to override both getItemId(int) and getItemPosition(Object) in your adapter.
getItemId(int) is used to identify your pages uniquely within your dataset. getItemPosition(Object) is used to fetch the (updated) position for this unique page in your dataset.
If you want to keep focus on the same page before and after updating (adding/removing pages), you should call viewpager.setCurrentItem(int) for the new position of your page after calling .notifyDataSetChanged() on your adapter.
I have tried using an extended FragmentStatePagerAdapter and an extended FragmentPagerAdapter to achieve what I'm trying and neither seem to work. I am sending down a JSON object from my server to fill an object model on the android client. On first load, everything works perfectly, I can swipe through pages just fine from beginning to end. Here is my object model:
ContestEntriesModel.java
public class ContestEntriesModel {
public int ContestId;
public String ContestName;
public ContestEntryModel[] entries;
}
As you can see, there is an array of ContestEntryModel. Each of those models looks like this:
ContestEntryModel.java
public class ContestEntryModel {
public long ContestEntryId;
public int UserId;
public String FullName;
public int Comments;
public string ImageUrl;
}
As stated above, when the first data payload comes down from the server, I am filling one fragment per item in the ContestEntryModel[] array with data from that model (including an image from an internet resource (ImageUrl)), and that works perfectly. I can swipe until my fingers bleed and all is good in the world.
I'm trying to now send another payload down from the server which has the items in the array in a different order (I will ultimately add new items too, but one step at a time). I have searched high and low on StackOverflow for a solution to this and posted a question yesterday which got me close to where I need to be (thanks Reinier), but I'm still struggling with getting all of my fragments to reorder and have the adapter completely refresh with the new order. The adapter seems to cache a few of the slides (the ones adjacent to the one I'm currently viewing) and they don't "refresh" until I've slid a few images past them and then return. Yesterday's trouble was getting the currently viewed slide to not "refresh" when the new data comes down from the server which I have working...sort of. Let's say I have 7 items in my array and I swipe to slide #3 (index 2) on the first load. I then push new data from the server which is just ordering the array in reverse. The currently viewed slide (slide #3) should then move to index position 4 and I should only be able to swipe to the right 2 times (index position 5 and 6). Instead what is happening is that it's remembering the current index position for that slide (index 2) and keeping it there. As I said above, slide #2 (index 1) and slide #4 (index 3) are also being maintained. My expected behavior is that those slides should reverse positions around slide #3 (index 2).
Here is my implementation of my PagerAdapter (this is the version that is a FragmentStatePagerAdapter not a FragmentPagerAdapter:
ScreenSlidePagerAdapter:
public class ScreenSlidePagerAdapter extends FragmentStatePagerAdapter {
public ContestEntriesModel entries;
public ScreenSlidePagerAdapter(FragmentManager fm, ContestEntriesModel Entries) {
super(fm);
this.entries = Entries;
}
public long getItemId(int position) {
return entries.entries[position].ContestEntryId;
}
#Override
public int getItemPosition(Object object) {
android.support.v4.app.Fragment f = (android.support.v4.app.Fragment)object;
for(int i = 0; i < getCount(); i++){
android.support.v4.app.Fragment fragment = getItem(i);
if(f.equals(fragment)){
//Log.d("currentPosition",Integer.toString(i));
return i;
}
}
return POSITION_NONE;
}
#Override
public android.support.v4.app.Fragment getItem(int position) {
long entryId = getItemId(position);
//Log.d("EntryIds",Integer.toString(entryId));
if(mItems.get(entryId) != null) {
return mItems.get(entryId);
}
Fragment f = ScreenSlidePageFragment.create(position);
mItems.put(entryId, f);
return f;
}
#Override
public int getCount() {
return NUM_PAGES;
}
}
currently I'm setting a static property on my activity for the adapter (I use it in a few other "external" classes) like so:
MainActivity:
public class MainActivity extends FragmentActivity {
public static int NUM_PAGES = 0;
public static ViewPager mPager;
public static PagerAdapter mPagerAdapter;
public static ContestEntriesModel Entries;
public static HashMap<Long, android.support.v4.app.Fragment> mItems = new HashMap<Long, android.support.v4.app.Fragment>();
public static Long CurrentEntryId;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.entries_layout);
GetEntriesTask task = new GetEntriesTask(43);
task.execute();
}
public void LoadEntries()
{
mPager = (ViewPager) findViewById(R.id.pager);
mPagerAdapter = new ScreenSlidePagerAdapter(getSupportFragmentManager(), Entries);
mPager.setAdapter(mPagerAdapter);
mPager.setPageTransformer(true, new MyPageTransformer());
mPager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
#Override
public void onPageSelected(int position) {
ContestEntries.CurrentEntryId = ContestEntries.Entries.entries[position].ContestEntryId;
Log.d("currentEntry",Long.toString(ContestEntries.CurrentEntryId));
invalidateOptionsMenu();
}
});
}
//the rest is omitted as it's not pertinent to my issue.
When a new dataload comes down from the sever, I am just setting my static property (Entries) to the new version. I also tried accessing the public property in the PagerAdapter (entries) to modify that directly, but that is not accessible for some reason (even though the class and the property both have a public accessor). I assume that is where this is going wrong because it's that instance that I need to be modifying, but honestly, my brain is fried at this point and I'm just not sure. Here is the code I'm using for the update:
UpdateCode:
#Override
protected void onPostExecute(ContestEntriesModel entries) {
Fragment currentFragment = MainActivity.mItems.get(MainActivity.CurrentEntryId);
Log.d("fromUpdate",Long.toString(MainActivity.CurrentEntryId));
MainActivity.Entries = entries;
MainActivity.NUM_PAGES = MainActivity.Entries.entries.length;
//MainActivity.mItems.clear();
MainActivity.mPagerAdapter.notifyDataSetChanged();
MainActivity.mPager.setCurrentItem(MainActivity.mPagerAdapter.getItemPosition(currentFragment));
}
So using that, it does keep me on the current slide (which is what I need), but that slide is not repositioned correctly as mentioned above, nor are the other adjacent slides "refreshed". Again, assuming I'm on slide #3, if i swipe to the left once the old slide #2 is still there. If I swipe to the left twice, slide #1 is updated to the "new" slide (slide #7 from the first load). If I then swipe back to the right once, slide #2 is still there (should be slide #6). If I then swipe to the right once more, the "original" slide that I was on that I need maintained, is now replaced with slide #5 (again that number is from the original load). If I keep swiping to the right all the way to the "new" slide #5, I see the original again. That is where it needs to be on the new data load, in that position.
I sincerely apologize for this wall of text, but I thought if I could provide as much code as possible, this would make more sense. I've read several different approaches to this problem but I can't wrap my head around any that suit my needs specifically.
Any suggestions / code help would be greatly appreciated.
You were working along the right lines. Your getItemPosition would never work because it is always instantiating a new Fragment object. You need to maintain your own ordering list and receive return POSITION_UNCHANGED if that objects view is still in the correct position. I will update my answer with a code example soon.
If all Fragments are of the same class you could just update the Fragments with new data intead oof reorder them.
MyFragment:
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
DataProvider.getInstance().addOnDataChangedListener(this);
if (mRoot == null) {
mRoot = inflater.inflate(R.layout.entitlement, container, false);
}
updateDetails();
return mRoot;
}
#Override
public void onDestroyView() {
DataProvider.getInstance().removeOnDataChangeListener(this);
super.onDestroyView();
}
public void dataChanged() {
updateDetails();
}
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
mContext = activity;
}
#Override
public void onDetach() {
super.onDetach();
mContext = null;
}
The DataProvider simply calls dataChanged() on registered listeners. Important: If you use getActivity() getResource() or similar in updateDetails() this may crash, since the Activity may still be null although OnAttach() has been called. To avoid this I use my own Context-Object to do these things, which is set in OnAttach() and removed in OnDetach().
My adapter just takes care of the page titles, since the fragments refresh on their own:
protected LinkedList<CharSequence> mPageTitles = new LinkedList<CharSequence>();
#Override
public CharSequence getPageTitle(int position) {
try {
return mPageTitles.get(position);
} catch (Exception ex) {
return null;
}
}
protected final void setData(List data) {
mPageTitles.clear();
if (list != null) {
for (int idx = 0; idx < list.size(); ++idx) {
mPageTitles.add(DataProvider.getInstance().getPageTitle(list.get(idx)));
}
}
}
#Override
public void notifyDataSetChanged() {
setData(DataProvider.getInstance().getData());
super.notifyDataSetChanged();
}
The time you update your DataProvider you also have to call myAdapter.notifyDataSetChanged().
This is probably not the best way of doing it, but you could give it a try.
This solution actually produces a hiccup as it's goal is to display new data instantly.
If your currentSlide should not be updated, you should mark it and take care of it in updateDetails()`:
private boolean sticky = false;
public void setSticky() {
sticky = true;
}
public void forceUpdate() {
sticky = false;
updateDetails();
}
protected void updateDetails() {
if (sticky) {
return;
}
// Do your update
}