Як запобігти втраті власного вигляду через зміни орієнтації екрана


248

Я успішно реалізував onRetainNonConfigurationInstance()своє основне, Activityщоб зберегти та відновити певні критичні компоненти через зміни орієнтації екрана.

Але, схоже, мої власні представлення відтворюються з нуля, коли орієнтація змінюється. Це має сенс, хоча в моєму випадку це незручно, оскільки розглянутий нестандартний вигляд - це X / Y-графік, а нанесені точки зберігаються у спеціальному представленні.

Чи є хитрий спосіб реалізувати щось подібне onRetainNonConfigurationInstance()для користувацького перегляду, чи мені потрібно просто реалізувати методи у власному режимі, які дозволяють мені отримати та встановити його "стан"?

Відповіді:


415

Ви можете зробити це шляхом впровадження View#onSaveInstanceStateі View#onRestoreInstanceStateта розширення View.BaseSavedStateкласу.

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

Робота розділена між класом View і класом SavedState View. Ви повинні зробити все роботу читання і запису і від Parcelв SavedStateкласі. Тоді ваш клас View може виконати роботу з вилучення членів штату і виконати роботу, необхідну для повернення класу до дійсного стану.

Примітки: View#onSavedInstanceStateі View#onRestoreInstanceStateвикликаються автоматично для вас, якщо View#getIdповертає значення> = 0. Це відбувається, коли ви надаєте йому ідентифікатор у xml або дзвоните setIdвручну. В іншому випадку ви повинні зателефонувати View#onSaveInstanceStateі написати Parcelable повернувся до посилці ви отримуєте в , Activity#onSaveInstanceStateщоб зберегти стан , а потім прочитати його і передати його View#onRestoreInstanceStateз Activity#onRestoreInstanceState.

Іншим простим прикладом цього є CompoundButton


14
Для тих, хто приїжджає сюди, оскільки це не працює при використанні фрагментів з бібліотекою підтримки v4, зауважу, що бібліотека підтримки, схоже, не викликає перегляду onSaveInstanceState / onRestoreInstanceState для вас; Ви повинні явно називати його самостійно з зручного місця в FragmentActivity або Fragment.
magneticMonster

69
Зауважте, що CustomView, до якого ви застосовуєте це, повинен мати унікальний набір ідентифікаторів, інакше вони поділять стан один з одним. SavedState зберігається проти ідентифікатора CustomView, тому якщо у вас є декілька CustomView з одним і тим же ідентифікатором або без ідентифікатора, то посилка, збережена в остаточному CustomView.onSaveInstanceState (), буде передана у всі виклики до CustomView.onRestoreInstanceState (), коли погляди відновлюються.
Нік-стріт

5
Цей метод не працював для мене з двома власними представленнями (один розширює інший). Я продовжував отримувати ClassNotFoundException, коли відновлював свій погляд. Мені довелося використовувати підхід до розшарування у відповіді Кобор42.
Кріс Фейст

3
onSaveInstanceState()і onRestoreInstanceState()має бути protected(як їх суперклас), а не public. Немає підстав їх виставляти ...
XåpplI'-I0llwlg'I -

7
Це не спрацьовує, коли зберігається звичай BaseSaveStateдля класу, який розширює RecyclerView, ви отримуєте, Parcel﹕ Class not found when unmarshalling: android.support.v7.widget.RecyclerView$SavedState java.lang.ClassNotFoundException: android.support.v7.widget.RecyclerView$SavedStateтому вам потрібно зробити виправлення помилок, записане тут: github.com/ksoichiro/Android-ObservableScrollView/commit/… (використовуючи ClassLoader програми RecyclerView.class для завантаження супердержави)
EpicPandaForce

459

Я думаю, що це набагато простіша версія. Bundleце вбудований тип, який реалізує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);
  }
}

5
Чому б не onRestoreInstanceState подзвонили з пачкою, якщо onSaveInstanceStateповернули пакет?
Qwertie

5
OnRestoreInstanceпередається у спадок. Ми не можемо змінити заголовок. Parcelableце просто інтерфейс, Bundleце реалізація для цього.
Kobor42

5
Завдяки цьому спосіб набагато краще і уникає BadParcelableException при використанні рамки SavedState для користувацьких представлень, оскільки збережений стан, здається, не в змозі правильно встановити завантажувач класів для вашого спеціального SavedState!
Ян Ворвік

3
У мене є кілька випадків одного погляду в діяльності. Усі вони мають унікальні ідентифікатори в xml. Але все ж усі вони отримують налаштування останнього виду. Будь-які ідеї?
Крістофер

15
Це рішення може бути нормальним, але це, безумовно, не є безпечним. Реалізуючи це, ви припускаєте, що базовий Viewстан не є Bundle. Звичайно, це вірно на даний момент, але ви покладаєтесь на цей чинний факт впровадження, який не гарантовано є правдою.
Дмитро Зайцев

