Como impedir que visualizações personalizadas percam o estado nas alterações de orientação da tela

248

Eu implementei com sucesso onRetainNonConfigurationInstance()para o meu principal Activitysalvar e restaurar certos componentes críticos nas alterações de orientação da tela.

Mas parece que minhas visualizações personalizadas estão sendo recriadas do zero quando a orientação muda. Isso faz sentido, embora no meu caso seja inconveniente, porque a visualização personalizada em questão é uma plotagem X / Y e os pontos plotados são armazenados na visualização customizada.

Existe uma maneira engenhosa de implementar algo semelhante a onRetainNonConfigurationInstance()um modo de exibição personalizado ou preciso apenas implementar métodos no modo de exibição personalizado que permitam obter e definir seu "estado"?

Brad Hein
fonte

Respostas:

415

Você faz isso através da implementação View#onSaveInstanceStatee View#onRestoreInstanceStatee estender a View.BaseSavedStateclasse.

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

O trabalho é dividido entre as classes View e SavedState da View. Você deve fazer todo o trabalho de ler e escrever de e para Parcela SavedStateturma. Em seguida, sua classe View poderá extrair os membros do estado e fazer o trabalho necessário para retornar a classe a um estado válido.

Notas: View#onSavedInstanceStatee View#onRestoreInstanceStatesão chamadas automaticamente para você se View#getIdretornar um valor> = 0. Isso acontece quando você fornece um ID em xml ou chama setIdmanualmente. Caso contrário, você tem que chamar View#onSaveInstanceStatee escrever o Parcelable voltou para o pacote que você entrar em Activity#onSaveInstanceStatepara salvar o estado e, posteriormente, lê-lo e passá-lo para View#onRestoreInstanceStatepartir Activity#onRestoreInstanceState.

Outro exemplo simples disso é o CompoundButton

Rich Schuler
fonte
14
Para aqueles que chegam aqui porque isso não está funcionando ao usar Fragments com a biblioteca de suporte v4, notei que a biblioteca de suporte não parece chamar onSaveInstanceState / onRestoreInstanceState do View; você deve explicitamente chamá-lo de um local conveniente no FragmentActivity ou Fragment.
magneticMonster
69
Observe que o CustomView ao qual você aplica isso deve ter um ID único definido, caso contrário, eles compartilharão o estado um com o outro. SavedState é armazenado com o ID do CustomView; portanto, se você tiver vários CustomViews com o mesmo ou nenhum ID, o pacote salvo no CustomView.onSaveInstanceState () final será passado para todas as chamadas para CustomView.onRestoreInstanceState () quando o as vistas são restauradas.
Nick Street
5
Esse método não funcionou para mim com duas visualizações personalizadas (uma estendendo a outra). Eu recebia uma ClassNotFoundException ao restaurar minha exibição. Eu tive que usar a abordagem Bundle na resposta de Kobor42.
Chris Feist
3
onSaveInstanceState()e onRestoreInstanceState()deveria ser protected(como a superclasse), não public. Não há razão para expô-los ...
XåpplI'-I0llwlg'I -
7
Isso não funciona bem ao salvar um costume BaseSaveStatepara uma classe que estende o RecyclerView, Parcel﹕ Class not found when unmarshalling: android.support.v7.widget.RecyclerView$SavedState java.lang.ClassNotFoundException: android.support.v7.widget.RecyclerView$SavedStatevocê precisa executar a correção de bug descrita aqui: github.com/ksoichiro/Android-ObservableScrollView/commit/… (usando o ClassLoader de RecyclerView.class para carregar o estado super)
EpicPandaForce
459

Eu acho que esta é uma versão muito mais simples. Bundleé um tipo embutido 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
fonte
5
Por quê que não onRestoreInstanceState seria chamado com um pacote se onSaveInstanceStateretornasse um pacote?
Qwertie
5
OnRestoreInstanceé herdado. Não podemos mudar o cabeçalho. Parcelableé apenas uma interface, Bundleé uma implementação para isso.
Kobor42
5
Graças a essa maneira, é muito melhor e evita BadParcelableException ao usar a estrutura SavedState para visualizações personalizadas, pois o estado salvo parece não conseguir definir o carregador de classes corretamente para o SavedState personalizado!
11263 Ian Warwick
3
Eu tenho várias instâncias da mesma exibição em uma atividade. Todos eles têm IDs exclusivos no xml. Mas ainda todos eles obtêm as configurações da última visualização. Alguma ideia?
Christoffer
15
Esta solução pode estar ok, mas definitivamente não é segura. Ao implementar isso, você assume que o Viewestado base não é um Bundle. Obviamente, isso é verdade no momento, mas você está contando com esse fato de implementação atual que não é garantido.
Dmitry Zaytsev
18

