Question ViewPager PagerAdapter ne met pas à jour la vue


J'utilise le ViewPager de la bibliothèque de compatibilité. Je l'ai reçu avec succès affichant plusieurs points de vue que je peux feuilleter.

Cependant, j'ai du mal à comprendre comment mettre à jour ViewPager avec un nouvel ensemble de vues.

J'ai essayé toutes sortes de choses comme appeler mAdapter.notifyDataSetChanged(), mViewPager.invalidate() même créer un nouvel adaptateur chaque fois que je veux utiliser une nouvelle liste de données.

Rien n'a aidé, les textviews restent inchangés à partir des données d'origine.

Mettre à jour:  J'ai fait un petit projet de test et j'ai presque pu mettre à jour les vues. Je vais coller la classe ci-dessous.

Ce qui ne semble pas mettre à jour est cependant la 2ème vue, le 'B' reste, il devrait afficher 'Y' après avoir appuyé sur le bouton de mise à jour.

public class ViewPagerBugActivity extends Activity {

    private ViewPager myViewPager;
    private List<String> data;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        data = new ArrayList<String>();
        data.add("A");
        data.add("B");
        data.add("C");

        myViewPager = (ViewPager) findViewById(R.id.my_view_pager);
        myViewPager.setAdapter(new MyViewPagerAdapter(this, data));

        Button updateButton = (Button) findViewById(R.id.update_button);
        updateButton.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                updateViewPager();
            }
        });
    }

    private void updateViewPager() {
        data.clear();
        data.add("X");
        data.add("Y");
        data.add("Z");
        myViewPager.getAdapter().notifyDataSetChanged();
    }

    private class MyViewPagerAdapter extends PagerAdapter {

        private List<String> data;
        private Context ctx;

        public MyViewPagerAdapter(Context ctx, List<String> data) {
            this.ctx = ctx;
            this.data = data;
        }

        @Override
        public int getCount() {
            return data.size();
        }

        @Override
        public Object instantiateItem(View collection, int position) {
            TextView view = new TextView(ctx);
            view.setText(data.get(position));
            ((ViewPager)collection).addView(view);
            return view;
        }

        @Override
        public void destroyItem(View collection, int position, Object view) {
             ((ViewPager) collection).removeView((View) view);
        }

        @Override
        public boolean isViewFromObject(View view, Object object) {
            return view == object;
        }

        @Override
        public Parcelable saveState() {
            return null;
        }

        @Override
        public void restoreState(Parcelable arg0, ClassLoader arg1) {
        }

        @Override
        public void startUpdate(View arg0) {
        }

        @Override
        public void finishUpdate(View arg0) {
        }
    }
}

516
2017-08-31 20:54


origine


Réponses:


Il y a plusieurs façons d'y parvenir.

La première option est plus facile, mais un peu plus inefficace.

Passer outre getItemPosition dans ton PagerAdapter comme ça:

public int getItemPosition(Object object) {
    return POSITION_NONE;
}

De cette façon, quand vous appelez notifyDataSetChanged(), le pager de vue supprimera toutes les vues et les rechargera toutes. Ainsi, l'effet de rechargement est obtenu.

La deuxième option, suggéré par Alvaro Luis Bustamante (anciennement alvarolb), est de setTag() méthode dans instantiateItem() lors de l'instanciation d'une nouvelle vue. Puis au lieu d'utiliser notifyDataSetChanged(), vous pouvez utiliser findViewWithTag() pour trouver la vue que vous souhaitez mettre à jour.

La deuxième approche est très flexible et performante. Bravo à alvarolb pour la recherche originale.


746
2017-09-02 17:28



Je ne pense pas qu'il y ait une sorte de bug dans le PagerAdapter. Le problème est que comprendre comment cela fonctionne est un peu complexe. En regardant les solutions expliquées ici, il y a un malentendu et donc une mauvaise utilisation des vues instanciées de mon point de vue.

