Related
Background
FragmentList contains a view pager with an underlying FragmentStatePagerAdapter to show pages of FragmentDetail
FragmentDetail contains a scroll view
What i want to do
As you swipe, i want the previous FragmentDetail scroll position reset to the top. At the moment, when you swipe back to it, the scroll position goes back to where you left off.
E.g. I am on page 1 of view pager, i scroll to bottom of the current detail fragment. I then go to next page. Finally i go back to the first page, i want the scroll to be at the top and not where i left it
I tried the following
in OnPause of fragment detail, i tried the following code
#Override
public void onPause() {
super.onPause();
mScrollView.scrollTo(0,0);
}
I also tried the following
#Override
public void onPause() {
super.onPause();
mScrollView.post(new Runnable() {
#Override
public void run() {
mScrollView.fullScroll(View.FOCUS_UP);
}
});
}
Also put the scrollTo code in a runnable is well.
Does not scroll to the top
Forget about onPause(); you will drive yourself insane.
The best way I have found to do this sort of thing is override setPrimaryItem() in the FragmentPagerAdapter subclass. setPrimaryItem() is called whenever a fragment is displayed by the ViewPager.
Let's say you have an interface that looks like this:
public interface ResettableFragment {
public void reset();
}
and so your fragment implementation is:
#Override
public void reset() {
mScrollView.scrollTo(0,0);
}
In your FragmentPagerAdapter subclass, create a member
private ResettableFragment mLast;
and override setPrimaryItem() like this:
#Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
super.setPrimaryItem(container, position, object);
if (mLast != null) {
mLast.reset();
mLast = null;
}
if (object instanceof ResettableFragment) {
mLast = (ResettableFragment) object;
}
}
Some people prefer to register a ViewPager.OnPageChangListener which overrides onPageSelected() to do the reset logic instead of overriding setPrimaryItem() in the adapter.
That might look something like this:
viewPager.addOnPageChangeListener(new SimpleOnPageChangeListener() {
#Override
public void onPageSelected(int position) {
if (position == POSITION_OF_PAGE_TO_RESET + 1 ||
position == POSITION_OF_PAGE_TO_RESET - 1) {
FragmentPagerAdapter adapter = (FragmentPagerAdapter) viewPager.getAdapter();
ResettableFragment fragment = (ResettableFragment) adapter.getItem(position);
fragment.reset();
}
}
});
Problem: Fragment onResume() in ViewPager is fired before the fragment becomes actually visible.
For example, I have 2 fragments with ViewPager and FragmentPagerAdapter. The second fragment is only available for authorized users and I need to ask the user to log in when the fragment becomes visible (using an alert dialog).
BUT the ViewPager creates the second fragment when the first is visible in order to cache the second fragment and makes it visible when the user starts swiping.
So the onResume() event is fired in the second fragment long before it becomes visible. That's why I'm trying to find an event which fires when the second fragment becomes visible to show a dialog at the appropriate moment.
How can this be done?
How to determine when Fragment becomes visible in ViewPager
You can do the following by overriding setUserVisibleHint in your Fragment:
public class MyFragment extends Fragment {
#Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (isVisibleToUser) {
}
else {
}
}
}
UPDATE: Android Support Library (rev 11) finally fixed the user visible hint issue, now if you use support library for fragments, then you can safely use getUserVisibleHint() or override setUserVisibleHint() to capture the changes as described by gorn's answer.
UPDATE 1 Here is one small problem with getUserVisibleHint(). This value is by default true.
// Hint provided by the app that this fragment is currently visible to the user.
boolean mUserVisibleHint = true;
So there might be a problem when you try to use it before setUserVisibleHint() was invoked. As a workaround you might set value in onCreate method like this.
public void onCreate(#Nullable Bundle savedInstanceState) {
setUserVisibleHint(false);
The outdated answer:
In most use cases, ViewPager only show one page at a time, but the pre-cached fragments are also put to "visible" state (actually invisible) if you are using FragmentStatePagerAdapter in Android Support Library pre-r11.
I override :
public class MyFragment extends Fragment {
#Override
public void setMenuVisibility(final boolean visible) {
super.setMenuVisibility(visible);
if (visible) {
// ...
}
}
// ...
}
To capture the focus state of fragment, which I think is the most suitable state of the "visibility" you mean, since only one fragment in ViewPager can actually place its menu items together with parent activity's items.
This seems to restore the normal onResume() behavior that you would expect. It plays well with pressing the home key to leave the app and then re-entering the app. onResume() is not called twice in a row.
#Override
public void setUserVisibleHint(boolean visible)
{
super.setUserVisibleHint(visible);
if (visible && isResumed())
{
//Only manually call onResume if fragment is already visible
//Otherwise allow natural fragment lifecycle to call onResume
onResume();
}
}
#Override
public void onResume()
{
super.onResume();
if (!getUserVisibleHint())
{
return;
}
//INSERT CUSTOM CODE HERE
}
Here is another way using onPageChangeListener:
ViewPager pager = (ViewPager) findByViewId(R.id.viewpager);
FragmentPagerAdapter adapter = new FragmentPageAdapter(getFragmentManager);
pager.setAdapter(adapter);
pager.setOnPageChangeListener(new OnPageChangeListener() {
public void onPageSelected(int pageNumber) {
// Just define a callback method in your fragment and call it like this!
adapter.getItem(pageNumber).imVisible();
}
public void onPageScrolled(int arg0, float arg1, int arg2) {
// TODO Auto-generated method stub
}
public void onPageScrollStateChanged(int arg0) {
// TODO Auto-generated method stub
}
});
In ViewPager2 and ViewPager from version androidx.fragment:fragment:1.1.0 you can just use onPause and onResume callbacks to determine which fragment is currently visible for the user. onResume callback is called when fragment became visible and onPause when it stops to be visible.
In case of ViewPager2 it is default behavior but the same behavior can be enabled for old good ViewPager easily.
To enable this behavior in the first ViewPager you have to pass FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT parameter as second argument of FragmentPagerAdapter constructor.
FragmentPagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT)
Note: setUserVisibleHint() method and FragmentPagerAdapter constructor with one parameter are now deprecated in the new version of Fragment from android jetpack.
setUserVisibleHint() gets called sometimes before onCreateView() and sometimes after which causes trouble.
To overcome this you need to check isResumed() as well inside setUserVisibleHint() method. But in this case i realized setUserVisibleHint() gets called only if Fragment is resumed and visible, NOT when Created.
So if you want to update something when Fragment is visible, put your update function both in onCreate() and setUserVisibleHint():
#Override
public View onCreateView(...){
...
myUIUpdate();
...
}
....
#Override
public void setUserVisibleHint(boolean visible){
super.setUserVisibleHint(visible);
if (visible && isResumed()){
myUIUpdate();
}
}
UPDATE: Still i realized myUIUpdate() gets called twice sometimes, the reason is, if you have 3 tabs and this code is on 2nd tab, when you first open 1st tab, the 2nd tab is also created even it is not visible and myUIUpdate() is called. Then when you swipe to 2nd tab, myUIUpdate() from if (visible && isResumed()) is called and as a result,myUIUpdate() may get called twice in a second.
The other problem is !visible in setUserVisibleHint gets called both 1) when you go out of fragment screen and 2) before it is created, when you switch to fragment screen first time.
Solution:
private boolean fragmentResume=false;
private boolean fragmentVisible=false;
private boolean fragmentOnCreated=false;
...
#Override
public View onCreateView(...){
...
//Initialize variables
if (!fragmentResume && fragmentVisible){ //only when first time fragment is created
myUIUpdate();
}
...
}
#Override
public void setUserVisibleHint(boolean visible){
super.setUserVisibleHint(visible);
if (visible && isResumed()){ // only at fragment screen is resumed
fragmentResume=true;
fragmentVisible=false;
fragmentOnCreated=true;
myUIUpdate();
}else if (visible){ // only at fragment onCreated
fragmentResume=false;
fragmentVisible=true;
fragmentOnCreated=true;
}
else if(!visible && fragmentOnCreated){// only when you go out of fragment screen
fragmentVisible=false;
fragmentResume=false;
}
}
Explanation:
fragmentResume,fragmentVisible: Makes sure myUIUpdate() in onCreateView() is called only when fragment is created and visible, not on resume. It also solves problem when you are at 1st tab, 2nd tab is created even if it is not visible. This solves that and checks if fragment screen is visible when onCreate.
fragmentOnCreated: Makes sure fragment is not visible, and not called when you create fragment first time. So now this if clause only gets called when you swipe out of fragment.
Update
You can put all this code in BaseFragment code like this and override method.
New
ViewPager2 + FragmentStateAdapter + onResume() (in Fragment)
solve the problem
Old Answer (deprecated)
To detect Fragment in ViewPager visible, I'm quite sure that only using setUserVisibleHint is not enough.
Here is my solution to check if a fragment is visible or invisible. First when launching viewpager, switch between page, go to another activity/fragment/ background/foreground`
public class BaseFragmentHelpLoadDataWhenVisible extends Fragment {
protected boolean mIsVisibleToUser; // you can see this variable may absolutely <=> getUserVisibleHint() but it not. Currently, after many test I find that
/**
* This method will be called when viewpager creates fragment and when we go to this fragment background or another activity or fragment
* NOT called when we switch between each page in ViewPager
*/
#Override
public void onStart() {
super.onStart();
if (mIsVisibleToUser) {
onVisible();
}
}
#Override
public void onStop() {
super.onStop();
if (mIsVisibleToUser) {
onInVisible();
}
}
/**
* This method will called at first time viewpager created and when we switch between each page
* NOT called when we go to background or another activity (fragment) when we go back
*/
#Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
mIsVisibleToUser = isVisibleToUser;
if (isResumed()) { // fragment have created
if (mIsVisibleToUser) {
onVisible();
} else {
onInVisible();
}
}
}
public void onVisible() {
Toast.makeText(getActivity(), TAG + "visible", Toast.LENGTH_SHORT).show();
}
public void onInVisible() {
Toast.makeText(getActivity(), TAG + "invisible", Toast.LENGTH_SHORT).show();
}
}
EXPLANATION
You can check the logcat below carefully then I think you may know why this solution will work
First launch
Fragment1: setUserVisibleHint: isVisibleToUser=false isResumed=false
Fragment2: setUserVisibleHint: isVisibleToUser=false isResumed=false
Fragment3: setUserVisibleHint: isVisibleToUser=false isResumed=false
Fragment1: setUserVisibleHint: isVisibleToUser=true isResumed=false // AT THIS TIME isVisibleToUser=true but fragment still not created. If you do something with View here, you will receive exception
Fragment1: onCreateView
Fragment1: onStart mIsVisibleToUser=true
Fragment2: onCreateView
Fragment3: onCreateView
Fragment2: onStart mIsVisibleToUser=false
Fragment3: onStart mIsVisibleToUser=false
Go to page2
Fragment1: setUserVisibleHint: isVisibleToUser=false isResumed=true
Fragment2: setUserVisibleHint: isVisibleToUser=true isResumed=true
Go to page3
Fragment2: setUserVisibleHint: isVisibleToUser=false isResumed=true
Fragment3: setUserVisibleHint: isVisibleToUser=true isResumed=true
Go to background:
Fragment1: onStop mIsVisibleToUser=false
Fragment2: onStop mIsVisibleToUser=false
Fragment3: onStop mIsVisibleToUser=true
Go to foreground
Fragment1: onStart mIsVisibleToUser=false
Fragment2: onStart mIsVisibleToUser=false
Fragment3: onStart mIsVisibleToUser=true
DEMO project here
Hope it help
package com.example.com.ui.fragment;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.example.com.R;
public class SubscribeFragment extends Fragment {
#Override
public View onCreateView(LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_subscribe, container, false);
return view;
}
#Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (isVisibleToUser) {
// called here
}
}
#Override
public void onViewCreated(View view, #Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
}
}
Override setPrimaryItem() in the FragmentPagerAdapter subclass. I use this method, and it works well.
#Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
// This is what calls setMenuVisibility() on the fragments
super.setPrimaryItem(container, position, object);
if (object instanceof MyWhizBangFragment) {
MyWhizBangFragment fragment = (MyWhizBangFragment) object;
fragment.doTheThingYouNeedToDoOnBecomingVisible();
}
}
Override Fragment.onHiddenChanged() for that.
public void onHiddenChanged(boolean hidden)
Called when the hidden state (as returned by isHidden()) of the fragment has changed. Fragments start out not hidden; this will be called whenever the fragment changes state from that.
Parameters
hidden - boolean: True if the fragment is now hidden, false if it is not visible.
Only this worked for me!! and setUserVisibleHint(...) is now deprecated (I attached docs at end), which means most of other answers are deprecated ;-)
public class FragmentFirewall extends Fragment {
/**
* Required cause "setMenuVisibility(...)" is not guaranteed to be
* called after "onResume()" and/or "onCreateView(...)" method.
*/
protected void didVisibilityChange() {
Activity activity = getActivity();
if (isResumed() && isMenuVisible()) {
// Once resumed and menu is visible, at last
// our Fragment is really visible to user.
}
}
#Override
public void onResume() {
super.onResume();
didVisibilityChange();
}
#Override
public void setMenuVisibility(boolean visible) {
super.setMenuVisibility(visible);
didVisibilityChange();
}
}
Tested and works with NaviagationDrawer as well,
there isMenuVisible() will always return true (and onResume() seems enough, but we want to support ViewPager too).
setUserVisibleHint is deprecated. If overriding this method, behavior implemented when passing in true should be moved to Fragment.onResume(), and behavior implemented when passing in false should be moved to Fragment.onPause().
setUserVisibleHint(boolean visible) is now deprecated So this is the correct solution
FragmentPagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT)
In ViewPager2 and ViewPager from version androidx.fragment:fragment:1.1.0 you can just use onPause() and onResume() to determine which fragment is currently visible for the user. onResume() is called when the fragment became visible and onPause when it stops to be visible.
To enable this behavior in the first ViewPager you have to pass FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT parameter as the second argument of the FragmentPagerAdapter constructor.
I figured out that onCreateOptionsMenu and onPrepareOptionsMenu methods called only in the case of the fragment really visible. I could not found any method which behaves like these, also I tried OnPageChangeListener but it did not work for the situations, for example, I need a variable initialized in onCreate method.
So these two methods can be used for this problem as a workaround, specifically for little and short jobs.
I think, this is the better solution but not the best. I will use this but wait for better solution at the same time.
Regards.
Another solution posted here overriding setPrimaryItem in the pageradapter by kris larson almost worked for me. But this method is called multiple times for each setup. Also I got NPE from views, etc. in the fragment as this is not ready the first few times this method is called. With the following changes this worked for me:
private int mCurrentPosition = -1;
#Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
super.setPrimaryItem(container, position, object);
if (position == mCurrentPosition) {
return;
}
if (object instanceof MyWhizBangFragment) {
MyWhizBangFragment fragment = (MyWhizBangFragment) object;
if (fragment.isResumed()) {
mCurrentPosition = position;
fragment.doTheThingYouNeedToDoOnBecomingVisible();
}
}
}
Add following Code inside fragment
#Override
public void setMenuVisibility(final boolean visible)
{
super.setMenuVisibility(visible);
if (visible && isResumed())
{
}
}
I encountered the same problem while working with FragmentStatePagerAdapters and 3 tabs. I had to show a Dilaog whenever the 1st tab was clicked and hide it on clicking other tabs.
Overriding setUserVisibleHint() alone didn't help to find the current visible fragment.
When clicking from 3rd tab -----> 1st tab.
It triggered twice for 2nd fragment and for 1st fragment.
I combined it with isResumed() method.
#Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
isVisible = isVisibleToUser;
// Make sure that fragment is currently visible
if (!isVisible && isResumed()) {
// Call code when Fragment not visible
} else if (isVisible && isResumed()) {
// Call code when Fragment becomes visible.
}
}
We have a special case with MVP where the fragment needs to notify the presenter that the view has become visible, and the presenter is injected by Dagger in fragment.onAttach().
setUserVisibleHint() is not enough, we've detected 3 different cases that needed to be addressed (onAttach() is mentioned so that you know when the presenter is available):
Fragment has just been created. The system makes the following calls:
setUserVisibleHint() // before fragment's lifecycle calls, so presenter is null
onAttach()
...
onResume()
Fragment already created and home button is pressed. When restoring the app to foreground, this is called:
onResume()
Orientation change:
onAttach() // presenter available
onResume()
setUserVisibleHint()
We only want the visibility hint to get to the presenter once, so this is how we do it:
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.fragment_list, container, false);
setHasOptionsMenu(true);
if (savedInstanceState != null) {
lastOrientation = savedInstanceState.getInt(STATE_LAST_ORIENTATION,
getResources().getConfiguration().orientation);
} else {
lastOrientation = getResources().getConfiguration().orientation;
}
return root;
}
#Override
public void onResume() {
super.onResume();
presenter.onResume();
int orientation = getResources().getConfiguration().orientation;
if (orientation == lastOrientation) {
if (getUserVisibleHint()) {
presenter.onViewBecomesVisible();
}
}
lastOrientation = orientation;
}
#Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (presenter != null && isResumed() && isVisibleToUser) {
presenter.onViewBecomesVisible();
}
}
#Override public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(STATE_LAST_ORIENTATION, lastOrientation);
}
Detecting by focused view!
This works for me
public static boolean isFragmentVisible(Fragment fragment) {
Activity activity = fragment.getActivity();
View focusedView = fragment.getView().findFocus();
return activity != null
&& focusedView != null
&& focusedView == activity.getWindow().getDecorView().findFocus();
}
I had the same issue. ViewPager executes other fragment life cycle events and I could not change that behavior. I wrote a simple pager using fragments and available animations.
SimplePager
I used this and it worked !
mContext.getWindow().getDecorView().isShown() //boolean
I support SectionsPagerAdapter with child fragments so after a lot of headache I finally got working version based on solutions from this topic:
public abstract class BaseFragment extends Fragment {
private boolean visible;
private boolean visibilityHintChanged;
/**
* Called when the visibility of the fragment changed
*/
protected void onVisibilityChanged(View view, boolean visible) {
}
private void triggerVisibilityChangedIfNeeded(boolean visible) {
if (this.visible == visible || getActivity() == null || getView() == null) {
return;
}
this.visible = visible;
onVisibilityChanged(getView(), visible);
}
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (!visibilityHintChanged) {
setUserVisibleHint(false);
}
}
#Override
public void onResume() {
super.onResume();
if (getUserVisibleHint() && !isHidden()) {
triggerVisibilityChangedIfNeeded(true);
}
}
#Override
public void onHiddenChanged(boolean hidden) {
super.onHiddenChanged(hidden);
triggerVisibilityChangedIfNeeded(!hidden);
}
#Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
visibilityHintChanged = true;
if (isVisibleToUser && isResumed() && !isHidden()) {
triggerVisibilityChangedIfNeeded(true);
} else if (!isVisibleToUser) {
triggerVisibilityChangedIfNeeded(false);
}
}
#Override
public void onPause() {
super.onPause();
triggerVisibilityChangedIfNeeded(false);
}
#Override
public void onStop() {
super.onStop();
triggerVisibilityChangedIfNeeded(false);
}
protected boolean isReallyVisible() {
return visible;
}
}
Note that setUserVisibleHint(false) is not called on activity / fragment stop. You'll still need to check start/stop to properly register/unregister any listeners/etc.
Also, you'll get setUserVisibleHint(false) if your fragment starts in a non-visible state; you don't want to unregister there since you've never registered before in that case.
#Override
public void onStart() {
super.onStart();
if (getUserVisibleHint()) {
// register
}
}
#Override
public void onStop() {
if (getUserVisibleHint()) {
// unregister
}
super.onStop();
}
#Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (isVisibleToUser && isResumed()) {
// register
if (!mHasBeenVisible) {
mHasBeenVisible = true;
}
} else if (mHasBeenVisible){
// unregister
}
}
I encountered this problem when I was trying to get a timer to fire when the fragment in the viewpager was on-screen for the user to see.
The timer always started just before the fragment was seen by the user.
This is because the onResume() method in the fragment is called before we can see the fragment.
My solution was to do a check in the onResume() method. I wanted to call a certain method 'foo()' when fragment 8 was the view pagers current fragment.
#Override
public void onResume() {
super.onResume();
if(viewPager.getCurrentItem() == 8){
foo();
//Your code here. Executed when fragment is seen by user.
}
}
Hope this helps. I've seen this problem pop up a lot. This seems to be the simplest solution I've seen. A lot of others are not compatible with lower APIs etc.
A simple way of implementing that is checking whether user is logged in before going to the fragment.
In your MainActivity you may do something like this inside the onNavigationItemSelected method.
case R.id.nav_profile_side:
if (User_is_logged_in) {
fragmentManager.beginTransaction()
.replace(R.id.content_frame
, new FragmentProfile())
.commit();
}else {
ShowLoginOrRegisterDialog(fragmentManager);
}
break;
However, if you are using navigation drawer, the selection in the drawer will have changed to Profile though we have not gone to the ProfileFragment.
To reset the selection to the current selection run the code below
navigationView.getMenu().getItem(0).setChecked(true);
May be very late. This is working for me. I slightly updated the code from #Gobar and #kris Solutions. We have to update the code in our PagerAdapter.
setPrimaryItem is called every time when a tab is visible and returns its position. If the position are same means we are unmoved. If position changed and current position is not our clicked tab set as -1.
private int mCurrentPosition = -1;
#Override
public void setPrimaryItem(#NotNull ViewGroup container, int position, #NotNull Object object) {
// This is what calls setMenuVisibility() on the fragments
super.setPrimaryItem(container, position, object);
if (position == mCurrentPosition) {
return;
}
if (object instanceof YourFragment) {
YourFragment fragment = (YourFragment) object;
if (fragment.isResumed()) {
mCurrentPosition = position;
fragment.doYourWork();//Update your function
}
} else {
mCurrentPosition = -1;
}
}
in Kotlin
override fun onHiddenChanged(hidden: Boolean) {
super.onHiddenChanged(hidden)
// Your code goes here..
}
I overrode the Count method of the associated FragmentStatePagerAdapter and have it return the total count minus the number of pages to hide:
public class MyAdapter : Android.Support.V13.App.FragmentStatePagerAdapter
{
private List<Fragment> _fragments;
public int TrimmedPages { get; set; }
public MyAdapter(Android.App.FragmentManager fm) : base(fm) { }
public MyAdapter(Android.App.FragmentManager fm, List<Android.App.Fragment> fragments) : base(fm)
{
_fragments = fragments;
TrimmedPages = 0;
}
public override int Count
{
//get { return _fragments.Count; }
get { return _fragments.Count - TrimmedPages; }
}
}
So, if there are 3 fragments initially added to the ViewPager, and only the first 2 should be shown until some condition is met, override the page count by setting TrimmedPages to 1 and it should only show the first two pages.
This works good for pages on the end, but wont really help for ones on the beginning or middle (though there are plenty of ways of doing this).
Thats a big problem for me right now because i need to call a method from an interface
all my fragments in my viewpager are implementing. I need to do something like this:
#Override
public void onPageSelected(int position) {
this.getActivity().getActionBar().setSelectedNavigationItem(position);
FragmentVisible fragment = (FragmentVisible) this.fragmentPager.instantiateItem(this.viewPager, position);
if (fragment != null) {
fragment.fragmentBecameVisible();
}
}
This works for the "normal startup" but when i rotate the screen i get nullpointer exceptions
because onPageSelected gets called before onViewCreated. I need my views to get updated everytime
a fragment gets visible. First i hoped onResume would get called everytime but it doesnt. For that
i implemented the interface:
public interface FragmentVisible {
public void fragmentBecameVisible();
}
Does someone has an idea how to solve this?
Per the FragmentPagerAdapter's setPrimaryItem() method (called when the ViewPager sets the current page), it calls setUserVisibleHint(true) for the current page's fragment. You can override that method in your Fragment and do your fragmentBecameVisible() method in there.
I'm using the support library v4 and my questions are, How to know if a Fragment is Visible? and How can I change the propierties of the Layout inflated in the Fragment?
I'm using fragments like in the android developers tutorial with a FragmentActivity.
You should be able to do the following:
MyFragmentClass test = (MyFragmentClass) getSupportFragmentManager().findFragmentByTag("testID");
if (test != null && test.isVisible()) {
//DO STUFF
}
else {
//Whatever
}
Both isVisible() and isAdded() return true as soon as the Fragment is created, and not even actually visible. The only solution that actually works is:
if (isAdded() && isVisible() && getUserVisibleHint()) {
// ... do your thing
}
This does the job. Period.
NOTICE:
getUserVisibleHint() is now deprecated. be careful.
If you want to know when use is looking at the fragment you should use
yourFragment.isResumed()
instead of
yourFragment.isVisible()
First of all isVisible() already checks for isAdded() so no need for calling both. Second, non-of these two means that user is actually seeing your fragment. Only isResumed() makes sure that your fragment is in front of the user and user can interact with it if thats whats you are looking for.
you can try this way:
Fragment currentFragment = getFragmentManager().findFragmentById(R.id.fragment_container);
or
Fragment currentFragment = getSupportFragmentManager().findFragmentById(R.id.fragment_container);
In this if, you check if currentFragment is instance of YourFragment
if (currentFragment instanceof YourFragment) {
Log.v(TAG, "your Fragment is Visible");
}
You can override setMenuVisibility like this:
#Override
public void setMenuVisibility(final boolean visible) {
if (visible) {
//Do your stuff here
}
super.setMenuVisibility(visible);
}
getUserVisibleHint() comes as true only when the fragment is on the view and visible
One thing to be aware of, is that isVisible() returns the visible state of the current fragment. There is a problem in the support library, where if you have nested fragments, and you hide the parent fragment (and therefore all the children), the child still says it is visible.
isVisible() is final, so can't override unfortunately. My workaround was to create a BaseFragment class that all my fragments extend, and then create a method like so:
public boolean getIsVisible()
{
if (getParentFragment() != null && getParentFragment() instanceof BaseFragment)
{
return isVisible() && ((BaseFragment) getParentFragment()).getIsVisible();
}
else
{
return isVisible();
}
}
I do isVisible() && ((BaseFragment) getParentFragment()).getIsVisible(); because we want to return false if any of the parent fragments are hidden.
This seems to do the trick for me.
ArticleFragment articleFrag = (ArticleFragment)
getSupportFragmentManager().findFragmentById(R.id.article_fragment);
if (articleFrag != null && articleFrag.isVisible()) {
// Call a method in the ArticleFragment to update its content
articleFrag.updateArticleView(position);
}
see http://developer.android.com/training/basics/fragments/communicating.html
Just in case you use a Fragment layout with a ViewPager (TabLayout), you can easily ask for the current (in front) fragment by ViewPager.getCurrentItem() method. It will give you the page index.
Mapping from page index to fragment[class] should be easy as you did the mapping in your FragmentPagerAdapter derived Adapter already.
int i = pager.getCurrentItem();
You may register for page change notifications by
ViewPager pager = (ViewPager) findViewById(R.id.container);
pager.addOnPageChangeListener(this);
Of course you must implement interface ViewPager.OnPageChangeListener
public class MainActivity
extends AppCompatActivity
implements ViewPager.OnPageChangeListener
{
public void onPageSelected (int position)
{
// we get notified here when user scrolls/switches Fragment in ViewPager -- so
// we know which one is in front.
Toast toast = Toast.makeText(this, "current page " + String.valueOf(position), Toast.LENGTH_LONG);
toast.show();
}
public void onPageScrolled (int position, float positionOffset, int positionOffsetPixels) {
}
public void onPageScrollStateChanged (int state) {
}
}
My answer here might be a little off the question. But as a newbie to Android Apps I was just facing exactly this problem and did not find an answer anywhere. So worked out above solution and posting it here -- perhaps someone finds it useful.
Edit: You might combine this method with LiveData on which the fragments subscribe. Further on, if you give your Fragments a page index as constructor argument, you can make a simple amIvisible() function in your fragment class.
In MainActivity:
private final MutableLiveData<Integer> current_page_ld = new MutableLiveData<>();
public LiveData<Integer> getCurrentPageIdx() { return current_page_ld; }
public void onPageSelected(int position) {
current_page_ld.setValue(position);
}
public class MyPagerAdapter extends FragmentPagerAdapter
{
public Fragment getItem(int position) {
// getItem is called to instantiate the fragment for the given page: But only on first
// creation -- not on restore state !!!
// see: https://stackoverflow.com/a/35677363/3290848
switch (position) {
case 0:
return MyFragment.newInstance(0);
case 1:
return OtherFragment.newInstance(1);
case 2:
return XYFragment.newInstance(2);
}
return null;
}
}
In Fragment:
public static MyFragment newInstance(int index) {
MyFragment fragment = new MyFragment();
Bundle args = new Bundle();
args.putInt("idx", index);
fragment.setArguments(args);
return fragment;
}
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
mPageIndex = getArguments().getInt(ARG_PARAM1);
}
...
}
public void onAttach(Context context)
{
super.onAttach(context);
MyActivity mActivity = (MyActivity)context;
mActivity.getCurrentPageIdx().observe(this, new Observer<Integer>() {
#Override
public void onChanged(Integer data) {
if (data == mPageIndex) {
// have focus
} else {
// not in front
}
}
});
}
Try this if you have only one Fragment
if (getSupportFragmentManager().getBackStackEntryCount() == 0) {
//TODO: Your Code Here
}
Adding some information here that I experienced:
fragment.isVisible is only working (true/false) when you replaceFragment() otherwise if you work with addFragment(), isVisible always returns true whether the fragment is in behind of some other fragment.
None of the above solutions worked for me.
The following however works like a charm:-
override fun setUserVisibleHint(isVisibleToUser: Boolean)
getUserVisibleHint is now deprecated, and I was having problems with isVisible being true when another fragment was added in front of it. This detects the fragment's visibility on the back stack using its view. This may be helpful if your issue is related to other fragments on the back stack.
View extension to detect if a view is being displayed on the screen: (see also How can you tell if a View is visible on screen in Android?)
fun View.isVisibleOnScreen(): Boolean {
if (!isShown) return false
val actualPosition = Rect().also { getGlobalVisibleRect(it) }
val screenWidth = Resources.getSystem().displayMetrics.widthPixels
val screenHeight = Resources.getSystem().displayMetrics.heightPixels
val screen = Rect(0, 0, screenWidth, screenHeight)
return Rect.intersects(actualPosition, screen)
}
Then defined a back stack listener from the fragment, watching the top fragment on the stack (the one added last)
fun Fragment.setOnFragmentStackVisibilityListener(onVisible: () -> Unit) {
val renderDelayMillis = 300L
parentFragmentManager.addOnBackStackChangedListener {
Handler(Looper.getMainLooper()).postDelayed({
if (isAdded) {
val topStackFragment = parentFragmentManager.fragments[parentFragmentManager.fragments.size - 1]
if (topStackFragment.view == view && isVisible && view!!.isVisibleOnScreen()) {
onVisible.invoke()
}
}
}, renderDelayMillis)
}
}
The back stack listener is called before the view is ready so an arbitrarily small delay was needed. The lambda is called when the view becomes visible.
I was using Android's BottomNavigationView and managing fragments with FragmentTransactions.hide(frag) and FragmentTransaction.show(frag). So, to detect if a fragment is visible or not, I used following:
abstract class BaseFragment : Fragment() {
open fun onFragmentVisible(){
}
override fun onStart() {
super.onStart()
if (!isHidden){
onFragmentVisible()
}
}
override fun onHiddenChanged(hidden: Boolean) {
super.onHiddenChanged(hidden)
if (!hidden){
onFragmentVisible()
}
}
}
You can extend BaseFragment in your fragment and implement it's onFragmentVisible function.
In Kotlin
if you use FragmentPagerAdapter and since getUserVisibleHint() is deprecated in api 29, I suggest you to add behaviour parameter BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT in your FragmentPagerAdapter like this:
FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT)
then in your fragment you can check using their lifecycle state:
if(lifecycle.currentState == Lifecycle.State.RESUMED) {
// do something when fragment is visible
}
Problem: Fragment onResume() in ViewPager is fired before the fragment becomes actually visible.
For example, I have 2 fragments with ViewPager and FragmentPagerAdapter. The second fragment is only available for authorized users and I need to ask the user to log in when the fragment becomes visible (using an alert dialog).
BUT the ViewPager creates the second fragment when the first is visible in order to cache the second fragment and makes it visible when the user starts swiping.
So the onResume() event is fired in the second fragment long before it becomes visible. That's why I'm trying to find an event which fires when the second fragment becomes visible to show a dialog at the appropriate moment.
How can this be done?
How to determine when Fragment becomes visible in ViewPager
You can do the following by overriding setUserVisibleHint in your Fragment:
public class MyFragment extends Fragment {
#Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (isVisibleToUser) {
}
else {
}
}
}
UPDATE: Android Support Library (rev 11) finally fixed the user visible hint issue, now if you use support library for fragments, then you can safely use getUserVisibleHint() or override setUserVisibleHint() to capture the changes as described by gorn's answer.
UPDATE 1 Here is one small problem with getUserVisibleHint(). This value is by default true.
// Hint provided by the app that this fragment is currently visible to the user.
boolean mUserVisibleHint = true;
So there might be a problem when you try to use it before setUserVisibleHint() was invoked. As a workaround you might set value in onCreate method like this.
public void onCreate(#Nullable Bundle savedInstanceState) {
setUserVisibleHint(false);
The outdated answer:
In most use cases, ViewPager only show one page at a time, but the pre-cached fragments are also put to "visible" state (actually invisible) if you are using FragmentStatePagerAdapter in Android Support Library pre-r11.
I override :
public class MyFragment extends Fragment {
#Override
public void setMenuVisibility(final boolean visible) {
super.setMenuVisibility(visible);
if (visible) {
// ...
}
}
// ...
}
To capture the focus state of fragment, which I think is the most suitable state of the "visibility" you mean, since only one fragment in ViewPager can actually place its menu items together with parent activity's items.
This seems to restore the normal onResume() behavior that you would expect. It plays well with pressing the home key to leave the app and then re-entering the app. onResume() is not called twice in a row.
#Override
public void setUserVisibleHint(boolean visible)
{
super.setUserVisibleHint(visible);
if (visible && isResumed())
{
//Only manually call onResume if fragment is already visible
//Otherwise allow natural fragment lifecycle to call onResume
onResume();
}
}
#Override
public void onResume()
{
super.onResume();
if (!getUserVisibleHint())
{
return;
}
//INSERT CUSTOM CODE HERE
}
Here is another way using onPageChangeListener:
ViewPager pager = (ViewPager) findByViewId(R.id.viewpager);
FragmentPagerAdapter adapter = new FragmentPageAdapter(getFragmentManager);
pager.setAdapter(adapter);
pager.setOnPageChangeListener(new OnPageChangeListener() {
public void onPageSelected(int pageNumber) {
// Just define a callback method in your fragment and call it like this!
adapter.getItem(pageNumber).imVisible();
}
public void onPageScrolled(int arg0, float arg1, int arg2) {
// TODO Auto-generated method stub
}
public void onPageScrollStateChanged(int arg0) {
// TODO Auto-generated method stub
}
});
In ViewPager2 and ViewPager from version androidx.fragment:fragment:1.1.0 you can just use onPause and onResume callbacks to determine which fragment is currently visible for the user. onResume callback is called when fragment became visible and onPause when it stops to be visible.
In case of ViewPager2 it is default behavior but the same behavior can be enabled for old good ViewPager easily.
To enable this behavior in the first ViewPager you have to pass FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT parameter as second argument of FragmentPagerAdapter constructor.
FragmentPagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT)
Note: setUserVisibleHint() method and FragmentPagerAdapter constructor with one parameter are now deprecated in the new version of Fragment from android jetpack.
setUserVisibleHint() gets called sometimes before onCreateView() and sometimes after which causes trouble.
To overcome this you need to check isResumed() as well inside setUserVisibleHint() method. But in this case i realized setUserVisibleHint() gets called only if Fragment is resumed and visible, NOT when Created.
So if you want to update something when Fragment is visible, put your update function both in onCreate() and setUserVisibleHint():
#Override
public View onCreateView(...){
...
myUIUpdate();
...
}
....
#Override
public void setUserVisibleHint(boolean visible){
super.setUserVisibleHint(visible);
if (visible && isResumed()){
myUIUpdate();
}
}
UPDATE: Still i realized myUIUpdate() gets called twice sometimes, the reason is, if you have 3 tabs and this code is on 2nd tab, when you first open 1st tab, the 2nd tab is also created even it is not visible and myUIUpdate() is called. Then when you swipe to 2nd tab, myUIUpdate() from if (visible && isResumed()) is called and as a result,myUIUpdate() may get called twice in a second.
The other problem is !visible in setUserVisibleHint gets called both 1) when you go out of fragment screen and 2) before it is created, when you switch to fragment screen first time.
Solution:
private boolean fragmentResume=false;
private boolean fragmentVisible=false;
private boolean fragmentOnCreated=false;
...
#Override
public View onCreateView(...){
...
//Initialize variables
if (!fragmentResume && fragmentVisible){ //only when first time fragment is created
myUIUpdate();
}
...
}
#Override
public void setUserVisibleHint(boolean visible){
super.setUserVisibleHint(visible);
if (visible && isResumed()){ // only at fragment screen is resumed
fragmentResume=true;
fragmentVisible=false;
fragmentOnCreated=true;
myUIUpdate();
}else if (visible){ // only at fragment onCreated
fragmentResume=false;
fragmentVisible=true;
fragmentOnCreated=true;
}
else if(!visible && fragmentOnCreated){// only when you go out of fragment screen
fragmentVisible=false;
fragmentResume=false;
}
}
Explanation:
fragmentResume,fragmentVisible: Makes sure myUIUpdate() in onCreateView() is called only when fragment is created and visible, not on resume. It also solves problem when you are at 1st tab, 2nd tab is created even if it is not visible. This solves that and checks if fragment screen is visible when onCreate.
fragmentOnCreated: Makes sure fragment is not visible, and not called when you create fragment first time. So now this if clause only gets called when you swipe out of fragment.
Update
You can put all this code in BaseFragment code like this and override method.
New
ViewPager2 + FragmentStateAdapter + onResume() (in Fragment)
solve the problem
Old Answer (deprecated)
To detect Fragment in ViewPager visible, I'm quite sure that only using setUserVisibleHint is not enough.
Here is my solution to check if a fragment is visible or invisible. First when launching viewpager, switch between page, go to another activity/fragment/ background/foreground`
public class BaseFragmentHelpLoadDataWhenVisible extends Fragment {
protected boolean mIsVisibleToUser; // you can see this variable may absolutely <=> getUserVisibleHint() but it not. Currently, after many test I find that
/**
* This method will be called when viewpager creates fragment and when we go to this fragment background or another activity or fragment
* NOT called when we switch between each page in ViewPager
*/
#Override
public void onStart() {
super.onStart();
if (mIsVisibleToUser) {
onVisible();
}
}
#Override
public void onStop() {
super.onStop();
if (mIsVisibleToUser) {
onInVisible();
}
}
/**
* This method will called at first time viewpager created and when we switch between each page
* NOT called when we go to background or another activity (fragment) when we go back
*/
#Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
mIsVisibleToUser = isVisibleToUser;
if (isResumed()) { // fragment have created
if (mIsVisibleToUser) {
onVisible();
} else {
onInVisible();
}
}
}
public void onVisible() {
Toast.makeText(getActivity(), TAG + "visible", Toast.LENGTH_SHORT).show();
}
public void onInVisible() {
Toast.makeText(getActivity(), TAG + "invisible", Toast.LENGTH_SHORT).show();
}
}
EXPLANATION
You can check the logcat below carefully then I think you may know why this solution will work
First launch
Fragment1: setUserVisibleHint: isVisibleToUser=false isResumed=false
Fragment2: setUserVisibleHint: isVisibleToUser=false isResumed=false
Fragment3: setUserVisibleHint: isVisibleToUser=false isResumed=false
Fragment1: setUserVisibleHint: isVisibleToUser=true isResumed=false // AT THIS TIME isVisibleToUser=true but fragment still not created. If you do something with View here, you will receive exception
Fragment1: onCreateView
Fragment1: onStart mIsVisibleToUser=true
Fragment2: onCreateView
Fragment3: onCreateView
Fragment2: onStart mIsVisibleToUser=false
Fragment3: onStart mIsVisibleToUser=false
Go to page2
Fragment1: setUserVisibleHint: isVisibleToUser=false isResumed=true
Fragment2: setUserVisibleHint: isVisibleToUser=true isResumed=true
Go to page3
Fragment2: setUserVisibleHint: isVisibleToUser=false isResumed=true
Fragment3: setUserVisibleHint: isVisibleToUser=true isResumed=true
Go to background:
Fragment1: onStop mIsVisibleToUser=false
Fragment2: onStop mIsVisibleToUser=false
Fragment3: onStop mIsVisibleToUser=true
Go to foreground
Fragment1: onStart mIsVisibleToUser=false
Fragment2: onStart mIsVisibleToUser=false
Fragment3: onStart mIsVisibleToUser=true
DEMO project here
Hope it help
package com.example.com.ui.fragment;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.example.com.R;
public class SubscribeFragment extends Fragment {
#Override
public View onCreateView(LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_subscribe, container, false);
return view;
}
#Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (isVisibleToUser) {
// called here
}
}
#Override
public void onViewCreated(View view, #Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
}
}
Override setPrimaryItem() in the FragmentPagerAdapter subclass. I use this method, and it works well.
#Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
// This is what calls setMenuVisibility() on the fragments
super.setPrimaryItem(container, position, object);
if (object instanceof MyWhizBangFragment) {
MyWhizBangFragment fragment = (MyWhizBangFragment) object;
fragment.doTheThingYouNeedToDoOnBecomingVisible();
}
}
Override Fragment.onHiddenChanged() for that.
public void onHiddenChanged(boolean hidden)
Called when the hidden state (as returned by isHidden()) of the fragment has changed. Fragments start out not hidden; this will be called whenever the fragment changes state from that.
Parameters
hidden - boolean: True if the fragment is now hidden, false if it is not visible.
Only this worked for me!! and setUserVisibleHint(...) is now deprecated (I attached docs at end), which means most of other answers are deprecated ;-)
public class FragmentFirewall extends Fragment {
/**
* Required cause "setMenuVisibility(...)" is not guaranteed to be
* called after "onResume()" and/or "onCreateView(...)" method.
*/
protected void didVisibilityChange() {
Activity activity = getActivity();
if (isResumed() && isMenuVisible()) {
// Once resumed and menu is visible, at last
// our Fragment is really visible to user.
}
}
#Override
public void onResume() {
super.onResume();
didVisibilityChange();
}
#Override
public void setMenuVisibility(boolean visible) {
super.setMenuVisibility(visible);
didVisibilityChange();
}
}
Tested and works with NaviagationDrawer as well,
there isMenuVisible() will always return true (and onResume() seems enough, but we want to support ViewPager too).
setUserVisibleHint is deprecated. If overriding this method, behavior implemented when passing in true should be moved to Fragment.onResume(), and behavior implemented when passing in false should be moved to Fragment.onPause().
setUserVisibleHint(boolean visible) is now deprecated So this is the correct solution
FragmentPagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT)
In ViewPager2 and ViewPager from version androidx.fragment:fragment:1.1.0 you can just use onPause() and onResume() to determine which fragment is currently visible for the user. onResume() is called when the fragment became visible and onPause when it stops to be visible.
To enable this behavior in the first ViewPager you have to pass FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT parameter as the second argument of the FragmentPagerAdapter constructor.
I figured out that onCreateOptionsMenu and onPrepareOptionsMenu methods called only in the case of the fragment really visible. I could not found any method which behaves like these, also I tried OnPageChangeListener but it did not work for the situations, for example, I need a variable initialized in onCreate method.
So these two methods can be used for this problem as a workaround, specifically for little and short jobs.
I think, this is the better solution but not the best. I will use this but wait for better solution at the same time.
Regards.
Another solution posted here overriding setPrimaryItem in the pageradapter by kris larson almost worked for me. But this method is called multiple times for each setup. Also I got NPE from views, etc. in the fragment as this is not ready the first few times this method is called. With the following changes this worked for me:
private int mCurrentPosition = -1;
#Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
super.setPrimaryItem(container, position, object);
if (position == mCurrentPosition) {
return;
}
if (object instanceof MyWhizBangFragment) {
MyWhizBangFragment fragment = (MyWhizBangFragment) object;
if (fragment.isResumed()) {
mCurrentPosition = position;
fragment.doTheThingYouNeedToDoOnBecomingVisible();
}
}
}
Add following Code inside fragment
#Override
public void setMenuVisibility(final boolean visible)
{
super.setMenuVisibility(visible);
if (visible && isResumed())
{
}
}
I encountered the same problem while working with FragmentStatePagerAdapters and 3 tabs. I had to show a Dilaog whenever the 1st tab was clicked and hide it on clicking other tabs.
Overriding setUserVisibleHint() alone didn't help to find the current visible fragment.
When clicking from 3rd tab -----> 1st tab.
It triggered twice for 2nd fragment and for 1st fragment.
I combined it with isResumed() method.
#Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
isVisible = isVisibleToUser;
// Make sure that fragment is currently visible
if (!isVisible && isResumed()) {
// Call code when Fragment not visible
} else if (isVisible && isResumed()) {
// Call code when Fragment becomes visible.
}
}
We have a special case with MVP where the fragment needs to notify the presenter that the view has become visible, and the presenter is injected by Dagger in fragment.onAttach().
setUserVisibleHint() is not enough, we've detected 3 different cases that needed to be addressed (onAttach() is mentioned so that you know when the presenter is available):
Fragment has just been created. The system makes the following calls:
setUserVisibleHint() // before fragment's lifecycle calls, so presenter is null
onAttach()
...
onResume()
Fragment already created and home button is pressed. When restoring the app to foreground, this is called:
onResume()
Orientation change:
onAttach() // presenter available
onResume()
setUserVisibleHint()
We only want the visibility hint to get to the presenter once, so this is how we do it:
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.fragment_list, container, false);
setHasOptionsMenu(true);
if (savedInstanceState != null) {
lastOrientation = savedInstanceState.getInt(STATE_LAST_ORIENTATION,
getResources().getConfiguration().orientation);
} else {
lastOrientation = getResources().getConfiguration().orientation;
}
return root;
}
#Override
public void onResume() {
super.onResume();
presenter.onResume();
int orientation = getResources().getConfiguration().orientation;
if (orientation == lastOrientation) {
if (getUserVisibleHint()) {
presenter.onViewBecomesVisible();
}
}
lastOrientation = orientation;
}
#Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (presenter != null && isResumed() && isVisibleToUser) {
presenter.onViewBecomesVisible();
}
}
#Override public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(STATE_LAST_ORIENTATION, lastOrientation);
}
Detecting by focused view!
This works for me
public static boolean isFragmentVisible(Fragment fragment) {
Activity activity = fragment.getActivity();
View focusedView = fragment.getView().findFocus();
return activity != null
&& focusedView != null
&& focusedView == activity.getWindow().getDecorView().findFocus();
}
I had the same issue. ViewPager executes other fragment life cycle events and I could not change that behavior. I wrote a simple pager using fragments and available animations.
SimplePager
I used this and it worked !
mContext.getWindow().getDecorView().isShown() //boolean
I support SectionsPagerAdapter with child fragments so after a lot of headache I finally got working version based on solutions from this topic:
public abstract class BaseFragment extends Fragment {
private boolean visible;
private boolean visibilityHintChanged;
/**
* Called when the visibility of the fragment changed
*/
protected void onVisibilityChanged(View view, boolean visible) {
}
private void triggerVisibilityChangedIfNeeded(boolean visible) {
if (this.visible == visible || getActivity() == null || getView() == null) {
return;
}
this.visible = visible;
onVisibilityChanged(getView(), visible);
}
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (!visibilityHintChanged) {
setUserVisibleHint(false);
}
}
#Override
public void onResume() {
super.onResume();
if (getUserVisibleHint() && !isHidden()) {
triggerVisibilityChangedIfNeeded(true);
}
}
#Override
public void onHiddenChanged(boolean hidden) {
super.onHiddenChanged(hidden);
triggerVisibilityChangedIfNeeded(!hidden);
}
#Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
visibilityHintChanged = true;
if (isVisibleToUser && isResumed() && !isHidden()) {
triggerVisibilityChangedIfNeeded(true);
} else if (!isVisibleToUser) {
triggerVisibilityChangedIfNeeded(false);
}
}
#Override
public void onPause() {
super.onPause();
triggerVisibilityChangedIfNeeded(false);
}
#Override
public void onStop() {
super.onStop();
triggerVisibilityChangedIfNeeded(false);
}
protected boolean isReallyVisible() {
return visible;
}
}
Note that setUserVisibleHint(false) is not called on activity / fragment stop. You'll still need to check start/stop to properly register/unregister any listeners/etc.
Also, you'll get setUserVisibleHint(false) if your fragment starts in a non-visible state; you don't want to unregister there since you've never registered before in that case.
#Override
public void onStart() {
super.onStart();
if (getUserVisibleHint()) {
// register
}
}
#Override
public void onStop() {
if (getUserVisibleHint()) {
// unregister
}
super.onStop();
}
#Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (isVisibleToUser && isResumed()) {
// register
if (!mHasBeenVisible) {
mHasBeenVisible = true;
}
} else if (mHasBeenVisible){
// unregister
}
}
I encountered this problem when I was trying to get a timer to fire when the fragment in the viewpager was on-screen for the user to see.
The timer always started just before the fragment was seen by the user.
This is because the onResume() method in the fragment is called before we can see the fragment.
My solution was to do a check in the onResume() method. I wanted to call a certain method 'foo()' when fragment 8 was the view pagers current fragment.
#Override
public void onResume() {
super.onResume();
if(viewPager.getCurrentItem() == 8){
foo();
//Your code here. Executed when fragment is seen by user.
}
}
Hope this helps. I've seen this problem pop up a lot. This seems to be the simplest solution I've seen. A lot of others are not compatible with lower APIs etc.
A simple way of implementing that is checking whether user is logged in before going to the fragment.
In your MainActivity you may do something like this inside the onNavigationItemSelected method.
case R.id.nav_profile_side:
if (User_is_logged_in) {
fragmentManager.beginTransaction()
.replace(R.id.content_frame
, new FragmentProfile())
.commit();
}else {
ShowLoginOrRegisterDialog(fragmentManager);
}
break;
However, if you are using navigation drawer, the selection in the drawer will have changed to Profile though we have not gone to the ProfileFragment.
To reset the selection to the current selection run the code below
navigationView.getMenu().getItem(0).setChecked(true);
May be very late. This is working for me. I slightly updated the code from #Gobar and #kris Solutions. We have to update the code in our PagerAdapter.
setPrimaryItem is called every time when a tab is visible and returns its position. If the position are same means we are unmoved. If position changed and current position is not our clicked tab set as -1.
private int mCurrentPosition = -1;
#Override
public void setPrimaryItem(#NotNull ViewGroup container, int position, #NotNull Object object) {
// This is what calls setMenuVisibility() on the fragments
super.setPrimaryItem(container, position, object);
if (position == mCurrentPosition) {
return;
}
if (object instanceof YourFragment) {
YourFragment fragment = (YourFragment) object;
if (fragment.isResumed()) {
mCurrentPosition = position;
fragment.doYourWork();//Update your function
}
} else {
mCurrentPosition = -1;
}
}
in Kotlin
override fun onHiddenChanged(hidden: Boolean) {
super.onHiddenChanged(hidden)
// Your code goes here..
}
I overrode the Count method of the associated FragmentStatePagerAdapter and have it return the total count minus the number of pages to hide:
public class MyAdapter : Android.Support.V13.App.FragmentStatePagerAdapter
{
private List<Fragment> _fragments;
public int TrimmedPages { get; set; }
public MyAdapter(Android.App.FragmentManager fm) : base(fm) { }
public MyAdapter(Android.App.FragmentManager fm, List<Android.App.Fragment> fragments) : base(fm)
{
_fragments = fragments;
TrimmedPages = 0;
}
public override int Count
{
//get { return _fragments.Count; }
get { return _fragments.Count - TrimmedPages; }
}
}
So, if there are 3 fragments initially added to the ViewPager, and only the first 2 should be shown until some condition is met, override the page count by setting TrimmedPages to 1 and it should only show the first two pages.
This works good for pages on the end, but wont really help for ones on the beginning or middle (though there are plenty of ways of doing this).