So verhindern Sie, dass benutzerdefinierte Ansichten bei Änderungen der Bildschirmausrichtung den Status verlieren

248

Ich habe erfolgreich onRetainNonConfigurationInstance()für mein Hauptgerät implementiert Activity, um bestimmte kritische Komponenten bei Änderungen der Bildschirmausrichtung zu speichern und wiederherzustellen.

Aber es scheint, dass meine benutzerdefinierten Ansichten von Grund auf neu erstellt werden, wenn sich die Ausrichtung ändert. Dies ist sinnvoll, obwohl es in meinem Fall unpraktisch ist, da es sich bei der fraglichen benutzerdefinierten Ansicht um ein X / Y-Diagramm handelt und die gezeichneten Punkte in der benutzerdefinierten Ansicht gespeichert werden.

Gibt es eine clevere Möglichkeit, etwas Ähnliches onRetainNonConfigurationInstance()für eine benutzerdefinierte Ansicht zu implementieren , oder muss ich nur Methoden in der benutzerdefinierten Ansicht implementieren, mit denen ich den "Status" abrufen und festlegen kann?

Brad Hein
quelle

Antworten:

415

Sie tun dies durch die Implementierung View#onSaveInstanceStateund View#onRestoreInstanceStateund die Verlängerung View.BaseSavedStateKlasse.

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

Die Arbeit wird zwischen der View- und der SavedState-Klasse der View aufgeteilt. Sie sollten die ganze Arbeit des Lesens und Schreibens zu und von Parcelder SavedStateKlasse erledigen . Anschließend kann Ihre View-Klasse die Statusmitglieder extrahieren und die erforderlichen Arbeiten ausführen, um die Klasse wieder in einen gültigen Status zu versetzen.

Hinweise: View#onSavedInstanceStateund View#onRestoreInstanceStatewerden automatisch für Sie aufgerufen, wenn View#getIdein Wert> = 0 zurückgegeben wird. Dies geschieht, wenn Sie ihm eine ID in XML geben oder setIdmanuell aufrufen . Andernfalls müssen Sie anrufen View#onSaveInstanceStateund schreiben die Parcel auf das Paket zurück Sie erhalten Activity#onSaveInstanceStateden Zustand zu speichern und sie anschließend und es passieren lesen View#onRestoreInstanceStateaus Activity#onRestoreInstanceState.

Ein weiteres einfaches Beispiel hierfür ist das CompoundButton

Rich Schuler
quelle
14
Für diejenigen, die hier ankommen, weil dies bei Verwendung von Fragmenten mit der v4-Unterstützungsbibliothek nicht funktioniert, stelle ich fest, dass die Unterstützungsbibliothek den onSaveInstanceState / onRestoreInstanceState der Ansicht nicht für Sie aufzurufen scheint. Sie müssen es explizit von einem geeigneten Ort in der FragmentActivity oder dem Fragment aus aufrufen.
magneticMonster
69
Beachten Sie, dass für die benutzerdefinierte Ansicht, auf die Sie dies anwenden, eine eindeutige ID festgelegt sein sollte, da sie sonst den Status miteinander teilen. SavedState wird anhand der ID von CustomView gespeichert. Wenn Sie also mehrere CustomViews mit derselben oder keiner ID haben, wird das in der endgültigen Datei CustomView.onSaveInstanceState () gespeicherte Paket an alle Aufrufe von CustomView.onRestoreInstanceState () übergeben Ansichten werden wiederhergestellt.
Nick Street
5
Diese Methode funktionierte bei mir nicht mit zwei benutzerdefinierten Ansichten (eine erweitert die andere). Beim Wiederherstellen meiner Ansicht wurde immer wieder eine ClassNotFoundException angezeigt. Ich musste den Bundle-Ansatz in Kobor42s Antwort verwenden.
Chris Feist
3
onSaveInstanceState()und onRestoreInstanceState()sollte protected(wie ihre Oberklasse) nicht sein public. Kein Grund, sie zu entlarven ...
XåpplI'-I0llwlg'I -
7
Dies funktioniert nicht gut, wenn Sie eine benutzerdefinierte BaseSaveStateKlasse für eine Klasse speichern , die RecyclerView erweitert. Parcel﹕ Class not found when unmarshalling: android.support.v7.widget.RecyclerView$SavedState java.lang.ClassNotFoundException: android.support.v7.widget.RecyclerView$SavedStateSie müssen daher die hier beschriebene Fehlerbehebung durchführen: github.com/ksoichiro/Android-ObservableScrollView/commit/… (mithilfe des ClassLoader von RecyclerView.class zum Laden des Super-Status)
EpicPandaForce
459