Les derniers jours avec lesquels j'ai travaillé PagerAdapter et ViewPageret j'ai trouvé ce qui suit:

le notifyDataSetChanged() méthode sur le PagerAdapter ne notifiera ViewPager que les pages sous-jacentes ont changé. Par exemple, si vous avez créé / supprimé dynamiquement des pages (en ajoutant ou en supprimant des éléments de votre liste), ViewPager devrait prendre soin de cela. Dans ce cas, je pense que le ViewPager détermine si une nouvelle vue doit être supprimée ou instanciée à l'aide du getItemPosition() et getCount() méthodes

je pense que ViewPager, après un notifyDataSetChanged() appel prend ses points de vue des enfants et vérifie leur position avec le getItemPosition(). Si pour une vue enfant cette méthode renvoie POSITION_NONE, la ViewPager comprend que la vue a été supprimée, appelant le destroyItem()et en supprimant cette vue.

De cette manière, dérogatoire getItemPosition() revenir toujours POSITION_NONE est complètement faux si vous voulez seulement mettre à jour le contenu des pages, parce que les vues précédemment créées seront détruites et de nouvelles seront créées chaque fois que vous appelez notifyDatasetChanged(). Il peut sembler ne pas être si mauvais juste pour quelques TextViews, mais lorsque vous avez des vues complexes, comme ListViews peuplées à partir d'une base de données, cela peut être un réel problème et un gaspillage de ressources.

Il existe donc plusieurs approches pour modifier efficacement le contenu d'une vue sans avoir à supprimer et à instancier à nouveau la vue. Cela dépend du problème que vous voulez résoudre. Mon approche consiste à utiliser le setTag() méthode pour toute vue instanciée dans le instantiateItem() méthode. Ainsi, lorsque vous souhaitez modifier les données ou invalider la vue dont vous avez besoin, vous pouvez appeler le findViewWithTag() méthode sur le ViewPager pour récupérer la vue précédemment instanciée et la modifier / utiliser comme vous voulez sans avoir à supprimer / créer une nouvelle vue chaque fois que vous voulez mettre à jour une valeur. 

Imaginez par exemple que vous ayez 100 pages avec 100 TextViews et vous souhaitez uniquement mettre à jour une valeur périodiquement. Avec les approches expliquées précédemment, cela signifie que vous supprimez et instanciez 100 TextViews sur chaque mise à jour. Cela n'a aucun sens...


445
2017-11-06 01:11



Changer la FragmentPagerAdapter à FragmentStatePagerAdapter.

Passer outre getItemPosition() méthode et retour POSITION_NONE.

Finalement, il va écouter le notifyDataSetChanged() sur le téléavertisseur.


69
2017-09-05 02:07



La réponse donné par alvarolb est certainement le meilleur moyen de le faire. S'appuyant sur sa réponse, un moyen facile de mettre en œuvre est de simplement stocker les vues actives par position:

SparseArray<View> views = new SparseArray<View>();

@Override
public Object instantiateItem(View container, int position) {
    View root = <build your view here>;
    ((ViewPager) container).addView(root);
    views.put(position, root);
    return root;
}

@Override
public void destroyItem(View collection, int position, Object o) {
    View view = (View)o;
    ((ViewPager) collection).removeView(view);
    views.remove(position);
    view = null;
}

Puis une fois en remplaçant le notifyDataSetChanged méthode que vous pouvez actualiser les vues ...

@Override
public void notifyDataSetChanged() {
    int key = 0;
    for(int i = 0; i < views.size(); i++) {
       key = views.keyAt(i);
       View view = views.get(key);
       <refresh view with new data>
    }
    super.notifyDataSetChanged();
}

Vous pouvez réellement utiliser un code similaire dans instantiateItem et notifyDataSetChanged pour rafraîchir votre vue. Dans mon code, j'utilise exactement la même méthode.


