代码之家  ›  专栏  ›  技术社区  ›  Brad Hein

如何防止自定义视图在屏幕方向更改时丢失状态

  •  236
  • Brad Hein  · 技术社区  · 14 年前

    onRetainNonConfigurationInstance() 对于我的主要 Activity 在屏幕方向更改时保存和恢复某些关键组件。

    但是,当方向改变时,我的自定义视图似乎正在从头开始重新创建。这是有意义的,尽管在我的例子中这是不方便的,因为所讨论的自定义视图是X/Y图,并且打印的点存储在自定义视图中。

    有没有一种巧妙的方法来实现类似于 onRetainNonConfigurationInstance()

    7 回复  |  直到 10 年前
        1
  •  416
  •   Daniel Lubarov    10 年前

    你可以通过实现 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 查看#onRestoreInstanceState View#getId 返回一个值>=0。当您在xml中给它一个id或调用 setId 手动。否则你得打电话 查看#onSaveInstanceState Activity#onSaveInstanceState 查看#onRestoreInstanceState Activity#onRestoreInstanceState

    CompoundButton

        2
  •  468
  •   Kobor42    9 年前

    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);
      }
    }
    
        3
  •  18
  •   Blundell    10 年前

    结合速度和正确性 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;
        }
    }
    
        4
  •  10
  •   mmBs Tom7    8 年前

    这里的答案已经很好了,但不一定适用于自定义视图组。要使所有自定义视图保持其状态,必须重写 onSaveInstanceState() onRestoreInstanceState(Parcelable state) 您还需要确保它们都具有唯一的id,无论它们是从xml扩展的还是以编程方式添加的。

    我得出的结果与Kobor42的答案非常相似,但错误仍然存在,因为我是通过编程方式将视图添加到自定义视图组中的,而不是指定唯一的id。

    mato共享的链接可以工作,但这意味着没有一个视图管理自己的状态-整个状态保存在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);
        } 
    }
    
        5
  •  9
  •   Zakhar Rodionov    4 年前

    和科特林在一起很容易

    @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
        }
    }
    
        6
  •  3
  •   chrigist    6 年前

    我的问题是onRestoreInstanceState用最后一个视图的状态还原了所有自定义视图。我通过在自定义视图中添加以下两种方法来解决此问题:

    @Override
    protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
        dispatchFreezeSelfOnly(container);
    }
    
    @Override
    protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
        dispatchThawSelfOnly(container);
    }
    
        7
  •  1
  •   Tom    6 年前

    为了补充其他答案—如果您有多个具有相同ID的自定义复合视图,并且它们都是使用配置更改时最后一个视图的状态进行还原的,那么您所需要做的就是通过重写两个方法来告诉视图只向其自身分派save/restore事件。

    class MyCompoundView : ViewGroup {
    
        ...
    
        override fun dispatchSaveInstanceState(container: SparseArray<Parcelable>) {
            dispatchFreezeSelfOnly(container)
        }
    
        override fun dispatchRestoreInstanceState(container: SparseArray<Parcelable>) {
            dispatchThawSelfOnly(container)
        }
    }
    

    为了解释发生了什么以及为什么这样做, see this blog post . 基本上,复合视图的子视图ID由每个复合视图共享,状态恢复会变得混乱。通过只为复合视图本身分派状态,我们可以防止它们的子视图从其他复合视图获得混合消息。

        8
  •  1
  •   Benedikt Köppel    6 年前

    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 .

        9
  •  1
  •   Wirling    4 年前

    this answer 在Android版本9和10上造成了一些崩溃。我认为这是一个很好的方法,但当我看到一些 Android code 我发现它少了一个构造器。答案很古老,所以当时可能没有必要。当我添加缺少的构造函数并从创建者调用它时,崩溃得到了修复。

    下面是经过编辑的代码:

    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];
                }
            };
        }
    }
    
        10
  •  0
  •   WindRider    4 年前

    根据@Fletcher Johns的回答,我得出:

    • 自定义布局
    • 可以从XML膨胀
    • 唯一的小缺点是必须事先声明可保存的子视图。
    
    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"
        }
    }