开发者

saving state on compound view widgets

Question

How do you save view widget instance state when, by using XML-defined widget layouts, components of the individual widget instances all have the same ID?

Example

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

Let's say you have three NumberPickers in an XML layout and you give them distinct IDs (开发者_如何学编程eg. R.id.hour, R.id.minute).¹ This layout is then inflated to the content view of an activity. We decide to change the activity's orientation, so Activity.onSaveInstanceState(Bundle) helpfully saves our view state for each view that has an ID (this is the default behavior).

Unfortunately, the three NumberPickers have EditTexts that all share the same ID — R.id.timepicker_input. Thus, when the activity is restored, the one furthest down in the view hierarchy is the one whose state seems to be preserved for all three of them. Additionally, the focus goes to the first NumberPicker when restored regardless of which one had focus when saved.

TimePicker gets around this problem by preserving the state itself, separately. Unfortunately, this won't preserve the cursor position or the focused view without a lot more work. I'm not sure how it preserves that state if it does at all (and quickly playing with a time input dialog seems to indicate that it can somehow).

Please see the sample code to demonstrate this issue: https://github.com/xxv/AndroidNumberPickerBug


¹ In the view hierarchy, this sets the ID of the LinearLayout that NumberPicker extends to your IDs.


I stumbled across the same issue when trying to create my own compound view. From looking at the Android source code I believe the correct way to implement compound views is for the compound view itself to take on the responsibility of saving and restoring the instance state of its children and to prevent the calls to save and restore instance state from being passed onto the child views. This works around the issue of the IDs of child views not being unique when you have more then one instance of the same compound view in an activity.

This might sound complicated but it is really quite easy and the APIs actually make provision for this exact scenario. I've written a blog post here on how this is done but essentially in your compound view you need to implement the following 4 methods, customising 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 problem with NumberPicker/TimePicker, as mentioned in another comment there appears to be a bug with NumberPicker and TimePicker. To fix it you could override both and implement the solution I've described.


I just ran into the exact same problem with a compound view with three NumberPickers. I found a solution on another site that involved reassigning the Id of the timepicker_input to a unique random Id. This works but is complicated because once a new Id is chosen, that Id must persist across configuration changes so extra code is required to do that.

For my application, and probably in 99% of others, a much simpler approach (hack) works. I realized that the namespace for Ids has room for 65536 unique identifiers (0x7f090000 to 0x7f09ffff), but my application is only using 20 or so, and they are allocated monotonically increasing from the beginning. Furthermore, the NumberPicker widget itself has a unique identfier within the tree (for example as in the original post, R.id.hour, R.id.minute and R.id.second). My solution is to reassign the Id of the EditText widget to the Id of the NumberPicker widget plus an offset.

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

mText.setId(getId() + 1000);

After the following line in NumberPicker.java:

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

The offset of course can be adjusted based on application requirements.

I imagine the same approach will work for other compound widgets.

For the above example, this approach allows the state of the individual EditText widgets to be saved and restored, and the focused view as well.


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

You can use the android:tag to group each View. Thus, you can 'reload' each state.

From the Android Developer website:

android:tag

Supply a tag for this view containing a String, to be retrieved later with View.getTag() or searched for with View.findViewWithTag().


I have a compound Widget with a state description consisting of a single integer mValue
Now override the following two methods like so:

@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're serializing your state. More complex states will require more complex storage structures.
Make sure to setDataPosition() correctly.
I'm only saving the state if the widget has an id assigned which I'm using as the parcel's tag

In dispatchRestoreInstanceState(), you're unpacking your parcel.
I'm only performing the unpack if the widget has an id assigned and the container contains a parcel with a tag matching this id.
The Parcelable must also be of the proper class.
Unpacking will be more complicated for more complex widgets.

The last component needed is a Parcelable subclass like the following:

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 framework is much easier than manipulating fragments and managing configuration


Quite simple: You don't. Just go and disable the orientation change crap in your manifest file. The view state save mechanism is inherently flawed, they simply didn't think this through.

If you want to keep your state, you cannot reuse an id within a single activity. Which essentially means that you cannot use a single layout more than once, which makes more complex Widgets like TimePicker basically impossible to do properly.

You can make children keep their state by overriding dispatchSaveInstanceState and hacking it in, but I didn't find a way to keep focus as well, besides managing that yourself as well.

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

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