Ich denke, das ist eine viel einfachere Version. Bundleist ein eingebauter Typ, der implementiertParcelable

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
quelle
5
Warum sollte man nicht onRestoreInstanceState mit einem Bundle angerufen werden, wenn man onSaveInstanceStateein Bundle zurückgibt?
Qwertie
5
OnRestoreInstancewird geerbt. Wir können den Header nicht ändern. Parcelableist nur eine Schnittstelle, Bundleist eine Implementierung dafür.
Kobor42
5
Dank dieser Methode ist viel besser und vermeidet BadParcelableException, wenn das SavedState-Framework für benutzerdefinierte Ansichten verwendet wird, da der gespeicherte Status den Klassenlader für Ihren benutzerdefinierten SavedState anscheinend nicht richtig einstellen kann!
Ian Warwick
3
Ich habe mehrere Instanzen derselben Ansicht in einer Aktivität. Sie haben alle eindeutige IDs in der XML. Trotzdem erhalten alle die Einstellungen der letzten Ansicht. Irgendwelche Ideen?
Christoffer
15
Diese Lösung mag in Ordnung sein, ist aber definitiv nicht sicher. Wenn Sie dies implementieren, gehen Sie davon aus, dass der Basiszustand Viewkein a ist Bundle. Natürlich ist das im Moment wahr, aber Sie verlassen sich auf diese aktuelle Implementierung, die nicht garantiert wahr ist.
Dmitry Zaytsev
18

Hier ist eine andere Variante, die eine Mischung der beiden oben genannten Methoden verwendet. Kombinieren Sie die Geschwindigkeit und Korrektheit von Parcelablemit der Einfachheit eines 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
quelle
3
Das funktioniert nicht. Ich erhalte eine ClassCastException ... Und das liegt daran, dass ein öffentlicher statischer CREATOR benötigt wird, damit er Sie Statevom Paket instanziiert . Bitte werfen Sie einen Blick auf: charlesharley.com/2012/programming/…
mato
8

Die Antworten hier sind bereits großartig, funktionieren aber nicht unbedingt für benutzerdefinierte ViewGroups. Damit alle benutzerdefinierten Ansichten ihren Status beibehalten, müssen Sie onSaveInstanceState()und onRestoreInstanceState(Parcelable state)in jeder Klasse überschreiben . Sie müssen auch sicherstellen, dass alle eindeutige IDs haben, unabhängig davon, ob sie aus XML aufgeblasen oder programmgesteuert hinzugefügt wurden.

Was ich mir ausgedacht habe, war bemerkenswert ähnlich wie die Antwort von Kobor42, aber der Fehler blieb bestehen, weil ich die Ansichten programmgesteuert zu einer benutzerdefinierten ViewGroup hinzufügte und keine eindeutigen IDs zuwies.

Der von mato freigegebene Link funktioniert, bedeutet jedoch, dass keine der einzelnen Ansichten ihren eigenen Status verwaltet. Der gesamte Status wird in den ViewGroup-Methoden gespeichert.

Das Problem ist, dass beim Hinzufügen mehrerer dieser ViewGroups zu einem Layout die IDs ihrer Elemente aus der XML nicht mehr eindeutig sind (sofern sie in XML definiert sind). Zur Laufzeit können Sie die statische Methode aufrufen View.generateViewId(), um eine eindeutige ID für eine Ansicht abzurufen. Dies ist nur über API 17 verfügbar.

