Comment empêcher les vues personnalisées de perdre leur état lors des changements d'orientation de l'écran

248

J'ai réussi à implémenter onRetainNonConfigurationInstance()mon principal Activitypour enregistrer et restaurer certains composants critiques lors des changements d'orientation d'écran.

Mais il semble que mes vues personnalisées soient recréées à partir de zéro lorsque l'orientation change. Cela a du sens, bien que dans mon cas, cela ne soit pas pratique car la vue personnalisée en question est un tracé X / Y et les points tracés sont stockés dans la vue personnalisée.

Existe-t-il un moyen astucieux d'implémenter quelque chose de similaire à onRetainNonConfigurationInstance()une vue personnalisée, ou dois-je simplement implémenter des méthodes dans la vue personnalisée qui me permettent d'obtenir et de définir son "état"?

Brad Hein
la source

Réponses:

415

Pour ce faire , la mise en œuvre View#onSaveInstanceStateet View#onRestoreInstanceStateet l' extension de la View.BaseSavedStateclasse.

public class CustomView extends View {

  private int stateToSave;

  ...

  @Override
  public Parcelable onSaveInstanceState() {
    //begin boilerplate code that allows parent classes to save state
    Parcelable superState = super.onSaveInstanceState();

    SavedState ss = new SavedState(superState);
    //end

    ss.stateToSave = this.stateToSave;

    return ss;
  }

  @Override
  public void onRestoreInstanceState(Parcelable state) {
    //begin boilerplate code so parent classes can restore state
    if(!(state instanceof SavedState)) {
      super.onRestoreInstanceState(state);
      return;
    }

    SavedState ss = (SavedState)state;
    super.onRestoreInstanceState(ss.getSuperState());
    //end

    this.stateToSave = ss.stateToSave;
  }

  static class SavedState extends BaseSavedState {
    int stateToSave;

    SavedState(Parcelable superState) {
      super(superState);
    }

    private SavedState(Parcel in) {
      super(in);
      this.stateToSave = in.readInt();
    }

    @Override
    public void writeToParcel(Parcel out, int flags) {
      super.writeToParcel(out, flags);
      out.writeInt(this.stateToSave);
    }

    //required field that makes Parcelables from a Parcel
    public static final Parcelable.Creator<SavedState> CREATOR =
        new Parcelable.Creator<SavedState>() {
          public SavedState createFromParcel(Parcel in) {
            return new SavedState(in);
          }
          public SavedState[] newArray(int size) {
            return new SavedState[size];
          }
    };
  }
}

Le travail est divisé entre la classe View et la classe SavedState de la vue. Vous devriez faire tout le travail de lecture et d'écriture vers et depuis Parcelle SavedStatecours. Ensuite, votre classe View peut effectuer le travail d'extraction des membres de l'état et effectuer le travail nécessaire pour ramener la classe à un état valide.

Remarques: View#onSavedInstanceStateet View#onRestoreInstanceStatesont appelés automatiquement pour vous si View#getIdrenvoie une valeur> = 0. Cela se produit lorsque vous lui donnez un identifiant en xml ou appelez setIdmanuellement. Sinon , vous devez appeler View#onSaveInstanceStateet écrire le Parcelable retourné au colis que vous obtenez dans Activity#onSaveInstanceStated'enregistrer l'état et de lire la suite et le transmettre à View#onRestoreInstanceStatepartir Activity#onRestoreInstanceState.

Un autre exemple simple de ceci est le CompoundButton

Rich Schuler
la source
14
Pour ceux qui arrivent ici parce que cela ne fonctionne pas lors de l'utilisation de fragments avec la bibliothèque de support v4, je note que la bibliothèque de support ne semble pas appeler onSaveInstanceState / onRestoreInstanceState de View pour vous; vous devez l'appeler vous-même explicitement à partir d'un endroit pratique dans FragmentActivity ou Fragment.
magneticMonster
69
Notez que le CustomView auquel vous appliquez cela doit avoir un ensemble d'identifiants unique, sinon ils partageront l'état entre eux. SavedState est stocké par rapport à l'ID de CustomView, donc si vous avez plusieurs CustomViews avec le même identifiant, ou aucun identifiant, le colis enregistré dans le CustomView.onSaveInstanceState () final sera transmis à tous les appels à CustomView.onRestoreInstanceState () lorsque le les vues sont restaurées.
Nick Street
5
Cette méthode ne fonctionnait pas pour moi avec deux vues personnalisées (l'une étendant l'autre). J'ai continué à recevoir une exception ClassNotFoundException lors de la restauration de ma vue. J'ai dû utiliser l'approche Bundle dans la réponse de Kobor42.
Chris Feist,
3
onSaveInstanceState()et onRestoreInstanceState()devrait être protected(comme leur superclasse), non public. Aucune raison de les exposer ...
XåpplI'-I0llwlg'I -
7
Cela ne fonctionne pas bien lors de l'enregistrement d'une personnalisation BaseSaveStatepour une classe qui étend RecyclerView, vous obtenez Parcel﹕ Class not found when unmarshalling: android.support.v7.widget.RecyclerView$SavedState java.lang.ClassNotFoundException: android.support.v7.widget.RecyclerView$SavedStatedonc vous devez faire le correctif de bogue qui est écrit ici: github.com/ksoichiro/Android-ObservableScrollView/commit/… (en utilisant le ClassLoader de RecyclerView.class pour charger le super état)
EpicPandaForce
459

