安卓复选框 -- 屏幕旋转后恢复状态

30

在尝试恢复一组CheckBox在屏幕旋转后的状态时,我遇到了一些非常意外(且异常令人沮丧)的功能。我想先尝试提供一个文字说明,不包含代码,以防有人能够在没有所有细节的情况下确定解决方案。如果任何人需要更多细节,我可以发布代码。

我有一个滚动列表由复杂的View组成,其中包括CheckBox。在屏幕旋转后,我无法成功地恢复这些复选框的状态。我已经实现了onSaveInstanceState,并已成功将所选复选框列表传输到onCreate方法中。这是通过将数据库ID的long[]传递给Bundle来处理的。

onCreate()中,我检查Bundle中的id数组。如果存在该数组,则在构建列表时使用它来确定要选中哪些复选框。我创建了许多测试方法并已确认基于ID数组正确设置了复选框。作为最后一个检查,我在onCreate()结束时检查所有复选框的状态。一切看起来都很好......除非我旋转屏幕。

当我旋转屏幕时,会发生以下两种情况之一:1)如果除最后一个之外的任何数量的复选框都被选中,则屏幕旋转后所有复选框都关闭了。2)如果旋转之前选择了最后一个复选框,则旋转后所有复选框都是选中的

就像我说的,我在onCreate()的最后一刻检查框的状态。问题在于,在onCreate结束时,复选框的状态根据我在旋转之前所选择的是正确的。然而,屏幕上的复选框状态并不反映这一点。

此外,我已经实现了每个复选框的setOnCheckedChangeListener()并确认我的复选框状态在onCreate方法返回后被更改。

有人知道发生了什么吗?为什么我的复选框状态会在onCreate方法返回后更改?

提前感谢您的帮助。我已经试图调试这个问题几天了。当我发现我的复选框显然在我的代码之外的某个地方更改时,我认为是时候去问问别人了。


我相信当你进行屏幕方向更改时,onResume会在onCreate之后被调用。在onResume中是否有任何操作? - Robby Pond
8个回答

56

我有类似的问题。屏幕上的应用有几个复选框。旋转手机后,应用程序会“手动”设置所有复选框的值。该代码在onStart()中执行。但屏幕上的所有复选框都设置为屏幕上的“最后一个复选框”的值。Android调用了onRestoreInstanceState(..),并以某种方式将所有复选框视为“一个”[来自屏幕的最后一个复选框]。

解决方案是禁用“恢复实例状态”:

<RadioButton
    ...
    android:saveEnabled="false" />

3
这正是问题所在。从其他所有评论中可以明显看出,大多数安卓开发者并没有很好地理解这个功能。 - Kyle Ivey
这个方法连同在onSavedInstanceState()中存储按钮状态的操作,帮助我解决了问题。 - Mark O'Sullivan
唯一解决我的问题的方法。由于状态已保存,屏幕旋转后,系统自动将其设置为true后,开关委托会被调用。我正在使用Xamarin,在C#中,您必须在更改开关、复选框之前删除委托,以便它们不会在手动设置时执行;在手动设置后,我们必须恢复委托。由于系统不会这样做,委托被触发了。Grzegorz Dev发现得很好。谢谢! - Gustavo Baiocchi Costa
哇,真是个坑!我在DialogFragment中使用一个带有XML中设置ID的复选框视图的多个实例,并且它们在方向更改时都保持选中状态。我相信相同的ID是罪魁祸首 - 所有复选框都获得了最后添加的复选框的值。使用这个答案解决了问题。 - Magnus
我喜欢这种类型的问题。那些让你花费数小时苦思冥想,最终却发现像这样的东西。谢谢您,先生。 - Melllvar

