Проблемы с экранами предпочтений с двумя панелями


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

Setup
Это только для ICS и up. У меня есть PreferenceActivity, который загружает preference-headers. Каждый заголовок связывается с A Fragment, который, в свою очередь, загружает a PreferenceScreen. Довольно далеко от Мила.

Подробности
Все работало хорошо, пока я не заметил, что Андроид будет только автоматическое переключение на двухпанельный поиск определенных экранов. После некоторых исследований я узнал из сообщенияCommonsware , что Android будет делать это только для sw720dp. Немного расточительно, если вы спросите меня, так как многие устройства def имеют достаточно места для двух стекол. Поэтому я переопределил метод onIsMultiPane(), чтобы вернуть true для w600dp и выше. Сработало как заклинание....вроде.

Дано устройство, которое будет показывать однопанельную в портретной и двухпанельную в альбомной ориентации; просмотр заголовков в портретной ориентации и поворот к пейзаж, прекрасно работает. Однако если выбрать заголовок и загрузить его последующий экран в портретном режиме, а затем повернуть в альбомную ориентацию, устройство останется однопанельным, а не переключится обратно в двухпанельное. Если вы затем вернетесь к экрану заголовков, он вернется к двухпанельному виду, за исключением того, что он не будет предварительно выбирать заголовок. В результате панель подробностей остается пустой.

Является ли это намеренным поведением? Во всяком случае, чтобы обойти его? Я также попытался переопределить onIsHidingHeaders(), но это только вызвало все, чтобы показать пустой экран.

Код
Предпочтительная Деятельность:

public class SettingsActivity extends PreferenceActivity {
@Override
public void onBuildHeaders(List<Header> target) {
    super.onBuildHeaders(target);
    loadHeadersFromResource(R.xml.preference, target);
}

@Override
public boolean onIsMultiPane() {
    return getResources().getBoolean(R.bool.pref_prefer_dual_pane);
}
}


Фрагмент Заголовка Предпочтения:

public class ExpansionsFragment extends PreferenceFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    addPreferencesFromResource(R.xml.pref_expansions);
}

public static ExpansionsFragment newInstance() {
    ExpansionsFragment frag = new ExpansionsFragment();

    return frag;
}
}
2 36

2 ответа:

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

Если вас не волнует объяснение, вы можете просто перейти к коду. Если вы не заботитесь о ICS, большая часть кода отслеживания заголовка может быть удалена как JB добавил геттер для списка массивов заголовков.

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

Однако в режиме одной панели при щелчке по заголовку соответствующий фрагмент присоединяется к новому действию PreferenceActivity. Этот новый фрагмент, содержащий PreferenceActivity, никогда не вызывает onBuildHeaders(). И с чего бы это? Ему не нужно их показывать. Эта ложь и есть проблема.

При повороте этого фрагмента в режим двойной панели у него нет никакого списка заголовков, поэтому он просто продолжает показывать только фрагмент. Даже если он показывал список заголовка, у вас будут некоторые проблемы с backstack, так как теперь у вас будет две копии PreferenceActivity, показывающие заголовки. Продолжайте нажимать достаточное количество заголовков, И вы получите довольно длинный стек действий для пользователя, чтобы вернуться к нему. В результате ответ прост. Просто finish() активность. Затем он загрузит исходную PreferenceActivity, которая имеет список заголовков и правильно покажет режим двойной панели.

Автоматический Выбор Заголовка
Следующая проблема, которая требовала решения, заключалась в том, что переключение между режимом одиночной и двойной панели с новым исправлением не автоматически выбирало заголовок. Вы остались со списком заголовков и без подробностей фрагмента нагруженный. Это исправление не так просто. В принципе, вы просто должны отслеживать, какой заголовок был в последний раз нажат и обеспечить создание PreferenceActivity...заголовок всегда выбирается.

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

Если вы не заботитесь о ICS, то вы можете просто использовать метод getHeaders(), представленный в JB, и не беспокоиться о любом из этих сохраненных/восстановленных состояний.

Код

