Özel görünümlerin ekran yönü değişikliklerinde durumu kaybetmesini önleme


248

Ekran yönlendirmesi değişikliklerinde belirli kritik bileşenleri kaydetmek ve geri yüklemek onRetainNonConfigurationInstance()için ana sistemim için başarıyla uyguladım Activity.

Ancak görünüşe göre, yönelim değiştiğinde özel görünümlerim sıfırdan yeniden oluşturuluyor. Söz konusu özel görünüm bir X / Y grafiği olduğundan ve çizilen noktalar özel görünümde saklandığından, benim durumumda rahatsız edici olsa da bu mantıklıdır.

onRetainNonConfigurationInstance()Özel bir görünüm için benzer bir şey uygulamak için kurnaz bir yolu var mı , ya da sadece özel durumunu almak ve "durumunu" ayarlamak için izin veren yöntemler uygulamak gerekir?

Yanıtlar:


415

Sen uygulayarak bunu View#onSaveInstanceStateve View#onRestoreInstanceStateve uzayan View.BaseSavedStatesınıfı.

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

Çalışma, View ve View'un SavedState sınıfı arasında bölünür. Tüm okuma ve yazma işlerini sınıfta Parcelve SavedStatesınıfta yapmalısınız . Daha sonra View sınıfınız devlet üyelerini çıkarma ve sınıfı geçerli bir duruma döndürmek için gerekli işi yapma işini yapabilir.

Notlar: View#onSavedInstanceStateve > = 0 değerini döndürürse View#onRestoreInstanceStatesizin için otomatik olarak çağrılır. View#getIdBu, xml'de bir kimlik verdiğinizde veya setIdmanuel olarak aradığınızda gerçekleşir . Aksi takdirde aramak zorunda View#onSaveInstanceStateve Parcelable sen almak parsel döndü yazma Activity#onSaveInstanceStatedurumunu kaydetmek ve daha sonra okumak ve onu geçmek View#onRestoreInstanceStatedan Activity#onRestoreInstanceState.

Bunun bir başka basit örneği CompoundButton


14
Buraya v4 destek kitaplığı ile Fragments kullanırken çalışmayanlar için, destek kitaplığının View's onSaveInstanceState / onRestoreInstanceState'i sizin için çağırdığını görmüyorum; açıkça FragmentActivity veya Fragment'ta uygun bir yerden kendiniz çağırmanız gerekir.
magneticMonster

69
Bunu uyguladığınız Özel Görünüm'ün benzersiz bir kimlik kümesine sahip olması gerektiğini, aksi takdirde durumu birbirleriyle paylaşacaklarını unutmayın. SavedState, CustomView kimliğine karşı saklanır, bu nedenle aynı kimliğe veya kimliğe sahip birden fazla CustomView'ınız varsa, son CustomView.onSaveInstanceState () öğesine kaydedilen parsel, görünümler geri yüklendi.
Nick Street

5
Bu yöntem benim için iki özel görünümle (biri diğerini genişleterek) işe yaramadı. Görüşümü geri yüklerken bir ClassNotFoundException alıyorum. Kobor42'nin cevabında Bundle yaklaşımını kullanmak zorunda kaldım.
Chris Feist

3
onSaveInstanceState()ve (üst sınıfları gibi) onRestoreInstanceState()olmalı protected, değil public. Onları ortaya çıkarmak için bir neden yok ...
XåpplI'-I0llwlg'I -

7
Bu iyi çalışmıyor özel bir kaydederken BaseSaveStateRecyclerView genişleten sınıf için almak, Parcel﹕ Class not found when unmarshalling: android.support.v7.widget.RecyclerView$SavedState java.lang.ClassNotFoundException: android.support.v7.widget.RecyclerView$SavedState: Burada aşağı yazıldığı hata düzeltmesi yapmak gerekir böylece github.com/ksoichiro/Android-ObservableScrollView/commit/... ClassLoader kullanımı ( Super State yüklemek için RecyclerView.class)
EpicPandaForce

459

Bence bu çok daha basit bir versiyon. Bundleyerleşik bir tiptirParcelable

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

5
Neden olmaz onRestoreInstanceState eğer bir Paket ile çağrılabilir onSaveInstanceStatebir Bundle döndü?
Qwertie

5
OnRestoreInstancemiras alınır. Başlığı değiştiremeyiz. Parcelablesadece bir arayüz, Bundlebunun için bir uygulamadır.
Kobor42

5
Bu şekilde teşekkürler çok daha iyi ve kaydedilen durum özel LoadState için sınıf yükleyici doğru ayarlayamıyor gibi görünüyor, çünkü özel görünümler için SavedState çerçevesini kullanırken BadParcelableException önler!
Ian Warwick

3
Bir etkinlikte aynı görünümde birkaç örneğim var. Hepsi xml'de benzersiz kimliklere sahiptir. Ama yine de hepsi son görünümün ayarlarını alıyor. Herhangi bir fikir?
Christoffer

15
Bu çözüm iyi olabilir, ancak kesinlikle güvenli değildir. Bunu uygulayarak temel Viewdurumun a olmadığını varsayıyorsunuz Bundle. Tabii ki, şu anda doğrudur, ancak gerçek olduğu garanti edilmeyen bu mevcut uygulama gerçeğine güveniyorsunuz.
Dmitry Zaytsev

18

