How to prevent custom views from losing state across screen orientation changes

后端 未结 9 895
时光说笑
时光说笑 2020-11-22 10:11

I\'ve successfully implemented onRetainNonConfigurationInstance() for my main Activity to save and restore certain critical components across screen orientation

相关标签:
9条回答
  • 2020-11-22 10:35

    Here is another variant that uses a mix of the two above methods. Combining the speed and correctness of Parcelable with the simplicity of 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;
        }
    }
    
    0 讨论(0)
  • 2020-11-22 10:42

    The answers here already are great, but don't necessarily work for custom ViewGroups. To get all custom Views to retain their state, you must override onSaveInstanceState() and onRestoreInstanceState(Parcelable state) in each class. You also need to ensure they all have unique ids, whether they're inflated from xml or added programmatically.

    What I came up with was remarkably like Kobor42's answer, but the error remained because I was adding the Views to a custom ViewGroup programmatically and not assigning unique ids.

    The link shared by mato will work, but it means none of the individual Views manage their own state - the entire state is saved in the ViewGroup methods.

    The problem is that when multiple of these ViewGroups are added to a layout, the ids of their elements from the xml are no longer unique (if its defined in xml). At runtime, you can call the static method View.generateViewId() to get a unique id for a View. This is only available from API 17.

    Here is my code from the ViewGroup (it is abstract, and mOriginalValue is a type variable):

    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);
        } 
    }
    
    0 讨论(0)
  • 2020-11-22 10:49

    You do this by implementing View#onSaveInstanceState and View#onRestoreInstanceState and extending the View.BaseSavedState class.

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

    The work is split between the View and the View's SavedState class. You should do all the work of reading and writing to and from the Parcel in the SavedState class. Then your View class can do the work of extracting the state members and doing the work necessary to get the class back to a valid state.

    Notes: View#onSavedInstanceState and View#onRestoreInstanceState are called automatically for you if View#getId returns a value >= 0. This happens when you give it an id in xml or call setId manually. Otherwise you have to call View#onSaveInstanceState and write the Parcelable returned to the parcel you get in Activity#onSaveInstanceState to save the state and subsequently read it and pass it to View#onRestoreInstanceState from Activity#onRestoreInstanceState.

    Another simple example of this is the CompoundButton

    0 讨论(0)
  • 2020-11-22 10:57

    Instead of using onSaveInstanceState and onRestoreInstanceState, you can also use a ViewModel. Make your data model extend ViewModel, and then you can use ViewModelProviders to get the same instance of your model every time the Activity is recreated:

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

    To use ViewModelProviders, add the following to dependencies in app/build.gradle:

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

    Note that your MyActivity extends FragmentActivity instead of just extending Activity.

    You can read more about ViewModels here:

    • Android Developer Guide, Handle configuration changes
    • Android Developer Guide, Saving UI States, Use ViewModel to handle configuration changes
    • Tutorial ViewModels : A Simple Example
    0 讨论(0)
  • 2020-11-22 10:57

    I found that this answer was causing some crashes on Android versions 9 and 10. I think it's a good approach but when I was looking at some Android code I found out it was missing a constructor. The answer is quite old so at the time there probably was no need for it. When I added the missing constructor and called it from the creator the crash was fixed.

    So here is the edited code:

    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 讨论(0)
  • 2020-11-22 10:58

    Easy with kotlin

    @Parcelize
    class MyState(val superSaveState: Parcelable?, val loading: Boolean) : View.BaseSavedState(superSaveState), 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)
    
            loading = myState?.loading ?: false
            //redraw
        }
    }
    
    0 讨论(0)
提交回复
热议问题