sourcecode

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

copyscript 2023. 8. 17. 21:45
반응형

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

메인 환경을 위해 성공적으로 구현했습니다.Activity화면 방향 변경에 따라 특정 중요 구성 요소를 저장 및 복원할 수 있습니다.

그러나 방향이 바뀌면 사용자 지정 보기가 처음부터 다시 생성되는 것 같습니다.이는 타당하지만, 문제의 사용자 정의 뷰가 X/Y 그림이고 표시된 점이 사용자 정의 뷰에 저장되므로 불편합니다.

유사한 것을 구현할 수 있는 교활한 방법이 있습니까?onRetainNonConfigurationInstance()사용자 정의 보기의 경우 또는 사용자 정의 보기에서 "상태"를 가져오고 설정할 수 있는 메서드만 구현해야 합니까?

저는 이것이 훨씬 더 간단한 버전이라고 생각합니다.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);
  }
}

클래스를 구현하고 확장하여 이 작업을 수행합니다.

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

작업은 보기 클래스와 보기의 저장된 상태 클래스로 분할됩니다.당신은 읽고 쓰는 모든 일을 해야 합니다.Parcel에 시대에SavedState그러면 View 클래스는 상태 멤버를 추출하고 클래스를 유효한 상태로 되돌리는 작업을 수행할 수 있습니다.

주의:View#onSavedInstanceState그리고.View#onRestoreInstanceState를 자동으로 호출합니다.View#getId>= 0 값을 반환합니다.하거나 xml에 ID를 할 때 합니다.setId수동으로그렇지 않으면 전화를 해야 합니다.View#onSaveInstanceState그리고 당신이 받은 소포에 반송된 소포를 적으세요.Activity#onSaveInstanceState국가를 구하고 그 후에 그것을 읽고 그것을 전달하는 것.View#onRestoreInstanceStateActivity#onRestoreInstanceState.

이것의 또 다른 간단한 예는

코틀린으로 간편하게

@Parcelize
class MyState(val superSavedState: Parcelable?, val loading: Boolean) : View.BaseSavedState(superSavedState), Parcelable


class MyView : View {

    var loading: Boolean = false

    override fun onSaveInstanceState(): Parcelable? {
        val superState = super.onSaveInstanceState()
        return MyState(superState, loading)
    }

    override fun onRestoreInstanceState(state: Parcelable?) {
        val myState = state as? MyState
        super.onRestoreInstanceState(myState?.superSaveState ?: state)

        loading = myState?.loading ?: false
        //redraw
    }
}

여기 위의 두 가지 방법을 혼합하여 사용하는 또 다른 변형이 있습니다.다의속도와정결다합니의 와 을 결합한 입니다.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;
    }
}

여기서는 이미 답변이 훌륭하지만 사용자 지정 보기 그룹에 반드시 적용되는 것은 아닙니다. 사용자 를 재정의해야 합니다.onSaveInstanceState()그리고.onRestoreInstanceState(Parcelable state) 프로그래밍ID가 .또한 xml에서 부풀려졌든 프로그래밍 방식으로 추가되었든 상관없이 모든 ID에 고유한 ID가 있는지 확인해야 합니다.

제가 생각해 낸 것은 Kobor42의 답변과 매우 비슷했지만, 사용자 지정 View Group에 Views를 프로그래밍 방식으로 추가하고 고유 ID를 할당하지 않았기 때문에 오류가 남아 있었습니다.

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

문제는 이러한 ViewGroup 중 여러 개를 레이아웃에 추가하면 xml의 요소 ID가 더 이상 고유하지 않다는 것입니다(xml로 정의된 경우). 메소드를 런임에정메호를수출있다습니할서드적타라고 수 .View.generateViewId()보기에 대한 고유 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);
    } 
}

Restore에서 문제가 발생했습니다.InstanceState가 마지막 보기의 상태로 모든 사용자 지정 보기를 복원했습니다.사용자 정의 보기에 다음 두 가지 방법을 추가하여 해결했습니다.

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

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

다른 답변을 추가하려면 - 동일한 ID의 사용자 지정 복합 보기가 여러 개 있고 구성 변경 시 마지막 보기의 상태로 모두 복원되는 경우, 몇 가지 방법을 재정의하여 해당 보기에 저장/복원 이벤트만 발송하도록 지시하기만 하면 됩니다.

class MyCompoundView : ViewGroup {

