화면 방향 변경시 사용자 정의보기의 상태가 손실되는 것을 방지하는 방법


248

화면 방향 변경에서 특정 중요 구성 요소를 저장하고 복원하기 위해 onRetainNonConfigurationInstance()메인 Activity을 성공적으로 구현 했습니다 .

그러나 방향이 변경되면 내 사용자 정의보기가 처음부터 다시 생성되는 것 같습니다. 내 경우에는 문제의 사용자 정의보기가 X / Y 플롯이고 플롯 된 점이 사용자 정의보기에 저장되기 때문에 불편하지만 의미가 있습니다.

onRetainNonConfigurationInstance()사용자 정의보기 와 비슷한 것을 구현하는 교묘 한 방법이 있습니까? 아니면 "상태"를 가져오고 설정할 수있는 사용자 정의보기에서 메소드를 구현해야합니까?

답변:


415

당신은 구현하여이 작업을 수행 View#onSaveInstanceState하고 View#onRestoreInstanceState하고 확장 View.BaseSavedState클래스를.

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

작업은 View와 View의 SavedState 클래스로 분할됩니다. 당신은 모두에게 읽기에와 서면의 작업을 수행해야합니다 Parcel에서 SavedState클래스를. 그러면 View 클래스는 상태 멤버를 추출하고 클래스를 유효한 상태로 되 돌리는 데 필요한 작업을 수행 할 수 있습니다.

노트 : View#onSavedInstanceStateView#onRestoreInstanceState경우 자동으로 호출 View#getId당신이 그것을 XML의 ID를 제공하거나 호출 할 때 반환 값이> = 0이 발생 setId수동. 그렇지 않으면 당신은 전화를해야 View#onSaveInstanceState하고 Parcelable 당신이 얻을 소포로 돌아 쓰기 Activity#onSaveInstanceState상태를 저장하고 나중에 그것을 읽고 그것을 전달하는 데 View#onRestoreInstanceState에서 Activity#onRestoreInstanceState.

이것의 또 다른 간단한 예는 CompoundButton


14
v4 지원 라이브러리와 함께 Fragments를 사용할 때 이것이 작동하지 않기 때문에 여기에 도착하는 사람들에게는 지원 라이브러리가 View의 onSaveInstanceState / onRestoreInstanceState를 호출하지 않는 것 같습니다. FragmentActivity 또는 Fragment의 편리한 위치에서 명시 적으로 직접 호출해야합니다.
magneticMonster

69
이를 적용하는 CustomView에는 고유 한 ID 세트가 있어야합니다. 그렇지 않으면 서로 상태를 공유합니다. SavedState는 CustomView의 ID에 대해 저장되므로 ID가 같거나 ID가없는 여러 개의 CustomView가있는 경우 최종 CustomView.onSaveInstanceState ()에 저장된 소포는 CustomView.onRestoreInstanceState ()에 대한 모든 호출에 전달됩니다. 뷰가 복원됩니다.
Nick Street

5
이 방법은 두 개의 사용자 정의보기 (하나는 다른 것으로 확장)에서 작동하지 않았습니다. 내보기를 복원 할 때 ClassNotFoundException이 계속 발생했습니다. Kobor42의 답변에 번들 접근 방식을 사용해야했습니다.
Chris Feist

3
onSaveInstanceState()그리고 (수퍼 클래스처럼) onRestoreInstanceState()이어야합니다 . 노출 할 이유가 없습니다 ...protectedpublic
XåpplI'-I0llwlg'I-

7
이것은 잘 작동하지 않는 사용자 지정 저장시 BaseSaveStateRecyclerView를 확장하는 클래스를 당신이 얻을 Parcel﹕ Class not found when unmarshalling: android.support.v7.widget.RecyclerView$SavedState java.lang.ClassNotFoundException: android.support.v7.widget.RecyclerView$SavedState: 당신이 여기 아래로 표기되는 버그 수정해야 할 수 있도록 github.com/ksoichiro/Android-ObservableScrollView/commit/...를 클래스 로더의 사용 (
Superr

459

나는 이것이 훨씬 간단한 버전이라고 생각합니다. Bundle구현하는 내장 유형입니다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);
  }
}

5
번들을 반환 한 경우 번들로 호출 되지 않는 이유 무엇 입니까? onRestoreInstanceStateonSaveInstanceState
Qwertie

5
OnRestoreInstance상속됩니다. 헤더를 변경할 수 없습니다. Parcelable단지 인터페이스 일뿐 Bundle입니다.
Kobor42

5
저장된 상태가 사용자 정의 SavedState에 대해 클래스 로더를 올바르게 설정할 수없는 것 같으므로이 방법이 훨씬 우수하고 사용자 정의보기에 SavedState 프레임 워크를 사용할 때 BadParcelableException을 피합니다!
이안 워릭

3
활동에 동일한보기의 여러 인스턴스가 있습니다. 그들은 모두 XML에 고유 ID가 있습니다. 그러나 여전히 그들 모두는 마지막보기의 설정을 얻습니다. 어떤 아이디어?
Christoffer

15
이 솔루션은 괜찮을 수도 있지만 확실히 안전하지는 않습니다. 이것을 구현함으로써 기본 View상태가 아닌 것으로 가정합니다 Bundle. 물론, 그것은 현재로서는 사실이지만, 현재 보장되지 않는 현재 구현 사실에 의존하고 있습니다.
Dmitry Zaytsev

