Làm cách nào để ngăn chế độ xem tùy chỉnh mất trạng thái trên các thay đổi hướng màn hình


248

Tôi đã thực hiện thành công onRetainNonConfigurationInstance()cho mục đích chính của mình Activityđể lưu và khôi phục một số thành phần quan trọng trên các thay đổi hướng màn hình.

Nhưng có vẻ như, các chế độ xem tùy chỉnh của tôi đang được tạo lại từ đầu khi định hướng thay đổi. Điều này có ý nghĩa, mặc dù trong trường hợp của tôi, nó bất tiện vì chế độ xem tùy chỉnh trong câu hỏi là một âm mưu X / Y và các điểm được vẽ được lưu trữ trong chế độ xem tùy chỉnh.

Có cách nào xảo quyệt để thực hiện một cái gì đó tương tự như onRetainNonConfigurationInstance()cho chế độ xem tùy chỉnh hay tôi chỉ cần thực hiện các phương thức trong chế độ xem tùy chỉnh cho phép tôi lấy và đặt "trạng thái" của nó?

Câu trả lời:


415

Bạn làm điều này bằng cách thực hiện View#onSaveInstanceStateView#onRestoreInstanceStatevà mở rộng các View.BaseSavedStatelớp.

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

Công việc được phân chia giữa lớp View và lớp SavingState của View. Bạn nên làm tất cả các công việc đọc và viết đến và đi Parceltrong SavedStatelớp. Sau đó, lớp View của bạn có thể thực hiện công việc trích xuất các thành viên trạng thái và thực hiện các công việc cần thiết để đưa lớp trở lại trạng thái hợp lệ.

Ghi chú: View#onSavedInstanceStateView#onRestoreInstanceStateđược gọi tự động cho bạn nếu View#getIdtrả về giá trị> = 0. Điều này xảy ra khi bạn cung cấp cho nó một id trong xml hoặc gọi setIdthủ công. Nếu không, bạn phải gọi View#onSaveInstanceStatevà viết Parcelable được trả lại cho bưu kiện bạn nhận được Activity#onSaveInstanceStateđể lưu trạng thái và sau đó đọc nó và chuyển nó View#onRestoreInstanceStatetừActivity#onRestoreInstanceState .

Một ví dụ đơn giản khác về điều này là CompoundButton


14
Đối với những người đến đây vì điều này không hoạt động khi sử dụng Fragment với thư viện hỗ trợ v4, tôi lưu ý rằng thư viện hỗ trợ dường như không gọi ViewS onSaveInstanceState / onRestoreInstanceState cho bạn; bạn phải tự gọi nó một cách rõ ràng từ một nơi thuận tiện trong FragmentActivity hoặc Fragment.
từ tính

69
Lưu ý rằng CustomView bạn áp dụng điều này để có một bộ id duy nhất, nếu không chúng sẽ chia sẻ trạng thái với nhau. SavingState được lưu trữ dựa trên id của CustomView, vì vậy nếu bạn có nhiều CustomViews có cùng id hoặc không có id, thì bưu kiện được lưu trong CustomView.onSaveInstanceState () sẽ được chuyển vào tất cả các lệnh gọi đến CustomView.onRestoreInstanceState () quan điểm được khôi phục.
Nick Street

5
Phương pháp này không hiệu quả với tôi với hai chế độ xem tùy chỉnh (một chế độ mở rộng khác). Tôi tiếp tục nhận được ClassNotFoundException khi khôi phục chế độ xem của mình. Tôi đã phải sử dụng cách tiếp cận Bundle trong câu trả lời của Kobor42.
Chris Feist

3
onSaveInstanceState()onRestoreInstanceState()nên protected(như siêu lớp của họ), không public. Không có lý do để phơi bày chúng ...
XåpplI'-I0llwlg'I -

