Search code examples
androidandroid-sharedpreferences

Android Preference - SwtichPrefrence changes when scrolling offscreen in SettingsActivity


I have a very strange problem. When I scroll in my Settings-Activity some checkboxes changes when they are not seen anymore. For Example: I uncheck the checkbox for ads then I scroll down and when the checkbox disappers completly the checkbox resets. This only happens when I uncheck a checkbox the default value doesnt have any influence.

My Activity:

public class SettingsActivity extends PreferenceActivity implements SharedPreferences.OnSharedPreferenceChangeListener {
/**
 * Determines whether to always show the simplified settings UI, where
 * settings are presented in a single list. When false, settings are shown
 * as a master/detail two-pane view on tablets. When true, a single pane is
 * shown on tablets.
 */
private static final boolean ALWAYS_SIMPLE_PREFS = false;
private SharedPreferences prefs;

private Tracker mTracker;

private void analytics() {
    if(prefs.getBoolean(Tags.PREF_GOOGLEANALYTICS, true)) {
        // Obtain the shared Tracker instance.
        AnalyticsApplication application = (AnalyticsApplication) getApplication();
        mTracker = application.getDefaultTracker();

        mTracker.setScreenName("SettingsActivity");
        mTracker.send(new HitBuilders.ScreenViewBuilder().build());
    }
}

private void analytics(String category, String action) {
    if(prefs.getBoolean(Tags.PREF_GOOGLEANALYTICS, true)) {
        if (mTracker == null) {
            AnalyticsApplication application = (AnalyticsApplication) getApplication();
            mTracker = application.getDefaultTracker();
        }

        mTracker.send(new HitBuilders.EventBuilder()
                .setCategory(category)
                .setAction(action)
                .build());

    }
}

private void setLocale(String lang) {
    /*Locale myLocale = new Locale(lang);
    Resources res = getResources();
    DisplayMetrics dm = res.getDisplayMetrics();
    Configuration conf = res.getConfiguration();
    conf.locale = myLocale;
    res.updateConfiguration(conf, dm);*/

    Locale locale = new Locale(lang);
    Locale.setDefault(locale);
    Configuration config = new Configuration();
    config.locale = locale;
    getBaseContext().getResources().updateConfiguration(config,
            getBaseContext().getResources().getDisplayMetrics());

    Intent refresh = new Intent(this, MainActivity.class);
    startActivity(refresh);
    finish();
}

@Override
protected void onResume() {
    super.onResume();
    prefs.registerOnSharedPreferenceChangeListener(this);
}

@Override
protected void onPause() {
    super.onPause();
    prefs.unregisterOnSharedPreferenceChangeListener(this);
}

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

    prefs = PreferenceManager.getDefaultSharedPreferences(this);
    analytics();

    Toolbar bar;

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
        LinearLayout root = (LinearLayout) findViewById(android.R.id.list).getParent().getParent().getParent();
        bar = (Toolbar) LayoutInflater.from(this).inflate(R.layout.settings_toolbar, root, false);
        root.addView(bar, 0); // insert at top
    } else {
        ViewGroup root = (ViewGroup) findViewById(android.R.id.content);
        ListView content = (ListView) root.getChildAt(0);

        root.removeAllViews();

        bar = (Toolbar) LayoutInflater.from(this).inflate(R.layout.settings_toolbar, root, false);


        int height;
        TypedValue tv = new TypedValue();
        if (getTheme().resolveAttribute(R.attr.actionBarSize, tv, true)) {
            height = TypedValue.complexToDimensionPixelSize(tv.data, getResources().getDisplayMetrics());
        }else{
            height = bar.getHeight();
        }

        content.setPadding(0, height, 0, 0);

        root.addView(content);
        root.addView(bar);
    }

    bar.setNavigationOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            finish();
        }
    });

    setupSimplePreferencesScreen();
}

/**
 * Shows the simplified settings UI if the device configuration if the
 * device configuration dictates that a simplified, single-pane UI should be
 * shown.
 */
