Jak zapobiec utracie stanu niestandardowych widoków przy zmianach orientacji ekranu

248

Z powodzeniem zaimplementowałem onRetainNonConfigurationInstance()dla mojego głównego, Activityaby zapisywać i przywracać niektóre krytyczne komponenty podczas zmian orientacji ekranu.

Ale wygląda na to, że moje niestandardowe widoki są odtwarzane od zera, gdy zmienia się orientacja. Ma to sens, chociaż w moim przypadku jest to niewygodne, ponieważ omawiany widok niestandardowy jest wykresem X / Y, a wykreślone punkty są przechowywane w widoku niestandardowym.

Czy istnieje sprytny sposób na implementację czegoś podobnego do onRetainNonConfigurationInstance()niestandardowego widoku, czy też muszę po prostu implementować metody w niestandardowym widoku, które pozwalają mi uzyskać i ustawić jego „stan”?

Brad Hein
źródło

Odpowiedzi:

415

Można to zrobić poprzez wdrożenie View#onSaveInstanceStatei View#onRestoreInstanceStateoraz rozszerzające View.BaseSavedStateklasę.

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

Praca jest podzielona między klasę View i SavedState klasy View. Trzeba zrobić wszystko, pracę czytania i pisania do iz Parcelw SavedStateklasie. Następnie klasa View może wykonać zadanie wyodrębnienia członków stanu i wykonania pracy niezbędnej do przywrócenia klasy do prawidłowego stanu.

Uwagi: View#onSavedInstanceStatei View#onRestoreInstanceStatesą wywoływane automatycznie, jeśli View#getIdzwraca wartość> = 0. Dzieje się tak, gdy podajesz identyfikator w formacie xml lub wywołujesz setIdręcznie. W przeciwnym razie trzeba zadzwonić View#onSaveInstanceStatei napisać Parcelable wrócił do działki można dostać się Activity#onSaveInstanceStatedo zachowania stanu, a następnie ją przeczytać i przekazać go View#onRestoreInstanceStateod Activity#onRestoreInstanceState.

Innym prostym przykładem tego jest CompoundButton

Rich Schuler
źródło
14
Dla przybywających tutaj, ponieważ nie działa to przy użyciu Fragmentów z biblioteką wsparcia v4, zauważam, że biblioteka obsługi nie wydaje się wywoływać dla ciebie widoku onSaveInstanceState / onRestoreInstanceState; musisz wyraźnie nazwać to samemu z dogodnego miejsca w FragmentActivity lub Fragment.
magneticMonster,
69
Zauważ, że CustomView, do którego chcesz to zastosować, powinien mieć unikalny zestaw identyfikatorów, w przeciwnym razie będą dzielić ze sobą stan. SavedState jest przechowywany z identyfikatorem CustomView, więc jeśli masz wiele CustomView z tym samym identyfikatorem lub bez identyfikatora, wówczas paczka zapisana w końcowej CustomView.onSaveInstanceState () zostanie przekazana do wszystkich wywołań CustomView.onRestoreInstanceState (), gdy widoki są przywracane.
Nick Street,
5
Ta metoda nie działała dla mnie z dwoma niestandardowymi widokami (jeden rozszerzający drugi). Podczas przywracania widoku ciągle pojawiał się wyjątek ClassNotFoundException. Musiałem użyć podejścia opartego na pakiecie w odpowiedzi Kobor42.
Chris Feist
3
onSaveInstanceState()i onRestoreInstanceState()powinno być protected(podobnie jak ich nadklasa), a nie public. Nie ma powodu, aby je ujawniać ...
XåpplI'-I0llwlg'I -
7
To nie działa dobrze, gdy zapisujesz niestandardowy BaseSaveStatedla klasy, która rozszerza RecyclerView, dostajesz, Parcel﹕ Class not found when unmarshalling: android.support.v7.widget.RecyclerView$SavedState java.lang.ClassNotFoundException: android.support.v7.widget.RecyclerView$SavedStatewięc musisz zrobić naprawioną usterkę, która jest zapisana tutaj: github.com/ksoichiro/Android-ObservableScrollView/commit/… (przy użyciu ClassLoader z RecyclerView.class, aby załadować super stan)
EpicPandaForce
459

Myślę, że jest to znacznie prostsza wersja. Bundlejest wbudowanym typem, który implementujeParcelable

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
źródło
5
Dlaczego nie zostałby onRestoreInstanceState wezwany z pakietem, gdyby onSaveInstanceStatezwrócił pakiet?
Qwertie
5
OnRestoreInstancejest dziedziczony. Nie możemy zmienić nagłówka. Parcelablejest tylko interfejsem, Bundlejest implementacją tego.
Kobor42,
5
Dzięki temu sposób jest znacznie lepszy i pozwala uniknąć wyjątku BadParcelableException podczas korzystania z frameworka SavedState do widoków niestandardowych, ponieważ stan zapisany wydaje się nie być w stanie poprawnie ustawić modułu ładującego klasy dla niestandardowego SavedState!
Ian Warwick,
3
Mam kilka wystąpień tego samego widoku w działaniu. Wszystkie mają unikatowe identyfikatory w pliku XML. Ale nadal wszystkie z nich mają ustawienia ostatniego widoku. Jakieś pomysły?
Christoffer
15
To rozwiązanie może być w porządku, ale zdecydowanie nie jest bezpieczne. Realizując to, zakładasz, że Viewstanem bazowym nie jest Bundle. Oczywiście jest to w tej chwili prawda, ale polegasz na obecnym fakcie dotyczącym implementacji, który nie jest gwarantowany.
Dmitrij Zajcew
18

