Come evitare che le visualizzazioni personalizzate perdano stato attraverso le modifiche all'orientamento dello schermo


248

Ho implementato con successo il onRetainNonConfigurationInstance()mio principale Activityper salvare e ripristinare alcuni componenti critici attraverso i cambiamenti di orientamento dello schermo.

Ma sembra che le mie viste personalizzate vengano ricreate da zero quando cambia l'orientamento. Questo ha senso, anche se nel mio caso è scomodo perché la vista personalizzata in questione è una trama X / Y e i punti tracciati sono memorizzati nella vista personalizzata.

Esiste un modo astuto per implementare qualcosa di simile a onRetainNonConfigurationInstance()per una vista personalizzata o devo semplicemente implementare metodi nella vista personalizzata che mi consentano di ottenere e impostare il suo "stato"?

Risposte:


415

A tale scopo, l'attuazione View#onSaveInstanceStatee View#onRestoreInstanceStateed estendendo 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];
          }
    };
  }
}

Il lavoro viene suddiviso tra la classe View e SavedState di View. Dovresti fare tutto il lavoro di lettura e scrittura da e verso Parcella SavedStateclasse. Quindi la tua classe View può fare il lavoro di estrarre i membri dello stato e fare il lavoro necessario per riportare la classe ad uno stato valido.

Note: View#onSavedInstanceStatee View#onRestoreInstanceStatevengono chiamati automaticamente se View#getIdrestituisce un valore> = 0. Ciò accade quando si assegna un ID in XML o si chiama setIdmanualmente. Altrimenti devi chiamare View#onSaveInstanceStatee scrivere il pacco restituito al pacco in cui ti trovi Activity#onSaveInstanceStateper salvare lo stato e successivamente leggerlo e passarlo View#onRestoreInstanceStateda Activity#onRestoreInstanceState.

Un altro semplice esempio di questo è il CompoundButton


14
Per coloro che arrivano qui perché non funziona quando si utilizzano Fragments con la libreria di supporto v4, noto che la libreria di supporto non sembra chiamare il View onSaveInstanceState / onRestoreInstanceState per te; devi chiamarlo esplicitamente da un posto conveniente in FragmentActivity o Fragment.
magneticMonster,

69
Si noti che CustomView a cui si applica questo dovrebbe avere un ID id univoco, altrimenti condivideranno lo stato tra loro. SavedState viene archiviato sull'ID di CustomView, quindi se si hanno più CustomView con lo stesso ID o nessun ID, il pacco salvato nel CustomView.onSaveInstanceState () finale verrà passato a tutte le chiamate a CustomView.onRestoreInstanceState () quando il le viste sono ripristinate.
Nick Street,

5
Questo metodo non ha funzionato per me con due viste personalizzate (una che si estende l'altra). Continuavo a ricevere ClassNotFoundException durante il ripristino della vista. Ho dovuto usare l'approccio Bundle nella risposta di Kobor42.
Chris Feist,

3
onSaveInstanceState()e onRestoreInstanceState()dovrebbe essere protected(come la loro superclasse), no public. Nessun motivo per esporli ...
XåpplI'-I0llwlg'I -

7
Questo non funziona bene quando si salva un'abitudine BaseSaveStateper una classe che estende RecyclerView, si ottiene Parcel﹕ Class not found when unmarshalling: android.support.v7.widget.RecyclerView$SavedState java.lang.ClassNotFoundException: android.support.v7.widget.RecyclerView$SavedStatequindi è necessario eseguire la correzione dei bug scritta qui: github.com/ksoichiro/Android-ObservableScrollView/commit/… (utilizzando ClassLoader di RecyclerView.class per caricare il super stato)
EpicPandaForce,

459

Penso che questa sia una versione molto più semplice. Bundleè un tipo incorporato che implementaParcelable

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

5
Perché non dovrebbe onRestoreInstanceState essere chiamato con un pacchetto se onSaveInstanceStaterestituito un pacchetto?
Qwertie,

5
OnRestoreInstanceè ereditato. Non possiamo cambiare l'intestazione. Parcelableè solo un'interfaccia, Bundleè un'implementazione per questo.
Kobor42,

5
Grazie in questo modo è molto meglio ed evita BadParcelableException quando si utilizza il framework SavedState per le viste personalizzate poiché lo stato salvato sembra non essere in grado di impostare correttamente il caricatore di classi per il SavedState personalizzato!
Ian Warwick,

3
Ho diverse istanze della stessa vista in un'attività. Hanno tutti ID univoci nel file XML. Ma comunque tutti ottengono le impostazioni dell'ultima vista. Qualche idea?
Christoffer,

15
Questa soluzione potrebbe essere ok, ma sicuramente non è sicura. Implementando ciò, si presuppone che lo Viewstato di base non sia un Bundle. Naturalmente, questo è vero al momento, ma stai facendo affidamento su questo fatto di implementazione attuale che non è garantito per essere vero.
Dmitry Zaytsev,

18

Ecco un'altra variante che utilizza un mix dei due metodi precedenti. Combinando la velocità e la correttezza di Parcelablecon la semplicità di 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;
    }
}