private void setupSimplePreferencesScreen() {
    if (!isSimplePreferences(this)) {
        return;
    }

    // In the simplified UI, fragments are not used at all and we instead
    // use the older PreferenceActivity APIs.

    // Add 'general' preferences.
    addPreferencesFromResource(R.xml.pref_general);


    // Add 'notifications' preferences, and a corresponding header.
    PreferenceCategory fakeHeader = new PreferenceCategory(this);
    fakeHeader.setTitle(getResources().getString(R.string.pref_notifications));
    getPreferenceScreen().addPreference(fakeHeader);
    addPreferencesFromResource(R.xml.pref_notification);

            PreferenceCategory fakeHeaderAudio = new PreferenceCategory(this);
            fakeHeaderAudio.setTitle(getResources().getString(R.string.pref_audio));
            getPreferenceScreen().addPreference(fakeHeaderAudio);
            addPreferencesFromResource(R.xml.pref_audio);


    // Bind the summaries of EditText/List/Dialog/Ringtone preferences to
    // their values. When their values change, their summaries are updated
    // to reflect the new value, per the Android Design guidelines.
    // bindPreferenceSummaryToValue(findPreference(getString(R.string.key_checkbox)));
}

/**
 * {@inheritDoc}
 */
@Override
public boolean onIsMultiPane() {
    return isXLargeTablet(this) && !isSimplePreferences(this);
}

/**
 * Helper method to determine if the device has an extra-large screen. For
 * example, 10" tablets are extra-large.
 */
private static boolean isXLargeTablet(Context context) {
    return (context.getResources().getConfiguration().screenLayout
            & Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_XLARGE;
}

/**
 * Determines whether the simplified settings UI should be shown. This is
 * true if this is forced via {@link #ALWAYS_SIMPLE_PREFS}, or the device
 * doesn't have newer APIs like {@link PreferenceFragment}, or the device
 * doesn't have an extra-large screen. In these cases, a single-pane
 * "simplified" settings UI should be shown.
 */
private static boolean isSimplePreferences(Context context) {
    return ALWAYS_SIMPLE_PREFS
            || (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB)
            || !isXLargeTablet(context);
}

/**
 * {@inheritDoc}
 */
@Override
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public void onBuildHeaders(List<Header> target) {
    if (!isSimplePreferences(this)) {
        loadHeadersFromResource(R.xml.pref_headers, target);
    }
}

@Override
protected boolean isValidFragment(String fragmentName) {
    return NotificationPreferenceFragment.class.getName().equals(fragmentName)
            || GeneralPreferenceFragment.class.getName().equals(fragmentName)
            || AudioPreferenceFragment.class.getName().equals(fragmentName);
}

@Override
public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, final String key) {
    if(key.equals(Tags.PREF_NOTIFICATION)
            || key.equals(Tags.PREF_NOTIFICATIONTIME)
            || key.equals(Tags.PREF_AUDIO_AUTODOWNLOAD)) {

        if(sharedPreferences.getBoolean(Tags.PREF_NOTIFICATION, true)
                || sharedPreferences.getBoolean(Tags.PREF_AUDIO_AUTODOWNLOAD, true)) {
            long time = sharedPreferences.getLong(Tags.PREF_NOTIFICATIONTIME, 60 * 7);
            Notifications.setNotifications(this, time * 60 * 1000);
        } else
            Notifications.removeNotifications(this);
    }

    if(key.equals(Tags.PREF_NOTIFICATION) || key.equals(Tags.PREF_ADS)
            || key.equals(Tags.PREF_GOOGLEANALYTICS) || key.equals(Tags.PREF_SHOWNOTES)) {

        //This toast is shown when I scroll down
        Toast.makeText( getApplicationContext(), "Notification || Analytics", Toast.LENGTH_SHORT).show();

        analytics("Settings", "Settings: " + key + ", " + sharedPreferences.getBoolean(key, true));
    }

    //Language change
    if(key.equals(Tags.PREF_LANGUAGE)) {
        try {
            setLocale(sharedPreferences.getString(key, "en"));
        } catch (Exception e) {
            Log.e("Losungen", "Error changing language: " + e.getMessage());
            e.printStackTrace();
        }
    }

    //SD-Card change
    if(key.equals(Tags.PREF_AUDIO_EXTERNAL_STORGAE)) {
        boolean sd_card = sharedPreferences.getBoolean(key, false);

        if(sd_card)
            MainActivity.toast(this, this.getString(R.string.still_in_internal), Toast.LENGTH_LONG);
        else
            MainActivity.toast(this, this.getString(R.string.still_in_external), Toast.LENGTH_LONG);
    }

}

/**
 * This fragment shows general preferences only. It is used when the
 * activity is showing a two-pane settings UI.
 */
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public static class GeneralPreferenceFragment extends PreferenceFragment {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        addPreferencesFromResource(R.xml.pref_general);
    }
}

/**
 * This fragment shows notification preferences only. It is used when the
 * activity is showing a two-pane settings UI.
 */
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
 public static class NotificationPreferenceFragment extends PreferenceFragment {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        addPreferencesFromResource(R.xml.pref_notification);
    }
}

