Как предотвратить потерю состояния пользовательских видов при изменении ориентации экрана

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

Работа разделена между представлением и классом представления SavedState. Вы должны сделать все работу чтения и записи и от 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, поэтому, если у вас есть несколько CustomViews с одинаковым идентификатором или без идентификатора, то посылка, сохраненная в окончательном CustomView.onSaveInstanceState (), будет передана во все вызовы CustomView.onRestoreInstanceState (), когда виды восстановлены.
Nick Street
5
Этот метод не работал для меня с двумя пользовательскими представлениями (одно расширяет другое). Я продолжал получать ClassNotFoundException при восстановлении моего представления. Мне пришлось использовать подход Bundle в ответе Kobor42.
Крис Файст
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);
  }
}
Kobor42
источник
5
Почему бы не onRestoreInstanceState вызвать с Bundle, если onSaveInstanceStateвернули Bundle?
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 ... И это потому, что ему нужен общедоступный статический CREATOR, чтобы он создавал ваш экземпляр 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);
    } 
}
Флетчер Джонс
источник
Пользовательский идентификатор - это действительно проблема, но я думаю, что его следует обрабатывать при инициализации представления, а не при сохранении состояния.
Kobor42
Хорошая точка зрения. Вы предлагаете установить mViewIds в конструкторе, а затем перезаписать, если состояние восстановлено?
Флетчер Джонс
2

У меня была проблема, что onRestoreInstanceState восстановил все мои пользовательские представления с состоянием последнего представления. Я решил это, добавив эти два метода в мой пользовательский вид:

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

@Override
protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
    dispatchThawSelfOnly(container);
}
chrigist
источник
Методы dispatchFreezeSelfOnly и dispatchThawSelfOnly принадлежат ViewGroup, а не View. Таким образом, в случае, ваш пользовательский вид расширяется от встроенного вида. Ваше решение не применимо.
Хау Луу
1

Вместо использования onSaveInstanceStateи onRestoreInstanceStateвы также можете использовать 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];
            }
        };
    }
}
Wirling
источник
0

Для дополнения других ответов - если у вас есть несколько пользовательских составных представлений с одним и тем же идентификатором, и все они восстанавливаются с состоянием последнего представления об изменении конфигурации, все, что вам нужно сделать, это указать представлению только отправлять события сохранения / восстановления. к себе, переопределяя пару методов.

class MyCompoundView : ViewGroup {

    ...

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

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

Для объяснения того, что происходит и почему это работает, смотрите этот пост в блоге . В основном идентификаторы дочерних видов вашего составного представления совместно используются каждым составным представлением, и восстановление состояния становится запутанным. Отправляя только состояние для самого составного представления, мы запрещаем их дочерним элементам получать смешанные сообщения из других составных представлений.

Том
источник
0

Легко с Kotlin

@Parcelize
class MyState(val superSaveState: Parcelable?, val loading: Boolean) : View.BaseSavedState(superSaveState), Parcelable


class MyView : View {

    var loading: Boolean = false

    override fun onSaveInstanceState(): Parcelable? {
        val superState = super.onSaveInstanceState()
        return MyState(superState, loading)
    }

    override fun onRestoreInstanceState(state: Parcelable?) {
        val myState = state as? MyState
        super.onRestoreInstanceState(myState?.superSaveState)

        loading = myState?.loading ?: false
        //redraw
    }
}
Захар Родионов
источник