web-dev-qa-db-fra.com

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

J'ai implémenté avec succès onRetainNonConfigurationInstance() pour ma Activity principale afin de sauvegarder et de restaurer certains composants critiques lors de modifications de l'orientation de l'é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() pour 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"?

231
Brad Hein

Pour ce faire, vous devez implémenter View#onSaveInstanceState et View#onRestoreInstanceState et étendre le View.BaseSavedState class.

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 les classes View et Saved's State. Vous devriez faire tout le travail de lecture et d’écriture sur la variable Parcel dans la classe SavedState. Ensuite, votre classe View peut extraire les membres de l'état et effectuer le travail nécessaire pour rétablir l'état valide de la classe.

Remarques: View#onSavedInstanceState et View#onRestoreInstanceState sont appelés automatiquement pour vous si View#getId renvoie une valeur> = 0. Cela se produit lorsque vous lui attribuez un identifiant xml ou appelez setId manuellement. Sinon, vous devez appeler View#onSaveInstanceState et écrire le colis pouvant être renvoyé dans le colis que vous obtenez dans Activity#onSaveInstanceState pour enregistrer l’état puis le lire et le transmettre au View#onRestoreInstanceState à partir de Activity#onRestoreInstanceState.

Un autre exemple simple est le CompoundButton

401
Rich Schuler

Je pense que c'est une version beaucoup plus simple. Bundle est un type intégré qui implémente Parcelable

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);
  }
}
433
Kobor42

Voici une autre variante qui utilise un mélange des deux méthodes ci-dessus . Combinant la rapidité et l'exactitude de Parcelable avec 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;
    }
}
18
Blundell

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'elles ont toutes 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 persistait parce que j’ajoutais les vues à un ViewGroup personnalisé par programme et que je n’attribuais pas d’ID uniques.

Le lien partagé par mato fonctionnera, mais cela signifie qu'aucune des vues individuelles ne gérera son propre état - l'ensemble de l'état 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 issus du XML ne sont plus uniques (s’il est défini dans 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 est uniquement disponible à partir de l'API 17.

Voici mon code de ViewGroup (c'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);
    } 
}
8
Fletcher Johns

J'ai eu le problème qui onRestoreInstanceState 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);
}
2
chrigist

Pour compléter d'autres réponses - si vous avez plusieurs vues composées personnalisées avec le même ID et si elles sont toutes restaurées avec l'état de la dernière vue lors d'un changement de configuration, il vous suffit d'indiquer à la vue de ne distribuer que des événements de sauvegarde/restauration. à lui-même en surchargeant quelques 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, voir 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 devient confuse. En envoyant uniquement l'état de la vue composée elle-même, nous empêchons leurs enfants de recevoir des messages contradictoires à partir d'autres vues composées.

0
Tom

Au lieu d'utiliser onSaveInstanceState et onRestoreInstanceState, vous pouvez également utiliser un ViewModel . Faites que votre modèle de données étende ViewModel, puis vous pourrez utiliser ViewModelProviders pour 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 utiliser ViewModelProviders, ajoutez ce qui suit à dependencies dans app/build.gradle:

implementation "Android.Arch.lifecycle:extensions:1.1.1"
implementation "Android.Arch.lifecycle:viewmodel:1.1.1"

Notez que votre MyActivity étend FragmentActivity au lieu de simplement prolonger Activity

Vous pouvez en savoir plus sur ViewModels ici:

0
Benedikt Köppel