3
Questo non funziona Ricevo una ClassCastException ... Ed è perché ha bisogno di un CREATOR statico pubblico in modo da creare un'istanza Statedal pacco. Dai
mato

8

Le risposte qui sono già ottime, ma non funzionano necessariamente per i ViewGroup personalizzati. Per ottenere che tutte le viste personalizzate mantengano il loro stato, è necessario eseguire l'override onSaveInstanceState()e onRestoreInstanceState(Parcelable state)in ciascuna classe. È inoltre necessario assicurarsi che tutti abbiano ID univoci, siano essi gonfiati da XML o aggiunti a livello di programmazione.

Quello che mi è venuto in mente è stato notevolmente simile alla risposta di Kobor42, ma l'errore è rimasto perché stavo aggiungendo le visualizzazioni a un ViewGroup personalizzato a livello di codice e non assegnando ID univoci.

Il collegamento condiviso da mato funzionerà, ma significa che nessuna delle singole viste gestisce il proprio stato: l'intero stato viene salvato nei metodi ViewGroup.

Il problema è che quando più di questi ViewGroup vengono aggiunti a un layout, gli ID dei loro elementi da xml non sono più univoci (se definito in xml). In fase di esecuzione, è possibile chiamare il metodo statico View.generateViewId()per ottenere un ID univoco per una vista. Questo è disponibile solo da API 17.

Ecco il mio codice dal ViewGroup (è astratto e mOriginalValue è una variabile di tipo):

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

L'ID personalizzato è davvero un problema, ma penso che dovrebbe essere gestito all'inizializzazione della vista e non al salvataggio dello stato.
Kobor42,

Buon punto. Suggerisci di impostare mViewIds nel costruttore, quindi sovrascrivere se lo stato viene ripristinato?
Fletcher Johns,

2

Ho avuto il problema che onRestoreInstanceState ha ripristinato tutte le mie viste personalizzate con lo stato dell'ultima vista. L'ho risolto aggiungendo questi due metodi alla mia vista personalizzata:

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

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

I metodi dispatchFreezeSelfOnly e dispatchThawSelfOnly appartengono a ViewGroup, non a View. Quindi, nel caso, la visualizzazione personalizzata viene estesa da una visualizzazione incorporata. La tua soluzione non è applicabile.
Hau Luu,

1

Invece di usare onSaveInstanceStatee onRestoreInstanceState, puoi anche usare a ViewModel. Estendi il tuo modello di dati ViewModele quindi puoi utilizzare ViewModelProvidersper ottenere la stessa istanza del tuo modello ogni volta che l'attività viene ricreata:

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

        ...
    }
}

Per utilizzare ViewModelProviders, aggiungere quanto segue in dependenciesin app/build.gradle:

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

Nota che l' MyActivityestensione si estende FragmentActivityinvece di estenderla Activity.

Puoi leggere di più su ViewModels qui:


1
Dai

1
@JJD Sono d'accordo con l'articolo che hai pubblicato, uno deve ancora gestire il salvataggio e il ripristino in modo corretto. ViewModelè particolarmente utile se si dispone di set di dati di grandi dimensioni da conservare durante un cambio di stato, come una rotazione dello schermo. Preferisco usare il ViewModelinvece di scriverlo Applicationperché è chiaramente definito, e posso avere più Attività della stessa Applicazione che si comportano correttamente.
Benedikt Köppel

1

Ho scoperto che questa risposta stava causando alcuni arresti anomali nelle versioni Android 9 e 10. Penso che sia un buon approccio ma quando stavo guardando un po 'di codice Android ho scoperto che mancava un costruttore. La risposta è piuttosto vecchia, quindi al momento probabilmente non ce n'era bisogno. Quando ho aggiunto il costruttore mancante e l'ho chiamato dal creatore, il crash è stato risolto.

Quindi ecco il codice modificato:

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

0

Per aumentare altre risposte: se si dispone di più viste composte personalizzate con lo stesso ID e vengono tutte ripristinate con lo stato dell'ultima vista in una modifica della configurazione, è sufficiente indicare alla vista di inviare solo eventi di salvataggio / ripristino a se stesso scavalcando un paio di metodi.

class MyCompoundView : ViewGroup {

    ...

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

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

Per una spiegazione di ciò che sta accadendo e perché funziona, consulta questo post sul blog . Fondamentalmente gli ID di visualizzazione per bambini della vista composta sono condivisi da ciascuna vista composta e il ripristino dello stato viene confuso. Inviando solo lo stato per la vista composta stessa, impediamo ai loro figli di ricevere messaggi misti da altre viste composte.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.