2010-08-22 4 views
208

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

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

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

ответ

377

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

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

Еще один простой пример это CompoundButton

+13

Для тех, кто прибывает сюда, потому что это не работает при использовании фрагментов с библиотекой поддержки v4, я обратите внимание, что библиотека поддержки, похоже, не вызывает для вас представление onSaveInstanceState/onRestoreInstanceState; вы должны явно называть это самим из удобного места в FragmentActivity или Fragment. – magneticMonster

+50

Обратите внимание, что в CustomView, к которому вы применяете это, должен быть установлен уникальный идентификатор, иначе они будут совместно использовать состояние друг с другом. SavedState хранится напротив идентификатора CustomView, поэтому, если у вас есть несколько CustomView с одним и тем же идентификатором или без id, то пакет, сохраненный в окончательном CustomView.onSaveInstanceState(), будет передан во все вызовы CustomView.onRestoreInstanceState(), когда взгляды будут восстановлены. –

+5

Этот метод не работал для меня с двумя пользовательскими представлениями (один расширял другой). При восстановлении моего представления я продолжал получать ClassNotFoundException. Мне пришлось использовать подход Bundle в ответе Kobor42. – Chris

394

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

Это было именно то, что мне нужно было найти. У меня было все, что нужно для сохранения с помощью пакетов, но я получал исключение. Отсутствующий кусок (который я получил от вас) состоял в том, что мне нужно было вернуть то же самое, что и супер, которое я спасло от супер в свой собственный набор. –

+0

Я бы все же переместил эту переменную состояния в свой собственный статический внутренний класс, чтобы ваши переменные состояния отсутствовали в ваших классовых классах. Но да Bundle намного проще, чем Parcelable. – Blundell

+5

Почему * не будет * 'onRestoreInstanceState' вызываться с Bundle, если' onSaveInstanceState' возвратил Bundle? – Qwertie

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; 
    } 
} 
+2

Это не работает. Я получаю ClassCastException ... И это потому, что ему нужен публичный статический CREATOR, чтобы он создавал ваше «государство» из посылки. Пожалуйста, взгляните на: http://charlesharley.com/2012/programming/views-saving-instance-state-in-android/ – mato

5

ответы здесь уже велики, но не обязательно работать на пользовательских ViewGroups. Чтобы все пользовательские представления сохраняли свое состояние, вы должны переопределить 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); 
    } 
} 
+0

Пользовательский идентификатор действительно является проблемой, но я думаю, что он должен быть обработан при инициализации просмотр, а не сохранение состояния. – Kobor42

+0

Хорошая точка. Предлагаете ли вы установить mViewIds в конструкторе, а затем перезаписать, если состояние восстановлено? –