Je pense que c'est une version beaucoup plus simple. Bundleest un type intégré qui implémenteParcelable

public class CustomView extends View
{
  private int stuff; // stuff

  @Override
  public Parcelable onSaveInstanceState()
  {
    Bundle bundle = new Bundle();
    bundle.putParcelable("superState", super.onSaveInstanceState());
    bundle.putInt("stuff", this.stuff); // ... save stuff 
    return bundle;
  }

  @Override
  public void onRestoreInstanceState(Parcelable state)
  {
    if (state instanceof Bundle) // implicit null check
    {
      Bundle bundle = (Bundle) state;
      this.stuff = bundle.getInt("stuff"); // ... load stuff
      state = bundle.getParcelable("superState");
    }
    super.onRestoreInstanceState(state);
  }
}
Kobor42
la source
5
Pourquoi ne serait - on pasonRestoreInstanceState appelé avec un Bundle si je lui rendais onSaveInstanceStateun Bundle?
Qwertie
5
OnRestoreInstanceest hérité. Nous ne pouvons pas changer l'en-tête. Parcelableest juste une interface, Bundleest une implémentation pour cela.
Kobor42
5
Merci, cette méthode est bien meilleure et évite BadParcelableException lors de l'utilisation du cadre SavedState pour des vues personnalisées car l'état enregistré ne semble pas pouvoir définir correctement le chargeur de classe pour votre SavedState personnalisé!
Ian Warwick
3
J'ai plusieurs instances de la même vue dans une activité. Ils ont tous des identifiants uniques dans le xml. Mais tous obtiennent toujours les paramètres de la dernière vue. Des idées?
Christoffer
15
Cette solution est peut-être correcte, mais elle n'est certainement pas sûre. En implémentant cela, vous supposez que l' Viewétat de base n'est pas a Bundle. Bien sûr, c'est vrai pour le moment, mais vous comptez sur ce fait de mise en œuvre actuel qui n'est pas garanti d'être vrai.
Dmitry Zaytsev
18

Voici une autre variante qui utilise un mélange des deux méthodes ci-dessus. Combiner la vitesse et la justesse de Parcelableavec la simplicité d'un Bundle:

@Override
public Parcelable onSaveInstanceState() {
    Bundle bundle = new Bundle();
    // The vars you want to save - in this instance a string and a boolean
    String someString = "something";
    boolean someBoolean = true;
    State state = new State(super.onSaveInstanceState(), someString, someBoolean);
    bundle.putParcelable(State.STATE, state);
    return bundle;
}

@Override
public void onRestoreInstanceState(Parcelable state) {
    if (state instanceof Bundle) {
        Bundle bundle = (Bundle) state;
        State customViewState = (State) bundle.getParcelable(State.STATE);
        // The vars you saved - do whatever you want with them
        String someString = customViewState.getText();
        boolean someBoolean = customViewState.isSomethingShowing());
        super.onRestoreInstanceState(customViewState.getSuperState());
        return;
    }
    // Stops a bug with the wrong state being passed to the super
    super.onRestoreInstanceState(BaseSavedState.EMPTY_STATE); 
}

protected static class State extends BaseSavedState {
    protected static final String STATE = "YourCustomView.STATE";

    private final String someText;
    private final boolean somethingShowing;

    public State(Parcelable superState, String someText, boolean somethingShowing) {
        super(superState);
        this.someText = someText;
        this.somethingShowing = somethingShowing;
    }

    public String getText(){
        return this.someText;
    }

    public boolean isSomethingShowing(){
        return this.somethingShowing;
    }
}
Blundell
la source
3
Ça ne marche pas. J'obtiens une ClassCastException ... Et c'est parce qu'il a besoin d'un CREATEUR statique public pour qu'il vous instancie à Statepartir du colis. Veuillez consulter: charlesharley.com/2012/programming/…
mato
8

