Cara mencegah tampilan kustom dari kehilangan status lintas perubahan orientasi

248

Saya telah berhasil menerapkan onRetainNonConfigurationInstance()untuk utama saya Activityuntuk menyimpan dan mengembalikan komponen penting tertentu di seluruh perubahan orientasi layar.

Tapi sepertinya, tampilan khusus saya sedang dibuat ulang dari awal ketika orientasinya berubah. Ini masuk akal, meskipun dalam kasus saya ini merepotkan karena tampilan ubahsuaian yang dimaksud adalah plot X / Y dan titik-titik yang diplot disimpan dalam tampilan ubahsuaian.

Apakah ada cara licik untuk mengimplementasikan sesuatu yang mirip dengan onRetainNonConfigurationInstance()untuk tampilan kustom, atau apakah saya perlu mengimplementasikan metode dalam tampilan kustom yang memungkinkan saya untuk mendapatkan dan mengatur "status" -nya?

Brad Hein
sumber

Jawaban:

415

Anda melakukan ini dengan menerapkan View#onSaveInstanceStatedan View#onRestoreInstanceStatedan memperluas View.BaseSavedStatekelas.

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

Pekerjaan dibagi antara kelas View dan SavedState. Anda harus melakukan semua pekerjaan membaca dan menulis ke dan dari Parceldalam SavedStatekelas. Kemudian kelas View Anda dapat melakukan pekerjaan mengekstraksi anggota negara dan melakukan pekerjaan yang diperlukan untuk mengembalikan kelas ke status yang valid.

Catatan: View#onSavedInstanceStatedan View#onRestoreInstanceStatedipanggil secara otomatis untuk Anda jika View#getIdmengembalikan nilai> = 0. Ini terjadi ketika Anda memberikannya id dalam xml atau menelepon setIdsecara manual. Kalau tidak, Anda harus menelepon View#onSaveInstanceStatedan menulis Parcelable dikembalikan ke paket Anda masuk Activity#onSaveInstanceStateuntuk menyelamatkan negara dan kemudian membacanya dan meneruskannya View#onRestoreInstanceStatedari Activity#onRestoreInstanceState.

Contoh sederhana lain dari ini adalah CompoundButton

Schuler yang kaya
sumber
14
Bagi mereka yang tiba di sini karena ini tidak berfungsi ketika menggunakan Fragmen dengan pustaka dukungan v4, saya perhatikan bahwa pustaka dukungan tampaknya tidak memanggil onSaveInstanceState View / onRestoreInstanceState untuk Anda; Anda harus secara eksplisit menyebutnya sendiri dari tempat yang nyaman di FragmentActivity atau Fragment.
magneticMonster
69
Perhatikan bahwa CustomView tempat Anda menerapkan ini harus memiliki set id unik, jika tidak mereka akan saling berbagi status. SavedState disimpan terhadap id CustomView, jadi jika Anda memiliki beberapa CustomViews dengan id yang sama, atau tanpa id, maka paket yang disimpan dalam CustomView.onSaveInstanceState akhir () akan diteruskan ke semua panggilan ke CustomView.onRestoreInstanceState () ketika tampilan dipulihkan.
Nick Street
5
Metode ini tidak berfungsi untuk saya dengan dua tampilan khusus (satu memperluas yang lain). Saya terus mendapatkan ClassNotFoundException ketika memulihkan pandangan saya. Saya harus menggunakan pendekatan Bundle dalam jawaban Kobor42.
Chris Feist
3
onSaveInstanceState()dan onRestoreInstanceState()harus protected(seperti superclass mereka), bukan public. Tidak ada alasan untuk mengekspos mereka ...
XåpplI'-I0llwlg'I -
7
Ini tidak berfungsi dengan baik ketika menyimpan kustom BaseSaveStateuntuk kelas yang memperluas RecyclerView, Anda mendapatkan Parcel﹕ Class not found when unmarshalling: android.support.v7.widget.RecyclerView$SavedState java.lang.ClassNotFoundException: android.support.v7.widget.RecyclerView$SavedStatesehingga Anda perlu melakukan perbaikan bug yang ditulis di sini: github.com/ksoichiro/Android-ObservableScrollView/commit/… (menggunakan ClassLoader dari RecyclerView.class untuk memuat super state)
EpicPandaForce
459

Saya pikir ini adalah versi yang jauh lebih sederhana. Bundleadalah tipe bawaan yang menerapkanParcelable

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
sumber
5
Mengapa tidak onRestoreInstanceState dipanggil dengan Bundle jika onSaveInstanceStatemengembalikan Bundle?
Qwertie
5
OnRestoreInstancediwariskan. Kami tidak dapat mengubah header. Parcelablehanyalah sebuah antarmuka, Bundlemerupakan implementasi untuk itu.
Kobor42
5
Terima kasih dengan cara ini jauh lebih baik dan menghindari BadParcelableException saat menggunakan kerangka kerja SavedState untuk tampilan khusus karena negara yang disimpan tampaknya tidak dapat mengatur pemuat kelas dengan benar untuk CustomisedState kustom Anda!
Ian Warwick
3
Saya memiliki beberapa contoh tampilan yang sama dalam suatu aktivitas. Mereka semua memiliki id unik di xml. Namun tetap saja semuanya mendapatkan pengaturan tampilan terakhir. Ada ide?
Christoffer
15
Solusi ini mungkin baik-baik saja, tetapi jelas tidak aman. Dengan menerapkan ini, Anda mengasumsikan bahwa Viewstatus dasar bukan a Bundle. Tentu saja, itu benar saat ini, tetapi Anda mengandalkan fakta implementasi saat ini yang tidak dijamin benar.
Dmitry Zaytsev
18

