Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UserVisibleHint is false on selected ViewPager Fragment managed by FragmentStatePagerAdapter

I've encountered a really hard to diagnose issue in an Android app. getUserVisibleHint() returns false on the currently selected fragment in a ViewPager when it should return true (because it is visible and selected).

I've characterized the instances I see this behavior as follows:

  • Fragment is selected and currently displayed in a ViewPager
  • ViewPager is managed by a FragmentStatePagerAdapter
  • Fragment was previously selected, its state was saved and later restored by the PagerAdapter
    • minimum of 3 tabs in the viewpager
    • user navigates to tab 3, then to tab 1 then back to tab 3.
  • App uses Support Library version 24.0.0 or greater
like image 724
Jon Avatar asked May 18 '17 15:05

Jon


1 Answers

Debugging revealed that FragmentStatePagerAdapter is actually setting the state of the selected tab properly in setPrimaryItem(ViewGroup container, int position, Object object) but that it is later set to false in FragmentManager#moveToState(Fragment f, int newState, int transit, int transitionStyle, boolean keepActive)

//from FragmentManager#moveToState(Fragment f, int newState, int transit, int transitionStyle, boolean keepActive)
f.mUserVisibleHint = f.mSavedFragmentState.getBoolean(
        FragmentManagerImpl.USER_VISIBLE_HINT_TAG, true);

f.mSavedFragmentState above has saved the visible state as false because it was saved when the fragment was no longer on the screen.

So the issue here is state loss; the visible state is being set in FragmentStatePagerAdapter#setPrimaryItem but is lost some time before the fragment's onResume method is called.

The Fix

Until this bug is fixed in the library, override setPrimaryItem in your PagerAdapter and force any pending transactions to commit first.

public static class SectionsPagerAdapter extends FragmentStatePagerAdapter {
    public SectionsPagerAdapter(FragmentManager fm) {
        super(fm);
    }

    @Override
    public void setPrimaryItem(ViewGroup container, int position, Object object) {
        //Force any pending transactions to save before we set an item as primary
        finishUpdate(null);
        super.setPrimaryItem(container, position, object);
    }

    @Override
    public Fragment getItem(int position) {
        Fragment fragment = new DummySectionFragment();
        Bundle args = new Bundle();
        args.putInt(DummySectionFragment.ARG_SECTION_NUMBER, position + 1);
        fragment.setArguments(args);
        return fragment;
    }

    @Override
    public int getCount() {
        return 4;
    }

    @Override
    public CharSequence getPageTitle(int position) {
        return "Page " + (position + 1);
    }
}

To fix this, FragmentStatePagerAdapter must commit any fragment transactions before setting the user visible hint.

FragmentStatePagerAdapter

Just to show what's happening inside FragmentStatePagerAdapter

@Override
public Object instantiateItem(ViewGroup container, int position) {
    // If we already have this item instantiated, there is nothing
    // to do.  This can happen when we are restoring the entire pager
    // from its saved state, where the fragment manager has already
    // taken care of restoring the fragments we previously had instantiated.
    if (mFragments.size() > position) {
        Fragment f = mFragments.get(position);
        if (f != null) {
            return f;
        }
    }

    if (mCurTransaction == null) {
        mCurTransaction = mFragmentManager.beginTransaction();
    }

    Fragment fragment = getItem(position);
    if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
    if (mSavedState.size() > position) {
        Fragment.SavedState fss = mSavedState.get(position);
        if (fss != null) {
            fragment.setInitialSavedState(fss);
        }
    }
    while (mFragments.size() <= position) {
        mFragments.add(null);
    }
    fragment.setMenuVisibility(false);
    fragment.setUserVisibleHint(false);
    mFragments.set(position, fragment);
    mCurTransaction.add(container.getId(), fragment);

    return fragment;
}

@Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
    Fragment fragment = (Fragment)object;
    if (fragment != mCurrentPrimaryItem) {
        if (mCurrentPrimaryItem != null) {
            mCurrentPrimaryItem.setMenuVisibility(false);
            mCurrentPrimaryItem.setUserVisibleHint(false);
        }
        if (fragment != null) {
            fragment.setMenuVisibility(true);
            fragment.setUserVisibleHint(true);
        }
        mCurrentPrimaryItem = fragment;
    }
}

@Override
public void finishUpdate(ViewGroup container) {
    if (mCurTransaction != null) {
        mCurTransaction.commitNowAllowingStateLoss();
        mCurTransaction = null;
    }
}
like image 147
Jon Avatar answered Sep 21 '22 12:09

Jon