Внутренний PreferenceScreen не открывается с PreferenceFragmentCompat

Мой внутренний PreferenceScreen PreferenceFragmentCompat не отображается или, кажется, игнорирует события касания.

я создал MyPreferenceFragment тот extends PreferenceFragmentCompat

public class MyPreferenceFragment extends PreferenceFragmentCompat {
 @Override
  public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
    addPreferencesFromResource(R.xml.preferences);
  }
}

Затем я изменил свою тему в styles.xml лайк

<style name="AppTheme" parent="@style/Theme.AppCompat.Light">
  <item name="preferenceTheme">@style/PreferenceThemeOverlay</item>
</style>

И, наконец, создать мой preferences.xml файл как

<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
    <CheckBoxPreference android:title="Check Me"/>
    <PreferenceScreen android:title="My Screen"> <!-- This is not opening -->
        <EditTextPreference android:title="Edit text" />
    </PreferenceScreen>
</PreferenceScreen>

На build.gradle Я добавил оба:

compile 'com.android.support:appcompat-v7:23.0.1'
compile 'com.android.support:preference-v7:23.0.1'

код деятельности

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

activity_main.xml

<fragment xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/fragment"
    android:name="com.mando.preferenceapp.MyPreferenceFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

Тестируя приведенный выше код, я не могу открыть / попасть на экран настроек. Я что-то пропустил? Почему это не работает?

8 ответов

Решение

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

Шаг 1. Activity

public class MyActivity extends AppCompatActivity implements
        PreferenceFragmentCompat.OnPreferenceStartScreenCallback {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if (savedInstanceState == null) {
            // Create the fragment only when the activity is created for the first time.
            // ie. not after orientation changes
            Fragment fragment = getSupportFragmentManager().findFragmentByTag(MyPreferenceFragment.FRAGMENT_TAG);
            if (fragment == null) {
                fragment = new MyPreferenceFragment();
            }

            FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
            ft.replace(R.id.fragment_container, fragment, MyPreferenceFragment.FRAGMENT_TAG);
            ft.commit();
        }
    }

    @Override
    public boolean onPreferenceStartScreen(PreferenceFragmentCompat preferenceFragmentCompat,
                                           PreferenceScreen preferenceScreen) {
        FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
        MyPreferenceFragment fragment = new MyPreferenceFragment();
        Bundle args = new Bundle();
        args.putString(PreferenceFragmentCompat.ARG_PREFERENCE_ROOT, preferenceScreen.getKey());
        fragment.setArguments(args);
        ft.replace(R.id.fragment_container, fragment, preferenceScreen.getKey());
        ft.addToBackStack(preferenceScreen.getKey());
        ft.commit();
        return true;
    }
}

Подсказки.

  • Не добавлять фрагмент xml у вас будут сбои при смене ориентации.
  • Обработайте воссоздание деятельности / фрагмент добавить в onCreate чтобы не потерять свой фрагмент в окне настроек.
  • Хост-активность фрагмента должна реализовывать PreferenceFragmentCompat.OnPreferenceStartScreenCallback и воссоздать фрагменты одного и того же экземпляра.

Шаг 2. PreferenceFragment

public class MyPreferenceFragment extends PreferenceFragmentCompat {

    public static final String FRAGMENT_TAG = "my_preference_fragment";

    public MyPreferenceFragment() {
    }

    @Override
    public void onCreatePreferences(Bundle bundle, String rootKey) {
        setPreferencesFromResource(R.xml.preferences, rootKey);
    }

}

Подсказки.

  • Используйте метод setPreferencesFromResource и воспользоваться rootKey каждого экрана. Таким образом, ваш код будет повторно использован правильно.
  • Имейте в виду, что если у вас есть такой код findPreference в вашем фрагменте должно быть null проверяет, как, когда вы были на внутренних экранах, это ничего не даст вам.

Сейчас не хватает реализации стрелки назад на панели действий (домашнее действие), но она никогда не работает сама по себе;-)

