сохранение состояния виджетов составного вида

вопрос

как сохранить состояние экземпляра виджета просмотра, когда с помощью XML-определенных макетов виджетов компоненты отдельных экземпляров виджета имеют одинаковый идентификатор?

пример

взять, например,NumberPicker виджет, который используется в элементе TimePicker виджет (обратите внимание, что NumberPicker не подвергается воздействию SDK). Это простой виджет с тремя компонентами, которые раздуваются от number_picker.xml: одна кнопка инкремента, одна кнопка декремента, и одно EditText где вы можете напрямую ввести номер. Для того, чтобы код взаимодействовал с этими виджетами, все они имеют идентификаторы (R.id.increment, R.id.decrement и R.id.timepicker_input соответственно).

допустим, у вас есть три NumberPickers в XML-макете, и вы даете им разные идентификаторы (например. R.id.hour, R.id.minute).1 затем этот макет раздувается до представления содержимого действия. Мы решаем изменить ориентацию деятельности, так что Activity.onSaveInstanceState(Bundle) услужливо сохраняет наше состояние представления для каждого представления, которое имеет идентификатор (это поведение по умолчанию.)

к сожалению, три NumberPickerс EditTexts, что все разделяют тот же ID -R.id.timepicker_input. Таким образом, когда действие восстанавливается, то дальше всех в иерархии взглядов находится тот, чье состояние, по-видимому, сохраняется для всех троих. Кроме того, фокус переходит на первый NumberPicker при восстановлении независимо от того, какой из них имел Фокус при сохранении.

TimePicker обходит эту проблему, сохраняя само состояние отдельно. К сожалению, это не сохранит позицию курсора или сфокусированное представление без дополнительной работы. Я не уверен, как он сохраняет это состояние, если он вообще это делает (и быстро играет с диалоговым окном ввода времени, похоже, указывает, что он может каким-то образом).

см. пример кода, чтобы продемонстрировать эту проблему: https://github.com/xxv/AndroidNumberPickerBug


1 в иерархии представлений это задает идентификатор LinearLayout это NumberPicker распространяется на ваше удостоверение личности.

5 ответов


я наткнулся на ту же проблему при попытке создать свой собственный составной вид. Глядя на исходный код Android, я считаю, что правильный способ реализации составных представлений заключается в том, чтобы само составное представление взяло на себя ответственность за сохранение и восстановление состояния экземпляра своих дочерних элементов и предотвратило передачу вызовов для сохранения и восстановления состояния экземпляра в дочерние представления. Это работает вокруг проблемы идентификаторов дочерних представлений, не являющихся уникальными, когда у вас есть больше один экземпляр одного и того же соединения в деятельности.

Это может показаться сложным, но это действительно довольно легко, и 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. Чтобы исправить это, вы можете переопределить оба и реализовать решение, которое я описал.


Я только что столкнулся с той же проблемой с составным видом с трех NumberPickers. Я нашел решение на другом сайте это включало переназначение идентификатора timepicker_input на уникальный случайный идентификатор. Это работает, но сложно, потому что однажды выбирается новый Id, который должен сохраняться в конфигурации изменения, поэтому для этого требуется дополнительный код.

для моего применения, и вероятно в 99% других, много более простой подход (hack) работает. Я понял, что пространство имен для Ids имеет место для 65536 уникальных идентификаторов (от 0x7f090000 до 0x7f09ffff), но мое приложение использует только 20 или около того, и они выделяются монотонно увеличивающимися от начало. Кроме того, сам виджет NumberPicker имеет уникальный identfier в дереве (например, в оригинальный пост, ИД Р..час, ИД Р..минуту и ИД Р..секунду). Мое решение-переназначить идентификатор виджета EditText к идентификатору виджета NumberPicker плюс смещение.

Это одна строка измените код NumberPicker. Просто добавить:

mText.setId(getId() + 1000);

после следующей строки в NumberPicker.java:

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

смещение конечно можно отрегулировать основало на требованиях к применения.

Я предполагаю, что тот же подход будет работать для других составных виджетов.

для приведенного выше примера, этот подход позволяет состояние отдельные виджеты EditText для сохранения и восстановления, а также сфокусированный взгляд также.


Я немного опоздал на игру, но я хотел бы внести свой вклад.

можно использовать android:tag для каждой группы View. Таким образом, вы можете "перезагрузить" каждое состояние.

С Android Разработчик:

android: tag

укажите тег для этого представления, содержащий строку, которая будет получена позже с представлениями.getTag () или поиск с помощью View.findViewWithTag().


у меня есть составной виджет с описанием состояния, состоящим из одного целого числа 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() правильно.
Я сохраняю состояние, только если виджет имеет назначенный идентификатор, который я использую в качестве тега посылки

на dispatchRestoreInstanceState(), ты распаковывал бандероль.
Я выполняю распаковку только в том случае, если виджет имеет назначенный идентификатор, а контейнер содержит посылку с тегом, соответствующим этому идентификатору.
на 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];
            }
    };

}

использование этой структуры намного проще, чем манипулирование фрагментами и управление конфигурацией


довольно просто: вы не делаете. Просто пойдите и отключите дерьмо изменения ориентации в файле манифеста. Механизм сохранения состояния представления по своей сути несовершенен,они просто не продумали это.

Если вы хотите сохранить свое состояние, вы не можете повторно использовать id в одном действии. Это по существу означает, что вы не можете использовать один макет более одного раза, что делает более сложные виджеты, такие как TimePicker, практически невозможно сделать правильно.

вы можете сделать детей сохраняйте их состояние, переопределяя dispatchSaveInstanceState и взломал его, но я не нашел способа сосредоточиться, кроме того, чтобы управлять этим самостоятельно.

Я думаю, что они могли бы исправить это, сделав область состояния без нарушения API, но не задерживайте дыхание.