Saving state in complex view widgets

Question

How to save the state of an instance of a view widget when, using XML-specific widget layouts, the components of individual widget instances have the same identifier?

Example

Take, for example, the NumberPicker widget that is used in the TimePicker TimePicker (note that NumberPicker does not appear in the SDK). This is a simple widget with three components that are pumped from number_picker.xml : one increment button, one decrease button and one EditText , where you can directly enter a number. In order for the code to interact with these widgets, they all have identifiers ( R.id.increment , R.id.decrement and R.id.timepicker_input respectively).

Let's say you have three NumberPicker in the XML layout, and you give them different identifiers (e.g. R.id.hour , R.id.minute ). ΒΉ This layout is then inflated to represent the contents of the activity. We decided to change the orientation of the activity, so Activity.onSaveInstanceState(Bundle) helps to keep our view state for each view with an identifier (this is the default behavior).

Unfortunately, the three NumberPicker have an EditText that all have the same identifier - R.id.timepicker_input . Thus, when activity is restored, the most remote in the hierarchy of representations is the one whose state seems to be preserved for all three of them. In addition, the focus moves to the first NumberPicker during recovery, regardless of which focus was saved.

TimePicker circumvents this problem by keeping the state itself separate. Unfortunately, this will not save the cursor position or focused image without additional work. I'm not sure how he saves this state, if he does at all (and fast playback with a time input dialog seems to indicate that it can somehow).

Please check out the sample code to demonstrate this problem: https://github.com/xxv/AndroidNumberPickerBug


ΒΉ In the view hierarchy, the LinearLayout identifier is LinearLayout , which NumberPicker applies to your identifiers.

+8
android android-layout android-custom-view
source share
5 answers

I encountered the same problem when trying to create my own composite view. From a look at the Android source code, I believe that the correct way to implement composite views is for the composite view itself to take responsibility for maintaining and restoring the instance state of its children and preventing calls to save and restore the instance state from going to views a child. This is because the identifiers of child views are not unique when there is more than one instance of the same composite view in the action.

It may seem complicated, but it's actually quite simple, and the APIs do provide this exact scenario. I wrote a blog post here about how this is done, but essentially in your complex presentation you need to implement the following 4 methods by setting onSaveInstanceState () and onRestoreInstanceState () to meet your specific requirements.

 @Override protected Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); return new SavedState(superState, numberPicker1.getValue(), numberPicker2.getValue(), numberPicker3.getValue()); } @Override protected void onRestoreInstanceState(Parcelable state) { SavedState savedState = (SavedState) state; super.onRestoreInstanceState(savedState.getSuperState()); numberPicker1.setValue(savedState.getNumber1()); numberPicker2.setValue(savedState.getNumber2()); numberPicker3.setValue(savedState.getNumber3()); } @Override protected void dispatchSaveInstanceState(SparseArray container) { // As we save our own instance state, ensure our children don't save // and restore their state as well. super.dispatchFreezeSelfOnly(container); } @Override protected void dispatchRestoreInstanceState(SparseArray container) { /** See comment in {@link #dispatchSaveInstanceState(android.util.SparseArray)} */ super.dispatchThawSelfOnly(container); } 

Regarding the issue with NumberPicker / TimePicker, as mentioned in another comment, an error appears with NumberPicker and TimePicker. To fix this, you can override both options and implement the solution that I described.

+14
source share

I ran into the same issue with a complex view with three NumberPickers. I found a solution on another site that involves overriding the timepicker_input identifier for a unique random identifier. This works, but it is difficult, because once a new identifier is selected, this identifier must be stored in the configuration changes, so this requires additional code.

For my application, and probably in 99% of others, a simpler approach (hack) works a lot. I realized that the namespace for Identifiers holds for 65,536 unique identifiers (0x7f090000 to 0x7f09ffff), but my application uses only 20 or so, and they stand out monotonically increasing from the beginning. In addition, the NumberPicker widget itself has a unique identifier inside the tree (for example, as in the original message, R.id.hour, R.id.minute and R.id.second). My solution is to reassign the widget ID of the EditText widget to the ID of the NumberPicker widget plus the offset.

This is a one line change to NumberPicker code. Just add:

 mText.setId(getId() + 1000); 

After the following line in NumberPicker.java:

 mText = (EditText) findViewById(R.id.timepicker_input); 

Course offset can be customized based on application requirements.

I assume that the same approach will work for other complex widgets.

In the above example, this approach allows you to determine the state of individual EditText widgets for saving and restoring, as well as a focused look.

0
source share

I was a little late to the game, but I wanted to give my input.

You can use android:tag to group each View . This way you can reload each state.

From Android Developer :

android: tag

Put a tag for this view containing a string that will be retrieved later with View.getTag () or search using View.findViewWithTag ().

0
source share

I have a composite state description widget consisting of a single integer mValue
Now override the following two methods:

 @Override protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) { if(getId() != NO_ID) { Parcel p = Parcel.obtain(); p.writeInt(mValue); p.setDataPosition(0); container.put(getId(), new MyParcelable(p)); } } @Override protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) { if(getId() != NO_ID) { Parcelable p = container.get(getId()); if(p != null && p instanceof MyParcelable) { MyParcelable mp = (MyParcelable) p; updateAllValues(mp.getValue()); } } } 

In dispatchSaveInstanceState() you serialize your state. More complex states will require more complex storage structures.
Make sure setDataPosition() correct.
I only save state if the widget has an identifier that I use as a send tag

In dispatchRestoreInstanceState() you unpack your package.
I only unpack if the widget has an identifier and the container has a package with a tag corresponding to this id.
Parcelable must also have an appropriate class.
Unpacking will be more difficult for more complex widgets.

The final component needed is a subclass of Parcelable , as shown below:

 static class MyParcelable implements Parcelable { private int mValue; private MyParcelable(Parcel in) { mValue = in.readInt(); } public int describeContents() { return 0; } public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mValue); } int getValue() { return mValue; } public static final Parcelable.Creator<MyParcelable> CREATOR = new Parcelable.Creator<MyParcelable>() { public MyParcelable createFromParcel(Parcel source) { return new MyParcelable(source); } public MyParcelable[] newArray(int size) { return new MyParcelable[size]; } }; } 

Using this structure is much easier than manipulating fragments and managing configurations.

0
source share

Pretty simple: you won't do it. Just go and turn off the orientation shit in your manifest file. The mechanism of preserving the state of representation is inherently erroneous; they simply did not think about it.

If you want to keep your state, you cannot reuse the identifier in a single action. Which essentially means that you cannot use the same layout more than once, which makes more complex widgets, such as TimePicker, basically impossible to work properly.

You can force children to maintain their state by overriding dispatchSaveInstanceState and hacking it, but I also did not find a way to maintain focus, except that I managed it myself.

I think they could fix this by making a state area without breaking the API, but don't hold your breath.

-3
source share

All Articles