web-dev-qa-db-fra.com

Comment déterminer quand Fragment devient visible dans ViewPager

Problème: Le fragment onResume() dans ViewPager est déclenché avant que le fragment ne devienne réellement visible.

Par exemple, j'ai 2 fragments avec ViewPager et FragmentPagerAdapter. Le deuxième fragment est uniquement disponible pour les utilisateurs autorisés et je dois demander à l'utilisateur de se connecter lorsque le fragment devient visible (à l'aide d'une boîte de dialogue d'alerte).

MAIS la ViewPager crée le deuxième fragment lorsque le premier est visible afin de mettre en cache le deuxième fragment et le rend visible lorsque l'utilisateur commence à balayer.

Ainsi, l'événement onResume() est déclenché dans le deuxième fragment bien avant qu'il ne devienne visible. C'est pourquoi j'essaie de trouver un événement qui se déclenche lorsque le deuxième fragment devient visible pour afficher une boîte de dialogue au moment opportun.

Comment cela peut-il être fait?

671
4ntoine

UPDATE: bibliothèque de support Android (rév. 11) enfin correction du problème de l'indicateur visible par l'utilisateur , si vous utilisez la bibliothèque de support pour les fragments, vous pouvez utiliser en toute sécurité getUserVisibleHint() ou remplacer setUserVisibleHint() pour capturer les modifications décrit par la réponse de Gorn.

UPDATE 1 Voici un petit problème avec getUserVisibleHint(). Cette valeur est par défaut true.

// Hint provided by the app that this fragment is currently visible to the user.
boolean mUserVisibleHint = true;

Il peut donc y avoir un problème lorsque vous essayez de l'utiliser avant que setUserVisibleHint() ait été appelé. Pour résoudre ce problème, vous pouvez définir une valeur dans la méthode onCreate comme ceci.

public void onCreate(@Nullable Bundle savedInstanceState) {
    setUserVisibleHint(false);

La réponse obsolète:

Dans la plupart des cas d'utilisation, ViewPager n'affiche qu'une page à la fois, mais les fragments pré-cachés passent également à l'état "visible" (réellement invisible) si vous utilisez FragmentStatePagerAdapter dans Android Support Library pre-r11.

Je remplace:

public class MyFragment extends Fragment {
    @Override
    public void setMenuVisibility(final boolean visible) {
        super.setMenuVisibility(visible);
        if (visible) {
            // ...
        }
    }
   // ...
}

Pour capturer l’état de focalisation du fragment, qui est selon moi l’état le plus approprié de la "visibilité" que vous voulez dire, puisqu'un seul fragment dans ViewPager peut réellement placer ses éléments de menu avec les éléments de l’activité parent.

493
Oasis Feng

Comment déterminer quand Fragment devient visible dans ViewPager

Vous pouvez effectuer les opérations suivantes en remplaçant setUserVisibleHint dans votre Fragment:

public class MyFragment extends Fragment {
    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        if (isVisibleToUser) {
        }
        else {
        }
    }
}
541
gorn

Cela semble rétablir le comportement onResume() normal auquel vous vous attendez. Il joue bien en appuyant sur la touche d'accueil pour quitter l'application, puis en entrant à nouveau dans l'application. onResume() n'est pas appelé deux fois de suite.

@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
}
121
craigrs84

Voici un autre moyen d'utiliser 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

  }
});
59
Ostkontentitan

setUserVisibleHint() est appelé parfois beforeonCreateView() et parfois après ce qui cause des problèmes. 

Pour résoudre ce problème, vous devez également vérifier isResumed() dans la méthode setUserVisibleHint(). Mais dans ce cas, j’ai réalisé que setUserVisibleHint() est appelé seulement si Fragment est repris et visible, PAS lorsqu’il a été créé.

Donc si vous voulez mettre à jour quelque chose quand Fragment vaut visible, mettez votre fonction de mise à jour à la fois dans onCreate() et setUserVisibleHint():

@Override
public View onCreateView(...){
    ...
    myUIUpdate();
    ...        
}
  ....
@Override
public void setUserVisibleHint(boolean visible){
    super.setUserVisibleHint(visible);
    if (visible && isResumed()){
        myUIUpdate();
    }
}

UPDATE: Pourtant, j'ai réalisé que myUIUpdate() est appelé deux fois, la raison en est que si vous avez 3 onglets et que ce code est sur le 2ème onglet, lorsque vous ouvrez le 1er onglet pour la première fois, le 2ème onglet est également créé même s'il n'est pas visible et myUIUpdate() est appelé. Ensuite, lorsque vous passez au 2e onglet, myUIUpdate() à partir de if (visible && isResumed()) est appelé. Par conséquent, myUIUpdate() peut être appelé deux fois par seconde. 