public class SettingsActivity extends PreferenceActivity {
private static final String STATE_CUR_HEADER_POS = "Current Position";
private static final String STATE_HEADERS_LIST   = "Headers List";

private int mCurPos = AdapterView.INVALID_POSITION;  //Manually track selected header position for dual pane mode
private ArrayList<Header> mHeaders;  //Manually track headers so we can select one. Required to support ICS.  Otherwise JB exposes a getter instead.

@Override
public void onBuildHeaders(List<Header> target) {
    loadHeadersFromResource(R.xml.preference, target);
    mHeaders = (ArrayList<Header>) target;  //Grab a ref of the headers list
}

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    //This is the only code required for ensuring a dual pane mode shows after rotation of a single paned preference screen
    if (onIsMultiPane() && onIsHidingHeaders()) {
        finish();
    }
}

@Override
public boolean onIsMultiPane() {
    //Override this if you want dual pane to show up on smaller screens
    return getResources().getBoolean(R.bool.pref_prefer_dual_pane);
}

@Override
protected void onListItemClick(ListView l, View v, int position, long id) {
    super.onListItemClick(l, v, position, id);

    //Intercept a header click event to record its position.
    mCurPos = position;
}

@Override
protected void onRestoreInstanceState(Bundle state) {
    super.onRestoreInstanceState(state);

    //Retrieve our saved header list and last clicked position and ensure we switch to the proper header.
    mHeaders = state.getParcelableArrayList(STATE_HEADERS_LIST);
    mCurPos = state.getInt(STATE_CUR_HEADER_POS);
    if (mHeaders != null) {
        if (mCurPos != AdapterView.INVALID_POSITION) {
            switchToHeader(mHeaders.get(mCurPos));
        } else {
            switchToHeader(onGetInitialHeader());
        }
    }
}

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

    //Persist our list and last clicked position
    if (mHeaders != null && mHeaders.size() > 0) {
        outState.putInt(STATE_CUR_HEADER_POS, mCurPos);
        outState.putParcelableArrayList(STATE_HEADERS_LIST, mHeaders);
    }
}
}

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

Класс настроек не должен иметь никакого отношения к проблеме ориентации, но включать его в любом случае, чтобы быть ясным.

В моем комментарии кода, смотрите, если checkNeedsResource вызов в onCreate поможет в все:

public class SettingsActivity
extends 
    PreferenceActivity
{

@SuppressWarnings("deprecation")
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // Show settings without headers for single pane or pre-Honeycomb. Make sure to check the
    // single pane or pre-Honeycomb condition again after orientation change.
    if (checkNeedsResource()) {
        MyApp app = (MyApp)getApplication();
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
        Settings settings = new Settings();
        addPreferencesFromResource(R.xml.prefs_api);
        settings.setupPreference(findPreference(MyApp.KEY_USERNAME), prefs.getString(MyApp.KEY_USERNAME, null), true);
        settings.setupPreference(findPreference(MyApp.KEY_API_URL_ROOT), prefs.getString(MyApp.KEY_API_URL_ROOT, null), true);
        if (this.isHoneycomb) {
            // Do not delete this. We may yet have settings that only apply to Honeycomb or higher.
            //addPreferencesFromResource(R.xml.prefs_general);
        }
        addPreferencesFromResource(R.xml.prefs_about);
        settings.setupPreference(findPreference(MyApp.KEY_VERSION_NAME), app.getVersionName());
    }
}

@TargetApi(Build.VERSION_CODES.HONEYCOMB)
@Override
public void onBuildHeaders(List<Header> target) {
    super.onBuildHeaders(target);

    // This check will enable showing settings without headers for single pane or pre-Honeycomb. 
    if (!checkNeedsResource()) {
        loadHeadersFromResource(R.xml.pref_headers, target);
    }
}

private boolean checkNeedsResource() {
    // This check will enable showing settings without headers for single pane or pre-Honeycomb. 
    return (!this.isHoneycomb || onIsHidingHeaders() || !onIsMultiPane());
}

private boolean isHoneycomb = (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB);

}

public class Settings {

public Settings() {
}

public void setupPreference(Preference pref, String summary, boolean setChangeListener) {
    if (pref != null) {
        if (summary != null) {
            pref.setSummary(summary);
        }

        pref.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {

            @Override
            public boolean onPreferenceChange(Preference pref, Object newValue) {
                pref.setSummary(newValue.toString());
                return true;
            }

        });
    }
}

public void setupPreference(Preference pref, String summary) {
    setupPreference(pref, summary, false);
}

}