8
大家好。看起来我解决了这个问题。复选框的状态在onRestoreInstanceState(Bundle)中被改变。此方法在onCreate() (更确切地说,在onStart()之后)之后被调用,是Android推荐恢复状态的另一个地方。
现在,我不知道为什么我的复选框会在onRestoreInstanceState中被更改,但至少我知道问题出现的地方了。令人惊讶的是,当我重写onRestoreInstanceState并且什么都不做(不调用super.onRestoreInstanceState)时,整个Activity就完美地工作了。
如果有人能告诉我为什么复选框的选中状态会在这个方法中被更改,我会非常想知道。在我看来,这看起来像是Android代码本身的一个bug。

4
勾选框的状态被改变(保留),因为它们具有 saveEnabled 属性并且默认设置为 true。请参见视图文档。即使在这个问题最初被提出三年后,这个 Android 功能仍然让开发人员感到惊讶。 - Kyle Ivey
谢谢你的提示。最近我在编写自定义MVVM模式时也遇到了类似的问题...无论如何,由于MVVM正在管理视图状态,我不得不设置myView.setSaveEnabled(false)。 - worked

2

如果你对这个问题有更多的了解,请告诉我。我也遇到了几乎完全相同的问题,只有覆盖onRestoreInstanceState()方法才起作用。非常奇怪。


1
Rpond,我没有重写onResume,所以我不认为那是问题所在。这里是Activity和相关布局供大家查看。在代码中,你会看到很多Log.d语句(曾经还有更多)。通过这些Log语句,我能够验证代码的工作方式与我的期望完全一致。此外,请注意我添加到每个CheckBox的onCheckChangedListener。它只是打印一个Log语句,告诉我其中一个CheckBox的状态已更改。正是通过这个,我能够确定CheckBoxes的状态在我的onCreate返回后被改变了。你会看到我在我的onCreate结尾处调用examineCheckboxes()。从这产生的Log语句并不是我屏幕上旋转后显示的内容,我可以看到我的框的状态在之后被改变了(因为onCheckChangedListener)。

SelectItems.java:

公共类SelectItems扩展自Activity {

public static final int SUCCESS = 95485839;

public static final String ITEM_LIST = "item list";
public static final String ITEM_NAME = "item name";

// Save state constants
private final String SAVE_SELECTED = "save selected";

private DbHelper db;

private ArrayList<Long> selectedIDs;

ArrayList<CheckBox> cboxes = new ArrayList<CheckBox>();

@Override
public void onCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);
    setContentView(R.layout.select_items);

    // Create database helper
    db = new DbHelper(this);
    db.open();

    initViews(savedInstanceState);

    examineCheckboxes();

}

private void initViews(Bundle savedState) {
    initButtons();

    initHeading();

    initList(savedState);
}

private void initButtons() {

    // Setup event for done button
    Button doneButton = (Button) findViewById(R.id.done_button);
    doneButton.setOnClickListener(new OnClickListener() {
        public void onClick(View v) {
            done();
        }
    });

    // Setup event for cancel button
    Button cancelButton = (Button) findViewById(R.id.cancel_button);
    cancelButton.setOnClickListener(new OnClickListener() {
        public void onClick(View v) {
            cancel();
        }
    });
}

private void initHeading() {

    String itemName = getIntent().getExtras().getString(ITEM_NAME);

    TextView headingField = (TextView) findViewById(R.id.heading_field);

    if (itemName.equals("")) {
        headingField.setText("No item name!");
    } else {
        headingField.setText("Item name: " + itemName);
    }
}