Yukarıdaki iki yöntemin bir karışımını kullanan başka bir varyant. Hız ve doğruluğun birleştirilmesiParcelable aşağıdakilerin basitliği ileBundle :

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

3
Bu işe yaramıyor. ClassCastException alıyorum ... Ve bunun nedeni, genel bir statik CREATOR'a ihtiyaç duymasıdır, böylece Stateparselinizden başlatır . Lütfen şuraya
mato

8

Buradaki yanıtlar zaten harika, ancak özel ViewGroup'lar için işe yaramıyor. Tüm özel Görünümlerin durumlarını korumasını sağlamak için geçersiz kılmanız onSaveInstanceState()veonRestoreInstanceState(Parcelable state) her sınıfta. Ayrıca, ister xml'den şişirilmiş ister programlı olarak eklenmiş olsunlar, hepsinin benzersiz kimliklere sahip olduğundan emin olmanız gerekir.

Ne geldi Kobor42 cevap gibi gibiydi, ama görünümleri özel bir ViewGroup programlı olarak eklemek ve benzersiz kimlikleri atama çünkü hata kaldı.

Mato tarafından paylaşılan bağlantı işe yarayacaktır, ancak tek tek Görünümlerin hiçbirinin kendi durumunu yönetmediği anlamına gelir - tüm durum ViewGroup yöntemlerine kaydedilir.

Sorun, bu ViewGroups birden çok bir düzene eklendiğinde, xml'deki öğelerinin kimlikleri artık benzersiz değildir (xml'de tanımlanmışsa). Çalışma zamanında, View.generateViewId()bir Görünüm için benzersiz bir kimlik almak üzere statik yöntemi çağırabilirsiniz . Bu yalnızca API 17'den edinilebilir.

İşte benim kod ViewGroup (soyut ve mOriginalValue bir tür değişkeni):

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

Özel kimlik gerçekten bir sorundur, ancak durumun kurtarılmasında değil, görünümün başlatılmasında ele alınması gerektiğini düşünüyorum.
Kobor42

İyi bir nokta. Yapıcıda mViewIds ayarını ve ardından durumun geri yüklenmesi durumunda üzerine yazmayı önerir misiniz?
Fletcher Johns

2

OnRestoreInstanceState tüm özel görünümlerimi son görünüm durumu ile geri yükledi sorun vardı. Özel görünümüme bu iki yöntemi ekleyerek çözdüm:

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

@Override
protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
    dispatchThawSelfOnly(container);
}

DispatchFreezeSelfOnly ve dispatchThawSelfOnly yöntemleri, View'a değil ViewGroup'a aittir. Bu nedenle, özel Görünümünüz yerleşik bir Görünüm'den genişletilir. Çözümünüz geçerli değil.
Hau Luu

1

Bunun yerine kullanmanın onSaveInstanceStateve onRestoreInstanceState, ayrıca bir de kullanabilirsiniz ViewModel. Veri modelinizi genişletin ViewModelve ardından ViewModelProvidersEtkinlik her yeniden oluşturulduğunda modelinizin aynı örneğini almak için kullanabilirsiniz :

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

        ...
    }
}

Kullanım için ViewModelProviders, aşağıdakileri ekleyin dependenciesiçinde app/build.gradle:

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

Senin unutmayın MyActivityuzanır FragmentActivityyerine uzanan Activity.

ViewModels hakkında daha fazla bilgiyi buradan edinebilirsiniz:



1
@JJD Gönderdiğiniz makaleye katılıyorum, hala kaydetme ve düzgün bir şekilde geri yükleme gerekiyor. ViewModelekran döndürme gibi bir durum değişikliği sırasında saklanacak büyük veri kümeleriniz varsa özellikle kullanışlıdır. Yazmak ViewModelyerine kullanmayı tercih ederim , Applicationçünkü açıkça kapsamlıdır ve aynı Uygulamanın birden fazla Etkinliğine doğru davranabilirim.
Benedikt Köppel

1

onu buldum Bu cevabın Android sürüm 9 ve 10'da bazı çökmelere neden . Bunun iyi bir yaklaşım olduğunu düşünüyorum, ancak bazı Android kodlarına bakarken bir kurucu eksik olduğunu öğrendim. Cevap oldukça eskidir, o zaman muhtemelen buna gerek yoktu. Eksik yapıcıyı ekleyip yaratıcıdan çağırdığımda kilitlenme düzeltildi.

İşte düzenlenmiş kod:

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

0

Diğer yanıtları artırmak için - aynı kimliğe sahip birden fazla özel bileşik görünümünüz varsa ve bunların tümü bir yapılandırma değişikliğindeki son görünümün durumu ile geri yükleniyorsa, tek yapmanız gereken görünüme yalnızca kaydetme / geri yükleme olaylarını gönderme birkaç yöntemi geçersiz kılarak kendi kendine.

class MyCompoundView : ViewGroup {

    ...

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

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

Neler olup bittiğine ve bunun neden işe yaradığına ilişkin açıklama için bu blog yayınına bakın . Temel olarak, bileşik görünümünüzün çocuk görünüm kimlikleri, her bir bileşik görünüm tarafından paylaşılır ve durum geri yüklemesi karıştırılır. Yalnızca bileşik görünümün kendisi için durum göndererek, çocuklarının diğer bileşik görünümlerden karışık mesajlar almasını önleriz.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.