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

Я успешно реализовал onRetainNonConfigurationInstance() для моего основного Activity для сохранения и восстановления некоторых критических компонентов в изменении ориентации экрана.

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

Есть ли хитрый способ реализовать что-то похожее на onRetainNonConfigurationInstance() для пользовательского представления или мне нужно просто реализовать методы в пользовательском представлении, которые позволяют мне получить и установить его "состояние"?

Ответ 1

Вы делаете это, реализуя 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 и View SavedState. Вы должны выполнять всю работу по чтению и записи в Parcel и Parcel в классе SavedState. Затем ваш класс View может выполнять работу по извлечению членов состояния и выполнению работы, необходимой для возвращения класса в допустимое состояние.

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

Другим простым примером этого является CompoundButton

Ответ 2

Я думаю, что это гораздо более простая версия. 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);
  }
}

Ответ 3

Вот еще один вариант, в котором используется сочетание двух вышеуказанных методов. Объединяя скорость и правильность Parcelable с простотой a 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;
    }
}

Ответ 4

Ответы здесь уже замечательные, но не обязательно работают для пользовательских групп представлений. Чтобы все пользовательские представления сохраняли свое состояние, вы должны переопределить onSaveInstanceState() и onRestoreInstanceState(Parcelable state) в каждом классе. Вам также необходимо убедиться, что у всех у них есть уникальные идентификаторы, независимо от того, были ли они завышены от xml или добавлены программно.

То, что я придумал, было замечательно, как ответ Kobor42, но ошибка осталась, потому что я добавлял Views в пользовательскую ViewGroup программно и не назначал уникальные идентификаторы.

Ссылка, совместно используемая mato, будет работать, но это означает, что ни один из отдельных представлений не управляет своим собственным состоянием - все состояние сохраняется в методах ViewGroup.

Проблема заключается в том, что, когда несколько кратных этих групп представлений добавляются в макет, идентификаторы их элементов из 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);
    } 
}

Ответ 5

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

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

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

Ответ 6

Вместо использования 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 здесь:

Ответ 7

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

class MyCompoundView : ViewGroup {

    ...

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

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

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