स्क्रीन ओरिएंटेशन परिवर्तनों में राज्य को खोने से कस्टम दृश्य कैसे रोकें


248

मैंने स्क्रीन ओरिएंटेशन परिवर्तनों में कुछ महत्वपूर्ण घटकों को बचाने और पुनर्स्थापित करने के onRetainNonConfigurationInstance()लिए अपने मुख्य के लिए सफलतापूर्वक लागू किया है Activity

लेकिन ऐसा लगता है, जब अभिविन्यास बदलता है, तो मेरे कस्टम दृश्य फिर से खरोंच से बनाए जा रहे हैं। यह समझ में आता है, हालांकि मेरे मामले में यह असुविधाजनक है क्योंकि प्रश्न में कस्टम दृश्य एक एक्स / वाई प्लॉट है और प्लॉट किए गए बिंदु कस्टम दृश्य में संग्रहीत हैं।

क्या 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];
          }
    };
  }
}

कार्य दृश्य और दृश्य के सहेजे गए वर्ग के बीच विभाजित है। आपको कक्षा Parcelमें पढ़ने और लिखने का सारा काम करना चाहिए SavedState। फिर आपका व्यू क्लास राज्य के सदस्यों को निकालने का काम कर सकता है और क्लास को एक वैध स्थिति में लाने के लिए आवश्यक काम कर सकता है।

नोट: View#onSavedInstanceStateऔर View#onRestoreInstanceStateआप के लिए स्वचालित रूप से कहा जाता है, तो View#getIdरिटर्न एक मूल्य के> = 0. यह तब होता है जब आप इसे एक्सएमएल में एक आईडी दे या फोन setIdमैन्युअल रूप से। अन्यथा आपको कॉल करना होगा View#onSaveInstanceStateऔर पार्सल को उस पार्सल में वापस भेजना होगा जिसे आप प्राप्त करते हैंActivity#onSaveInstanceState राज्य बचाने के लिए और बाद में इसे पढ़ा और इसे करने के लिए पारित करने के लिए View#onRestoreInstanceStateसे Activity#onRestoreInstanceState

इसका एक और सरल उदाहरण है CompoundButton


14
यहां पहुंचने वालों के लिए क्योंकि यह v4 समर्थन पुस्तकालय के साथ फ्रेगमेंट का उपयोग करते समय काम नहीं कर रहा है, मैं ध्यान देता हूं कि समर्थन पुस्तकालय आपके लिए व्यू के onSaveInstanceState / onRestoreInstanceState पर कॉल करने के लिए प्रतीत नहीं होता है; आपको स्पष्ट रूप से इसे FragmentActivity या Fragment में एक सुविधाजनक स्थान से खुद को कॉल करना होगा।
मैग्नेटोमीटर

69
ध्यान दें कि आपके द्वारा लागू किया गया CustomView एक अद्वितीय आईडी सेट होना चाहिए, अन्यथा वे एक दूसरे के साथ राज्य साझा करेंगे। SavedState को CustomView की आईडी के विरुद्ध संग्रहीत किया जाता है, इसलिए यदि आपके पास एक ही id, या कोई id के साथ कई CustomViews हैं, तो अंतिम CustomView.onSaveInstanceState () में सहेजे गए पार्सल को CustomView.onRestoreInstanceState () में सभी कॉल में पास कर दिया जाएगा। विचारों को पुनर्स्थापित किया जाता है।
निक स्ट्रीट 12

5
यह विधि मेरे लिए दो कस्टम दृश्यों (दूसरे को विस्तार देने वाले) के साथ काम नहीं करती थी। मैं अपना दृष्टिकोण बहाल करते समय ClassNotFoundException प्राप्त करता रहा। मुझे Kobor42 के उत्तर में बंड दृष्टिकोण का उपयोग करना था।
क्रिस Feist

