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().
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
I'm fairly new to Android programming, and I'm trying to design an app where when I swipe through several images on a ViewPager that takes up some of the screen, other elements of that same activity/screen will change with it without necessarily 'scrolling' horizontally like the ViewPager does. My problem is that I can't find a way to identify which image in the ViewPager is the 'current' one outside of the ViewPager.
I tried to create a getter for grabbing the position from instantiateItem method, but no luck - because I assume it simply creates everything once, not updating it again, so when I swipe nothing will happen. Also, I realize that my dieValue variable doesn't do anything - but it's meant to serve as an example of what I want to accomplish. The dieValue variable would change based on which image was the current one.
CustomSwipeAdapter.java
public class CustomSwipeAdapter extends PagerAdapter {
private Context ctx;
public int[] image_resources = {R.drawable.d20, R.drawable.d8, R.drawable.d6};
public CustomSwipeAdapter(Context ctx) {
this.ctx = ctx;
}
#Override
public int getCount() {
return image_resources.length;
}
#Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}
#Override
public Object instantiateItem(ViewGroup container, int position) {
LayoutInflater layoutInflater = (LayoutInflater) ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View item_view = layoutInflater.inflate(R.layout.swipe_layout, container, false);
ImageView imageView = (ImageView) item_view.findViewById(R.id.dieImageView);
imageView.setImageResource(image_resources[position]);
TextView textView = (TextView) item_view.findViewById(R.id.dieValueTop);
if (position == 0) { textView.setText(R.string.d20Label); }
if (position == 1) { textView.setText(R.string.d8Label); }
if (position == 2) { textView.setText(R.string.d6Label); }
container.addView(item_view);
return item_view;
}
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView((LinearLayout) object);
}
}
DiceRollerActivity.java
public class DiceRollerActivity extends Activity implements View.OnClickListener {
ViewPager viewPager;
CustomSwipeAdapter adapter;
private int dieValue;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_dice_roller);
viewPager = (ViewPager) findViewById(R.id.dieViewPager);
adapter = new CustomSwipeAdapter(this);
viewPager.setAdapter(adapter);
Button RollDieButton = (Button) findViewById(R.id.rollDieButton);
RollDieButton.setOnClickListener(this);
}
#Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.rollDieButton:
Random rand = new Random();
int random = rand.nextInt(dieValue) + 1;
setDieResults(random);
}
}
// Prints the results of the die roll to the results log
private void setDieResults(int random) {
TextView dieResults = (TextView) findViewById(R.id.dieResultsLabel);
if (random == dieValue) {
dieResults.setText(getString(R.string.YouRolledLabel) + random + getString(R.string.criticalHit));
} else if (random == dieValue) {
dieResults.setText(getString(R.string.YouRolledLabel) + random + getString(R.string.criticalMiss));
}else{
dieResults.setText(getString(R.string.YouRolledLabel) + random);
}
}
}
Create a method inside your CustomSwipeAdapter
public int getCurrentImageResource(int currentPosition){
return image_resources[currentPosition];
}
To access to the current image call this method passing current position with viewPager.getCurrentItem() like this
adapter.getCurrentImageResource(viewPager.getCurrentItem());
Hope this helps!
You could use
viewPager.getCurrentItem();
on your DiceRollerActivity.class to get the current position.
Then, to get the image a better approach would be to have the image array on your DiceRollerActivity.class and pass it onto the CustomSwipeAdapter constructor.
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;
}
}
I have an adapter (FragmentStatePagerAdapter) with arrows, that each click creates the next page:
leftArrow.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
viewPager.setCurrentItem(viewPager.getCurrentItem() - 1);
}
});
rightArrow.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
viewPager.setCurrentItem(viewPager.getCurrentItem() + 1);
}
});
#Override
public Object instantiateItem(ViewGroup collection, int position) {
int i=0;
for (i=position; i<position+2;i++){
MyPage currentPage = pages.get(i);
if(page == null){
currentPage = new MyPage(i);
pages.put(i, page);
super.instantiateItem(collection, i);
}
}
return super.instantiateItem(collection, position);
}
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
MyPage pageToRemove = pages.get(position);
if(pageToRemove != null){
pageToRemove.dispose();
pages.remove(position);
}
super.destroyItem(container, position, object);
}
}
I want to create on each iteration not 1 page but 3, because I have delays while paging. I tried to create the pages and insert them into array but the problem is that I have only ONE return that return return super.instantiateItem(collection, position);
Any ideas how can I call it few more times?
Any ideas how can I call it few more times?
You're not the one doing the calling -- ViewPager is. You can call setOffscreenPageLimit() to increase the number of pages that ViewPager will cache. By default, the value is 1, meaning that ViewPager will cache 3 pages (the current one, plus one to either side). Bumping that to 2 will have ViewPager cache 5 pages, and so on.
Bear in mind:
This slows down setting up the ViewPager, and so it is not really a solution for slow pages
This consumes more heap space
I would like to keep a list of 3 views at all times. The app starts at position 1 (out of positions 0,1,2). When someone scrolls to position 0, I would like to remove view 2, and create a view before position 0. This way, it appears to the user, that there are unlimited views. In the same way, when someone scrolls to position 2, I would like to remove the view at position 0 and add one at the end.
However I'm having problems with both adding and removing views. When I get to position 0, nothing changes unless I try scrolling past position 0 (to position -1, i.e. the boundary is hit). At that point, I can see that it is the boundary of my views, but then setCurrentItem(1,false) is triggered and I'm brought back to the middle of the views. When I scroll to position 2 I see that position 2 has been updated. However position 0 and 1 remain the same.
When I scroll to position 2, nothing happens. However if I try and scroll to the boundary, for some reason, position 0 gets updated and setCurrentItem(1,false) is triggered.
I have no idea why its happening like this. Can anyone shed some light on this?
Here is my code:
public class MainActivity extends Activity {
ArrayList<Integer> showThree = new ArrayList<Integer>();
int focusedPage = 0;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
showThree.add(0,5); //adding integers 5,6,7 for positions 0,1,2
showThree.add(1,6);
showThree.add(2,7);
final MyPagerAdapter adapter = new MyPagerAdapter(getApplicationContext(),showThree);
final ViewPager myPager = (ViewPager) findViewById(R.id.mypanelpager);
myPager.setAdapter(adapter);
myPager.setCurrentItem(1);
myPager.setOnPageChangeListener(new OnPageChangeListener(){
#Override
public void onPageScrollStateChanged(int state) {
if (state == ViewPager.SCROLL_STATE_IDLE) {
//when position= 0, change the 3 views from 5,6,7 to 4,5,6.
if (focusedPage == 0) {
showThreeMonths.set(0,4);
showThreeMonths.set(1,5);
showThreeMonths.set(2,6);
adapter.notifyDataSetChanged();
adapter.startUpdate(myPager);
}
else if (focusedPage ==2){
//ignore, just testing focusPage=0 for now }
}
//set current page to the middle of the 3 new views, which would be
//the same view at position 0 of the old 3 views.
//Thus user doesn't experience the views changing despite being 3 new views.
myPager.setCurrentItem(1,false);
}
#Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
// TODO Auto-generated method stub
}
#Override
public void onPageSelected(int position) {
focusedPage = position;
}
});
}
PagerAdapter
public class MyPagerAdapter extends PagerAdapter {
private ArrayList<Integer> showThreeMonths;
private Context ctx;
public MyPagerAdapter (Context ctx, ArrayList<Integer> showThree){
this.ctx = ctx ;
this.showThree = showThree;
}
#Override
public int getCount() {
return showThree.size();
}
public Object instantiateItem(ViewGroup collection, int position ){
//NewCustomView is a class I made that takes parameters context and an integer and creates a view based on the integer
NewCustomView MyOwnView = new NewCustomView(ctx, showThree.get(position));
View customViewLayout = MyOwnView.newLayout; //part of the class object
collection.addView(customViewLayout);
return customViewLayout;
}
#Override
public void destroyItem(ViewGroup collection, int position, Object arg2) {
((ViewPager) collection).removeView((ViewGroup) arg2);}
#Override
public Parcelable saveState() {
return null;}
#Override
public boolean isViewFromObject(View view, Object arg1) {
return view==arg1;}
#Override
public void startUpdate(ViewGroup collection) {}
#Override
public void finishUpdate(ViewGroup collection) {}
}
The instantiateItem() method creates the 2 view pages in the memory by default. Therefore when you swipe to the second page then 0 page is recreated as it's outside the range of the 2 pages saved in the memory. Please try to use
myViewPager.setOffscreenPageLimit(numberOfPages)
method that receives an integer as a parameter and declares how many pages it should be keeping before recycling them.