メインのActivity
に onRetainNonConfigurationInstance()
を正常に実装し、画面の向きを変更しても特定の重要なコンポーネントを保存および復元しました。
しかし、方向が変わると、カスタムビューがゼロから再作成されているようです。これは理にかなっていますが、私の場合、問題のカスタムビューはX/Yプロットであり、プロットされたポイントはカスタムビューに保存されるため不便です。
カスタムビューにonRetainNonConfigurationInstance()
に似たものを実装する巧妙な方法はありますか、それとも「ビュー」を取得および設定できるメソッドをカスタムビューに実装するだけですか?
これを行うには、 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];
}
};
}
}
作業は、ビューとビューのSavedStateクラスに分割されます。 Parcel
クラスのSavedState
の読み取りと書き込みのすべての作業を行う必要があります。その後、Viewクラスは、状態メンバーを抽出し、クラスを有効な状態に戻すために必要な作業を実行できます。
注:View#onSavedInstanceState
とView#onRestoreInstanceState
は、View#getId
が0以上の値を返すと自動的に呼び出されます。これは、xmlでidを指定するか、手動でsetId
を呼び出すと発生します。それ以外の場合は、View#onSaveInstanceState
を呼び出して、Activity#onSaveInstanceState
で取得したパーセルに返されたParcelableを書き込んで状態を保存し、続いてそれを読み取ってView#onRestoreInstanceState
からActivity#onRestoreInstanceState
に渡す必要があります。
これの別の簡単な例は CompoundButton
です
これはもっと簡単なバージョンだと思います。 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);
}
}
上記の2つの方法を組み合わせて使用する別のバリエーションがあります。 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;
}
}
ここでの回答はすでに素晴らしいですが、必ずしもカスタムViewGroupsで機能するとは限りません。すべてのカスタムビューで状態を保持するには、各クラスでonSaveInstanceState()
およびonRestoreInstanceState(Parcelable state)
をオーバーライドする必要があります。また、xmlから膨らんだり、プログラムで追加したりする場合でも、それらがすべて一意のIDを持つようにする必要があります。
私が思いついたのはKobor42の答えに非常に似ていましたが、ビューをプログラムでカスタムViewGroupに追加し、一意の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);
}
}
OnRestoreInstanceStateがすべてのカスタムビューを最後のビューの状態で復元するという問題がありました。これらの2つのメソッドをカスタムビューに追加して解決しました。
@Override
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
dispatchFreezeSelfOnly(container);
}
@Override
protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
dispatchThawSelfOnly(container);
}
onSaveInstanceState
とonRestoreInstanceState
を使用する代わりに、 ViewModel
を使用することもできます。データモデルが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
を使用するには、app/build.gradle
のdependencies
に次を追加します。
implementation "Android.Arch.lifecycle:extensions:1.1.1"
implementation "Android.Arch.lifecycle:viewmodel:1.1.1"
MyActivity
は、単にFragmentActivity
を拡張するのではなく、Activity
を拡張することに注意してください。
ViewModelの詳細については、こちらをご覧ください。
他の回答を補強するには-同じIDのカスタム複合ビューが複数あり、それらがすべて構成変更の最後のビューの状態で復元される場合、必要なことは、保存/復元イベントのみをディスパッチするようにビューに指示することだけですいくつかのメソッドをオーバーライドすることにより、それ自体に。
class MyCompoundView : ViewGroup {
...
override fun dispatchSaveInstanceState(container: SparseArray<Parcelable>) {
dispatchFreezeSelfOnly(container)
}
override fun dispatchRestoreInstanceState(container: SparseArray<Parcelable>) {
dispatchThawSelfOnly(container)
}
}
何が起こっているのか、なぜこれが機能するのかの説明については、 このブログ投稿を参照 。基本的に、複合ビューの子のビューIDは各複合ビューで共有され、状態の復元が混乱します。複合ビュー自体の状態のみをディスパッチすることにより、他の複合ビューからの子が混合メッセージを取得しないようにします。