3
onSaveInstanceState()और (उनके सुपरक्लास की तरह) onRestoreInstanceState()होना चाहिए protected, नहीं public। उन्हें बेनकाब करने का कोई कारण नहीं ...
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/… (ClassLoader का उपयोग करके) सुपर स्टेट को लोड करने के लिए RecyclerView.class)
एपिकपांडफर्सेज

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
यदि एक बंडल वापस किया जाता है तो उसे बंडल के साथ क्यों नहीं onRestoreInstanceState बुलाया जाएगा onSaveInstanceState?
क्वर्टी जू

5
OnRestoreInstanceविरासत में मिला है। हम हेडर नहीं बदल सकते। Parcelableसिर्फ एक इंटरफ़ेस है, उसके Bundleलिए एक कार्यान्वयन है।
Kobor42

5
इस तरह से धन्यवाद बहुत बेहतर है और सहेजे गए राज्य के कस्टम दृश्य के लिए सहेजे गए ढांचे का उपयोग करते समय BadParcelableException से बचा जाता है क्योंकि लगता है कि आपके कस्टम सहेजेस्टेट के लिए कक्षा लोडर को सही तरीके से सेट करने में असमर्थ है!
इयान वारविक

3
मेरे पास एक गतिविधि में एक ही दृश्य के कई उदाहरण हैं। इन सभी के पास xml में अद्वितीय आईडी है। लेकिन फिर भी उन सभी को अंतिम दृश्य की सेटिंग मिलती है। कोई विचार?
क्रिस्टोफर

15
यह समाधान ठीक हो सकता है, लेकिन यह निश्चित रूप से सुरक्षित नहीं है। इसे लागू करने से आप मान रहे हैं कि आधार Viewस्थिति नहीं है Bundle। बेशक, इस समय यह सच है, लेकिन आप इस वर्तमान कार्यान्वयन तथ्य पर भरोसा कर रहे हैं जो सच होने की गारंटी नहीं है।
दिमित्री ज़ेत्सेव

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पार्सल से तुरंत हो जाए । कृपया एक नज़र डालें: charlesharley.com/2012/programming/…
mato

8

यहाँ उत्तर पहले से ही शानदार हैं, लेकिन जरूरी नहीं कि कस्टम व्यूग्रुप्स के लिए काम करें। अपने राज्य को बनाए रखने के लिए सभी कस्टम दृश्य प्राप्त करने के लिए, आपको ओवरराइड करना चाहिए onSaveInstanceState()औरonRestoreInstanceState(Parcelable state) प्रत्येक वर्ग में। आपको यह सुनिश्चित करने की भी ज़रूरत है कि वे सभी अद्वितीय आईडी हैं, चाहे वे xml से फुलाए गए हों या प्रोग्रामेटिक रूप से जोड़े गए हों।

मैं कोबरा 42 के जवाब की तरह उल्लेखनीय रूप से आया था, लेकिन त्रुटि बनी हुई थी क्योंकि मैं दृश्य को एक कस्टम व्यूग्रुप में प्रोग्रामेटिक रूप से जोड़ रहा था और अद्वितीय आईडी नहीं दे रहा था।

माटो द्वारा साझा किया गया लिंक काम करेगा, लेकिन इसका मतलब है कि कोई भी व्यक्ति अपने स्वयं के राज्य का प्रबंधन नहीं करता है - पूरे राज्य को व्यूपोर्ट विधियों में सहेजा गया है।

समस्या यह है कि जब इनमें से कई व्यूग्रुप्स को एक लेआउट में जोड़ा जाता है, तो xml से उनके तत्वों के आईडी अब अद्वितीय नहीं हैं (यदि इसका xml में परिभाषित किया गया है)। रनटाइम के दौरान, आप View.generateViewId()किसी दृश्य के लिए एक अद्वितीय आईडी प्राप्त करने के लिए स्थैतिक विधि को कॉल कर सकते हैं । यह केवल एपीआई 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);
    } 
}

