在复合视图窗口小部件上保存状态

时间:2011-02-08 10:57:37

标签: android android-layout android-custom-view

问题

如果使用XML定义的窗口小部件布局,各个窗口小部件实例的组件都具有相同的ID,那么如何保存视图窗口小部件实例状态?

实施例

例如,使用NumberPicker窗口小部件中使用的TimePicker窗口小部件(请注意NumberPicker未向SDK公开)。这是一个简单的小部件,其中包含三个从number_picker.xml膨胀的组件:一个增量按钮,一个减量按钮和一个EditText,您可以在其中直接输入数字。为了使代码与这些小部件进行交互,它们都具有ID(分别为R.id.incrementR.id.decrementR.id.timepicker_input

假设您在XML布局中有三个NumberPicker,并且您为它们提供了不同的ID(例如R.id.hourR.id.minute)。¹此布局随后会扩展到内容视图一项活动。我们决定更改活动的方向,因此Activity.onSaveInstanceState(Bundle)有助于为每个具有ID的视图保存视图状态(这是默认行为)。

不幸的是,三个NumberPicker的{​​{1}}个EditText共享相同的ID - R.id.timepicker_input。因此,当活动恢复时,视图层次结构中最下面的那个是其状态似乎为所有这三个都保留的状态。此外,无论在保存时哪个焦点都有焦点,焦点都会转移到第一个NumberPicker

TimePicker通过分别保留状态本身来解决这个问题。不幸的是,如果没有更多的工作,这将不会保留光标位置或聚焦视图。我不确定它是如何保留该状态的(如果它完全一样)(并且快速播放时间输入对话框似乎表明它可能以某种方式)。

请参阅示例代码以演示此问题: https://github.com/xxv/AndroidNumberPickerBug


¹在视图层次结构中,这会设置LinearLayout扩展到您的ID的NumberPicker的ID。

5 个答案:

答案 0 :(得分:16)

我在尝试创建自己的复合视图时遇到了同样的问题。从查看Android源代码我相信实现复合视图的正确方法是复合视图本身承担保存和恢复其子实例状态的责任,并防止调用保存和恢复实例状态传递到子视图。当您在活动中有多个同一复合视图的实例时,这解决了子视图的ID不唯一的问题。

这可能听起来很复杂,但实际上非常简单,API实际上为这个确切的场景做了准备。我已经写了一篇关于如何完成这篇文章的博文here,但实际上在你的复合视图中你需要实现以下4种方法,自定义onSaveInstanceState()和onRestoreInstanceState()以满足你的特定要求。

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

关于NumberPicker / TimePicker的问题,如另一条评论所述,似乎有NumberPicker和TimePicker的错误。要修复它,你可以覆盖它们并实现我所描述的解决方案。

答案 1 :(得分:0)

我刚刚用复合视图遇到了完全相同的问题 有三个NumberPickers。我在另一个网站上找到了解决方案 这涉及重新分配timepicker_input的Id 独特的随机ID。这有效但很复杂,因为曾经 选择新的Id,Id必须在配置中保持不变 更改所以需要额外的代码。

对于我的应用程序,可能在99%的其他人中,很多 更简单的方法(hack)有效。我意识到了名称空间 Ids有65536个唯一标识符的空间(0x7f090000到 0x7f09ffff),但我的应用程序只使用20左右, 并且他们被分配从单调增加 开始。此外,NumberPicker小部件本身 树中有一个唯一的标识符(例如,如 原帖,R.id.hour,R.id.minute和R.id.second)。 我的解决方案是重新分配EditText小部件的Id 到NumberPicker小部件的Id加上一个偏移量。

这是对NumberPicker代码的一行更改。只是 添加:

mText.setId(getId() + 1000);

在NumberPicker.java中的以下行之后:

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

当然可以根据应用要求调整偏移量。

我认为同样的方法适用于其他复合小部件。

对于上面的例子,这种方法允许的状态 要保存和恢复的单个EditText小部件,以及 重点观点。

答案 2 :(得分:0)

我比赛有点晚了,但我想提出意见。

您可以使用android:tag对每个View进行分组。因此,您可以“重新加载”每个州。

来自Android Developer网站:

  

机器人:标签

     

为此视图提供包含String的标记,以便稍后检索   使用View.getTag()或使用View.findViewWithTag()搜索。

答案 3 :(得分:0)

我有一个复合Widget,其状态描述由一个整数mValue组成
现在覆盖以下两种方法,如下所示:

@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());
        }
    }
}

dispatchSaveInstanceState()中,您正在序列化您的州。更复杂的状态将需要更复杂的存储结构。
确保setDataPosition()正确。
我只保存状态,如果小部件已经分配了我用作包裹标签的ID

dispatchRestoreInstanceState(),您正在打开包裹。
如果窗口小部件已分配id且容器包含带有与此id匹配的标记的parcel,则我只执行解压缩。
Parcelable也必须属于正确的班级。
对于更复杂的小部件,解包会更复杂。

所需的最后一个组件是Parcelable子类,如下所示:

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

}

使用此框架比处理片段和管理配置要容易得多

答案 4 :(得分:-3)

很简单:你没有。只需在清单文件中禁用方向更改废话。视图状态保存机制本质上存在缺陷,他们根本就没有想到这一点。

如果您想保留自己的状态,则无法在单个活动中重复使用ID。这实际上意味着您不能多次使用单个布局,这使得像TimePicker这样的更复杂的小部件基本上无法正常完成。

你可以让孩子通过覆盖dispatchSaveInstanceState并将其入侵来保持自己的状态,但除了自己管理之外,我也没有办法保持专注。

我认为他们可以通过制作状态范围而不破坏API来解决这个问题,但不要屏住呼吸。