private void initList(Bundle savedState) {

    // Init selected id list
    if (savedState != null && savedState.containsKey(SAVE_SELECTED)) {
        long[] array = savedState.getLongArray(SAVE_SELECTED);
        selectedIDs = new ArrayList<Long>();

        for (long id : array) {
            selectedIDs.add(id);
        }

        Log.d("initList", "restoring from saved state");
        logIDList();
    } else {
        selectedIDs = (ArrayList<Long>) getIntent().getExtras().get(
                ITEM_LIST);

        Log.d("initList", "using database values");
        logIDList();
    }

    // Begin building item list
    LayoutInflater li = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    LinearLayout scrollContent = (LinearLayout) findViewById(R.id.scroll_content);

    Cursor cursor = db.getAllItems();
    startManagingCursor(cursor);
    cursor.moveToFirst();

    // For each Item entry, create a list element
    while (!cursor.isAfterLast()) {

        View view = li.inflate(R.layout.item_element, null);
        TextView name = (TextView) view.findViewById(R.id.item_name);
        TextView id = (TextView) view.findViewById(R.id.item_id);
        CheckBox cbox = (CheckBox) view.findViewById(R.id.checkbox);

        name.setText(cursor.getString(cursor
                .getColumnIndexOrThrow(DbHelper.COL_ITEM_NAME)));

        final long itemID = cursor.getLong(cursor
                .getColumnIndexOrThrow(DbHelper.COL_ID));
        id.setText(String.valueOf(itemID));

        // Set check box states based on selectedIDs array
        if (selectedIDs.contains(itemID)) {
            Log.d("set check state", "setting check to true for " + itemID);
            cbox.setChecked(true);
        } else {
            Log.d("set check state", "setting check to false for " + itemID);
            cbox.setChecked(false);
        }

        cbox.setOnClickListener(new OnClickListener() {

            public void onClick(View v) {
                Log.d("onClick", "id: " + itemID + ". button ref: "
                        + ((CheckBox) v));
                checkChanged(itemID);
            }

        });

        //I implemented this listener just so I could see when my
        //CheckBoxes were changing.  Through this I was able to determine
        //that my CheckBoxes were being altered outside my own code.
        cbox.setOnCheckedChangeListener(new OnCheckedChangeListener() {

            public void onCheckedChanged(CompoundButton arg0, boolean arg1) {

                Log.d("check changed", "button: " + arg0 + " changed to: "
                        + arg1);
            }

        });


        cboxes.add(cbox);

        scrollContent.addView(view);

        cursor.moveToNext();
    }

    cursor.close();

    examineCheckboxes();
}

private void done() {
    Intent i = new Intent();
    i.putExtra(ITEM_LIST, selectedIDs);
    setResult(SUCCESS, i);
    this.finish();
}

private void cancel() {
    db.close();
    finish();
}

private void checkChanged(long itemID) {
    Log.d("checkChaged", "checkChanged for: "+itemID);

    if (selectedIDs.contains(itemID)) {
        selectedIDs.remove(itemID);
    } else {
        selectedIDs.add(itemID);
    }
}

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);

    long[] array = new long[selectedIDs.size()];
    for (int i = 0; i < array.length; i++) {
        array[i] = selectedIDs.get(i);
    }

    outState.putLongArray(SAVE_SELECTED, array);
}

//Debugging method used to see what is in selectedIDs at any point in time.
private void logIDList() {
    String list = "";

    for (long id : selectedIDs) {
        list += id + " ";
    }

    Log.d("ID List", list);
}

//Debugging method used to check the state of all CheckBoxes.
private void examineCheckboxes(){
    for(CheckBox cbox : cboxes){
        Log.d("Check Cbox", "obj: "+cbox+" checked: "+cbox.isChecked());
    }
}

}

select_items.xml:

<?xml version="1.0" encoding="utf-8"?>

<TextView android:id="@+id/heading_field"
    android:layout_width="fill_parent" android:layout_height="wrap_content"
    android:layout_marginBottom="10dip" android:textSize="18sp"
    android:textStyle="bold" />

<LinearLayout android:id="@+id/button_layout"
    android:orientation="horizontal" android:layout_width="fill_parent"
    android:layout_height="wrap_content" android:layout_alignParentBottom="true">

    <Button android:id="@+id/done_button" android:layout_weight="1"
        android:layout_width="wrap_content" android:layout_height="wrap_content"
        android:text="Done" />

    <Button android:id="@+id/cancel_button" android:layout_weight="1"
        android:layout_width="wrap_content" android:layout_height="wrap_content"
        android:text="Cancel" />