L'autre problème est !visible dans setUserVisibleHint est appelé à la fois 1) lorsque vous sortez de l'écran fragment et 2) avant sa création, lorsque vous passez à l'écran fragment pour la première fois.

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;
    }
}

Explication: 

fragmentResume, fragmentVisible: s'assure que myUIUpdate() dans onCreateView() est appelé uniquement lorsque fragment est créé et visible, et non sur CV. Il résout également le problème lorsque vous êtes au premier onglet, le deuxième onglet est créé même s'il n'est pas visible. Cela résout le problème et vérifie si l’écran de fragment est visible lorsque onCreate.

fragmentOnCreated: s'assure que le fragment n'est pas visible et qu'il n'est pas appelé lorsque vous créez un fragment pour la première fois. Alors maintenant, cette clause if n'est appelée que lorsque vous balayez du fragment.

Update Vous pouvez mettre tout ce code dans le code BaseFragmentcomme ceci et la méthode override.

53
Jemshit Iskenderov
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);
    }
}
24
w3hacker

Pour détecter Fragment dans ViewPager visible, je suis à peu près sûr que utiliser uniquementsetUserVisibleHint n'est pas suffisant.
Voici ma solution pour vérifier si un fragment est visible ou invisible. Tout d'abord, lors du lancement de viewpager, changez de page, allez à une autre activité/fragment/fond/premier plan`

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();
    }
}

EXPLICATION Vous pouvez vérifier le logcat ci-dessous avec soin puis je pense que vous savez peut-être pourquoi cette solution fonctionnera

Premier lancement

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

Aller à la page2

Fragment1: setUserVisibleHint: isVisibleToUser=false isResumed=true
Fragment2: setUserVisibleHint: isVisibleToUser=true isResumed=true

Aller à la page3

Fragment2: setUserVisibleHint: isVisibleToUser=false isResumed=true
Fragment3: setUserVisibleHint: isVisibleToUser=true isResumed=true

Aller à l'arrière-plan:

Fragment1: onStop mIsVisibleToUser=false
Fragment2: onStop mIsVisibleToUser=false
Fragment3: onStop mIsVisibleToUser=true

Aller au premier plan

Fragment1: onStart mIsVisibleToUser=false
Fragment2: onStart mIsVisibleToUser=false
Fragment3: onStart mIsVisibleToUser=true

Projet DEMO ici

J'espère que ça aide

19
Linh

Remplacez setPrimaryItem() dans la sous-classe FragmentPagerAdapter. J'utilise cette méthode et ça marche bien.

@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();
    }
}
14
kris larson

Remplacez Fragment.onHiddenChanged() pour cela.

public void onHiddenChanged(boolean hidden)

Appelé lorsque l'état caché (tel qu'il est renvoyé par isHidden() ) du fragment a changé. Les fragments commencent pas cachés; cela sera appelé chaque fois que le fragment changera d'état.

Paramètres
hidden - boolean: Vrai si le fragment est maintenant caché, faux si il n'est pas visible.

10
dan

J'ai compris que les méthodes onCreateOptionsMenu et onPrepareOptionsMenu n'étaient appelées que dans le cas du fragment vraiment visible. Je ne pouvais trouver aucune méthode qui se comporte de la sorte, j'ai aussi essayé OnPageChangeListener mais cela n'a pas fonctionné pour les situations. Par exemple, j'ai besoin d'une variable initialisée dans la méthode onCreate.

Donc, ces deux méthodes peuvent être utilisées pour résoudre ce problème comme solution de contournement, en particulier pour les travaux petits et courts.

Je pense que c'est la meilleure solution mais pas la meilleure. Je vais l'utiliser mais attendre une meilleure solution en même temps.

Cordialement.

4
ismailarilik

J'ai rencontré le même problème lorsque je travaillais avec FragmentStatePagerAdapters et 3 onglets. Je devais montrer un Dilaog chaque fois que le premier onglet était cliqué et le masquer en cliquant sur d'autres onglets.

Remplacer setUserVisibleHint() seul n'a pas aidé à trouver le fragment visible actuel.

En cliquant sur le 3ème onglet -----> 1er onglet . Il s'est déclenché deux fois pour le 2ème fragment et pour le 1er fragment . Je l'ai combiné avec la méthode isResumed ().

    @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.
    }

}
2
Aman Arora

