I'm using a ViewPager to cycle through a set of fragments, and I want to update each fragment after it slides onto the screen. Basically, I want the text to "fade in" after the fragment has settled.
I tried using the fragment's onStart and onResume methods, and while this works for most of the pages, it does NOT work for the second page, because for whatever dumb reason, the first page AND the second page have their onStart/onResume methods called at the same time (before the second page ever hits the screen).
Now I'm trying to get it to work with the onPageChangeListener's onPageSelected callback. That method looks like this:
#Override
public void onPageSelected(final int position) {
mCurrentPosition = position;
PageFragment fragment = (PageFragment) ((MainActivity.ScreenSlidePagerAdapter) mViewPager.getAdapter()).getItem(position);
fragment.onSelect();
}
And the onSelect method in the fragment looks like this:
public void onSelect(){
new android.os.Handler().postDelayed(
new Runnable() {
public void run() {
mSwitcher.setText("");
mNum = getArguments() != null ? getArguments().getInt("num") : 1;
Media currentMedia = slideshow.getMedia().get(mNum);
mSwitcher.setText(currentMedia.getDisplayName());
}
},
4000);
}
The problem with this way is that the line mSwitcher.setText(""); throws a NullPointerException
java.lang.NullPointerException: Attempt to invoke virtual method 'void android.widget.TextSwitcher.setText(java.lang.CharSequence)' on a null object reference
Which would suggest that the onCreateView method in that class has yet to run since that's where the mSwitcher variable is instantiated. Which seems bananas to me, since the view is already sliding onto the screen at this point.
Any ideas about how to solve this problem would be greatly appreciated. This is my first Android experience, and I've been trying to solve this stupid text-fade-in issue for a full week with no luck. At this point I'm almost ready to abandon mobile as a platform because of how painful every minor change has been so far.
ViewPager keeps the next page in memory & this is it's default behaviour. You could adjust it by calling like:
viewPager.setOffscreenPageLimit(2);
However this might not be useful as if you pass 0 in above method, viewPager will ignore it.
You are going in right direction. I believe now problem is in your ScreenSlidePagerAdapter. In getItem(int position) you might have something like
if(position == 1)
return new PageFragment();
instead change the adapter to something like following,
public class ScreenSlidePagerAdapter extends FragmentPagerAdapter {
private List<Fragment> mFragments = new ArrayList<>();
public ScreenSlidePagerAdapter(FragmentManager fm, List<Item> items) {
super(fm);
for (Item item : items) {
mFragments.add(new PageFragment());
}
}
#Override
public int getCount() {
return mFragments.size();
}
#Override
public Fragment getItem(int position) {
return mFragments.get(position); // Return from list instead of new PagerFragment()
}
}
I have the similar problem as yours, onPageSelected() is called before the fragments are initialized, but your description is not detailed enough, such as how you select the second page.
When adapter is fed with Fragments, or we say getCount() > 0, getItem() will whatever returns a Fragment, which is not null. But this doesn't mean it is initialized, at least it doesn't if you extend from FragmentStatePagerAdapter.
when adapter is fed with data and called notifyDataSetChange(), adapter will initialize the first two pages by default. If you call setCurrentItem() to move to other pages immediately after notifyDataSetChange() the issue might happen. During the runtime, setCurrentItem() -> onPageSelected() might be called before the fragments are initialized.
my solution is using view.post() when setCurrentItem(). e.g.
viewPager.post(() -> viewPager.setCurrentItem(index));
I have a ViewPager using a FragmentPagerAdapter for displaying three tabs, each represented by its ow fragment. One of these fragments contains a list, that should be updated on switching / swiping to that tab. But I don't find any way to make it happen. I tried using the onResume method, but the fragments seem not to be paused and resumed on tab change. I also tried using ViewPager.OnPageChangeListener in my MainActivity:
#Override
public void onPageSelected(int position)
{
FragmentRefreshInterface currentFragment = (FragmentRefreshInterface) mSectionsPagerAdapter.getItem(position);
currentFragment.onRefreshed();
}
And in the fragment I use the following:
#Override
public void onRefreshed()
{
List<Record> records = mRecordingService.getRecords();
mRecordAdapter.clear();
mRecordAdapter.add(record);
}
But using this code I can't access my RecordingService class that is used to provide the database functions (because mRecordingService seems to be null). I initialize it in the fragment like this:
#Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
mRecordingService = new RecordingService(getContext());
}
Using the onPageChangeListener is the correct way to do it. I believe the reason why your code is not working, is because you are calling getItem on your pager adapter: getItem() actually returns a new instance of the fragment. In order to get the current instance, you use instantiateItem() (which returns a reference to the fragment actually being used).
Change your code to look something like this:
#Override
public void onPageSelected(int position)
{
FragmentRefreshInterface currentFragment = (FragmentRefreshInterface) mSectionsPagerAdapter.instantiateItem(viewPager,position);
currentFragment.onRefreshed();
}
And it should work.
I suggest that the code you have in onRefreshed() go in onResume() instead. Fragment doesn't have an onRefreshed() method. You must be implementing another interface that declares this method.
Since you are storing data in a database, you should be use a CursorAdapter or subclass such as SimpleCursorAdapter. If you do this correctly, the ListView will automatically update when you add a record to the database. Then the service can add records without needing to access the service from the fragment.
In your MainActivity:
private FirstFragment firstFragment;
private WantedFragment wantedFragment;
private ThirdFragment thirdfragment;
In getItem
switch(postition){
//return first, wanted, third fragments depending on position
}
onPageSelected:
if(position == 1) // position of the wanted fragment
wantedfragment.onRefreshed()
I have Nested Fragments [1 main Fragment called "MainFrag" and many child Fragments]
the App can show MainFrag or can show other fragments.
what I want to do is to setCurrentItem of MainFrag's ViewPager every time the user selects to show that MainFrag so I tried to put it at it's onCreateView but this only works for the first time the User Shows MainFrag as if user Selects another fragment and then re-selects MainFrag the seCurrentItem is not working
I also tried to put it inside MainFrag's onResume, It worked as required but it also causes a problem that if the user moved the App to background and then reOpen it viewPager will Scroll and i don't want this
So where Exactly Can I put setCurrentItem ?
mViewPager.setCurrentItem(mCurrentWeekFragmentItemNumber);
Edit: I am using Navigation Menu to Move between 3 main fragments where MainFrag is one of them.
After your mViewPager.setAdapter(adapter); method you can put
mViewPager.setCurrentItem(mCurrentWeekFragmentItemNumber);
After setting adapter to viewpager add setCurrentItem method
mViewPager.setAdapter(adapter);
mViewPager.setCurrentItem(mCurrentWeekFragmentItemNumber);
Try this:
viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
#Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
#Override
public void onPageSelected(int position) {
viewPager.setCurrentItem(position);
}
#Override
public void onPageScrollStateChanged(int state) {
}
});
}
I use a FragmentPagerAdapter to switch from fragments. I need some functions to be called when a fragmentswitch is made and had some troubles with OnPause and OnResume, so as suggested by THIS question I have implemented an interface OnPageSelectListener :
public interface OnPageSelectListener {
void onPageSelected();
void onPageNotVisible();
}
It calls the function OnPageSelected whenever this page comes to the foreground. Works nice, except that I want to call a function on my adapter. I thought that would work, except that my adapter returns NULL all the times (even though it is initialized and data is loaded in my listview as prefered).
public class AfterCheckFragment extends Fragment implements OnPageSelectListener{
private ListView listView;
private List<Check> checkList;
private CheckListAdapter adapter;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
View view = inflater.inflate(R.layout.fragment_check, container, false);
System.out.println("VIEW create called");
//(.. some other stuff, not relevant for question..)
//initializing the adapter
listView = (ListView) view.findViewById(R.id.listView);
adapter = new CheckListAdapter(checkList,getActivity(),trainPosition);
listView.setAdapter(adapter);
adapter.handleButtonVisibility();
return view;
}
#Override
public void onPageSelected() {
if(this.adapter != null) {
System.out.println("adapter not null");
this.adapter.checkForActive();
}else{
System.out.println("Adapter is NULL");
}
}
#Override
public void onPageNotVisible() { //page is moved to backgroung
System.out.println("AFTER not active any more ");
}
}
Now is my question: Why does adapter (or any other object in the fragment) return null when I return to my fragment? When the fragmentPager is initialized the onActivityCreate function of the fragment is called one time, but after that not any more, and the adapter return null....
you have to call the onPageSelected() after initialization of the adapter and setAdapter() otherwise adapter will return null always
Here is why I think your CheckListAdapter (i'll call it listAdapter) is null:
You give the pagerAdapter to the ViewPager
The ViewPager asks the pagerAdapter for a new Fragment
The ViewPager tells the FragmentManager to use it
onPageSelected gets called
You try and use listAdapter. It hasn't been initialized yet at this point. (NPE)
The FragmentManager drags the Fragment through all its stages.
onCreateView gets called. Your listAdapter is created.
Don't try and use internal data of a fragment outside of it. It is meant to work as a standalone unit, it won't be very good if you use it differently. Since the fragment is initialized at a later stage, you can't use it like you intend.
You can try and do what you want to do in the fragment, rather than the pagerAdapter, or write a method in the hosting Activity and call it from the fragment when ready, or even launch an event.
ViewPager will create and destroy fragments as the user changes pages (see ViewPager.setOffscreenPageLimit()). So onActivityCreated() is only called on the fragment when it is being restored or set up for the first time. Hence, fragments can be created without ever having onActivityCreated() called.
Instead of onActivityCreated(), I would recommend overriding onViewCreated() and setting up your adapter there. No fragment can be displayed without having a view created, so this is a good place to do that kind of stuff.
If you have your OnPageSelectListener logic working, that's good. I found the best way to know when your fragment is actually in front of the user is by overriding setPrimaryItem() in the FragmentPagerAdapter. Getting the page out of view event is a little trickier, since you have to keep a reference to the fragment from the previous setPrimaryItem() call.
This is because Viewpager calls OnpageSelected way before Fragments in oncreateView()/onActivityCreated() is called .
The best way for you is to inflate your views in the constructor of the Fragment and set the Adapters.
Or
Use a member variable to store whether the Fragment is active or not. And use the variable in oncreateview() to call function on your adapter.
Why don't you use a viewpager.addOnPageChangeListener, in you pager , after setting its adapter and the setOffscreenPageLimit() instead of implements it on your fragment?
Heres a sample code:
viewpager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
#Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
#Override
public void onPageSelected(int position) {
if(position == 1){ // if you want the second page, for example
//Your code here
}
}
#Override
public void onPageScrollStateChanged(int state) {
}
});
Make it in your Activity, where you setup your ViewPager, of course.
for me i had to call this on my viewpager:
myViewPager.setSaveFromParentEnabled(false);
I've got this strange issue, ViewPager's setCurrentItem(position, false) works perfectly fine, then im switching to another activity, and after I'm back to the first activity, the ViewPager always ends up on the first item. Even though I've added setCurrentItem to onResume method it still ignores it. It's not even throwing any exception when I'm trying to set item to out of bounds index.
Though later on when I call this method, when the button "next" is tapped, it works like expected.
Checked my code 10 times for any possible calls to setCurrentItem(0) or something but it's just not there at all.
i can't really answer WHY exactly this happens, but if you delay the setCurrentItem call for a few milliseconds it should work. My guess is that because during onResume there hasn't been a rendering pass yet, and the ViewPager needs one or something like that.
private ViewPager viewPager;
#Override
public void onResume() {
final int pos = 3;
viewPager.postDelayed(new Runnable() {
#Override
public void run() {
viewPager.setCurrentItem(pos);
}
}, 100);
}
UPDATE: story time
so today i had the problem that the viewpager ignored my setCurrentItem action, and i searched stackoverflow for a solution. i found someone with the same problem and a fix; i implemented the fix and it didn't work. whoa! back to stackoverflow to downvote that faux-fix-provider, and ...
it was me. i implemented my own faulty non-fix, which i came up with the first time i stumbled over the problem (and which was later forgotten). i'll now have to downvote myself for providing bad information.
the reason my initial "fix" worked was not because of of a "rendering pass"; the problem was that the pager's content was controlled by a spinner. both the spinners and the pagers state were restored onResume, and because of this the spinners onItemSelected listener was called during the next event propagation cycle, which did repopulate the viewpager - this time using a different default value.
removing and resetting the listener during the initial state restoration fixed the issue.
the fix above kind-of worked the first time, because it set the pagers current position after the onItemSelected event fired. later, it ceased to work for some reason (probably the app became too slow - in my implementation i didn't use 100ms, but 10ms). i then removed the postDelayed in a cleanup cycle, because it didn't change the already faulty behaviour.
update 2: i can't downvote my own post. i assume, honorable seppuku is the only option left.
I had a similar issue in the OnCreate of my Activity.
The adapter was set up with the correct count and I
applied setCurrentItem after setting the adapter to the
ViewPager however is would return index out of bounds. I think the ViewPager had not loaded all my Fragments at the point i set the current item. By posting a runnable on the ViewPager i was able to work around this. Here is an example with a little bit of context.
// Locate the viewpager in activity_main.xml
final ViewPager viewPager = (ViewPager) findViewById(R.id.pager);
// Set the ViewPagerAdapter into ViewPager
viewPager.setAdapter(new ViewPagerAdapter(getSupportFragmentManager()));
viewPager.setOffscreenPageLimit(2);
viewPager.post(new Runnable() {
#Override
public void run() {
viewPager.setCurrentItem(ViewPagerAdapter.CENTER_PAGE);
}
});
I found a very simple workaround for this:
if (mViewPager.getAdapter() != null)
mViewPager.setAdapter(null);
mViewPager.setAdapter(mPagerAdapter);
mViewPager.setCurrentItem(desiredPos);
And, if that doesn't work, you can put it in a handler, but there's no need for a timed delay:
new Handler().post(new Runnable() {
#Override
public void run() {
mViewPager.setCurrentItem(desiredPos);
}
});
ViewTreeObserver can be used to avoid a static delay.
Kotlin:
Feel free to use Kotlin extension as a concise option.
view_pager.doOnPreDraw {
view_pager.currentItem = 1
}
Please, make sure you have a gradle dependency: implementation 'androidx.core:core-ktx:1.3.2' or above
Java
OneShotPreDrawListener.add(view_pager, () -> view_pager.currentItem = 1);
A modern approach in a Fragment or Activity is to call ViewPager.setcurrentItem(Int) function in a coroutine in the context of Dispatchers.Main :
lifecycleScope.launch(Dispatchers.Main) {
val index = 1
viewPager.setCurrentItem(index)
}
I had similar bug in the code, the problem was that I was setting the position before changing the data.
The solution was simply to set the position afterwards and notify the data changed
notifyDataSetChanged()
setCurrentItem()
I have the same problem and I edit
#Override
public int getCount() { return NUM_PAGES; }
I set NUM_PAGES be mistake to 1 only.
some guy wrote on forums here. https://code.i-harness.com/en/q/126bff9 worked for me
if (mViewPager.getAdapter() != null)
mViewPager.setAdapter(null);
mViewPager.setAdapter(mPagerAdapter);
mViewPager.setCurrentItem(desiredPos);
Solution (in Kotlin with ViewModel etc.) for those trying to set the current item in the onCreate of Activity without the hacky Runnable "solutions":
class MyActivity : AppCompatActivity() {
lateinit var mAdapter: MyAdapter
lateinit var mPager: ViewPager
// ...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.fragment_pager)
// ...
mainViewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
mAdapter = MyAdapter(supportFragmentManager)
mPager = findViewById(R.id.pager)
mainViewModel.someData.observe(this, Observer { items ->
items?.let {
// first give the data to the adapter
// this is where the notifyDataSetChanged() happens
mAdapter.setItems(it)
mPager.adapter = mAdapter // assign adapter to pager
mPager.currentItem = idx // finally set the current page
}
})
This will obviously do the correct order of operations without any hacks with Runnable or delays.
For the completeness, you usually implement the setItems() of the adapter (in this case FragmentStatePagerAdapter) like this:
internal fun setItems(items: List<Item>) {
this.items = items
notifyDataSetChanged()
}
I've used the post() method described here and sure enough it was working great under some scenarios but because my data comes from the server, it was not the holy grail.
My problem was that i want to have
notifyDataSetChanged
called at an arbitrary time and then switch tabs on my viewPager. So right after the notify call i have this
ViewUtilities.waitForLayout(myViewPager, new Runnable() {
#Override
public void run() {
myViewPager.setCurrentItem(tabIndex , false);
}
});
and
public final class ViewUtilities {
public static void waitForLayout(final View view, final Runnable runnable) {
view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
//noinspection deprecation
view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
runnable.run();
}
});
}
}
Fun fact: the //noinspection deprecation at the end is because there is a spelling mistake in the API that was fixed after API 16, so that should read
removeOnGlobalLayoutListener
^^
ON Global
instead of
removeGlobalOnLayoutListener
^^
ON Layout
This seems to be covering all cases for me.
I was working on this problem for one week and I realized that this problem happens because I was using home activity context in view pager fragments and we can only use context in fragment after it gets attached to activity..
When a view pager gets created, activity only attach to the first (0) and second (1) page. When you open the second page, the third page gets attached and so on! When you use setCurrentItem() method and the argument is greater than 1, it wants to open that page before it is attached, so the context in fragment of that page will be null and the application gets crashed! That's why when you delay setCurrentItem(), it works! At first it gets attached and then it'll open the page...
This is a lifecycle issue, as pointed out by several posters here. However, I find the solutions with posting a Runnable to be unpredictable and probably error prone. It seems like a way to ignore the problem by posting it into the future.
I am not saying that this is the best solution, but it definitely works without using Runnable. I keep a separate integer inside the Fragment that has the ViewPager. This integer will hold the page we want to set as the current page when onResume is called next. The integer's value can be set at any point and can thus be set before a FragmentTransaction or when resuming an activity. Also note that all the members are set up in onResume(), not in onCreateView().
public class MyFragment extends Fragment
{
private ViewPager mViewPager;
private MyPagerAdapter mAdapter;
private TabLayout mTabLayout;
private int mCurrentItem = 0; // Used to keep the page we want to set in onResume().
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
View view = inflater.inflate(R.layout.my_layout, container, false);
mViewPager = (ViewPager) view.findViewById(R.id.my_viewpager);
mTabLayout = (TabLayout) view.findViewById(R.id.my_tablayout);
return view;
}
#Override
public void onResume()
{
super.onResume();
MyActivity myActivity = (MyActivity) getActivity();
myActivity.getSupportActionBar().setTitle(getString(R.string.my_title));
mAdapter = new MyPagerAdapter(getChildFragmentManager(), myActivity);
mViewPager.setAdapter(mAdapter);
mViewPager.setOffscreenPageLimit(PagerConstants.OFFSCREEN_PAGE_LIMIT);
mViewPager.setCurrentItem(mCurrentItem); // <-- Note the use of mCurrentItem here!
mTabLayout.setupWithViewPager(mViewPager);
}
/**
* Call this at any point before needed, for example before performing a FragmentTransaction.
*/
public void setCurrentItem(int currentItem)
{
mCurrentItem = currentItem;
// This should be called in cases where onResume() is not called later,
// for example if you only want to change the page in the ViewPager
// when clicking a Button or whatever. Just omit if not needed.
mViewPager.setCurrentItem(mCurrentItem);
}
}
For me this worked setting current item after setting adapter
viewPager.setAdapter(new MyPagerAdapter(getSupportFragmentManager()));
viewPager.setCurrentItem(idx);
pagerSlidingTabStrip.setViewPager(viewPager);// assign viewpager to tabs
I've done it this way to restore the current item:
#Override
protected void onSaveInstanceState(Bundle outState) {
if (mViewPager != null) {
outState.putInt(STATE_PAGE_NO, mViewPager.getCurrentItem());
}
super.onSaveInstanceState(outState);
}
#Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
if (savedInstanceState != null) {
mCurrentPage = savedInstanceState.getInt(STATE_PAGE_NO, 0);
}
super.onRestoreInstanceState(savedInstanceState);
}
#Override
protected void onRestart() {
mViewPager.setCurrentItem(mCurrentPage);
super.onRestart();
}
By the time I call setCurrentItem() the view is about to be recreated. So in fact I invoke setCurrentItem() for the viewpager and afterwards the system calls onCreateView() and hence creates a new viewpager.
This is the reason for me why I do not see any changes. And this is the reason why a postDelayed() may help.
Theoretical solution: Postpone the setCurrentItem() invocation until the view has been recreated.
Practical solution: I have no clue for a stable and simple solution. We should be able to check if the class is about to recreate it's view and if that is the case postpone the invocation of setCurrentItem() to the end of onCreateView()
I use the dsalaj code as a reference. If necessary I share the code with the complete solution.
I also strongly recommend using ViewPager2
Solution
Both cases have to go within the Observer {}:
First case: Initialize the adapter only when we have the first data set and not before, since this would generate inconsistencies in the paging. To the first data set we have to pass it as the argument of the Adapter.
Second case: From the first change in the observable we would have from the second data sets onwards which have to be passed to the Adapter through a public method only if we have already initialized the adapter with a first data set.
GL
I was confused with the onActivityCreated() getting invoked for unrelated tab #Mahdi Arabpour was an eye opener for me :)
For me the problem was the third page (as elaborated by #Mahdi Arabpour above) was getting reconstructed when I click the second tab, etc and it was losing its data adapter, setting it again in onActivityCreted solves my problems:
if (myXXRecyclerAdapter != null) {
myXXRecyclerAdapter = new MyXXRecyclerAdapter(myStoredData);
mRecyclerView.setAdapter(myXXRecyclerAdapter );
return;
}
You need to call pager.setCurrentItem(activePage) right after pager.setAdapter(buildAdapter())
#Override
public void onResume() {
if (pager.getAdapter() != null) {
activePage=pager.getCurrentItem();
Log.w(getClass().getSimpleName(), "pager.getAdapter()!=null");
pager.setAdapter(null);
}
pager.setAdapter(buildAdapter());
pager.setCurrentItem(activePage);
}