18

Ось ще один варіант, в якому використовується суміш двох вищеописаних методів. Поєднуючи швидкість і правильність роботи Parcelableз простотою 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
Це не працює. Я отримую ClassCastException ... І це тому, що йому потрібен загальнодоступний статичний СТВОРИТЕЛЬ, щоб він інстанціював ваш Stateз посилки. Будь ласка , зверніть увагу на: charlesharley.com/2012/programming / ...
Мат

8

Відповіді тут вже чудові, але вони не обов'язково працюють для користувацьких ViewGroups. Щоб отримати всі власні перегляди для збереження їх стану, ви повинні перекрити їх onSaveInstanceState()і onRestoreInstanceState(Parcelable state)в кожному класі. Вам також потрібно переконатися, що всі вони мають унікальні ідентифікатори, незалежно від того, чи вони завищені від xml чи додані програмно.

Те, що я придумав, було надзвичайно схожим на відповідь Kobor42, але помилка залишилася, оскільки я додавав перегляди до користувацького ViewGroup програмно і не призначав унікальні ідентифікатори.

Посилання, яким поділяється mato, буде працювати, але це означає, що жоден з окремих представлень не управляє власним станом - весь спосіб зберігається методами ViewGroup.

Проблема полягає в тому, що коли декілька цих ViewGroups додаються до макета, ідентифікатори їх елементів із xml вже не є унікальними (якщо їх визначено у xml). Під час виконання ви можете викликати статичний метод, View.generateViewId()щоб отримати унікальний ідентифікатор для представлення даних. Це доступно лише в API 17.

Ось мій код з ViewGroup (він абстрактний, а mOriginalValue - змінна тип):

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

Користувацький ідентифікатор справді є проблемою, але я думаю, що його слід обробляти при ініціалізації перегляду, а не при збереженні стану.
Кобор42

Гарна думка. Ви пропонуєте встановити mViewIds в конструкторі, а потім перезаписати, якщо стан відновлено?
Флетчер Джонс

2

У мене виникла проблема, що OnRestoreInstanceState відновив усі мої власні представлення зі станом останнього перегляду. Я вирішив це, додавши ці два методи до власного перегляду:

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

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

Методи dispatchFFreezeSelfOnly та dispatchThawSelfOnly належать до ViewGroup, а не View. Тож у випадку, якщо ваш власний перегляд буде розширено із перегляду вбудованих даних Ваше рішення не застосовується.
Hau Luu

1

Замість використання onSaveInstanceStateі onRestoreInstanceState, ви також можете використовувати a ViewModel. Зробіть свою модель даних розширеною ViewModel, а потім ви можете використовувати один ViewModelProvidersі той же екземпляр своєї моделі щоразу, коли активність відтворена:

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

        ...
    }
}

Щоб скористатися ViewModelProviders, додайте dependenciesв app/build.gradle:

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

Зауважте, що ваші MyActivityрозширення FragmentActivityзамість того, щоб просто збільшувати Activity.

Більше про ViewModels ви можете прочитати тут:



1
@JJD Я погоджуюся з опублікованою вами статтею, все-таки потрібно правильно обробляти збереження та відновлення. ViewModelособливо зручно, якщо у вас є великі набори даних для збереження під час зміни стану, наприклад обертання екрана. Я вважаю за краще використовувати ViewModelзамість того, щоб писати це, Applicationтому що він чітко визначений, і я можу мати кілька дій однієї програми, що ведуть себе правильно.
Бенедікт Кьоппель

1

Я виявив, що ця відповідь спричиняє деякі збої в версіях Android 9 і 10. Я думаю, що це вдалий підхід, але, переглядаючи якийсь код Android, я виявив, що в ньому відсутній конструктор. Відповідь досить стара, тому, напевно, в цьому не було потреби. Коли я додав відсутній конструктор і зателефонував йому від творця, збій був виправлений.

Отож ось відредагований код:

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

Щоб збільшити інші відповіді - якщо у вас декілька спеціальних представлених складних представлень з однаковим ідентифікатором, і всі вони відновлюються зі станом останнього перегляду про зміну конфігурації, все, що вам потрібно зробити, - це сказати погляду, щоб лише відправити збереження / відновлення подій до себе, перекривши пару методів.

class MyCompoundView : ViewGroup {

    ...

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

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

Пояснення того, що відбувається і чому це працює, дивіться у цій публікації блогу . В основному, дитячі ідентифікатори представленого представлення вашого спільного представлення поділяються на кожен складений вид, і відновлення стану плутається. Лише розсилаючи стан для самого складеного представлення, ми не даємо дітям отримувати змішані повідомлення від інших складних представлень.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.