    ...

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

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

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

답변이 Android 버전 9와 10에서 일부 충돌을 일으킨다는 을 발견했습니다.좋은 접근법이라고 생각하지만 안드로이드 코드를 보고 있자니 생성자가 없다는 것을 알게 되었습니다.답은 꽤 오래된 것이기 때문에 그 당시에는 아마 그럴 필요가 없었을 것입니다.누락된 생성자를 추가하여 생성자로부터 호출했더니 충돌이 수정되었습니다.

다음은 편집된 코드입니다.

public class CustomView extends LinearLayout {

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

사하는대를 사용하는 에.onSaveInstanceState그리고.onRestoreInstanceState를 사용할 수도 있습니다. 데이터 모델을 확장합니다.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.

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

제가 생각해 낸 @Fletcher Johns 답변을 기반으로 합니다.

  • 사용자 정의 레이아웃
  • XML에서 확장 가능
  • 직접 및 간접 하위 항목을 저장/복원할 수 있습니다.IntArray 대신 String->Id map에 id를 저장하기 위해 @Fletcher Jones의 답변을 개선했습니다.
  • 유일한 작은 단점은 저장 가능한 하위 보기를 미리 선언해야 한다는 것입니다.

open class AddressView @JvmOverloads constructor(
        context: Context,
        attrs: AttributeSet? = null,
        defStyleAttr: Int = 0,
        defStyleRes: Int = 0
) : LinearLayout(context, attrs, defStyleAttr, defStyleRes) {

    protected lateinit var countryInputLayout: TextInputLayout
    protected lateinit var countryAutoCompleteTextView: CountryAutoCompleteTextView
    protected lateinit var cityInputLayout: TextInputLayout
    protected lateinit var cityEditText: CityEditText
    protected lateinit var postCodeInputLayout: TextInputLayout
    protected lateinit var postCodeEditText: PostCodeEditText
    protected lateinit var streetInputLayout: TextInputLayout
    protected lateinit var streetEditText: StreetEditText
    
    init {
        initView()
    }

    private fun initView() {
        val view = inflate(context, R.layout.view_address, this)

        orientation = VERTICAL

        countryInputLayout = view.findViewById(R.id.countryInputLayout)
        countryAutoCompleteTextView = view.findViewById(R.id.countryAutoCompleteTextView)

        streetInputLayout = view.findViewById(R.id.streetInputLayout)
        streetEditText = view.findViewById(R.id.streetEditText)

        cityInputLayout = view.findViewById(R.id.cityInputLayout)
        cityEditText = view.findViewById(R.id.cityEditText)

        postCodeInputLayout = view.findViewById(R.id.postCodeInputLayout)
        postCodeEditText = view.findViewById(R.id.postCodeEditText)
    }

    // Declare your direct and indirect child views that need to be saved
    private val childrenToSave get() = mapOf<String, View>(
            "coutryIL" to countryInputLayout,
            "countryACTV" to countryAutoCompleteTextView,
            "streetIL" to streetInputLayout,
            "streetET" to streetEditText,
            "cityIL" to cityInputLayout,
            "cityET" to cityEditText,
            "postCodeIL" to postCodeInputLayout,
            "postCodeET" to postCodeEditText,
    )
    private var viewIds: HashMap<String, Int>? = null

    override fun onSaveInstanceState(): Parcelable? {
        // Create a bundle to put super parcelable in
        val bundle = Bundle()
        bundle.putParcelable(SUPER_INSTANCE_STATE, super.onSaveInstanceState())
        // Store viewIds in the bundle - initialize if necessary.
        if (viewIds == null) {
            childrenToSave.values.forEach { view -> view.id = generateViewId() }
            viewIds = HashMap<String, Int>(childrenToSave.mapValues { (key, view) -> view.id })
        }

        bundle.putSerializable(STATE_VIEW_IDS, viewIds)

        return bundle
    }

    override fun onRestoreInstanceState(state: Parcelable?) {
        // We know state is a Bundle:
        val bundle = state as Bundle
        // Get mViewIds out of the bundle
        viewIds = bundle.getSerializable(STATE_VIEW_IDS) as HashMap<String, Int>
        // For each id, assign to the view of same index
        if (viewIds != null) {
            viewIds!!.forEach { (key, id) -> childrenToSave[key]!!.id = id }
        }
        super.onRestoreInstanceState(bundle.getParcelable(SUPER_INSTANCE_STATE))
    }

    companion object {
        private const val SUPER_INSTANCE_STATE = "saved_instance_state_parcelable"
        private const val STATE_VIEW_IDS = "state_view_ids"
    }
}

언급URL : https://stackoverflow.com/questions/3542333/how-to-prevent-custom-views-from-losing-state-across-screen-orientation-changes

반응형