Les réponses ici sont déjà excellentes, mais ne fonctionnent pas nécessairement pour les ViewGroups personnalisés. Pour que toutes les vues personnalisées conservent leur état, vous devez remplacer onSaveInstanceState()et onRestoreInstanceState(Parcelable state)dans chaque classe. Vous devez également vous assurer qu'ils ont tous des identifiants uniques, qu'ils soient gonflés à partir de XML ou ajoutés par programme.

Ce que j'ai trouvé ressemblait remarquablement à la réponse de Kobor42, mais l'erreur est restée parce que j'ajoutais les vues à un ViewGroup personnalisé par programme et que je n'attribuais pas d'identifiants uniques.

Le lien partagé par mato fonctionnera, mais cela signifie qu'aucune des vues individuelles ne gère son propre état - l'état entier est enregistré dans les méthodes ViewGroup.

Le problème est que lorsque plusieurs de ces ViewGroups sont ajoutés à une mise en page, les identifiants de leurs éléments du XML ne sont plus uniques (s'ils sont définis en XML). Au moment de l'exécution, vous pouvez appeler la méthode statique View.generateViewId()pour obtenir un identifiant unique pour une vue. Ceci n'est disponible qu'à partir de l'API 17.

Voici mon code du ViewGroup (il est abstrait et mOriginalValue est une variable de type):

public abstract class DetailRow<E> extends LinearLayout {

    private static final String SUPER_INSTANCE_STATE = "saved_instance_state_parcelable";
    private static final String STATE_VIEW_IDS = "state_view_ids";
    private static final String STATE_ORIGINAL_VALUE = "state_original_value";

    private E mOriginalValue;
    private int[] mViewIds;

// ...

    @Override
    protected Parcelable onSaveInstanceState() {

        // Create a bundle to put super parcelable in
        Bundle bundle = new Bundle();
        bundle.putParcelable(SUPER_INSTANCE_STATE, super.onSaveInstanceState());
        // Use abstract method to put mOriginalValue in the bundle;
        putValueInTheBundle(mOriginalValue, bundle, STATE_ORIGINAL_VALUE);
        // Store mViewIds in the bundle - initialize if necessary.
        if (mViewIds == null) {
            // We need as many ids as child views
            mViewIds = new int[getChildCount()];
            for (int i = 0; i < mViewIds.length; i++) {
                // generate a unique id for each view
                mViewIds[i] = View.generateViewId();
                // assign the id to the view at the same index
                getChildAt(i).setId(mViewIds[i]);
            }
        }
        bundle.putIntArray(STATE_VIEW_IDS, mViewIds);
        // return the bundle
        return bundle;
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {

        // We know state is a Bundle:
        Bundle bundle = (Bundle) state;
        // Get mViewIds out of the bundle
        mViewIds = bundle.getIntArray(STATE_VIEW_IDS);
        // For each id, assign to the view of same index
        if (mViewIds != null) {
            for (int i = 0; i < mViewIds.length; i++) {
                getChildAt(i).setId(mViewIds[i]);
            }
        }
        // Get mOriginalValue out of the bundle
        mOriginalValue = getValueBackOutOfTheBundle(bundle, STATE_ORIGINAL_VALUE);
        // get super parcelable back out of the bundle and pass it to
        // super.onRestoreInstanceState(Parcelable)
        state = bundle.getParcelable(SUPER_INSTANCE_STATE);
        super.onRestoreInstanceState(state);
    } 
}
Fletcher Johns
la source
L'identification personnalisée est vraiment un problème, mais je pense qu'elle doit être traitée lors de l'initialisation de la vue, et non lors de la sauvegarde de l'état.
Kobor42
Bon point. Suggérez-vous de définir mViewIds dans le constructeur puis d'écraser si l'état est restauré?
Fletcher Johns
2

J'ai eu le problème que onRestoreInstanceState a restauré toutes mes vues personnalisées avec l'état de la dernière vue. Je l'ai résolu en ajoutant ces deux méthodes à ma vue personnalisée:

@Override
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
    dispatchFreezeSelfOnly(container);
}

@Override
protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
    dispatchThawSelfOnly(container);
}
chrigist
la source
Les méthodes dispatchFreezeSelfOnly et dispatchThawSelfOnly appartiennent à ViewGroup, pas à View. Donc, au cas où, votre vue personnalisée est étendue à partir d'une vue intégrée. Votre solution n'est pas applicable.
Hau Luu
1

Au lieu d'utiliser onSaveInstanceStateet onRestoreInstanceState, vous pouvez également utiliser a ViewModel. Faites étendre votre modèle de données ViewModel, puis vous pouvez utiliser ViewModelProviderspour obtenir la même instance de votre modèle chaque fois que l'activité est recréée:

class MyData extends ViewModel {
    // have all your properties with getters and setters here
}

