Android复合自定义视图的恢复和保存状态

8

我创建了一个复合自定义视图,其中包含名为LabledEditTextTextViewEditText,因为在片段中我会有很多EditText字段。

我创建了一个包含以下内容的XML文件:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/linearLayoutLabeledEditText"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:id="@+id/label_textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/label"
        android:textAppearance="?android:attr/textAppearanceMedium" />

    <EditText
        android:id="@+id/value_editText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:freezesText="true"
        android:saveEnabled="true"/>
</LinearLayout>

在View类中,代码如下:

public class LabeledEditText extends LinearLayout {
    private EditText editText;
    private TextView label;

    public LabeledEditText(Context context, AttributeSet attrs) {
        super(context, attrs);
        inflate(context,R.layout.labeled_edit_text, this);
        label = (TextView) this.findViewById(R.id.label_textView);
        label.setText("some label");
        editText = (EditText) this.findViewById(R.id.value_editText);
    }

    protected void onRestoreInstanceState(Parcelable state) {
        String id= this.getId()+" ";
        if (state instanceof Bundle) // implicit null check
        {
            Bundle bundle = (Bundle) state;
            state = bundle.getParcelable(id+"super");
            super.onRestoreInstanceState(state);
            editText.setText(bundle.getString(id+"editText"));
        }
    }

    protected Parcelable onSaveInstanceState() {
        String id= this.getId()+" ";
        Bundle bundle = new Bundle();
        bundle.putParcelable(id+"super",super.onSaveInstanceState());
        bundle.putString(id+"editText",editText.getText().toString());
        return bundle;
    }   
}

然后我将它用于代表3个步骤的3个片段中。当我在第1步/片段中插入值时

第一次访问第一步后

然后切换到其他片段并再次返回到第1步/片段,我发现以下情况

第二次访问第一步

是什么导致了这个问题?

我已经调试了至少5天,记住每个自定义视图在片段布局中使用时都有不同的ID。

我还尝试在保存状态期间将自定义视图的ID作为键的一部分添加this.getId()+"editText",但仍然存在相同的问题。

编辑 api < 17的genrateViewId

修改后的代码

import java.util.concurrent.atomic.AtomicInteger;

public class LabeledEditText extends LinearLayout {
    private EditText editText;
    private TextView label;

    public LabeledEditText(Context context, AttributeSet attrs) {
        super(context, attrs);
        inflate(context,R.layout.labeled_edit_text, this);
        label = (TextView) this.findViewById(R.id.label_textView);
        editText = (EditText) this.findViewById(R.id.value_editText);
        editText.setId(generateViewId());
        applyAttr(context,attrs);
    }

    @Override
    protected Parcelable onSaveInstanceState() {
        Bundle bundle = new Bundle();
        //adding the id of the parent view as part of the key so that
        //editText state won't get overwritten by other editText 
        //holding the same id
        bundle.putParcelable("super",super.onSaveInstanceState());
        bundle.putString("editText",editText.getText().toString());
        return bundle;
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        if (state instanceof Bundle) // implicit null check
        {
            Bundle bundle = (Bundle) state;
            state = bundle.getParcelable("super");
            super.onRestoreInstanceState(state);
            editText.setText(bundle.getString("editText"));
        }
    }

    private static final AtomicInteger sNextGeneratedId = new AtomicInteger(1);

    public static int generateViewId() {
        for (;;) {
            final int result = sNextGeneratedId.get();
            // aapt-generated IDs have the high byte nonzero; clamp to the range under that.
            int newValue = result + 1;
            if (newValue > 0x00FFFFFF) newValue = 1; // Roll over to 1, not 0.
            if (sNextGeneratedId.compareAndSet(result, newValue)) {
                return result;
            }
        }
    }
}

我不知道你尝试了什么其他方法,但是你在这里发布的代码肯定会返回这些结果。你一直将所有值保存到同一个editText键中,因此它会被覆盖,所以你只保留了最后一个值,即5。你说你尝试了其他方法,请发布那段代码。 - lionscribe
我会考虑的,但是每个视图不都是将其状态存储在不同的包中吗? - john-salib
为什么你要处理保存状态的问题。操作系统应该保存EditText的基本状态。你只需要为自定义变量保存状态。 - lionscribe
否则我根本不会考虑保存状态,这个问题也会发生。 - john-salib
1个回答

10

你的问题是有多个ViewGroups(LinearLayout),其中子项具有相同的id。因此,在保存状态时,所有这些视图都保存在同一个状态中,并且最后一个视图会覆盖所有其他视图。
要解决此问题,您必须在填充时为每个视图分配一个唯一的ID。在v17及更高版本中,您可以使用View.generateViewId();,在旧版本中,您将需要在ids文件中手动创建静态ID。
您的代码应该如下所示;

public LabeledEditText(Context context, AttributeSet attrs) { 
    super(context, attrs); inflate(context,R.layout.labeled_edit_text, this); 
    label = (TextView) this.findViewById(R.id.label_textView); label.setText("some label"); 
    editText = (EditText) this.findViewById(R.id.value_editText); 
    label.setId(View.generateViewId());
    editText.setId(View.generateViewId());
}

无论如何,使用静态ID可能更好,因为以后更容易引用它们。如果您使用静态ID,则甚至可能不需要覆盖onSave和onRestore。


1
  1. 2个评论。你最好把myGenerateViewId改名,因为现在的方式不清楚你正在使用哪一个,并且可能会创建编译器问题。2. 你可以从键名中删除getId(),因为那不是问题所在。3. 我不确定恢复时会发生什么,可能会调用构造函数。如果是这样,你将会有新的editText id,那么它如何知道要恢复什么。如果听起来很混乱,那是因为我有点困惑复合视图的恢复工作方式。
- lionscribe
1
如果您这样做,有什么保证恢复期间创建的ID与创建期间生成的ID相同? - Ashok Koyi
1
@kalinga 我会简短地解释一下。活动(Activities)和片段(Fragments)之间有很大的区别。在片段中,操作系统会恢复实际的视图,所以除了确保每个视图都有唯一的ID之外,不需要做任何其他事情。而在活动中,应用程序必须重新创建视图,然后操作系统通过调用restoreState方法来恢复它们的状态。为了使这个过程正常工作,应用程序有责任确保ID与第一次相同,否则restoreState无法恢复视图。这就是为什么我建议使用静态ID,这样你可以使用相同的逻辑和相同的ID。 - lionscribe
1
在Fragments中使用generateID是可以的,因为操作系统会重新创建所有视图。您在示例中验证了这个语句吗?我自己进行了测试,它不起作用。Android对视图一无所知,因为新生成的ID与旧ID不同,即使视图不是动态的。请创建一个简单的Fragment,并使用generateId编程方式设置其简单自定义视图的ID,看看视图的状态是否被恢复。 - Ashok Koyi
抱歉,我记不清了。我现在正在使用一些约定来使用稳定的ID生成器。 - Ashok Koyi
显示剩余7条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接