I am calling an api that returns json. I parse the json into an arraylist of objects. I have a class that i created to randomly select an element from the list and remove it and display it in the viewpager. I have the viewpager and pageradapter set up and asyncloader to create the dataset. I get a runtime exception with the following. If i change the getCount() to return a static number then it works as i want it to but it doesnt accomplish my requirements that i listed below.
The application's PagerAdapter changed the adapter's contents without calling PagerAdapter#notifyDataSetChanged! Expected adapter item count: 14, found: 12 Pager id
I totally understand why it is happening. The following code is why.
random element class: this method provides the random object i want to display in the viewpager at the same time removing from the list so i do not retrieve it again.
public T spin() throws IllegalStateException, NullPointerException {
if (list == null)
throw new NullPointerException("A list has not been set.");
if (list.size() > 0) {
int i = rand.nextInt(list.size());
T b = list.remove(i);
return b;
} else {
throw new IllegalStateException("There are no more elements left. Please query more.") ;
}
}
This is my inner pageradapter class - r is the object of the randomelement class:
private class RPagerAdapter extends PagerAdapter {
private Context context;
public RPagerAdapter(Context context) {
this.context = context;
}
#Override
public int getCount() {
return r.size();
}
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView((View) object);
}
#Override
public Object instantiateItem(ViewGroup container, int position) {
ViewHolder mViewHolder = new ViewHolder(context);
mViewHolder.setItem(r.spin());
container.addView(mViewHolder);
return mViewHolder;
}
#Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}
}
What im trying to accomplish is the following:
The user can only swipe forward (to the left) thus the spin method will be invoked correctly. (priority)
When the the spin method returns an exception the loader is called to load more data. (not a priority right now)
The user cannot be allowed to swipe right because the functionality of the app is for the user to only see a random element once (which the randomelement class accomplishes) so i need the viewpager to understand to only move in one direction meaning a new random element is displayed.
Please let me know if there is a different way that i can implement the above or point me in the right android apis to use.
A call to the PagerAdapter method startUpdate(ViewGroup) indicates
that the contents of the ViewPager are about to change. One or more
calls to instantiateItem(ViewGroup, int) and/or destroyItem(ViewGroup,
int, Object) will follow, and the end of an update will be signaled by
a call to finishUpdate(ViewGroup).
Therefore, you should only perform your update in the list in finishUpdate(ViewGroup)
forget about your whole spin structure, try something like this, pseudo code only:
private class RPagerAdapter extends PagerAdapter {
private Context context;
private List<T> items;
public RPagerAdapter(Context context, List<T> items) {
this.context = context;
this.items = items;
}
#Override
public int getCount() {
return items.size();
}
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView((View) object);
}
#Override
public Object instantiateItem(ViewGroup container, int position) {
ViewHolder mViewHolder = new ViewHolder(context);
mViewHolder.setItem(items.get(position));
container.addView(mViewHolder);
container.setTag(String.valueOf(position));
return mViewHolder;
}
#Override
public void finishUpdate(ViewGroup container) {
int position = Integer.parseInt(container.getTag());
//remove all items before position
//add items at the end if you want
notifyDataSetChanged();
viewPager.setCurrentItem(0, false);
}
#Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}
}
Related
I have a gallery/slideshow activity that allows user to swipe between Photos. Each "Page" is just a TouchImageView. There is a pagination logic in there, and I can see that it is calling the API accordingly. However, I am not able to swipe further even after notifyDataSetChanged has been called. Here's the code:
Activity {
ViewPager vp;
CustomPagerAdapter pagerAdapter;
onCreate() {
//api callback
pagerAdapter = new CustomPagerAdapter();
vp.setAdapter(pagerAdapter);
}
}
CustomPagerAdapter() {
TouchImageView imageView;
List<Photos> photos;
int getCount() {
return photos == null ? 0 : photos.size();
}
#Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}
#Override
public Object instantiateItem(ViewGroup container, int position) {
View itemView = mLayoutInflater.inflate(R.layout.pager_image, container, false);
imageView = (TouchImageView) itemView.findViewById(R.id.photo);
Glide.with(context)
.load(photos.get(position).getUrl())
.into(imageView);
container.addView(itemView);
if (position == photos.size()-1) {
loadMorePhotos();
}
return itemView;
}
#Override
public int getItemPosition(Object object) {
return POSITION_NONE;
}
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView((RelativeLayout) object);
}
void loadMorePhotos() {
//call api and stuff
void onResponse(Call call, Response<List<Photo>> response) {
photos.addAll(response.body);
notifyDataSetChanged();
}
}
}
And this is the result:
http://imgur.com/nA1TzfI
I have no idea what is going on for 2 days. Please help!
This may not be ideal, but where you call notifyDatasetChanged() you could instead try calling vp.setAdapter(pagerAdapter);
(i.e. actually re-set the adapter). A downside of this is that the pager will probably go back to having the first page selected. To avoid this, you'd need to save the latest page somewhere and restore it afterwards.
As I say, not ideal, but it may help.
You dont have to load more photos from adapter class, in your activity class you can use onPageScrolled and call the api when the last item is scrolled and set adapter again also save state of view pager before calling api to retain it when adapter is set again.
private boolean isLastPageSwiped;
private int counterPageScroll;
private OnPageChangeListener mListener = new OnPageChangeListener() {
#Override
public void onPageSelected(int arg0) {
// TODO Auto-generated method stub
}
#Override
public void onPageScrolled(int arg0, float arg1, int arg2) {
// TODO Auto-generated method stub
if (position == 6 && positionOffset == 0 && !isLastPageSwiped){
if(counterPageScroll != 0){
isLastPageSwiped=true;
//call api
}
counterPageScroll++;
}else{
counterPageScroll=0;
}
}
#Override
public void onPageScrollStateChanged(int arg0) {
// TODO Auto-generated method stub
}
};
and use this listener
vp.setOnPageChangeListener(mListener);
We can use this approach for doing pagination on ViewPager (with fragment)
load new page data but not append to the adapter
make sure the page is not scrolling, then append new page data into the adapter (ViewPager.OnPageChangeListener#onPageScrolled's positionOffsetPixels == 0).
update current item for view pager with offset = new page data's size (let's call it newPosition)
rebind data to 3 fragments: newPosition - 1, newPosition, newPosition + 1
For getting current fragment, we can use fragment manager
private fun getFragment(position: Int): PageFragment? =
supportFragmentManager.fragments.firstOrNull {
(it as? PageFragment)?.position == position // always pass possition value into the fragment
} as? PageFragment
We need to append data when not scrolling (step 2) to have a better UX to users. If we append data immediately when it comes and if user is scrolling, view pager's setCurrentItem will make a strange move.
I create a simple version here https://github.com/tuanchauict/DemoPaginationViewPager. Please take a look
In my app I use a ViewPager in conjunction with PageTransformer to display some data list as a card deck. That list is sometimes refreshed and it contains plenty of items in a common case, so I use FragmentStatePagerAdapter with needed page limit:
public class PlaceListPagerAdapter extends FragmentStatePagerAdapter {
private List<PlaceListData> items;
public PlaceListPagerAdapter (FragmentManager manager) {
super (manager);
items = new ArrayList<>();
}
#Override
public Fragment getItem(int position) {
String id = items.get(position).getId();
String name = items.get(position).getName();
String address = items.get(position).getAddress();
return PlaceFragment.newInstance(id, name, address);
}
#Override
public int getCount() {
return items.size();
}
#Override
public int getItemPosition(Object object) {
PlaceFragment frag = (PlaceFragment) object;
String id = frag.getDataId();
for (PlaceListData data : items) {
if (id != null && id.equals(data.getId())) {
return POSITION_UNCHANGED;
}
}
return POSITION_NONE;
}
public void addItems(List<PlaceListData> items) {
this.items.addAll(items);
notifyDataSetChanged();
}
public void replaceItems(List<PlaceListData> items) {
this.items.clear();
addItems(items);
}
public void clearItems () {
this.items.clear();
notifyDataSetChanged();
}
}
Nothing unusual - list of pojos, overriden getItemPosition() and add/clear methods with notifyDataSetChanged(). The problem is when I try to add/replace items over network, method transformPage(View view, float position) of my transformer is called with 0.0 position for all new views and they are placed one over another (transformation is broken).
After debugging ViewPager class I noticed that transformPage(View view, float position) is invoked only in one place in onPageScrolled(..) method of ViewPager and the latter is invoked in onLayout(..) after the first layout pass.
notifyDataSetChanged() invokes onPageScrolled(..) (and transformPage(View view, float position) respectively) and requestLayout() after that. The problem is that first invocation of transformPage(View view, float position) happens before layout pass and position parameter depends on View.left() which returns 0 as child views are not layouted. In onLayout(..) which follows the requestLayout() my transformPage(View view, float position) is not called at all because yeap, views are layouted now, but it is not the first layout pass.
The problem is cured by resetting adapter (it sets mFirstLayout flag in ViewPager to "true") or by calling any public method of ViewPager which invokes transformPage(View view, float position) in it's code such as scrollTo, fakeDrag etc, but I'd like to know - is all that a feature of ViewPager or is it a bug or am I doing something very very wrong?
Sorry for my bad English and thanks in advance!
I am using two Viewpager in my app,
1) First Viewpager displays images only
2) I am displaying price
now the issue is i have 4 images displaying in my viewpager1, and in second pager i have price as per selected product. first time it does not show anything, but when i scroll image and goes to next, it shows price..
pager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
#Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
Picasso.with(ProductLandingActivity.this) .load(categorylist.get(position).getProductLanding_packLink())
.error(R.drawable.nopreview )
.placeholder(R.drawable.progress_animation)
.into(selectedImage);
System.out.println("Selected is"+position);
selectedname.setText(categorylist.get(position).getProductLanding_packDesc());
for (int i = 0; i < categorylist.get(position).getItems().size(); i++) {
System.out.println("ProductPack_ID : " + categorylist.get(position).getItems().get(i).getPackSize_sellingPrice());
}
temp = categorylist.get(position).getItems();
packadapter = new MyPacksPagerAdapter(ProductLandingActivity.this,categorylist);
pagerpacks.setAdapter(packadapter);
}
#Override
public void onPageSelected(int position) {
}
#Override
public void onPageScrollStateChanged(int state) {
}
});
adapter
private class MyPacksPagerAdapter extends PagerAdapter {
Context context;
ArrayList<PackListModel> packsizedata ;
public MyPacksPagerAdapter(Context context,ArrayList<PackListModel> packsizedata) {
this.context = context;
this.packsizedata = packsizedata;
}
#Override
public Object instantiateItem(ViewGroup container, int position) {
final OneActor oneActor;
View view;
LayoutInflater infl = ((Activity)context).getLayoutInflater();
view = infl.inflate(R.layout.list_item_pagerpacktitles, container,false);
oneActor = new OneActor();
// oneActor.avatar = (ImageView) view.findViewById(R.id.image);
oneActor.name = (TextView) view.findViewById(R.id.product_landing_packsname);
oneActor.cmtCount = (TextView) view.findViewById(R.id.product_landing_packsprice);
view.setTag(oneActor);
oneActor.name.setText(temp.get(position).getPackSize_packSize());
oneActor.cmtCount.setText(temp.get(position).getPackSize_sellingPrice());
((ViewGroup) container).addView(view);
return view;
}
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView((View)object);
}
#Override
public int getCount() {
return packsizedata.size();
}
#Override
public boolean isViewFromObject(View view, Object object) {
return (view == object);
}
class OneActor{
// ImageView avatar;
TextView name,cmtCount;
}
}
By defaulat when i run the app it shows like this,in second pager it is not showing product price,
But when i scroll image it shows price
My Expected output is
This is my json response
http://pastebin.com/fbJang2B
First, as mentioned by #NotobharatKumar, you're loading the second adapter on a wrong event of the parent adapter, ie in onPageScrolled method. Second, you're setting a new adapter each time on that specific event, it seems useless. Finally, you are setting datas in an event, (I'm not sure why you're doing this but) I'd prefer to set it in a separate adapter and let the events listener for specific behaviors.
For me, it seems that you have two separate adapters, but one datas list shared by both. Assuming that, you have to set them both at start with their datas respectly, and on the event onPageSelected of the top adapter, you just have to automatically scroll the second. And if they have the same position in the list, onPageSelected should do the work correctly.
So, these modifications should solve your issue:
Your code in onScrollChanged, when you set the image and the text, seems really weird to me. I'd use a first adapter where I'll set all the datas like these two for the first ViewPager:
#Override
public void onCreate(Bundle savedInstansteState) {
...
// set a simple adapter for the first ViewPager
FirstPagerAdapter imageadapter =
new FirstPagerAdapter(ProductLandingActivity.this, categorylist);
pagerimages.setAdapter(imageadapter);
...
}
Then, as usual, set your datas in the FirstPagerAdapter:
#Override
public View getView(int position, View view, ViewGroup container) {
...
// set the content
Picasso.with(ProductLandingActivity.this)
.load(categorylist.get(position).getProductLanding_packLink())
.error(R.drawable.nopreview )
.placeholder(R.drawable.progress_animation)
.into(selectedImage);
selectedname.setText(
categorylist.get(position).getProductLanding_packDesc());
...
}
Then no need to (re)load the image or the text when an event is triggered, since they will be holding by the adapter.
You only use getPackSize_packSize() and getPackSize_sellingPrice() in the second adapter, so you should create a separate list to only fill with these datas but outside the swiping event. Start by initializing the second list and the adapters:
// get the Items to fill the pack items list (like you did for `temp`)
ArrayList<Items> packItems = new ArrayList<>();
for (int i = 0; i < categorylist.size(); i++) {
packItems.add(categorylist.get(position).getItems());
}
// fill the first adapter with your list of products (ie categorylist)
...
pagerimages.setAdapter(imageadapter);
...
// fill the second adapter with the pack items list
packadapter = new MyPacksPagerAdapter(ProductLandingActivity.this, packItems);
pagerpacks.setAdapter(packadapter);
You have to do this when categorylist is created and populated. So place this above code for example in your callback, when you retrieve the datas from your server.
Since the second list packItems is filling in the same order than categorylist, there will be no weird behavior by changing the both positions.
Now, in the second adapter, it's preferable to use the local list packsizedata, as follows:
#Override
public Object instantiateItem(ViewGroup container, int position) {
...
oneActor.name.setText(
packsizedata.get(position).getPackSize_packSize());
oneActor.cmtCount.setText(
packsizedata.get(position).getPackSize_sellingPrice());
...
}
Finally, control the bottom ViewPager by using onPageSelected event of the first:
pager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
#Override
public void onPageScrolled(int position, float positionOffset,
int positionOffsetPixels) { }
#Override
public void onPageSelected(int position) {
// set the second current page regarding the position of the first
pagerpacks.setCurrentItem(position);
}
#Override
public void onPageScrollStateChanged(int state) { }
});
Hope this will be helpful.
You should implement code for action at onPageSelected method because after page changed of your ViewPager then onPageSelected will be called.
I am trying to implement a ViewPager without using Fragments. Let's say, I have an unspecific amount of data stored inside some kind of database which is mapped to specific days. What I am trying to do is to display the data for a specific day on ViewPager pages.
This is what I am currently doing:
#Override
protected void onCreate(Bundle savedInstanceState) {
dataList = (ViewPager) findViewById(R.id.dataList);
dataList.setAdapter(new DataListSlidePagerAdapter());
dataList.setCurrentItem(Integer.MAX_VALUE / 2);
}
private class DataListSlidePagerAdapter extends PagerAdapter {
int lastPosition;
#Override
public Object instantiateItem(ViewGroup collection, int position) {
if (position > lastPosition) {
GregorianCalendar today = Settings.getCurrentDay();
today.add(GregorianCalendar.DATE, 1);
Settings.loadData();
}
else if (position < lastPosition) {
GregorianCalendar today = Settings.getCurrentDay();
today.add(GregorianCalendar.DATE, -1);
Settings.loadData();
}
lastPosition = position;
LinearLayout v = createDataList(Settings.getTodaysData());
collection.addView(v);
return v;
}
#Override
public void destroyItem(ViewGroup collection, int position, Object view) {
((ViewPager) collection).removeView((LinearLayout) view);
}
#Override
public int getCount() {
return Integer.MAX_VALUE;
}
#Override
public boolean isViewFromObject(View arg0, Object arg1) {
return arg0 == ((View) arg1);
}
public int getItemPosition(Object object) {
return POSITION_NONE;
}
}
This actually almost works, but unfortunately, the data for the current day is never shown on the current page, but rather on the next or previous page. My guess would be, that this is because instantiateItem does not necessarily build the view of the current page, but rather of some page which is probably needed to be shown next. createListData(), however, always creates the view of the current day.
Any idea how to fix this and to display the right data on the right pages? You can assume, that there are also functions like Settings.getTomorrowsData() and Settings.getYesterdaysData().
Why is PagerAdapter.setPrimaryItem() called more than once (with the same values) after I select a new page with ViewPager.setCurrentItem(index) ?
Yes, for me it even kept calling infinitely. However, if you need something to be called once, here is a simple solution
public class MyPagerAdapter extends PagerAdapter {
private int lastPosition = -1;
#Override public void setPrimaryItem(ViewGroup container, int position, Object object) {
super.setPrimaryItem(container, position, object);
// Only refresh when primary changes
if(lastPosition != position) {
lastPosition = position;
yourFunction();
}
}
}
I think the reason why PagerAdapter.setPrimaryItem() gets called more than once is because of ViewPager.setOffscreenPageLimit(int)
See official docs for more info:
https://developer.android.com/reference/kotlin/androidx/viewpager/widget/ViewPager#setOffscreenPageLimit(kotlin.Int)