Я также создал демонстрационное приложение, упаковывающее весь этот код, который вы можете найти на github.

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

@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey){
    if(getArguments() != null){
        String key = getArguments().getString("rootKey");
        setPreferencesFromResource(R.xml.preferences, key);
    }else{
        setPreferencesFromResource(R.xml.preferences, rootKey);
    }
}

@Override
public void onNavigateToScreen(PreferenceScreen preferenceScreen){
    ApplicationPreferencesFragment applicationPreferencesFragment = new ApplicationPreferencesFragment();
    Bundle args = new Bundle();
    args.putString("rootKey", preferenceScreen.getKey());
    applicationPreferencesFragment.setArguments(args);
    getFragmentManager()
            .beginTransaction()
            .replace(getId(), applicationPreferencesFragment)
            .addToBackStack(null)
            .commit();
}

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

public class PreferencesActivity extends AppCompatActivity implements PreferenceFragmentCompat.OnPreferenceStartScreenCallback {
    final static private String KEY = "key";

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

        setContentView(R.layout.preferences);

        setSupportActionBar((Toolbar) findViewById(R.id.toolbar));
        ActionBar actionBar = getSupportActionBar();
        if (actionBar != null) actionBar.setDisplayHomeAsUpEnabled(true);

        if (savedInstanceState != null)
            return;

        Fragment p = new PreferencesFragment();

        String key = getIntent().getStringExtra(KEY);
        if (key != null) {
            Bundle args = new Bundle();
            args.putString(PreferenceFragmentCompat.ARG_PREFERENCE_ROOT, key);
            p.setArguments(args);
        }