Une autre solution affichée ici en remplaçant setPrimaryItem dans le pageradapter de kris Larson a presque fonctionné pour moi. Mais cette méthode est appelée plusieurs fois pour chaque configuration. De plus, j'ai obtenu NPE à partir de vues, etc. dans le fragment, car ce n'est pas prêt les premières fois où cette méthode est appelée. Avec les modifications suivantes, cela a fonctionné pour moi:

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();
        }
    }
}
2
Gober

Ajouter le code suivant dans le fragment

@Override
public void setMenuVisibility(final boolean visible) 
 {
    super.setMenuVisibility(visible);
    if (visible && isResumed()) 
     {

     }
}
2
Afzaal Iftikhar

Essayez ceci, c'est un travail pour moi:

@Override
public void onHiddenChanged(boolean hidden) {
        super.onHiddenChanged(hidden);

        if (hidden) {

        }else
        {}
    }
2
Kyo Huu

Nous avons un cas spécial avec MVP où le fragment doit informer le présentateur que la vue est devenue visible et le présentateur est injecté par Dagger dans fragment.onAttach().

setUserVisibleHint() ne suffit pas, nous avons détecté 3 cas différents à traiter (nous mentionnons onAttach() pour que vous sachiez quand le présentateur est disponible):

  1. Le fragment vient d'être créé. Le système effectue les appels suivants:

    setUserVisibleHint() // before fragment's lifecycle calls, so presenter is null
    onAttach()
    ...
    onResume()
    
  2. Fragment déjà créé et le bouton d'accueil est enfoncé. Lors de la restauration de l'application au premier plan, cela s'appelle:

    onResume()
    
  3. Changement d'orientation:

    onAttach() // presenter available
    onResume()
    setUserVisibleHint()
    

Nous souhaitons seulement que l'indicateur de visibilité parvienne une fois au présentateur. C'est pourquoi nous procédons ainsi:

@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);
}
2
Pin

Détecter par focused view!

Ça marche pour moi

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();
}
2

Notez que setUserVisibleHint(false) n'est pas appelé à l'activité/arrêt de fragment. Vous aurez toujours besoin de vérifier le démarrage/arrêt pour que register/unregister puisse écouter correctement les écouteurs/etc.

De plus, vous obtiendrez setUserVisibleHint(false) si votre fragment commence dans un état non visible; vous ne voulez pas unregister car vous ne vous êtes jamais inscrit auparavant.

@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
    }
}
1
elevenfive

J'ai rencontré ce problème lorsque j'essayais de déclencher une minuterie lorsque le fragment dans le viewpager était affiché à l'écran. 

La minuterie a toujours démarré juste avant que le fragment soit vu par l'utilisateur . C'est parce que la méthode onResume() du fragment est appelée avant que nous puissions voir le fragment.

Ma solution a été de faire un contrôle dans la méthode onResume(). Je voulais appeler une certaine méthode 'foo ()' lorsque le fragment 8 était le fragment en cours de la vue. 

@Override
public void onResume() {
    super.onResume();
    if(viewPager.getCurrentItem() == 8){
        foo();
        //Your code here. Executed when fragment is seen by user.
    }
}

J'espère que cela t'aides. J'ai vu ce problème surgir beaucoup. Cela semble être la solution la plus simple que j'ai vue. Beaucoup d'autres ne sont pas compatibles avec les API inférieures, etc.

1
Malone

J'ai eu le même problème. ViewPager exécute d'autres événements de cycle de vie de fragment et je ne pouvais pas changer ce comportement. J'ai écrit un simple pager utilisant des fragments et des animations disponibles . SimplePager

1
Rukmal Dias

Je supporte SectionsPagerAdapter avec des fragments enfants. Après de nombreux maux de tête, j'ai finalement obtenu une version de travail basée sur les solutions de ce sujet:

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;
    }
}
0
Andoctorey

Un moyen simple d'implémenter est de vérifier si l'utilisateur est connecté avant aller au fragment.

Dans votre MainActivity, vous pouvez faire quelque chose comme ceci dans la méthode onNavigationItemSelected .

 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;

Toutefois, si vous utilisez le tiroir de navigation, la sélection dans le tiroir aura été modifiée en Profil bien que nous n'ayons pas accès à ProfileFragment.

Pour réinitialiser la sélection à la sélection actuelle, exécutez le code ci-dessous.

        navigationView.getMenu().getItem(0).setChecked(true);
0
Martin Mbae

Je l'ai utilisé et cela a fonctionné!

mContext.getWindow().getDecorView().isShown() //boolean
0
Ali Akram