Berikut adalah varian lain yang menggunakan campuran dari dua metode di atas. Menggabungkan kecepatan dan ketepatan Parcelabledengan kesederhanaan 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
sumber
3
Ini tidak berfungsi. Saya mendapatkan ClassCastException ... Dan itu karena itu membutuhkan PENCIPTA statis publik sehingga instantiates Anda Statedari paket. Silakan lihat di: charlesharley.com/2012/programming/…
mato
8

Jawaban di sini sudah bagus, tetapi tidak selalu berfungsi untuk Grup View kustom. Untuk mendapatkan semua Tampilan kustom untuk mempertahankan statusnya, Anda harus mengganti onSaveInstanceState()dan onRestoreInstanceState(Parcelable state)di setiap kelas. Anda juga perlu memastikan mereka semua memiliki id unik, apakah mereka digelembungkan dari xml atau ditambahkan secara programatis.

Apa yang saya hasilkan sangat mirip dengan jawaban Kobor42, tetapi kesalahannya tetap karena saya menambahkan Tampilan ke ViewGroup kustom secara terprogram dan tidak menetapkan id unik.

Tautan yang dibagikan oleh mato akan berfungsi, tetapi itu berarti tidak ada satu pun Tampilan individu yang mengelola keadaan mereka sendiri - seluruh negara disimpan dalam metode ViewGroup.

Masalahnya adalah bahwa ketika beberapa ViewGroup ini ditambahkan ke tata letak, id elemen mereka dari xml tidak lagi unik (jika didefinisikan dalam xml). Saat runtime, Anda dapat memanggil metode statis View.generateViewId()untuk mendapatkan id unik untuk Tampilan. Ini hanya tersedia dari API 17.

Ini kode saya dari ViewGroup (abstrak, dan mOriginalValue adalah variabel tipe):

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
sumber
Id khusus benar-benar masalah, tapi saya pikir itu harus ditangani pada inisialisasi tampilan, dan bukan pada keadaan save.
Kobor42
Poin yang bagus. Apakah Anda menyarankan pengaturan mViewIds di konstruktor lalu timpa jika status dikembalikan?
Fletcher Johns
2

Saya punya masalah yang onRestoreInstanceState memulihkan semua tampilan kustom saya dengan keadaan tampilan terakhir. Saya menyelesaikannya dengan menambahkan dua metode ini ke tampilan kustom saya:

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

@Override
protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
    dispatchThawSelfOnly(container);
}
chrigist
sumber
Metode dispatchFreezeSelfOnly dan dispatchThawSelfOnly milik ViewGroup, bukan View. Jadi jika, Tampilan kustom Anda diperpanjang dari Tampilan bawaan. Solusi Anda tidak berlaku.
Hau Luu
1

Alih-alih menggunakan onSaveInstanceStatedan onRestoreInstanceState, Anda juga dapat menggunakan a ViewModel. Buat model data Anda diperluas ViewModel, dan kemudian Anda bisa menggunakan ViewModelProvidersuntuk mendapatkan contoh yang sama dari model Anda setiap kali Kegiatan diciptakan kembali:

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

        ...
    }
}

Untuk menggunakan ViewModelProviders, tambahkan yang berikut ke dependenciesdalam app/build.gradle:

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

Perhatikan bahwa Anda MyActivitymeluas FragmentActivitybukan hanya memperpanjang Activity.

Anda dapat membaca lebih lanjut tentang ViewModels di sini:

Benedikt Köppel
sumber
1
@ JJD Saya setuju dengan artikel yang Anda posting, masih harus menangani menyimpan dan mengembalikan dengan benar. ViewModelsangat berguna jika Anda memiliki set data besar untuk dipertahankan selama perubahan status, seperti rotasi layar. Saya lebih suka menggunakan ViewModeldaripada menulis ke dalam Applicationkarena jelas cakupannya, dan saya dapat memiliki beberapa Kegiatan dari Aplikasi yang sama berperilaku dengan benar.
Benedikt Köppel
1

Saya menemukan bahwa jawaban ini menyebabkan beberapa crash pada Android versi 9 dan 10. Saya pikir ini pendekatan yang bagus tetapi ketika saya melihat beberapa kode Android saya menemukan itu hilang konstruktor. Jawabannya cukup lama sehingga pada saat itu mungkin tidak perlu. Ketika saya menambahkan konstruktor yang hilang dan memanggilnya dari pencipta, crash diperbaiki.

Jadi di sini adalah kode yang diedit:

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

Untuk menambah jawaban lain - jika Anda memiliki beberapa tampilan majemuk khusus dengan ID yang sama dan semuanya dipulihkan dengan keadaan tampilan terakhir pada perubahan konfigurasi, yang perlu Anda lakukan adalah memberi tahu pandangan untuk hanya mengirim save / restore events untuk dirinya sendiri dengan menimpa beberapa metode.

class MyCompoundView : ViewGroup {

    ...

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

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

Untuk penjelasan tentang apa yang terjadi dan mengapa ini berhasil, lihat posting blog ini . Pada dasarnya ID tampilan anak-anak pandangan majemuk Anda dibagikan oleh setiap tampilan majemuk dan pemulihan keadaan menjadi membingungkan. Dengan hanya mengirim status untuk tampilan halaman majemuk itu sendiri, kami mencegah anak-anak mereka dari menerima pesan campuran dari tampilan halaman lain.

Tom
sumber