18

위의 두 가지 방법을 혼합하여 사용하는 또 다른 변형이 있습니다. Parcelable의 단순성과 속도와 정확성의 결합 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
작동하지 않습니다. ClassCastException이 발생합니다 ... State소포에서 인스턴스를 생성하기 위해 공개 정적 CREATOR가 필요하기 때문 입니다. 참조하십시오 : charlesharley.com/2012/programming/…
mato

8

여기에 대한 답변은 이미 훌륭하지만 사용자 정의 ViewGroup에 반드시 작동하는 것은 아닙니다. 모든 사용자 정의보기가 자신의 상태를 유지하기 위해 얻으려면, 당신은 오버라이드 (override) 할 필요 onSaveInstanceState()onRestoreInstanceState(Parcelable state)각 클래스이다. 또한 XML에서 팽창되었거나 프로그래밍 방식으로 추가되었는지 여부에 관계없이 모두 고유 한 ID를 갖도록해야합니다.

내가 생각해 낸 것은 Kobor42의 대답과 매우 비슷하지만 프로그래밍 방식으로 사용자 정의 ViewGroup에 뷰를 추가하고 고유 ID를 할당하지 않았기 때문에 오류가 계속 발생했습니다.

mato가 공유하는 링크는 작동하지만 개별 뷰가 자신의 상태를 관리하지 않음을 의미합니다. 전체 상태는 ViewGroup 메서드에 저장됩니다.

문제는 이러한 ViewGroup을 여러 개 레이아웃에 추가 할 때 xml에서 해당 요소의 ID가 더 이상 고유하지 않다는 것입니다 (xml에 정의 된 경우). 런타임시 정적 메소드 View.generateViewId()를 호출하여 View의 고유 ID를 얻을 수 있습니다. API 17에서만 사용할 수 있습니다.

다음은 ViewGroup의 코드입니다 (추상적이며 mOriginalValue는 유형 변수입니다).

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는 실제로 문제가되지만 상태 저장이 아닌보기 초기화시 처리해야한다고 생각합니다.
Kobor42

좋은 지적. 생성자에서 mViewIds 설정을 제안한 다음 상태가 복원되면 덮어 쓰시겠습니까?
Fletcher Johns

2

onRestoreInstanceState가 모든 사용자 정의보기를 마지막보기 상태로 복원한다는 문제가있었습니다. 이 두 가지 방법을 내 사용자 정의보기에 추가하여 해결했습니다.

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

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

dispatchFreezeSelfOnly 및 dispatchThawSelfOnly 메소드는 View가 아닌 ​​ViewGroup에 속합니다. 따라서 사용자 정의보기가 내장보기에서 확장 된 경우입니다. 귀하의 솔루션은 적용되지 않습니다.
Hau Luu

1

onSaveInstanceState및 을 사용하는 대신을 사용할 onRestoreInstanceState수도 있습니다 ViewModel. 데이터 모델을 extend ViewModel로 만들고 ViewModelProviders활동을 다시 만들 때마다 동일한 모델 인스턴스를 얻는 데 사용할 수 있습니다 .

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

        ...
    }
}

사용하는 방법 ViewModelProviders은 다음을 추가 dependencies에서 app/build.gradle:

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

당신의 참고 MyActivity확장 FragmentActivity대신 확장하는 Activity.

ViewModel에 대한 자세한 내용은 여기를 참조하십시오.



1
@JJD 나는 당신이 게시 한 기사에 동의하지만 여전히 저장 및 복원을 올바르게 처리해야합니다. ViewModel화면 회전과 같은 상태 변경 중에 보관할 큰 데이터 세트가있는 경우 특히 유용합니다. 범위가 명확하기 때문에 ViewModel작성 하는 대신 대신 사용하는 것이 좋습니다 Application. 동일한 응용 프로그램의 여러 활동이 올바르게 작동 할 수 있습니다.
베네딕트 pp 펠

1

이 답변 이 Android 버전 9 및 10에서 충돌을 일으킨다는 것을 알았 습니다. 좋은 접근 방식이라고 생각하지만 일부 Android 코드를 볼 때 생성자가 누락 된 것으로 나타났습니다. 대답은 꽤 오래 되었기 때문에 아마도 그럴 필요가 없었습니다. 누락 된 생성자를 추가하고 생성자에서 호출하면 충돌이 수정되었습니다.

편집 된 코드는 다음과 같습니다.

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

다른 답변을 보강하려면 동일한 ID를 가진 여러 개의 사용자 지정 복합 뷰가 있고 구성 변경시 마지막 뷰 상태로 모두 복원되는 경우 뷰에 저장 / 복원 이벤트 만 전달하도록 지시하면됩니다. 몇 가지 방법을 재정 의하여 자체적으로.

class MyCompoundView : ViewGroup {

    ...

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

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

무슨 일이 일어나고 왜 작동하는지에 대한 설명 은이 블로그 게시물을 참조하십시오 . 기본적으로 복합 뷰의 하위 뷰 ID는 각 복합 뷰에서 공유되며 상태 복원은 혼동됩니다. 복합보기 자체에 대한 상태 만 전달하면 자녀가 다른 복합보기에서 혼합 된 메시지를받지 못하게됩니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.