</LinearLayout>

<ScrollView android:orientation="vertical"
    android:layout_below="@id/heading_field" android:layout_above="@id/button_layout"
    android:layout_width="fill_parent" android:layout_height="wrap_content">
    <LinearLayout android:id="@+id/scroll_content"
        android:orientation="vertical" android:layout_width="fill_parent"
        android:layout_height="fill_parent">
    </LinearLayout>
</ScrollView>

item_element.xml:

<?xml version="1.0" encoding="utf-8"?>

<CheckBox android:id="@+id/checkbox" android:layout_width="wrap_content"
    android:layout_height="wrap_content" android:layout_alignParentRight="true"
    android:layout_marginRight="5dip" />

<TextView android:id="@+id/item_name" android:layout_width="wrap_content"
    android:layout_height="wrap_content" android:textSize="18sp"
    android:layout_centerVertical="true" android:layout_toLeftOf="@id/checkbox"
    android:layout_alignParentLeft="true" />

<TextView android:id="@+id/item_id" android:layout_width="wrap_content"
    android:layout_height="wrap_content" android:visibility="invisible" />


1

您还可以使用 onSaveInstanceState(Bundle savedInstanceState)onRestoreInstanceState(Bundle savedInstanceState)

示例

@Override
public void onSaveInstanceState(Bundle savedInstanceState)
{
  // EditTexts text
  savedInstanceState.putString("first_et",((EditText)findViewById(R.id.et_first)).getText().toString());
  // EditTexts text
  savedInstanceState.putInt("first_et_v", ((EditText)findViewById(R.id.et_first)).getVisibility());
  super.onSaveInstanceState(savedInstanceState);
}

@Override
public void onRestoreInstanceState(Bundle savedInstanceState)
{
  super.onRestoreInstanceState(savedInstanceState);
  // EditTexts text
  ((EditText)findViewById(R.id.et_first)).setText(savedInstanceState.getString("first_et"));
  // EditText visibility
  ((EditText)findViewById(R.id.et_first)).setVisibility(savedInstanceState.getInt("first_et_v"));
}

1

我也遇到过这个问题。你应该在onRestoreInstanceState()中恢复复选框的状态,而不是在onCreate()中。

当屏幕方向改变时,活动被销毁,并在onCreate()之后调用onRestoreInstanceState()。由于onRestoreInstanceState()的父/默认实现会自动恢复具有ID的视图的状态,因此它会在onCreate()之后恢复你的复选框并覆盖它们 - 显然是由于它们具有相同的ID(框架错误?)。

http://developer.android.com/reference/android/app/Activity.html

http://developer.android.com/reference/android/app/Activity.html#onRestoreInstanceState(android.os.Bundle)


这是由于saveEnabled字段引起的。请参阅View文档。似乎存在一个错误,即具有相同ID的视图仅将其状态恢复为最后一个该视图的实例。 - Kyle Ivey

1

可能的问题是每当调用onRestoreInstanceState(Bundle)时,它会将您的应用程序的某些或所有“设置”重置为启动时的“默认值”。解决此问题的最佳方法是通过应用程序生命周期方法调用和管理。此外,每当您更改应用程序中不想丢失的内容时,请将更改保存到saveInstanceState(Bundle)或创建自己的Bundle()以暂时保存更改,直到您可以将更改持久保存在文件或其他地方。通过Intent在活动之间传递bundle非常容易。根据需要保留“设置”的时间长短来保存所需内容。


0

有一个更容易解释的解决方案。

如果在Android CHECKBOXES和RADIOBUTTON以及RADIOGROUPS上没有设置"id"属性,它们的行为会变得奇怪。

我在我的代码中遇到了完全相同的问题,在我给复选框设置id之后,它开始正常工作,而不需要禁用任何超类方法。


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