Oto kolejny wariant, który wykorzystuje połączenie dwóch powyższych metod. Łącząc szybkość i poprawność Parcelablez prostotą 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
źródło
3
To nie działa Dostaję wyjątek ClassCastException ... A to dlatego, że potrzebuje publicznego statycznego KREATORA, aby utworzył wystąpienie Statez paczki. Proszę spojrzeć na: charlesharley.com/2012/programming/…
mato
8

Odpowiedzi tutaj są już świetne, ale niekoniecznie działają w przypadku niestandardowych grup widoków. Aby wszystkie niestandardowe widoki zachowały swój stan, należy przesłonić onSaveInstanceState()i onRestoreInstanceState(Parcelable state)w każdej klasie. Musisz także upewnić się, że wszystkie mają unikalne identyfikatory, niezależnie od tego, czy są zawyżone z XML lub dodane programowo.

To, co wymyśliłem, było zadziwiająco podobne do odpowiedzi Kobor42, ale błąd pozostał, ponieważ programowo dodawałem Widoki do niestandardowej grupy ViewGroup i nie przypisywałem unikalnych identyfikatorów.

Łącze udostępnione przez mato będzie działać, ale oznacza to, że żaden z poszczególnych widoków nie zarządza własnym stanem - cały stan jest zapisywany w metodach ViewGroup.

Problem polega na tym, że gdy wiele z tych grup ViewGroup jest dodawanych do układu, identyfikatory ich elementów z pliku xml nie są już unikalne (jeśli są zdefiniowane w pliku xml). W czasie wykonywania można wywołać metodę statyczną, View.generateViewId()aby uzyskać unikalny identyfikator widoku. Jest to dostępne tylko w API 17.

Oto mój kod z grupy ViewGroup (jest abstrakcyjny, a mOriginalValue jest zmienną typu):

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
źródło
Niestandardowy identyfikator jest naprawdę problemem, ale myślę, że należy go obsłużyć przy inicjalizacji widoku, a nie przy zapisie stanu.
Kobor42
Słuszna uwaga. Czy sugerujesz ustawienie mViewIds w konstruktorze, a następnie nadpisanie, jeśli stan zostanie przywrócony?
Fletcher Johns
2

Miałem problem, że onRestoreInstanceState przywrócił wszystkie moje niestandardowe widoki ze stanem ostatniego widoku. Rozwiązałem go, dodając te dwie metody do mojego widoku niestandardowego:

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

@Override
protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
    dispatchThawSelfOnly(container);
}
chrigist
źródło
Metody dispatchFreezeSelfOnly i dispatchThawSelfOnly należą do ViewGroup, a nie View. Na wypadek gdyby Twój widok niestandardowy został rozszerzony z widoku wbudowanego. Twoje rozwiązanie nie ma zastosowania.
Hau Luu,
1

Zamiast używać onSaveInstanceStatei onRestoreInstanceStatemożesz także użyć ViewModel. Poszerz swój model danych ViewModel, abyś mógł ViewModelProvidersuzyskać tę samą instancję swojego modelu za każdym razem, gdy działanie zostanie odtworzone:

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

        ...
    }
}

Do użytku ViewModelProviders, dodać następujące dependenciesw app/build.gradle:

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

Zauważ, że twoje MyActivityrozszerzenia FragmentActivityzamiast tylko przedłużania Activity.

Możesz przeczytać więcej o ViewModels tutaj:

Benedikt Köppel
źródło
1
@JJD Zgadzam się z opublikowanym artykułem, nadal trzeba odpowiednio obsługiwać zapisywanie i przywracanie. ViewModeljest szczególnie przydatny, jeśli masz duże zestawy danych do zachowania podczas zmiany stanu, takiej jak obrót ekranu. Wolę używać ViewModelzamiast pisać w, Applicationponieważ ma wyraźny zakres i mogę mieć wiele działań tej samej aplikacji, które działają poprawnie.
Benedikt Köppel,
1

Przekonałem się, że ta odpowiedź spowodowała awarie w wersjach Androida 9 i 10. Myślę, że to dobre podejście, ale kiedy patrzyłem na kod Androida , okazało się, że brakuje konstruktora. Odpowiedź jest dość stara, więc prawdopodobnie nie było takiej potrzeby. Kiedy dodałem brakujący konstruktor i nazwałem go od twórcy, awaria została naprawiona.

Oto edytowany kod:

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];
            }
        };
    }
}
Wirowanie
źródło
0

Aby rozszerzyć inne odpowiedzi - jeśli masz wiele niestandardowych widoków złożonych o tym samym identyfikatorze i wszystkie są przywracane ze stanem ostatniego widoku przy zmianie konfiguracji, wystarczy, że powiesz widokowi, aby wysyłał tylko zdarzenia zapisu / przywracania do siebie, zastępując kilka metod.

class MyCompoundView : ViewGroup {

    ...

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

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

Aby dowiedzieć się, co się dzieje i dlaczego to działa, zobacz ten post na blogu . Zasadniczo identyfikatory widoków dzieci w widoku złożonym są wspólne dla każdego widoku złożonego, a przywracanie stanu jest mylone. Wyłącznie wysyłając stan dla samego widoku złożonego, zapobiegamy otrzymywaniu przez ich dzieci mieszanych wiadomości z innych widoków złożonych.

Tomek
źródło