@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public static class AudioPreferenceFragment extends PreferenceFragment {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        addPreferencesFromResource(R.xml.pref_audio);
    }
}
}

And my xml-files: pref_general.xml

<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<SwitchPreference
    android:key="show_notes"
    android:title="@string/pref_notes_title"
    android:defaultValue="true" />

<SwitchPreference
    android:key="show_ads"
    android:title="@string/pref_ads_title"
    android:summary="@string/pref_ads_summary"
    android:defaultValue="false"
    />

<SwitchPreference
    android:key="google_analytics"
    android:title="@string/pref_google_title"
    android:summary="@string/pref_google_summary"
    android:defaultValue="true" />

<ListPreference
    android:key="language"
    android:title="@string/pref_language"
    android:summary="@string/pref_language_summary"
    android:entries="@array/pref_language_list_titles"
    android:entryValues="@array/pref_language_list_values"
    android:negativeButtonText="@null"
    android:positiveButtonText="@null" />

</PreferenceScreen>

pref_notification.xml

<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<!-- A 'parent' preference, which enables/disables child preferences (below)
     when checked/unchecked. -->
<SwitchPreference
    android:key="notifications_losung"
    android:title="@string/pref_notifications"
    android:defaultValue="true" />

<ListPreference
    android:key="notifications_art"
    android:dependency="notifications_losung"
    android:title="@string/pref_notification_art"
    android:defaultValue="0"
    android:entries="@array/pref_notifications_list_titles"
    android:entryValues="@array/pref_notifications_list_values"
    android:negativeButtonText="@null"
    android:positiveButtonText="@null" />

<de.schalter.losungen.preferences.TimePreference
    android:key="notification_time"
    android:defaultValue="420"
    android:title="@string/pref_time"
    android:dependency="notifications_losung"
    />

</PreferenceScreen>

pref_audio.xml

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<!-- oder stream -->
<SwitchPreference
    android:key="audio_download"
    android:title="@string/pref_audio_download_title"
    android:summaryOn="@string/pref_audio_download_summary_on"
    android:summaryOff="@string/pref_audio_download_summary_off"
    android:defaultValue="true" />

<!-- oder erst bei Nutzer interaktion -->
<SwitchPreference
    android:key="audio_autodownload"
    android:dependency="audio_download"
    android:title="@string/pref_audio_autodownload_title"
    android:summaryOn="@string/pref_audio_autodownload_summary_on"
    android:summaryOff="@string/pref_audio_autodownload_summary_off"
    android:defaultValue="true" />

<ListPreference
    android:key="audio_autodownload_network"
    android:dependency="audio_autodownload"
    android:title="@string/pref_audio_autodownload_network_title"
    android:defaultValue="0"
    android:entries="@array/pref_audio_autodownload_network_entries"
    android:entryValues="@array/pref_audio_autodownload_network_values"
    android:negativeButtonText="@null"
    android:positiveButtonText="@null" />


<SwitchPreference
    android:key="audio_external_storage"
    android:dependency="audio_download"
    android:title="@string/pref_audio_externalstorage_title"
    android:summaryOff="@string/pref_audio_externalstorage_summary_off"
    android:summaryOn="@string/pref_audio_externalstorage_summary_on"
    android:defaultValue="false" />
</PreferenceScreen>

I have no idea why this is happening. Some comments are in german but I think you can understand anything. Any ideas?

Here is a similar question but no helpful answer: Android Preferences - On/Off Switch Icons Reset When Scrolled Offscreen


Solution

  • Refer to the answer on Preference items being automatically re-set?, which recommends you subclass SwitchPreference with nothing more than constructors that call through to super, then use that in place of the standard SwitchPreference. I've noticed that this happens on earlier versions of Android (4.x specifically in our case), but not 5+.

    I suspect the reason this works is because of these lines in the constructor of Preference:

    From API 17 (which requires this modification):

    if (!getClass().getName().startsWith("android.preference")) {
        // For subclasses not in this package, assume the worst and don't cache views
        mHasSpecifiedLayout = true;
    }
    

    Or from API 23 (which does work correctly without modification):

    if (!getClass().getName().startsWith("android.preference")
            && !getClass().getName().startsWith("com.android")) {
        // For non-framework subclasses, assume the worst and don't cache views.
        mCanRecycleLayout = false;
    }
    

    Following things down through the PreferenceGroupAdapter, it looks like when it recycles the view it sets things up and then notifies listeners that the preference has changed. I can't find where/why it actually changes the preference in place so I'm also not sure why the change in newer APIs fixes it, but that seems to be what's happening.