сохранение состояния в составных виджетах - PullRequest
9 голосов
/ 08 февраля 2011

Вопрос

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

Пример

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

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

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

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

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


¹ В иерархии представлений устанавливается идентификатор LinearLayout, который NumberPicker распространяется на ваши идентификаторы.

Ответы [ 5 ]

16 голосов
/ 03 марта 2012

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

0 голосов
/ 13 января 2012

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

Последний необходимый компонент - это подкласс <a href="http://developer.android.com/reference/android/os/Parcelable.html" rel="nofollow">Parcelable</a>, подобный следующему:

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

}

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

0 голосов
/ 09 июня 2011

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

Для моего приложения, и, вероятно, в 99% других, гораздо более простой подход (взлом) работает. Я понял, что пространство имен для У идентификаторов есть место для 65536 уникальных идентификаторов (0x7f090000 до 0x7f09ffff), но мое приложение использует только 20 или около того, и они выделяются монотонно возрастающими от начало. Кроме того, сам виджет NumberPicker имеет уникальный идентификатор в дереве (например, как в оригинальный пост, R.id.hour, R.id.minute и R.id.second). Мое решение - переназначить Id виджета EditText. к идентификатору виджета NumberPicker плюс смещение.

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

mText.setId(getId() + 1000);

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

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

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

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

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

0 голосов
/ 17 ноября 2011

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

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

С веб-сайта Android Developer :

android: tag

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

0 голосов
/ 11 марта 2011

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

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

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

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

...