7
Điều này không hoạt động tốt khi lưu một tùy chỉnh BaseSaveStatecho một lớp mở rộng RecyclerView, bạn nhận được Parcel﹕ Class not found when unmarshalling: android.support.v7.widget.RecyclerView$SavedState java.lang.ClassNotFoundException: android.support.v7.widget.RecyclerView$SavedStatevì vậy bạn cần thực hiện sửa lỗi được ghi ở đây: github.com/ksoichiro/Android-ObservableScrollView/commit/ tựa (sử dụng ClassLoader của RecyclerView
Class

459

Tôi nghĩ rằng đây là một phiên bản đơn giản hơn nhiều. Bundlelà một loại tích hợp thực hiệnParcelable

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
Tại sao sẽ không onRestoreInstanceState được gọi với Bundle nếu onSaveInstanceStatetrả lại Bundle?
Qwertie

5
OnRestoreInstanceđược thừa hưởng. Chúng tôi không thể thay đổi tiêu đề. Parcelablechỉ là một giao diện, Bundlelà một thực hiện cho điều đó.
Kobor42

5
Cảm ơn cách này tốt hơn nhiều và tránh BadParcelableException khi sử dụng khung SavingState cho chế độ xem tùy chỉnh vì trạng thái đã lưu dường như không thể đặt trình tải lớp chính xác cho SavingState tùy chỉnh của bạn!
Ian Warwick

3
Tôi có một vài trường hợp có cùng quan điểm trong một hoạt động. Tất cả đều có id duy nhất trong xml. Nhưng tất cả trong số họ có được các thiết lập của chế độ xem cuối cùng. Có ý kiến ​​gì không?
Christoffer

15
Giải pháp này có thể ổn, nhưng nó chắc chắn không an toàn. Bằng cách thực hiện điều này, bạn cho rằng Viewtrạng thái cơ sở không phải là a Bundle. Tất nhiên, điều đó là đúng vào lúc này, nhưng bạn đang dựa vào thực tế triển khai hiện tại này không được đảm bảo là đúng.
Dmitry Zaytsev

18

Đây là một biến thể khác sử dụng kết hợp của hai phương pháp trên. Kết hợp tốc độ và tính chính xác Parcelablevới sự đơn giản của 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;
    }
}

3
Điều này không hoạt động. Tôi nhận được ClassCastException ... Và đó là vì nó cần một TẠO tĩnh công khai để nó khởi tạo bạn Statetừ bưu kiện. Xin hãy xem tại: charlesharley.com/2012/programming/ từ
mato

8

Các câu trả lời ở đây đã rất hay, nhưng không nhất thiết phải có hiệu quả đối với Viewgroup tùy chỉnh. Để có được tất cả các Chế độ xem tùy chỉnh để giữ trạng thái của chúng, bạn phải ghi đè onSaveInstanceState()onRestoreInstanceState(Parcelable state)trong mỗi lớp. Bạn cũng cần đảm bảo tất cả chúng đều có id duy nhất, cho dù chúng được thổi phồng từ xml hoặc được thêm vào theo chương trình.

Những gì tôi nghĩ ra giống như câu trả lời của Kobor42, nhưng lỗi vẫn còn do tôi đang thêm Chế độ xem vào một Viewgroup tùy chỉnh theo chương trình và không gán id duy nhất.

Liên kết được chia sẻ bởi mato sẽ hoạt động, nhưng điều đó có nghĩa là không có Chế độ xem riêng lẻ nào quản lý trạng thái của riêng họ - toàn bộ trạng thái được lưu trong các phương thức Viewgroup.

Vấn đề là khi nhiều Viewgroup này được thêm vào bố cục, id của các thành phần của chúng từ xml không còn là duy nhất (nếu được xác định trong xml). Khi chạy, bạn có thể gọi phương thức tĩnh View.generateViewId()để lấy id duy nhất cho Chế độ xem. Điều này chỉ có sẵn từ API 17.

Đây là mã của tôi từ Viewgroup (nó là trừu tượng và mOrigenValue là một biến loại):

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

