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

248

He implementado con éxito onRetainNonConfigurationInstance()para mi principalActivity 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 trazados se almacenan en la vista personalizada.

¿Hay una manera astuta de implementar algo similar a onRetainNonConfigurationInstance()una vista personalizada, o solo necesito implementar métodos en la vista personalizada que me permitan obtener y establecer su "estado"?

Brad Hein
fuente

Respuestas:

415

Esto se hace mediante la implementación View#onSaveInstanceStatey View#onRestoreInstanceStatey extendiendo la View.BaseSavedStateclase.

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

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

Notas: View#onSavedInstanceStatey se View#onRestoreInstanceStateles llama automáticamente si View#getIddevuelve un valor> = 0. Esto sucede cuando le da una identificación en xml o llama setIdmanualmente. De lo contrario, usted tiene que llamar View#onSaveInstanceStatey escribir el parcelable regresó a la parcela que se obtiene en Activity#onSaveInstanceStateguardar el estado y posteriormente leerlo y pasarlo a View#onRestoreInstanceStatepartir Activity#onRestoreInstanceState.

Otro ejemplo simple de esto es el CompoundButton

Rich Schuler
fuente
14
Para aquellos que llegan aquí porque esto no funciona cuando se usan Fragmentos con la biblioteca de soporte v4, noto que la biblioteca de soporte no parece llamar a View's onSaveInstanceState / onRestoreInstanceState por usted; tienes que llamarlo explícitamente desde un lugar conveniente en FragmentActivity o Fragment.
magneticMonster
69
Tenga en cuenta que el CustomView al que aplica esto debe tener un conjunto de identificación único, de lo contrario compartirán el estado entre sí. SavedState se almacena con el ID de CustomView, por lo que si tiene múltiples CustomViews con el mismo id, o sin id, el paquete guardado en el CustomView.onSaveInstanceState () final se pasará a todas las llamadas a CustomView.onRestoreInstanceState () cuando el Se restauran las vistas.
Nick Street el
55
Este método no funcionó para mí con dos vistas personalizadas (una que se extiende a la otra). Seguía recibiendo una ClassNotFoundException cuando restauraba mi vista. Tuve que usar el enfoque Bundle en la respuesta de Kobor42.
Chris Feist
3
onSaveInstanceState()y onRestoreInstanceState()debería ser protected(como su superclase), no public. No hay razón para exponerlos ...
XåpplI'-I0llwlg'I -
77
Esto no funciona bien cuando se guarda una costumbre BaseSaveStatepara una clase que extiende RecyclerView, por Parcel﹕ Class not found when unmarshalling: android.support.v7.widget.RecyclerView$SavedState java.lang.ClassNotFoundException: android.support.v7.widget.RecyclerView$SavedStatelo que debe hacer la corrección de errores que está escrita aquí: github.com/ksoichiro/Android-ObservableScrollView/commit/… (usando el ClassLoader de RecyclerView.class para cargar el súper estado)
EpicPandaForce
459

Creo que esta es una versión mucho más simple. Bundlees un tipo incorporado que implementaParcelable

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
fuente
55
¿Por qué no onRestoreInstanceState se llamaría con un paquete si se onSaveInstanceStatedevuelve un paquete?
Qwertie
55
OnRestoreInstancees heredado No podemos cambiar el encabezado. Parcelablees solo una interfaz, Bundlees una implementación para eso.
Kobor42
55
¡Gracias de esta manera es mucho mejor y evita BadParcelableException cuando se usa el marco SavedState para vistas personalizadas ya que el estado guardado parece no poder configurar el cargador de clases correctamente para su SavedState personalizado!
Ian Warwick
3
Tengo varias instancias de la misma vista en una actividad. Todos tienen identificadores únicos en el xml. Pero aún así todos obtienen la configuración de la última vista. ¿Algunas ideas?
Christoffer
15
Esta solución puede estar bien, pero definitivamente no es segura. Al implementar esto, está asumiendo que el Viewestado base no es un Bundle. Por supuesto, eso es cierto en este momento, pero usted confía en este hecho de implementación actual que no se garantiza que sea cierto.
Dmitry Zaytsev
18

Aquí hay otra variante que utiliza una combinación de los dos métodos anteriores. Combinando la velocidad y la corrección de Parcelablecon 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;
    }
}
Blundell
fuente
3
Esto no funciona Obtengo una ClassCastException ... Y eso es porque necesita un CREADOR estático público para que instancia tu Statepaquete. Por favor, eche un vistazo a: charlesharley.com/2012/programming/…
mato
8

Las respuestas aquí ya son geniales, pero no necesariamente funcionan para grupos de vista personalizados. Para que todas las Vistas personalizadas conserven 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 programació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 Grupo de Vistas personalizado mediante programación y no asignando identificadores únicos.

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

El problema es que cuando se agregan múltiples de estos ViewGroups a un diseño, los identificadores de sus elementos del xml ya no son únicos (si está definido 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 API 17.

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

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);
    } 
}
Fletcher Johns
fuente
La identificación personalizada es realmente un problema, pero creo que debería manejarse en la inicialización de la vista y no en el estado de guardar.
Kobor42
Buen punto. ¿Sugiere configurar mViewIds en el constructor y luego sobrescribir si se restaura el estado?
Fletcher Johns
2

Tuve el problema de que onRestoreInstanceState restauró todas mis vistas personalizadas con el estado de la última vista. Lo resolví agregando estos dos métodos a mi vista personalizada:

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

@Override
protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
    dispatchThawSelfOnly(container);
}
chrigist
fuente
Los métodos dispatchFreezeSelfOnly y dispatchThawSelfOnly pertenecen a ViewGroup, no View. Entonces, en caso de que su Vista personalizada se extienda desde una Vista integrada. Su solución no es aplicable.
Hau Luu
1

En lugar de usar onSaveInstanceStatey onRestoreInstanceState, también puede usar a ViewModel. Extienda su modelo de datos ViewModely luego puede usarlo ViewModelProviderspara obtener la misma instancia de su modelo cada vez que se recrea la Actividad:

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

        ...
    }
}

Para usar ViewModelProviders, agregue lo siguiente a dependenciesen app/build.gradle:

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

Tenga en cuenta que su MyActivityextiende en FragmentActivitylugar de solo extender Activity.

Puede leer más sobre ViewModels aquí:

Benedikt Köppel
fuente
1
@JJD Estoy de acuerdo con el artículo que publicó, uno todavía tiene que manejar guardar y restaurar correctamente. ViewModeles especialmente útil si tiene grandes conjuntos de datos para retener durante un cambio de estado, como una rotación de pantalla. Prefiero usar el en ViewModellugar de escribirlo Applicationporque tiene un alcance claro, y puedo tener múltiples actividades de la misma aplicación comportándose correctamente.
Benedikt Köppel
1

Descubrí que esta respuesta estaba causando algunos bloqueos en las versiones de Android 9 y 10. Creo que es un buen enfoque, pero cuando estaba buscando un código de Android descubrí que le faltaba un constructor. La respuesta es bastante antigua, por lo que en ese momento probablemente no haya sido necesaria. Cuando agregué el constructor faltante y lo llamé desde el creador, se solucionó el bloqueo.

Así que aquí está el código editado:

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
fuente
0

Para aumentar otras respuestas: si tiene varias vistas compuestas personalizadas con la misma ID y todas están siendo restauradas con el estado de la última vista en un cambio de configuración, todo lo que necesita hacer es decirle 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<Parcelable>) {
        dispatchFreezeSelfOnly(container)
    }

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

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

Tom
fuente