web-dev-qa-db-fra.com

Inner PreferenceScreen ne s'ouvre pas avec PreferenceFragmentCompat

Mon écran de préférences interne de PreferenceFragmentCompat ne s'affiche pas ou semble ignorer les événements de tapotement.

J'ai créé MyPreferenceFragment que extends PreferenceFragmentCompat 

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

alors j'ai changé mon thème à styles.xml comme

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

Et enfin, créez mon fichier preferences.xml comme

<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>

Au build.gradle j'ai ajouté les deux:

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

code de l'activité

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" />

Test du code ci-dessus Je ne peux pas ouvrir/entrer dans l'écran des préférences. Est-ce que je manque quelque chose? Pourquoi ça ne marche pas?

21
madlymad

Après avoir passé de nombreuses heures à essayer, à chercher et heureusement avec l’aide des créateurs de la bibliothèque de support. J'ai réussi à le faire fonctionner.

Étape 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;
    }
}

Conseils. 

  • N'ajoutez pas le fragment de xml, vous aurez des plantages lors de changements d'orientation.
  • Gérez les reconstitutions de l'activité/du fragment ajouté dans onCreate afin d'éviter de perdre votre fragment lorsque vous vous trouvez dans un écran de préférences.
  • L'activité hôte du fragment doit implémenter le PreferenceFragmentCompat.OnPreferenceStartScreenCallback et recréer des fragments de la même instance.

Étape 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);
    }

}

Conseils.

  • Utilisez la méthode setPreferencesFromResource et tirez parti de la rootKey de chaque écran. De cette façon, votre code sera réutilisé correctement.
  • Gardez à l'esprit que si vous avez un code comme findPreference dans votre fragment, il devrait avoir des vérifications null comme lorsque vous étiez dans les écrans intérieurs, cela ne vous donnerait rien.

Ce qui manque maintenant, c’est l’implémentation de la flèche arrière dans la barre d’action (action home) mais cela ne marche jamais tout seul ;-)

J'ai également créé une application de démonstration contenant tout ce code que vous pouvez trouver sur github .

31
madlymad

La solution consiste à démarrer un autre fragment de la même classe mais avec une clé racine différente. Aucune activité d'activité impliquée.

@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();
}
5
Quireg

Je l'ai fait légèrement différemment, je lance une nouvelle activité pour chaque écran. Cela semble nécessiter moins de piratage: inutile de gâcher l'échange de fragments et de couleurs d'arrière-plan. Vous bénéficiez également d'une animation de changement d'activité en bonus!

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>
5
squirrel

Une autre solution consiste à suivre vous-même les écrans de préférences et à utiliser l’application PreferenceFragmentCompat api.

Voici la solution de base. (Cela ne couvre pas tous les cas Edge, voir la solution avancée ci-dessous)

Assurez-vous d'avoir configChanges = "orientation" pour empêcher la création/destruction

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

Dans l'activité, vous souhaitez conserver une pile d'écrans de préférence et de push/pop selon vos besoins

    /* 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());
        }
    }

Facultatif: dans votre fragment qui étend PreferenceFragmentCompat, ajoutez setRetainInstance (true). (Notez que sans , Cela fonctionnera probablement aussi, mais il pourrait "se" casser de temps en temps. Si vous définissez "Ne pas garder les activités" sur true, et Vous verrez que cela va être collecté)

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

        setRetainInstance(true);

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

C'est tout! Sauf que si vous voulez couvrir les cas Edge ...

Solution avancée (si vous définissez 'Ne pas garder les activités sur True, vous devez vous assurer de pouvoir tout reconstruire à partir de savedInstanceState)

Notez que la réponse acceptée ne conserve pas réellement l'état. 

  1. définir 'Ne pas garder les activités' sur True
  2. accédez à un écran PreferenceScreen imbriqué
  3. Appuyez sur home puis revenez à l'application
  4. Il devrait toujours figurer sur l'écran de préférences imbriqué, mais il se trouve en fait sur celui-ci

Solution complète et avancée utilisant PreferenceFragmentCompat api et préservant la pile 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);
        }
    }

}

Vous pouvez également gérer tout cela dans votre fragment au lieu de l'activité. En voici un résumé https://Gist.github.com/aaronvargas/0f210ad8643b512efda4acfd524e1232

0
aaronvargas

Basé sur la solution @squirrel Intent, je l’ai fait fonctionner de cette façon. Cela nécessite encore moins de piratage.
Activité:

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();
    }
}

Fragment:

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

Il est regrettable que vous ayez besoin de tant de hacks dans les bibliothèques de support d’appcompat pour obtenir une solution qui fonctionne parfaitement avec Android standard.

0
DSchmidt