9

问题

当通过使用 XML 定义的小部件布局,各个小部件实例的组件都具有相同的 ID 时,如何保存视图小部件实例状态?

例子

NumberPicker以小部件中使用的小部件为例TimePicker(请注意,NumberPicker它不向 SDK 公开)。这是一个简单的小部件,由三个组件组成number_picker.xml:一个增量按钮、一个减量按钮和一个EditText可以直接输入数字的按钮。为了让代码与这些小部件交互,它们都具有 ID(R.id.increment分别为R.id.decrementR.id.timepicker_input)。

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

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

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

请查看示例代码来演示此问题: https ://github.com/xxv/AndroidNumberPickerBug


¹ 在视图层次结构中,这会设置扩展为您的 ID 的 ID LinearLayoutNumberPicker

4

5 回答 5

17

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

这听起来可能很复杂,但实际上非常简单,API 实际上为这个确切的场景提供了准备。我在这里写了一篇关于如何完成的博客文章,但基本上在您的复合视图中,您需要实现以下 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 似乎存在错误。要修复它,您可以覆盖两者并实施我描述的解决方案。

于 2012-03-03T18:13:04.830 回答
0

我玩游戏有点晚了,但我想提供我的意见。

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

来自Android 开发者网站:

安卓:标签

为此视图提供一个包含字符串的标签,稍后使用 View.getTag() 检索或使用 View.findViewWithTag() 进行搜索。

于 2011-11-16T21:17:30.097 回答
0

我有一个复合小部件,其状态描述由一个整数组成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 匹配的标签的包裹,我只会执行解包。 也必须是适当的类
。对于更复杂的小部件,拆包将更加复杂。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];
            }
    };

}

使用这个框架比操作片段和管理配置要容易得多

于 2012-01-12T23:22:31.797 回答
0

我刚刚在使用三个 NumberPicker 的复合视图时遇到了完全相同的问题。我在另一个站点上找到了一个解决方案,该解决方案涉及将 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 小部件的状态,以及焦点视图。

于 2011-06-08T21:50:55.923 回答
-4

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

如果你想保持你的状态,你不能在单个活动中重用一个 id。这本质上意味着您不能多次使用单个布局,这使得像 TimePicker 这样更复杂的 Widget 基本上无法正确执行。

您可以通过覆盖dispatchSaveInstanceState和侵入它来让孩子们保持他们的状态,但我没有找到一种方法来保持专注,除了自己管理它。

我认为他们可以通过在不破坏 API 的情况下创建状态范围来解决这个问题,但不要屏住呼吸。

于 2011-03-11T12:33:51.633 回答