public class MyActivity extends FragmentActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // the first time, ViewModelProvider will create a new MyData
        // object. When the Activity is recreated (e.g. because the screen
        // is rotated), ViewModelProvider will give you the initial MyData
        // object back, without creating a new one, so all your property
        // values are retained from the previous view.
        myData = ViewModelProviders.of(this).get(MyData.class);

        ...
    }
}

Pour l'utiliser ViewModelProviders, ajoutez les éléments suivants dependenciesdans app/build.gradle:

implementation "android.arch.lifecycle:extensions:1.1.1"
implementation "android.arch.lifecycle:viewmodel:1.1.1"

Notez que votre MyActivityétend FragmentActivityau lieu de simplement s'étendre Activity.

Vous pouvez en savoir plus sur ViewModels ici:

Benedikt Köppel
la source
1
@JJD Je suis d'accord avec l'article que vous avez publié, il faut encore gérer correctement la sauvegarde et la restauration. ViewModelest particulièrement pratique si vous avez de grands ensembles de données à conserver lors d'un changement d'état, comme une rotation d'écran. Je préfère utiliser le ViewModelau lieu de l'écrire Applicationcar il est clairement défini et je peux avoir plusieurs activités de la même application se comportant correctement.
Benedikt Köppel
1

Je l'ai trouvé cette réponse provoquait des plantages sur les versions 9 et 10 d'Android. Je pense que c'est une bonne approche mais quand j'ai regardé du code Android, j'ai découvert qu'il manquait un constructeur. La réponse est assez ancienne, donc à l'époque, elle n'était probablement pas nécessaire. Quand j'ai ajouté le constructeur manquant et l'ai appelé du créateur, le crash a été corrigé.

Voici donc le code édité:

public class CustomView extends View {

    private int stateToSave;

    ...

    @Override
    public Parcelable onSaveInstanceState() {
        Parcelable superState = super.onSaveInstanceState();
        SavedState ss = new SavedState(superState);

        // your custom state
        ss.stateToSave = this.stateToSave;

        return ss;
    }

    @Override
    protected void dispatchSaveInstanceState(SparseArray<Parcelable> container)
    {
        dispatchFreezeSelfOnly(container);
    }

    @Override
    public void onRestoreInstanceState(Parcelable state) {
        SavedState ss = (SavedState) state;
        super.onRestoreInstanceState(ss.getSuperState());

        // your custom state
        this.stateToSave = ss.stateToSave;
    }

    @Override
    protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container)
    {
        dispatchThawSelfOnly(container);
    }

    static class SavedState extends BaseSavedState {
        int stateToSave;

        SavedState(Parcelable superState) {
            super(superState);
        }

        private SavedState(Parcel in) {
            super(in);
            this.stateToSave = in.readInt();
        }

        // This was the missing constructor
        @RequiresApi(Build.VERSION_CODES.N)
        SavedState(Parcel in, ClassLoader loader)
        {
            super(in, loader);
            this.stateToSave = in.readInt();
        }

        @Override
        public void writeToParcel(Parcel out, int flags) {
            super.writeToParcel(out, flags);
            out.writeInt(this.stateToSave);
        }    

        public static final Creator<SavedState> CREATOR =
            new ClassLoaderCreator<SavedState>() {

            // This was also missing
            @Override
            public SavedState createFromParcel(Parcel in, ClassLoader loader)
            {
                return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ? new SavedState(in, loader) : new SavedState(in);
            }

            @Override
            public SavedState createFromParcel(Parcel in) {
                return new SavedState(in, null);
            }

            @Override
            public SavedState[] newArray(int size) {
                return new SavedState[size];
            }
        };
    }
}
Tourbillonnant
la source
0

Pour augmenter les autres réponses - si vous avez plusieurs vues composées personnalisées avec le même ID et qu'elles sont toutes en cours de restauration avec l'état de la dernière vue lors d'un changement de configuration, tout ce que vous devez faire est de dire à la vue de ne distribuer que les événements de sauvegarde / restauration à lui-même en remplaçant un couple de méthodes.

class MyCompoundView : ViewGroup {

    ...

    override fun dispatchSaveInstanceState(container: SparseArray<Parcelable>) {
        dispatchFreezeSelfOnly(container)
    }

    override fun dispatchRestoreInstanceState(container: SparseArray<Parcelable>) {
        dispatchThawSelfOnly(container)
    }
}

Pour une explication de ce qui se passe et pourquoi cela fonctionne, consultez cet article de blog . Fondamentalement, les ID de vue des enfants de votre vue composée sont partagés par chaque vue composée et la restauration de l'état est confuse. En ne répartissant l'état que pour la vue composée elle-même, nous empêchons leurs enfants d'obtenir des messages mixtes provenant d'autres vues composées.

À M
la source