34
2018-06-06 01:55



Avait le même problème. Pour moi, cela a fonctionné pour étendre FragmentStatePagerAdapter, et remplacer les méthodes ci-dessous:

@Override
public Parcelable saveState() {
    return null;
}

@Override
public void restoreState(Parcelable state, ClassLoader loader) {

}

21
2018-06-17 07:57



Après des heures de frustration en essayant toutes les solutions ci-dessus pour surmonter ce problème et aussi essayer de nombreuses solutions sur d'autres questions similaires comme ce, ce et ce qui ont tous échoué avec moi pour résoudre ce problème et pour faire le ViewPager détruire le vieux Fragment et remplissez le pager avec le nouveau Fragments. J'ai résolu le problème comme suit:

1) Faire le ViewPager classe à étendre FragmentPagerAdapter comme suit:

 public class myPagerAdapter extends FragmentPagerAdapter {

2) Créer un article pour le ViewPager ce magasin le title et le fragment comme suit:

public class PagerItem {
private String mTitle;
private Fragment mFragment;


public PagerItem(String mTitle, Fragment mFragment) {
    this.mTitle = mTitle;
    this.mFragment = mFragment;
}
public String getTitle() {
    return mTitle;
}
public Fragment getFragment() {
    return mFragment;
}
public void setTitle(String mTitle) {
    this.mTitle = mTitle;
}

public void setFragment(Fragment mFragment) {
    this.mFragment = mFragment;
}

}

3) Faire le constructeur du ViewPager prendre mon FragmentManager par exemple pour le stocker dans mon class comme suit:

private FragmentManager mFragmentManager;
private ArrayList<PagerItem> mPagerItems;

public MyPagerAdapter(FragmentManager fragmentManager, ArrayList<PagerItem> pagerItems) {
    super(fragmentManager);
    mFragmentManager = fragmentManager;
    mPagerItems = pagerItems;
}

4) Créer une méthode pour redéfinir le adapter données avec les nouvelles données en supprimant toutes les précédentes fragment du fragmentManager lui-même directement pour faire le adapter pour définir le nouveau fragment de la nouvelle liste à nouveau comme suit:

public void setPagerItems(ArrayList<PagerItem> pagerItems) {
    if (mPagerItems != null)
        for (int i = 0; i < mPagerItems.size(); i++) {
            mFragmentManager.beginTransaction().remove(mPagerItems.get(i).getFragment()).commit();
        }
    mPagerItems = pagerItems;
}

5) Du conteneur Activity ou Fragment ne pas réinitialiser l'adaptateur avec les nouvelles données. Définir les nouvelles données via la méthode setPagerItems avec les nouvelles données comme suit:

ArrayList<PagerItem> pagerItems = new ArrayList<PagerItem>();
pagerItems.add(new PagerItem("Fragment1", new MyFragment1()));
pagerItems.add(new PagerItem("Fragment2", new MyFragment2()));

mPagerAdapter.setPagerItems(pagerItems);
mPagerAdapter.notifyDataSetChanged();

J'espère que ça aide.


19
2018-03-08 18:01



Deux ans et demi après que le PO a posé sa question, cette question est encore, toujours, un problème. Il est évident que la priorité de Google sur ce n'est pas particulièrement élevé, donc plutôt que de trouver une solution, j'ai trouvé une solution de contournement. La grande percée pour moi était de découvrir quelle était la véritable cause du problème (voir la réponse acceptée dans ce post ). Une fois qu'il était évident que le problème était que les pages actives ne sont pas correctement actualisées, ma solution de contournement était évidente:

