画面の向きを変更してもカスタムビューの状態が失われないようにする方法


248

画面の向きを変えても、特定の重要なコンポーネントを保存および復元するonRetainNonConfigurationInstance()ために、メインActivityを正常に実装しました。

しかし、方向が変わると、カスタムビューが最初から再作成されているようです。これは理にかなっていますが、私の場合、問題のカスタムビューはX / Yプロットであり、プロットされたポイントはカスタムビューに保存されるため、不便です。

onRetainNonConfigurationInstance()カスタムビューに似たものを実装する巧妙な方法はありますか、または「状態」を取得および設定できるメソッドをカスタムビューに実装するだけでよいですか?

回答:


415

あなたは、実装することによって、これを行うView#onSaveInstanceStateView#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];
          }
    };
  }
}

作業は、ビューとビューのSavedStateクラスに分割されます。クラスParcel内の読み書きのすべての作業を行う必要がありますSavedState。次に、Viewクラスは、状態メンバーを抽出する作業と、クラスを有効な状態に戻すために必要な作業を実行できます。

注:View#onSavedInstanceStateそして、0以上の値を返すView#onRestoreInstanceState場合View#getIdは自動的に呼び出されます。これは、xmlでIDを指定するか、setId手動で呼び出すと発生します。そうしないと、呼び出す必要がありますView#onSaveInstanceStateし、Parcelableはあなたが取得小包に戻った書き込みActivity#onSaveInstanceState状態を保存し、その後、それを読んで、それを渡すことView#onRestoreInstanceStateから、Activity#onRestoreInstanceStateます。

これのもう1つの簡単な例は CompoundButton


14
v4サポートライブラリでFragmentsを使用しているときにこれが機能しないためにここに到着した場合、サポートライブラリがビューのonSaveInstanceState / onRestoreInstanceStateを呼び出さないようです。FragmentActivityまたはFragmentの便利な場所から自分で明示的に呼び出す必要があります。
MagneticMonster

69
これを適用するCustomViewには一意のIDが設定されている必要があります。そうでない場合、それらは互いに状態を共有します。SavedStateはCustomViewのIDに対して保存されるため、同じIDのIDが複数あるか、IDがない場合、最終的なCustomView.onSaveInstanceState()に保存されたパーセルは、CustomView.onRestoreInstanceState()へのすべての呼び出しに渡されます。ビューが復元されます。
Nick Street

5
この方法は、2つのカスタムビュー(一方が他方を拡張する)では機能しませんでした。ビューを復元するときにClassNotFoundExceptionが発生し続けました。Kobor42の回答では、バンドルアプローチを使用する必要がありました。
Chris Feist 2012

3
onSaveInstanceState()そして、onRestoreInstanceState()する必要がありprotected、(そのスーパークラスのような)ではありませんpublic。それらを公開する理由ない...
XåpplI'-I0llwlg'I -

7
RecyclerViewを拡張するクラスのカスタムを保存する場合、これはうまく機能しません。BaseSaveStateそのため、github.comParcel﹕ Class not found when unmarshalling: android.support.v7.widget.RecyclerView$SavedState java.lang.ClassNotFoundException: android.support.v7.widget.RecyclerView$SavedState / ksoichiro / Android-ObservableScrollView / commit / (ClassLoaderのClassLoaderを使用して)に記載されているバグ修正を行う必要がありますRecyclerView.classがスーパー状態をロードします)
EpicPandaForce

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
Bundleが返された場合、Bundleで呼び出されないのは なぜですか?onRestoreInstanceStateonSaveInstanceState
Qwertie

5
OnRestoreInstance継承されます。ヘッダーは変更できません。Parcelable単なるインターフェースでBundleあり、その実装です。
Kobor42 2012

5
保存された状態ではカスタムSavedStateのクラスローダーを正しく設定できないように見えるため、この方法ははるかに優れており、カスタムビューにSavedStateフレームワークを使用するときにBadParcelableExceptionを回避します。
Ian Warwick 2013年

3
アクティビティに同じビューのインスタンスがいくつかあります。それらはすべてxmlで一意のIDを持っています。しかし、それでもそれらすべてが最後のビューの設定を取得します。何か案は?
Christoffer 2014年

15
この解決策は問題ないかもしれませんが、間違いなく安全ではありません。これを実装することで、基本View状態がでないことを想定していますBundle。もちろん、それは現時点では真実ですが、真実であることが保証されていないこの現在の実装の事実に依存しています。
Dmitry Zaytsev 2014

18

上記の2つの方法を組み合わせて使用​​する別のバリ​​アントを次に示します。のスピードと正確Parcelableさを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
これは機能しません。ClassCastExceptionが発生します...これStateは、区画からをインスタンス化するために、パブリック静的CREATORが必要なためです。見てください:charlesharley.com/2012/programming/...を
マト

8

ここでの答えはすでに素晴らしいですが、カスタムViewGroupsで必ずしも機能するとは限りません。すべてのカスタムビューはその状態を維持するために取得するには、オーバーライドする必要がありますonSaveInstanceState()し、onRestoreInstanceState(Parcelable state)各クラスインチ また、XMLからインフレートされているか、プログラムで追加されているかに関係なく、すべてが一意のIDを持っていることを確認する必要があります。

私が思いついたのは、Kobor42の答えのようでしたが、カスタムのViewGroupにプログラムでビューを追加し、一意のIDを割り当てなかったため、エラーが残っていました。

matoが共有するリンクは機能しますが、個々のビューが独自の状態を管理することはなく、状態全体がViewGroupメソッドに保存されます。

問題は、これらのViewGroupsの複数がレイアウトに追加されると、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);
    } 
}

カスタムIDは本当に問題ですが、状態の保存ではなく、ビューの初期化時に処理する必要があると思います。
Kobor42

いい視点ね。コンストラクターでmViewIdsを設定し、状態が復元された場合は上書きすることをお勧めしますか?
フレッチャージョンズ

2

onRestoreInstanceStateがすべてのカスタムビューを最後のビューの状態で復元するという問題がありました。私はこれらの2つのメソッドをカスタムビューに追加することで解決しました。

@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。データモデルを拡張するViewModelViewModelProviders、アクティビティを再作成するたびに、モデルの同じインスタンスを取得できます。

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"

単に拡張するのでMyActivityFragmentActivityなく、拡張することに注意してくださいActivity

ViewModelの詳細については、こちらをご覧ください。



1
@JJD私はあなたが投稿した記事に同意しますが、保存と復元を適切に処理する必要があります。ViewModel画面の回転などの状態変更中に保持する大規模なデータセットがある場合は、特に便利です。スコープが明確であり、同じアプリケーションの複数のアクティビティが正しく動作ViewModelするApplicationため、書き込む代わりにを使用することを好みます。
BenediktKöppel19年

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.