Cómo evitar que las vistas personalizadas pierdan estado en los cambios de orientación de pantalla

Implementé exitosamente onRetainNonConfigurationInstance() para mi Activity principal para guardar y restaurar ciertos componentes críticos en los cambios de orientación de la pantalla.

Pero parece que mis vistas personalizadas se vuelven a crear desde cero cuando cambia la orientación. Esto tiene sentido, aunque en mi caso es inconveniente porque la vista personalizada en cuestión es un gráfico X / Y y los puntos graficados se almacenan en la vista personalizada.

¿Existe alguna manera astuta de implementar algo similar a onRetainNonConfigurationInstance() para una vista personalizada, o simplemente debo implementar métodos en la vista personalizada que me permitan obtener y establecer su “estado”?

Esto se hace implementando View#onSaveInstanceState y View#onRestoreInstanceState y extendiendo la clase 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 restre 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 CREATOR = new Parcelable.Creator() { public SavedState createFromParcel(Parcel in) { return new SavedState(in); } public SavedState[] newArray(int size) { return new SavedState[size]; } }; } } 

El trabajo se divide entre la vista y la clase SavedState de la vista. Debe hacer todo el trabajo de lectura y escritura hacia y desde el Parcel en la clase SavedState . Luego, su clase View puede hacer el trabajo de extraer a los miembros del estado y hacer el trabajo necesario para que la clase vuelva a tener un estado válido.

Notas: La View#onSavedInstanceState y la View#onRestoreInstanceState se View#onRestoreInstanceState automáticamente si View#getId un valor> = 0. Esto ocurre cuando le das una identificación en xml o setId a setId manualmente. De lo contrario, debe llamar a View#onSaveInstanceState y escribir el Parcelable devuelto al paquete que obtiene en Activity#onSaveInstanceState para guardar el estado y, posteriormente, leerlo y pasarlo a View#onRestoreInstanceState from Activity#onRestoreInstanceState .

Otro ejemplo simple de esto es el CompoundButton

Creo que esta es una versión mucho más simple. Bundle es un tipo incorporado que implementa 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); } } 

Aquí hay otra variante que usa una combinación de los dos métodos anteriores. Combinando la velocidad y la corrección de Parcelable con la simplicidad de un 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; } } 

Las respuestas aquí ya son geniales, pero no necesariamente funcionan para ViewGroups personalizados. Para obtener todas las vistas personalizadas y conservar su estado, debe anular onSaveInstanceState() y onRestoreInstanceState(Parcelable state) en cada clase. También debe asegurarse de que todos tengan identificadores únicos, ya sea que estén inflados desde xml o agregados mediante progtwigción.

Lo que se me ocurrió fue notablemente como la respuesta de Kobor42, pero el error se mantuvo porque estaba agregando las Vistas a un ViewGroup personalizado programáticamente y no asignaba identificadores únicos.

El enlace compartido por mato funcionará, pero significa que ninguna de las Vistas individuales administra su propio estado: todo el estado se guarda en los métodos de ViewGroup.

El problema es que cuando se agregan varios de estos ViewGroups a un diseño, los identificadores de sus elementos del xml ya no son únicos (si están definidos en xml). En tiempo de ejecución, puede llamar al método estático View.generateViewId() para obtener una identificación única para una vista. Esto solo está disponible en la API 17.

Aquí está mi código del ViewGroup (es abstracto, y mOriginalValue es una variable de tipo):

 public abstract class DetailRow extends LinearLayout { private static final String SUPER_INSTANCE_STATE = "saved_instance_state_plotble"; 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 plotble 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); } } 

Para boost otras respuestas: si tiene varias vistas compuestas personalizadas con la misma ID y todas se restauran con el estado de la última vista en un cambio de configuración, todo lo que necesita hacer es indicar a la vista que solo envíe eventos de guardar / restaurar a sí mismo anulando un par de métodos.

 class MyCompoundView : ViewGroup { ... override fun dispatchSaveInstanceState(container: SparseArray) { dispatchFreezeSelfOnly(container) } override fun dispatchRestoreInstanceState(container: SparseArray) { dispatchThawSelfOnly(container) } } 

Para obtener una explicación de lo que está sucediendo y por qué esto funciona, consulte esta publicación en el blog . Básicamente, las vistas de vista de los niños de su vista compuesta son compartidas por cada vista compuesta y la restauración de estado se confunde. Al solo enviar el estado de la vista compuesta en sí, impedimos que sus hijos reciban mensajes mezclados de otras vistas compuestas.