Aqui está outra variante que usa uma mistura dos dois métodos acima. Combinando a velocidade e a correção Parcelablecom a simplicidade de um 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
fonte
3
Isso não funciona. Recebo uma ClassCastException ... E isso é porque ele precisa de um CREATOR estático público para instanciar o seu Statepacote. Por favor, dê uma olhada em: charlesharley.com/2012/programming/…
mato
8

As respostas aqui já são ótimas, mas não necessariamente funcionam para grupos de vistas personalizados. Para que todas as Views personalizadas mantenham seu estado, você deve substituir onSaveInstanceState()e onRestoreInstanceState(Parcelable state)em cada classe. Você também precisa garantir que todos tenham IDs exclusivos, sejam inflados a partir do xml ou adicionados programaticamente.

O que eu criei foi notavelmente como a resposta do Kobor42, mas o erro permaneceu porque eu estava adicionando as Views a um ViewGroup personalizado programaticamente e não atribuindo IDs exclusivos.

O link compartilhado por mato funcionará, mas significa que nenhuma das Visualizações individuais gerencia seu próprio estado - todo o estado é salvo nos métodos do ViewGroup.

O problema é que, quando vários desses grupos de vistas são adicionados a um layout, os IDs de seus elementos do xml não são mais exclusivos (se definidos em xml). No tempo de execução, você pode chamar o método estático View.generateViewId()para obter um ID exclusivo para uma Visualização. Isso está disponível apenas na API 17.

Aqui está o meu código do ViewGroup (é abstrato e mOriginalValue é uma variável 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
fonte
O ID personalizado é realmente um problema, mas acho que deve ser tratado na inicialização da exibição, e não no salvamento do estado.
precisa saber é o seguinte
Bom ponto. Você sugere definir mViewIds no construtor e substituir se o estado for restaurado?
Fletcher Johns
2

Eu tive o problema de que onRestoreInstanceState restaurou todas as minhas visualizações personalizadas com o estado da última visualização. Eu o resolvi adicionando esses dois métodos à minha exibição personalizada:

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

@Override
protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
    dispatchThawSelfOnly(container);
}
chrigist
fonte
Os métodos dispatchFreezeSelfOnly e dispatchThawSelfOnly pertencem ao ViewGroup, não ao View. Portanto, no caso, sua visualização personalizada é estendida de uma visualização incorporada. Sua solução não é aplicável.
Hau Luu
1

Em vez de usar onSaveInstanceStatee onRestoreInstanceState, você também pode usar a ViewModel. Amplie seu modelo de dados ViewModele, em seguida, você pode usar ViewModelProviderspara obter a mesma instância do seu modelo toda vez que a Atividade for recriada:

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, adicione o seguinte dependenciesem app/build.gradle:

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

Observe que você MyActivityestende em FragmentActivityvez de apenas estenderActivity .

Você pode ler mais sobre o ViewModels aqui:

Benedikt Köppel
fonte
1
1
@JJD Concordo com o artigo que você postou, ainda é preciso lidar com salvar e restaurar corretamente. ViewModelé especialmente útil se você tiver grandes conjuntos de dados para reter durante uma alteração de estado, como uma rotação de tela. Eu prefiro usá- ViewModello em vez de escrevê-lo Applicationporque ele tem um escopo claro e posso ter várias atividades do mesmo aplicativo se comportando corretamente.
Benedikt Köppel
1

Descobri que essa resposta estava causando algumas falhas nas versões 9 e 10. do Android. Acho que é uma boa abordagem, mas quando eu estava olhando para algum código do Android , descobri que estava faltando um construtor. A resposta é bastante antiga, portanto, na época, provavelmente não havia necessidade. Quando adicionei o construtor ausente e o chamei do criador, a falha foi corrigida.

Então, aqui está o 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
fonte
0

Para aumentar outras respostas - se você tiver várias visualizações compostas personalizadas com o mesmo ID e todas elas estiverem sendo restauradas com o estado da última visualização em uma alteração na configuração, tudo que você precisa fazer é dizer à visualização para enviar apenas os eventos de salvar / restaurar substituindo alguns métodos.

class MyCompoundView : ViewGroup {

    ...

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

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

Para uma explicação do que está acontecendo e por que isso funciona, consulte esta postagem no blog . Basicamente, os IDs de exibição dos filhos da visualização composta são compartilhados por cada visualização composta e a restauração do estado fica confusa. Ao despachar apenas o estado da própria exibição composta, impedimos que seus filhos recebam mensagens misturadas de outras visualizações compostas.

Tom
fonte