कस्टम आईडी वास्तव में एक मुद्दा है, लेकिन मुझे लगता है कि इसे दृश्य के आरंभ में संभाला जाना चाहिए, न कि राज्य बचाओ पर।
Kobor42

अच्छी बात। क्या आप कन्स्ट्रक्टर में mViewIds स्थापित करने का सुझाव देते हैं, तो राज्य को बहाल करने पर अधिलेखित कर दें?
फ्लेचर जॉन्स

2

मुझे समस्या थी कि onRestoreInstanceState ने अंतिम दृश्य की स्थिति के साथ मेरे सभी कस्टम दृश्य बहाल कर दिए। मैंने अपने कस्टम दृश्य में इन दोनों विधियों को जोड़कर इसे हल किया:

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

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

डिस्पैचफ्रीसेल्फऑनली और डिस्पैचव्हास्फेल्ली तरीके व्यूग्रुप के हैं, न कि व्यू के। इसलिए यदि आपका कस्टम व्यू बिल्ड-इन व्यू से बढ़ा है। आपका समाधान लागू नहीं है।
हाऊ लुऊ

1

उपयोग करने के बजाय 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, जोड़ने के लिए निम्नलिखित dependenciesमें app/build.gradle:

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

ध्यान दें कि आपका MyActivityविस्तार FragmentActivityकेवल विस्तार करने के बजाय हैActivity

आप यहाँ ViewModels के बारे में अधिक पढ़ सकते हैं:



1
@JJD मैं आपके द्वारा पोस्ट किए गए लेख से सहमत हूं, एक को अभी भी सहेजना और ठीक से संभालना है। ViewModelविशेष रूप से आसान है यदि आपके पास राज्य परिवर्तन के दौरान बनाए रखने के लिए बड़े डेटा सेट हैं, जैसे कि स्क्रीन रोटेशन। मैं ViewModelइसे लिखने के बजाय इसका उपयोग करना पसंद करता हूं Applicationक्योंकि यह स्पष्ट रूप से स्कोप है, और मेरे पास एक ही एप्लिकेशन की कई गतिविधियां सही तरीके से व्यवहार कर सकती हैं।
बेनेडिकट कोप्पेल

1

मैंने पाया कि यह उत्तर एंड्रॉइड के संस्करणों 9 और 10 पर कुछ क्रैश कर रहा था। मुझे लगता है कि यह एक अच्छा तरीका है लेकिन जब मैं कुछ एंड्रॉइड कोड देख रहा था तो मुझे पता चला कि यह एक कंस्ट्रक्टर गायब था। जवाब काफी पुराना है इसलिए उस समय शायद इसकी कोई जरूरत नहीं थी। जब मैंने लापता कंस्ट्रक्टर को जोड़ा और निर्माता से इसे बुलाया तो दुर्घटना तय हो गई।

तो यहाँ संपादित कोड है:

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

अन्य उत्तरों को बढ़ाने के लिए - यदि आपके पास एक ही आईडी के साथ कई कस्टम यौगिक विचार हैं और वे सभी कॉन्फ़िगरेशन परिवर्तन पर अंतिम दृश्य की स्थिति के साथ बहाल किए जा रहे हैं, तो आपको केवल यह देखने की जरूरत है कि घटनाओं को बचाने / पुनर्स्थापना करने के लिए दृश्य को बताएं कुछ तरीकों को ओवरराइड करके ही।

class MyCompoundView : ViewGroup {

    ...

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

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

क्या हो रहा है और यह क्यों काम करता है, इसकी व्याख्या के लिए, इस ब्लॉग पोस्ट को देखें । मूल रूप से आपके यौगिक दृश्य के बच्चों के दृश्य आईडी को प्रत्येक यौगिक दृश्य द्वारा साझा किया जाता है और राज्य बहाली भ्रमित हो जाती है। केवल यौगिक दृश्य के लिए प्रेषण राज्य द्वारा, हम उनके बच्चों को अन्य मिश्रित विचारों से मिश्रित संदेश प्राप्त करने से रोकते हैं।

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.