Comment éviter des vues personnalisées de perdre l'état à travers des changements d'orientation de l'écran
-
30-09-2019 - |
Question
Je l'ai mis en œuvre avec succès onRetainNonConfigurationInstance()
pour ma principale Activity
pour sauver et restaurer certains composants critiques à travers des changements d'orientation de l'écran.
Mais il semble, mes vues personnalisées sont recréés à partir de zéro lorsque les changements d'orientation. Cela est logique, bien que dans mon cas, il est peu pratique parce que la vue personnalisée en question est un tracé X / Y et les points tracés sont stockés dans la vue personnalisée.
Y at-il un moyen astucieux de mettre en œuvre quelque chose de similaire à onRetainNonConfigurationInstance()
pour une vue personnalisée, ou dois-je simplement mettre en œuvre des méthodes dans la vue personnalisée qui me permettent d'obtenir et mis son « état »?
La solution
Pour ce faire, la mise en œuvre View#onSaveInstanceState
et View#onRestoreInstanceState
et étendant le View.BaseSavedState
classe.
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];
}
};
}
}
Le travail est partagé entre la vue et la classe SavedState de la vue. Vous devez faire tout le travail de la lecture et l'écriture et de la Parcel
dans la classe SavedState
. Ensuite, votre classe View peut faire le travail d'extraction des membres de l'Etat et de faire le travail nécessaire pour obtenir le retour de classe à un état valide.
Notes: View#onSavedInstanceState
et View#onRestoreInstanceState
sont appelés automatiquement pour vous si View#getId
retourne une valeur> = 0. Cela se produit lorsque vous lui donnez une carte d'identité en xml ou appelez setId
manuellement. Sinon, vous devez appeler View#onSaveInstanceState
et écrire le Parcelable retourné au colis que vous obtenez dans Activity#onSaveInstanceState
pour enregistrer l'état et de lire la suite et le transmettre à View#onRestoreInstanceState
de Activity#onRestoreInstanceState
.
Un autre exemple simple est le CompoundButton
Autres conseils
Je pense que ceci est une version beaucoup plus simple. Bundle
est un type intégré qui implémente 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);
}
}
Voici une autre variante qui utilise un mélange des deux méthodes ci-dessus.
La combinaison de la vitesse et l'exactitude des Parcelable
avec la simplicité d'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;
}
}
Les réponses sont ici déjà très bien, mais ne fonctionnent pas nécessairement pour ViewGroups personnalisés. Pour obtenir toutes les vues personnalisées de conserver leur état, vous devez remplacer onSaveInstanceState()
et onRestoreInstanceState(Parcelable state)
dans chaque classe.
Vous devez également vous assurer qu'ils ont tous ids uniques, qu'ils soient gonflés de xml ou ajouté un programme.
Qu'est-ce que je suis venu avec était remarquablement comme la réponse de Kobor42, mais l'erreur resté parce que j'ajoutais les vues à une ViewGroup personnalisée et non programmation attribuer des identifiants uniques.
Le lien partagé par mato fonctionnera, mais cela signifie qu'aucun des vues individuelles gérer leur propre état -. Tout l'état est enregistré dans les méthodes de ViewGroup
Le problème est que lorsque plusieurs de ces ViewGroups sont ajoutés à une mise en page, les ids de leurs éléments à partir du XML ne sont plus uniques (si son défini en XML). Lors de l'exécution, vous pouvez appeler la méthode View.generateViewId()
statique pour obtenir un identifiant unique pour une vue. Ceci est uniquement disponible à partir de l'API 17.
Voici mon code de la ViewGroup (elle est abstraite et mOriginalValue est une variable de type):
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);
}
}
J'ai eu le problème onRestoreInstanceState restauré toutes mes vues personnalisées avec l'état de la dernière vue. Je l'ai résolu en ajoutant ces deux méthodes à mon point de vue personnalisé:
@Override
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
dispatchFreezeSelfOnly(container);
}
@Override
protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
dispatchThawSelfOnly(container);
}
Au lieu d'utiliser onSaveInstanceState
et onRestoreInstanceState
, vous pouvez également utiliser un ViewModel
. Assurez-vous étendre votre modèle de données ViewModel
, et vous pouvez utiliser ViewModelProviders
pour obtenir à chaque fois la même instance de votre modèle l'activité est recréée:
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);
...
}
}
Pour utiliser ViewModelProviders
, ajoutez ce qui suit dans dependencies
app/build.gradle
:
implementation "android.arch.lifecycle:extensions:1.1.1"
implementation "android.arch.lifecycle:viewmodel:1.1.1"
Notez que votre MyActivity
étend FragmentActivity
au lieu d'étendre Activity
.
Vous pouvez en savoir plus sur ViewModels ici:
Pour compléter d'autres réponses - si vous avez plusieurs vues composé personnalisé avec le même ID et ils sont tous en cours de restauration à l'état de la dernière vue sur un changement de configuration, tout ce que vous devez faire est de dire la vue seulement expédition sauver / restauration des événements à lui-même en remplaçant deux méthodes.
class MyCompoundView : ViewGroup {
...
override fun dispatchSaveInstanceState(container: SparseArray<Parcelable>) {
dispatchFreezeSelfOnly(container)
}
override fun dispatchRestoreInstanceState(container: SparseArray<Parcelable>) {
dispatchThawSelfOnly(container)
}
}
Pour une explication de ce qui se passe et pourquoi cela fonctionne, voir ce billet de blog . Fondamentalement, votre ID de vue sur la vue des enfants de composés sont partagés par chaque vue composé et est perturbé la restauration de l'État. En seul état de répartition pour la vue composé lui-même, nous empêchons leurs enfants d'obtenir des messages contradictoires d'autres vues composés.