        getSupportFragmentManager().beginTransaction()
                .add(R.id.preferences, p, null)
                .commit();
    }

    @Override public boolean onPreferenceStartScreen(PreferenceFragmentCompat preferenceFragmentCompat, PreferenceScreen preferenceScreen) {
        Intent intent = new Intent(PreferencesActivity.this, PreferencesActivity.class);
        intent.putExtra(KEY, preferenceScreen.getKey());
        startActivity(intent);
        return true;
    }

    @Override public boolean onOptionsItemSelected(MenuItem item) {
        if (item.getItemId() == android.R.id.home) {
            onBackPressed();
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    public static class PreferencesFragment extends PreferenceFragmentCompat implements ... {

        private static final String FRAGMENT_DIALOG_TAG = "android.support.v7.preference.PreferenceFragment.DIALOG";
        private String key;


        @Override public void onCreatePreferences(Bundle bundle, String key) {
            setPreferencesFromResource(R.xml.preferences, this.key = key);
        }

        // this only sets the title of the action bar
        @Override public void onActivityCreated(Bundle savedInstanceState) {
            ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar();
            if (actionBar != null) actionBar.setTitle((key == null) ? "Settings" : findPreference(key).getTitle());
            super.onActivityCreated(savedInstanceState);
        }
    }
}

XML:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_margin="0dp"
    android:orientation="vertical"
    android:padding="0dp"
    android:id="@+id/preferences">

    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="?attr/colorPrimary" />

    <!-- preference fragment will be inserted here programmatically -->

</LinearLayout>

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

Вот основное решение. (Он не охватывает все крайние случаи, см. Расширенное решение ниже)

Убедитесь, что у вас есть configChanges="direction", чтобы предотвратить создание / уничтожение

    <activity
        android:name=".MyPreferencesActivity"
        android:configChanges="orientation" />

В упражнении вы хотите сохранить стек PreferenceScreens и нажимать / выталкивать по мере необходимости

    /* track the screens as a Stack */
    private Stack<PreferenceScreen> preferenceScreens = new Stack<>();

    // ensure your Activity implements PreferenceFragmentCompat.OnPreferenceStartScreenCallback
    @Override
    public boolean onPreferenceStartScreen(PreferenceFragmentCompat preferenceFragmentCompat, PreferenceScreen preferenceScreen) {
        preferenceScreens.push(preferenceFragmentCompat.getPreferenceScreen());
        preferenceFragmentCompat.setPreferenceScreen(preferenceScreen);
        return true;
    }

    @Override
    public void onBackPressed() {
        if (preferenceScreens.empty()) {
            super.onBackPressed();
        } else {
            prefsFragment.setPreferenceScreen(preferenceScreens.pop());
        }
    }

Необязательно: В свой фрагмент, который расширяет PreferenceFragmentCompat, добавьте setRetainInstance(true). (Обратите внимание, что без этого он, скорее всего, будет работать, но иногда он может "сломаться". Если вы установите для параметра "Не сохранять активность" значение true, и вы увидите, что он будет собран)

    @Override
    public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {

        setRetainInstance(true);

        // Load the preferences from an XML resource
        setPreferencesFromResource(R.xml.preferences, rootKey);
    ...

Это оно! За исключением того, что если вы хотите охватить крайние случаи...

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

Обратите внимание, что принятый ответ на самом деле не сохраняет состояние.

  1. установите для параметра "Не сохранять активность" значение True
  2. перейти к вложенному PreferenceScreen
  3. Нажмите домой и затем вернитесь к приложению
  4. Он "должен" все еще находиться на Nested PreferenceScreen, но на самом деле он находится на корневом

Полное расширенное решение с использованием API PreferenceFragmentCompat и сохранением стека PreferenceScreen

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.preference.PreferenceFragmentCompat;
import android.support.v7.preference.PreferenceScreen;
import java.util.ArrayList;
import java.util.Objects;
import java.util.Stack;

/**
 * Class to Show the preference screen with Activity keeping state
 * @author Aaron Vargas
 */
public class MyPreferencesActivityStateful extends AppCompatActivity implements PreferenceFragmentCompat.OnPreferenceStartScreenCallback {
    private static final String PREFERENCE_SCREENS = "PREFERENCE_SCREENS";
    private PrefsFragment prefsFragment;
    private Stack<PreferenceScreen> preferenceScreens = new Stack<>();

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

        // Display the fragment as the main content. Re-Use if possible
        String tag = PrefsFragment.class.getName();
        prefsFragment = (PrefsFragment) getSupportFragmentManager().findFragmentByTag(tag);
        if (prefsFragment == null) prefsFragment = new PrefsFragment();

        getSupportFragmentManager().beginTransaction().replace(android.R.id.content,
                prefsFragment, tag).commit();
    }

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

        // rebuild preferenceScreen stack
        for (String screenKey : Objects.requireNonNull(savedInstanceState.getStringArrayList(PREFERENCE_SCREENS))) {
            preferenceScreens.push((PreferenceScreen) prefsFragment.findPreference(screenKey));
        }

        PreferenceScreen preferenceScreen = preferenceScreens.pop();
        if (preferenceScreen != prefsFragment.getPreferenceScreen()) { // optimize if same
            prefsFragment.setPreferenceScreen(preferenceScreen);
        }
    }

    @Override
    public boolean onPreferenceStartScreen(PreferenceFragmentCompat preferenceFragmentCompat, PreferenceScreen preferenceScreen) {
        preferenceScreens.push(preferenceFragmentCompat.getPreferenceScreen());
        preferenceFragmentCompat.setPreferenceScreen(preferenceScreen);
        return true;
    }

    @Override
    public void onBackPressed() {
        // account for onRestore not getting called equally to onSave
        while (preferenceScreens.contains(prefsFragment.getPreferenceScreen())) {
            preferenceScreens.remove(prefsFragment.getPreferenceScreen());
        }

        if (preferenceScreens.empty()) {
            super.onBackPressed();
        } else {
            prefsFragment.setPreferenceScreen(preferenceScreens.pop());
        }
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        preferenceScreens.push(prefsFragment.getPreferenceScreen());

        ArrayList<String> keys = new ArrayList<>(preferenceScreens.size());
        for (PreferenceScreen screen : preferenceScreens) {
            keys.add(screen.getKey());
        }
        outState.putStringArrayList(PREFERENCE_SCREENS, keys);
    }

    public static class PrefsFragment extends PreferenceFragmentCompat {

        @Override
        public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {

            setRetainInstance(true); // ensure in manifest - android:configChanges="orientation"

            // Load the preferences from an XML resource
            setPreferencesFromResource(R.xml.preferences, rootKey);
        }
    }

}

