【问题标题】:How to prevent custom views from losing state across screen orientation changes如何防止自定义视图在屏幕方向更改时丢失状态
【发布时间】:2011-04-02 07:45:06
【问题描述】:

我已经成功地为我的主要Activity 实现了onRetainNonConfigurationInstance(),以在屏幕方向更改时保存和恢复某些关键组件。

但看起来,当方向改变时,我的自定义视图正在从头开始重新创建。这是有道理的,尽管在我的情况下这很不方便,因为有问题的自定义视图是 X/Y 图,并且绘制的点存储在自定义视图中。

是否有一种巧妙的方法可以为自定义视图实现类似于onRetainNonConfigurationInstance() 的东西,还是我只需要在自定义视图中实现允许我获取和设置其“状态”的方法?

【问题讨论】:

    标签: android android-activity state screen-orientation


    【解决方案1】:

    我认为这是一个更简单的版本。 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);
      }
    }
    

    【讨论】:

    • 如果onSaveInstanceState返回一个Bundle,为什么不会用一个Bundle调用onRestoreInstanceState
    • OnRestoreInstance 被继承。我们无法更改标题。 Parcelable 只是一个接口,Bundle 是一个实现。
    • 感谢这种方式更好,并且在将 SavedState 框架用于自定义视图时避免了 BadParcelableException,因为保存的状态似乎无法为您的自定义 SavedState 正确设置类加载器!
    • 我在一个活动中有多个相同视图的实例。它们在 xml 中都有唯一的 id。但仍然所有这些都获得了最后一个视图的设置。有什么想法吗?
    • 这个解决方案可能没问题,但绝对不安全。通过实现这一点,您假设基本View 状态不是Bundle。当然,目前确实如此,但您依赖的是当前的实现事实,但不能保证是真的。
    【解决方案2】:

    您可以通过实现 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];
              }
        };
      }
    }
    

    工作在 View 和 View 的 SavedState 类之间进行拆分。您应该在SavedState 类中完成与Parcel 之间的所有读写工作。然后,您的 View 类可以完成提取状态成员的工作,并完成使类恢复到有效状态所需的工作。

    注意:View#onSavedInstanceStateView#onRestoreInstanceState 如果View#getId 返回值 >= 0,则会自动为您调用。当您在 xml 中给它一个 id 或手动调用 setId 时,会发生这种情况。否则,您必须调用 View#onSaveInstanceState 并将 Parcelable 返回到您在 Activity#onSaveInstanceState 中获得的包裹中以保存状态,然后读取它并将其从 Activity#onRestoreInstanceState 传递给 View#onRestoreInstanceState

    另一个简单的例子是CompoundButton

    【讨论】:

    • 对于那些到达这里的人,因为在使用带有 v4 支持库的 Fragments 时这不起作用,我注意到支持库似乎没有为您调用视图的 onSaveInstanceState/onRestoreInstanceState;您必须自己从 FragmentActivity 或 Fragment 中方便的位置显式调用它。
    • 请注意,您应用此的 CustomView 应该有一个唯一的 id 集,否则它们将相互共享状态。 SavedState 是针对 CustomView 的 id 存储的,因此如果您有多个具有相同 id 或没有 id 的 CustomView,则保存在最终 CustomView.onSaveInstanceState() 中的包裹将传递给所有对 CustomView.onRestoreInstanceState() 的调用,当视图已恢复。
    • 此方法不适用于我的两个自定义视图(一个扩展另一个)。恢复视图时,我不断收到 ClassNotFoundException。我不得不在 Kobor42 的回答中使用 Bundle 方法。
    • onSaveInstanceState()onRestoreInstanceState() 应该是 protected(就像它们的超类),而不是 public。没有理由暴露他们......
    • 当为扩展 RecyclerView 的类保存自定义 BaseSaveState 时,这效果不佳,您会得到 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/…(使用 RecyclerView.class 的 ClassLoader 加载超级状态)
    【解决方案3】:

    使用 kotlin 很容易

    @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
        }
    }
    

    【讨论】:

    • 在这种情况下扩展View.BaseSavedState 有什么好处?有什么区别吗?
    【解决方案4】:

    这是另一个混​​合使用上述两种方法的变体。 将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;
        }
    }
    

    【讨论】:

    • 这不起作用。我得到一个 ClassCastException... 那是因为它需要一个公共静态 CREATOR 以便它从包裹中实例化您的State。请看:charlesharley.com/2012/programming/…
    【解决方案5】:

    这里的答案已经很好了,但不一定适用于自定义 ViewGroup。要让所有自定义视图保持其状态,您必须在每个类中覆盖 onSaveInstanceState()onRestoreInstanceState(Parcelable state)。 您还需要确保它们都有唯一的 id,无论它们是从 xml 膨胀还是以编程方式添加。

    我想出的结果与 Kobor42 的答案非常相似,但错误仍然存​​在,因为我是以编程方式将视图添加到自定义 ViewGroup 而不是分配唯一 ID。

    mato 共享的链接可以使用,但这意味着没有一个单独的 View 管理自己的状态 - 整个状态都保存在 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 确实是个问题,但我认为应该在视图初始化时处理,而不是在状态保存时处理。
    • 好点。您是否建议在构造函数中设置 mViewIds,然后在状态恢复时覆盖?
    • 这是对充气自定义布局有效的最完整答案。不幸的是,答案只为直接孩子生成 id,但通常情况并非如此。例如。子项可以是 TextInputLayout 内的 TextInputEditText。在这种情况下,解决方案会复杂得多。
    【解决方案6】:

    我遇到的问题是 onRestoreInstanceState 使用最后一个视图的状态恢复了我的所有自定义视图。我通过将这两种方法添加到我的自定义视图来解决它:

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

    【讨论】:

    • dispatchFreezeSelfOnly 和 dispatchThawSelfOnly 方法属于 ViewGroup,而不是 View。因此,以防万一,您的自定义视图是从内置视图扩展而来的。您的解决方案不适用。
    【解决方案7】:

    为了补充其他答案 - 如果您有多个具有相同 ID 的自定义复合视图,并且它们都在配置更改时恢复为最后一个视图的状态,您需要做的就是告诉视图仅调度保存/通过覆盖几个方法将事件恢复到自身。

    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】:

      我发现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];
                  }
              };
          }
      }
      

      【讨论】:

      • 您自己尝试过代码吗?方法dispatchFreezeSelfOnlydispatchFreezeSelfOnly属于ViewGroup,而不是View
      • @zeleven 你说得对,我使用了另一个答案的代码并且没有正确编辑它。您应该只扩展 LinearLayoutViewGroup 而不是 View 就可以了。我确实在我的应用程序中使用了这个解决方案的稍微改动的版本,它工作正常。我会尝试编辑我的答案。
      【解决方案9】:

      您也可以使用ViewModel,而不是使用onSaveInstanceStateonRestoreInstanceState。让您的数据模型扩展ViewModel,然后您可以使用ViewModelProviders 在每次重新创建Activity 时获取您的模型的相同实例:

      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 的更多信息:

      【讨论】:

      • @JJD 我同意您发布的文章,仍然需要正确处理保存和恢复。 ViewModel 如果在状态更改(例如屏幕旋转)期间要保留大量数据集,则特别方便。我更喜欢使用ViewModel 而不是将其写入Application,因为它的范围很明确,而且我可以让同一应用程序的多个活动正常运行。
      【解决方案10】:

      根据@Fletcher Johns 我想出的答案:

      • 自定义布局
      • 可以从 XML 膨胀
      • 能够保存/恢复直接和间接子级。我改进了@Fletcher Johns 的答案,将 ID 保存在 String->Id 映射而不是 IntArray 中。
      • 唯一的小缺点是您必须事先声明可保存的子视图。
      
      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"
          }
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-06-15
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多