Dans mon fragment (les pages):

  • J'ai pris tout le code qui remplit le formulaire à partir de onCreateView et le place dans une fonction appelée PopulateForm qui peut être appelée de n'importe où, plutôt que par le framework. Cette fonction tente d'obtenir la vue en cours en utilisant getView, et si elle est nulle, elle retourne simplement. Il est important que PopulateForm ne contienne que le code qui s'affiche - tout le code qui crée les écouteurs FocusChange et similaires est toujours dans OnCreate
  • Créez un booléen qui peut être utilisé comme un drapeau indiquant que le formulaire doit être rechargé. Le mien est mbReloadForm
  • Substituez OnResume () pour appeler PopulateForm () si mbReloadForm est défini.

Dans mon activité, où je charge les pages:

  • Allez à la page 0 avant de changer quelque chose. J'utilise FragmentStatePagerAdapter, donc je sais que deux ou trois pages sont affectées au maximum. Passer à la page 0 garantit que je n'ai jamais le problème sur les pages 0, 1 et 2.
  • Avant de vider l'ancienne liste, prenez sa taille (). De cette façon, vous savez combien de pages sont affectées par le bogue. Si> 3, réduisez-le à 3 - si vous utilisez un PagerAdapter différent, vous devrez voir combien de pages vous avez à traiter (peut-être tout?)
  • Rechargez les données et appelez pageAdapter.notifyDataSetChanged ()
  • Maintenant, pour chacune des pages affectées, voir si la page est active en utilisant pager.getChildAt (i) - cela vous indique si vous avez une vue. Si c'est le cas, appelez pager.PopulateView (). Sinon, définissez l'indicateur ReloadForm.

Après cela, lorsque vous rechargez un deuxième jeu de pages, le bogue entraînera toujours certains à afficher les anciennes données. Cependant, ils seront maintenant actualisés et vous verrez les nouvelles données - vos utilisateurs ne sauront pas que la page n'a jamais été incorrecte parce que cette actualisation aura lieu avant qu'ils voient la page.

J'espère que cela aide quelqu'un!


4
2018-02-07 11:08



Un moyen beaucoup plus facile: utiliser un FragmentPagerAdapteret emballez vos vues paginées sur des fragments. Ils sont mis à jour


4
2017-11-14 22:08



Juste au cas où quelqu'un utilise FragmentStatePagerAdapter basé sur l'adaptateur (qui permettra à ViewPager de créer des pages minimales nécessaires pour l'affichage, au maximum 2 pour mon cas), la réponse de @ rui.araujo de l'écrasement getItemPosition dans votre adaptateur ne causera pas de gaspillage significatif, mais peut encore être améliorée.

En pseudo code:

public int getItemPosition(Object object) {
    YourFragment f = (YourFragment) object;
    YourData d = f.data;
    logger.info("validate item position on page index: " + d.pageNo);

    int dataObjIdx = this.dataPages.indexOf(d);

    if (dataObjIdx < 0 || dataObjIdx != d.pageNo) {
        logger.info("data changed, discard this fragment.");
        return POSITION_NONE;
    }

    return POSITION_UNCHANGED;
}

3
2017-07-11 05:56



J'ai eu le même problème et ma solution remplace ViewPagerAdapter#getItemId(int position):

@Override
public long getItemId(int position) {
    return mPages.get(position).getId();
}

Par défaut, cette méthode renvoie la position de l'élément. je suppose que ViewPager vérifie si itemId a été changé et recrée la page seulement si c'était le cas. Mais la version non remplacée renvoie la même position que itemId même si la page est réellement différente, et ViewPager ne définit pas cette page est remplacé et doit être recréé.

Pour l'utiliser, long id est nécessaire pour chaque page. Normalement, on s'attend à ce qu'il soit unique, mais je suggère, pour ce cas, qu'il devrait juste être différent de la valeur précédente pour la même page. Ainsi, il est possible d'utiliser un compteur continu dans un adaptateur ou des entiers aléatoires (avec une distribution large) ici.

Je pense que c'est plus cohérent plutôt d'utiliser des Tags de vue mentionnés comme une solution dans ce sujet. Mais probablement pas pour tous les cas.


3
2018-05-25 10:02