Вы также можете обрабатывать все это в своем фрагменте вместо действия. Вот суть этого https://gist.github.com/aaronvargas/0f210ad8643b512efda4acfd524e1232

Используя компонент навигации ( Android Jetpack) и Kotlin, теперь это очень просто:

class PrefsFragment : PreferenceFragmentCompat() {
    private val args: PrefsFragmentArgs by navArgs()

    override fun onCreatePreferences(state: Bundle?, rootKey: String?) {
        setPreferencesFromResource(R.xml.prefs, args.rootKey)
    }

    override fun onNavigateToScreen(preferenceScreen: PreferenceScreen?) {
        findNavController().navigate(
            PrefsFragmentDirections.changeRoot(preferenceScreen!!.key)
        )
    }
}

Альтернатива с использованием компонента навигации + androidx.appcomat: /questions/6690603/kak-otkryit-novyij-preferencefragment-iz-tekuschego-ispolzuya-novyij-android-x-api/55278525#55278525

При этом вы не потеряете задний стек и не вернетесь к настройкам главной страницы при нажатии кнопки возврата.

Основываясь на решении @squirrel Intent, я заставил его работать таким образом. Это требует еще меньше взлома.
Деятельность:

import android.support.v7.app.AppCompatActivity;

public class SettingsActivity extends AppCompatActivity {

    public static final String TARGET_SETTING_PAGE = "target";

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

        SettingsFragment settingsFragment = new SettingsFragment();
        Intent intent = getIntent();
        if (intent != null) {
            String rootKey = intent.getStringExtra(TARGET_SETTING_PAGE);
            if (rootKey != null) {
                settingsFragment.setArguments(Bundler.single(TARGET_SETTING_PAGE, rootKey));
            }
        }

        getFragmentManager().beginTransaction()
                .replace(android.R.id.content, settingsFragment)
                .commit();
    }
}

Фрагмент:

import android.support.v14.preference.PreferenceFragment;

public class SettingsFragment extends PreferenceFragment {

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

        Bundle arguments = getArguments();
        if (arguments != null && arguments.getString(TARGET_SETTING_PAGE) != null) {
            setPreferencesFromResource(R.xml.preferences, arguments.getString(TARGET_SETTING_PAGE));
        } else {
            addPreferencesFromResource(R.xml.preferences);
        }
    }

    @Override
    public void onNavigateToScreen(PreferenceScreen preferenceScreen) {
        Intent intent = new Intent(getActivity(), SettingsActivity.class)
                .putExtra(TARGET_SETTING_PAGE, preferenceScreen.getKey());
        startActivity(intent);

        super.onNavigateToScreen(preferenceScreen);
    }
}

Печально, что вам нужно так много взломов в библиотеках поддержки приложений app для чего-то, что работает безупречно "из коробки" в стандартном Android.

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

Образец кода:

       <PreferenceCategory app:title="@string/choose_theme"
        android:icon="@drawable/ic_baseline_color_lens_24">
        <SwitchPreference
            android:title="@string/apply_night_mode"
            android:key="@string/key_enable_night_mode"/>
        <PreferenceScreen
            android:fragment="com.example.simbokeyboard.BlankFragment"
            android:title="Custom Theme"
            android:summary="@string/theme_summary">
            <Preference
                android:key="@string/choose_theme"
                android:title="@string/choose_theme"
                android:layout="@layout/theme_chooser"/>
        </PreferenceScreen>
    </PreferenceCategory>
Другие вопросы по тегам