Id tùy chỉnh thực sự là một vấn đề, nhưng tôi nghĩ nó nên được xử lý khi khởi tạo chế độ xem chứ không phải ở trạng thái lưu.
Kobor42

Điểm tốt. Bạn có đề nghị đặt mViewIds trong hàm tạo sau đó ghi đè nếu trạng thái được khôi phục không?
Fletcher Johns

2

Tôi gặp vấn đề là onRestoreInstanceState đã khôi phục tất cả các chế độ xem tùy chỉnh của tôi với trạng thái của chế độ xem cuối cùng. Tôi đã giải quyết nó bằng cách thêm hai phương thức này vào chế độ xem tùy chỉnh của mình:

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

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

Các phương thức ClarkFreezeSelfOnly và ClarkThawSelfOnly thuộc về Viewgroup, không phải View. Vì vậy, trong trường hợp, Chế độ xem tùy chỉnh của bạn được mở rộng từ Chế độ xem tích hợp. Giải pháp của bạn không được áp dụng.
Hậu Lưu

1

Thay vì sử dụng onSaveInstanceStateonRestoreInstanceState, bạn cũng có thể sử dụng a ViewModel. Làm cho mô hình dữ liệu của bạn mở rộng ViewModelvà sau đó bạn có thể sử dụng ViewModelProvidersđể có cùng phiên bản mô hình của mình mỗi khi Hoạt động được tạo lại:

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

        ...
    }
}

Để sử dụng ViewModelProviders, thêm phần sau dependenciesvào app/build.gradle:

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

Lưu ý rằng phần MyActivitymở rộng của bạn FragmentActivitythay vì chỉ mở rộng Activity.

Bạn có thể đọc thêm về ViewModels tại đây:



1
@JJD Tôi đồng ý với bài viết bạn đăng, người ta vẫn phải xử lý lưu và khôi phục đúng cách. ViewModelđặc biệt tiện dụng nếu bạn có các bộ dữ liệu lớn để giữ lại trong khi thay đổi trạng thái, chẳng hạn như xoay màn hình. Tôi thích sử dụng ViewModelthay vì viết nó vào Applicationvì nó có phạm vi rõ ràng và tôi có thể có nhiều Hoạt động của cùng một Ứng dụng hoạt động chính xác.
Benedikt Köppel

1

Tôi thấy rằng câu trả lời này đã gây ra một số sự cố trên Android phiên bản 9 và 10. Tôi nghĩ rằng đó là một cách tiếp cận tốt nhưng khi tôi nhìn vào một số mã Android, tôi phát hiện ra rằng nó đang thiếu một nhà xây dựng. Câu trả lời khá cũ nên có lẽ không cần nó. Khi tôi thêm hàm tạo bị thiếu và gọi nó từ trình tạo, sự cố đã được khắc phục.

Vì vậy, đây là mã chỉnh sửa:

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

Để tăng thêm các câu trả lời khác - nếu bạn có nhiều chế độ xem ghép tùy chỉnh với cùng một ID và tất cả chúng đều được khôi phục với trạng thái của chế độ xem cuối cùng về thay đổi cấu hình, tất cả những gì bạn cần làm là chỉ cho chế độ xem chỉ gửi các sự kiện lưu / khôi phục với chính nó bằng cách ghi đè một vài phương thức.

class MyCompoundView : ViewGroup {

    ...

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

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

Để biết giải thích về những gì đang xảy ra và tại sao điều này hoạt động, xem bài đăng trên blog này . Về cơ bản ID ID của chế độ xem kết hợp của bạn được chia sẻ bởi mỗi chế độ xem tổng hợp và khôi phục trạng thái bị nhầm lẫn. Bằng cách chỉ gửi trạng thái cho chế độ xem hỗn hợp, chúng tôi ngăn chặn con cái của họ nhận được thông điệp hỗn hợp từ các chế độ xem khác.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.