Hier ist mein Code aus der ViewGroup (er ist abstrakt und mOriginalValue ist eine Typvariable):

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
quelle
Benutzerdefinierte ID ist wirklich ein Problem, aber ich denke, sie sollte bei der Initialisierung der Ansicht und nicht beim Speichern des Status behandelt werden.
Kobor42
Guter Punkt. Schlagen Sie vor, mViewIds im Konstruktor festzulegen und dann zu überschreiben, wenn der Status wiederhergestellt wird?
Fletcher Johns
2

Ich hatte das Problem, dass onRestoreInstanceState alle meine benutzerdefinierten Ansichten mit dem Status der letzten Ansicht wiederherstellte. Ich habe es gelöst, indem ich meiner benutzerdefinierten Ansicht diese beiden Methoden hinzugefügt habe:

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

@Override
protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
    dispatchThawSelfOnly(container);
}
Chrigist
quelle
Die Methoden dispatchFreezeSelfOnly und dispatchThawSelfOnly gehören zu ViewGroup und nicht zu View. Für den Fall, dass Ihre benutzerdefinierte Ansicht von einer integrierten Ansicht erweitert wird. Ihre Lösung ist nicht anwendbar.
Hau Luu
1

Anstelle von onSaveInstanceStateund onRestoreInstanceStatekönnen Sie auch a verwendenViewModel . Erweitern Sie Ihr Datenmodell ViewModel, und verwenden Sie es dann ViewModelProviders, um bei jeder Neuerstellung der Aktivität dieselbe Instanz Ihres Modells abzurufen:

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

        ...
    }
}

Benutzen ViewModelProviders Folgendes zu dependenciesin hinzu app/build.gradle:

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

Beachten Sie, dass Sie sich MyActivityerweitern, FragmentActivityanstatt nur zu erweitern Activity.

Weitere Informationen zu ViewModels finden Sie hier:

Benedikt Köppel
quelle
1
Bitte werfen Sie einen Blick auf Android ViewModel Architecture Component als schädlich
JJD
1
@JJD Ich stimme dem Artikel zu, den Sie gepostet haben. Man muss immer noch richtig mit Speichern und Wiederherstellen umgehen. ViewModelDies ist besonders praktisch, wenn Sie während einer Statusänderung große Datenmengen behalten müssen, z. B. bei einer Bildschirmdrehung. Ich bevorzuge es, das zu verwenden, ViewModelanstatt es zu schreiben, Applicationda es einen klaren Umfang hat und ich mehrere Aktivitäten derselben Anwendung haben kann, die sich korrekt verhalten.
Benedikt Köppel
1

Ich fand heraus, dass diese Antwort einige Abstürze in den Android-Versionen 9 und 10 verursachte. Ich denke, es ist ein guter Ansatz, aber als ich mir einen Android-Code ansah ich fest, dass ein Konstruktor fehlte. Die Antwort ist ziemlich alt, so dass es zu der Zeit wahrscheinlich keine Notwendigkeit dafür gab. Als ich den fehlenden Konstruktor hinzufügte und ihn vom Ersteller aus aufrief, wurde der Absturz behoben.

Also hier ist der bearbeitete Code:

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];
            }
        };
    }
}
Verkabelung
quelle
0

Um andere Antworten zu erweitern: Wenn Sie mehrere benutzerdefinierte zusammengesetzte Ansichten mit derselben ID haben und alle mit dem Status der letzten Ansicht bei einer Konfigurationsänderung wiederhergestellt werden, müssen Sie der Ansicht lediglich mitteilen, dass nur Speicher- / Wiederherstellungsereignisse ausgelöst werden sollen zu sich selbst durch Überschreiben einiger Methoden.

class MyCompoundView : ViewGroup {

    ...

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

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

Eine Erklärung, was passiert und warum dies funktioniert, finden Sie in diesem Blogbeitrag . Grundsätzlich werden die IDs der untergeordneten Ansicht Ihrer zusammengesetzten Ansicht von jeder zusammengesetzten Ansicht gemeinsam genutzt, und die Wiederherstellung des Status wird verwirrt. Indem wir nur den Status für die zusammengesetzte Ansicht selbst senden, verhindern wir, dass ihre Kinder gemischte Nachrichten aus anderen zusammengesetzten Ansichten erhalten.

Tom
quelle