/* * Copyright 2014 - 2019 Michael Rapp * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package de.mrapp.android.preference.activity; import android.content.SharedPreferences; import android.content.res.Resources.NotFoundException; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.Button; import android.widget.FrameLayout; import java.util.LinkedHashSet; import java.util.Set; import androidx.annotation.CallSuper; import androidx.annotation.ColorInt; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.core.content.ContextCompat; import androidx.preference.AndroidResources; import androidx.preference.Preference; import androidx.preference.PreferenceGroup; import de.mrapp.android.preference.activity.animation.HideViewOnScrollAnimation; import de.mrapp.android.preference.activity.fragment.AbstractPreferenceFragment; import de.mrapp.android.util.ThemeUtil; import de.mrapp.android.util.ViewUtil; import de.mrapp.android.util.view.ElevationShadowView; import de.mrapp.util.Condition; import static de.mrapp.android.util.DisplayUtil.pixelsToDp; /** * A fragment, which allows to show multiple preferences. Additionally, a button, which allows to * restore the preferences' default values, can be shown. * * @author Michael Rapp * @since 1.1.0 */ public abstract class PreferenceFragment extends AbstractPreferenceFragment { /** * When attaching this fragment to an activity, the passed bundle can contain this extra boolean * to display the button, which allows to restore the preferences' default values. */ public static final String EXTRA_SHOW_RESTORE_DEFAULTS_BUTTON = "extra_prefs_show_restore_defaults_button"; /** * When attaching this fragment to an activity and using <code>EXTRA_SHOW_RESTORE_DEFAULTS_BUTTON</code>, * this extra can also be specified to supply a custom text for the button, which allows to * restore the preferences' default values. */ public static final String EXTRA_RESTORE_DEFAULTS_BUTTON_TEXT = "extra_prefs_restore_defaults_button_text"; /** * A set, which contains the listeners, which should be notified, when the preferences' default * values should be restored. */ private final Set<RestoreDefaultsListener> restoreDefaultsListeners = new LinkedHashSet<>(); /** * The frame layout, which contains the fragment's views. It is the root view of the fragment. */ private FrameLayout frameLayout; /** * The parent view of the button bar. */ private ViewGroup buttonBarParent; /** * The button bar. */ private ViewGroup buttonBar; /** * The view, which is used to draw a shadow above the button bar. */ private ElevationShadowView shadowView; /** * The button, which allows to restore the preferences' default values. */ private Button restoreDefaultsButton; /** * True, if the button, which allows to restore the preferences' default values, is shown, false * otherwise. */ private boolean showRestoreDefaultsButton; /** * The text of the button, which allows to restore the preferences' default values. */ private CharSequence restoreDefaultsButtonText; /** * The background of the button bar. */ private Drawable buttonBarBackground; /** * The elevation of the button bar in dp. */ private int buttonBarElevation; /** * Obtains all relevant attributes from the activity's current theme. */ private void obtainStyledAttributes() { obtainShowRestoreDefaultsButton(); obtainRestoreDefaultsButtonText(); obtainButtonBarBackground(); obtainButtonBarElevation(); } /** * Obtains, whether the button, which allows to restore the preferences' default values, should * be shown, or not, from the activity's current theme. */ private void obtainShowRestoreDefaultsButton() { boolean show = ThemeUtil.getBoolean(getActivity(), R.attr.showRestoreDefaultsButton, false); showRestoreDefaultsButton(show); } /** * Obtains the text of the button, which allows to restore the preferences' default values, from * the activity's current theme. */ private void obtainRestoreDefaultsButtonText() { CharSequence text; try { text = ThemeUtil.getText(getActivity(), R.attr.restoreDefaultsButtonText); } catch (NotFoundException e) { text = getText(R.string.restore_defaults_button_text); } setRestoreDefaultsButtonText(text); } /** * Obtains the background of the button bar from the activity's current theme. */ private void obtainButtonBarBackground() { try { int color = ThemeUtil.getColor(getActivity(), R.attr.restoreDefaultsButtonBarBackground); setButtonBarBackgroundColor(color); } catch (NotFoundException e) { int resourceId = ThemeUtil .getResId(getActivity(), R.attr.restoreDefaultsButtonBarBackground, -1); if (resourceId != -1) { setButtonBarBackground(resourceId); } else { setButtonBarBackgroundColor( ContextCompat.getColor(getActivity(), R.color.button_bar_background_light)); } } } /** * Obtains the elevation of the button bar from the activity's current theme. */ private void obtainButtonBarElevation() { int elevation; try { elevation = ThemeUtil .getDimensionPixelSize(getActivity(), R.attr.restoreDefaultsButtonBarElevation); } catch (NotFoundException e) { elevation = getResources().getDimensionPixelSize(R.dimen.button_bar_elevation); } setButtonBarElevation(pixelsToDp(getActivity(), elevation)); } /** * Handles the arguments, which have been passed to the fragment. */ private void handleArguments() { Bundle arguments = getArguments(); if (arguments != null) { handleShowRestoreDefaultsButtonArgument(arguments); handleRestoreDefaultsButtonTextArgument(arguments); } } /** * Handles the extra of the arguments, which have been passed to the fragment, that allows to * show the button, which allows to restore the preferences' default values. * * @param arguments * The arguments, which have been passed to the fragment, as an instance of the class * {@link Bundle}. The arguments may not be null */ private void handleShowRestoreDefaultsButtonArgument(@NonNull final Bundle arguments) { boolean showButton = arguments.getBoolean(EXTRA_SHOW_RESTORE_DEFAULTS_BUTTON, false); showRestoreDefaultsButton(showButton); } /** * Handles the extra of the arguments, which have been passed to the fragment, that allows to * specify a custom text for the button, which allows to restore the preferences' default * values. * * @param arguments * The arguments, which have been passed to the fragment, as an instance of the class * {@link Bundle}. The arguments may not be null */ private void handleRestoreDefaultsButtonTextArgument(@NonNull final Bundle arguments) { CharSequence buttonText = getCharSequenceFromArguments(arguments, EXTRA_RESTORE_DEFAULTS_BUTTON_TEXT); if (!TextUtils.isEmpty(buttonText)) { setRestoreDefaultsButtonText(buttonText); } } /** * Returns the char sequence, which is specified by a specific extra of the arguments, which * have been passed to the fragment. The char sequence can either be specified as a string or as * a resource id. * * @param arguments * The arguments, which have been passed to the fragment, as an instance of the class * {@link Bundle}. The arguments may not be null * @param name * The name of the extra, which specifies the char sequence, as a {@link String}. The * name may not be null * @return The char sequence, which is specified by the arguments, as an instance of the class * {@link CharSequence} or null, if the arguments do not specify a char sequence with the given * name */ private CharSequence getCharSequenceFromArguments(@NonNull final Bundle arguments, @NonNull final String name) { CharSequence charSequence = arguments.getCharSequence(name); if (charSequence == null) { int resourceId = arguments.getInt(name, -1); if (resourceId != -1) { charSequence = getText(resourceId); } } return charSequence; } /** * Creates and returns a listener, which allows to restore the preferences' default values. * * @return The listener, which has been created, as an instance of the type {@link * OnClickListener} */ private OnClickListener createRestoreDefaultsListener() { return new OnClickListener() { @Override public void onClick(final View v) { if (notifyOnRestoreDefaultValuesRequested()) { restoreDefaults(); } } }; } /** * Restores the default preferences, which are contained by a specific preference group. * * @param preferenceGroup * The preference group, whose preferences should be restored, as an instance of the * class {@link PreferenceGroup}. The preference group may not be null * @param sharedPreferences * The shared preferences, which should be used to restore the preferences, as an * instance of the type {@link SharedPreferences}. The shared preferences may not be * null */ private void restoreDefaults(@NonNull final PreferenceGroup preferenceGroup, @NonNull final SharedPreferences sharedPreferences) { for (int i = 0; i < preferenceGroup.getPreferenceCount(); i++) { Preference preference = preferenceGroup.getPreference(i); if (preference instanceof PreferenceGroup) { restoreDefaults((PreferenceGroup) preference, sharedPreferences); } else if (preference.getKey() != null && !preference.getKey().isEmpty()) { Object oldValue = sharedPreferences.getAll().get(preference.getKey()); if (notifyOnRestoreDefaultValueRequested(preference, oldValue)) { sharedPreferences.edit().remove(preference.getKey()).apply(); preferenceGroup.removePreference(preference); preferenceGroup.addPreference(preference); Object newValue = sharedPreferences.getAll().get(preference.getKey()); notifyOnRestoredDefaultValue(preference, oldValue, newValue); } else { preferenceGroup.removePreference(preference); preferenceGroup.addPreference(preference); } } } } /** * Notifies all registered listeners, that the preferences' default values should be restored. * * @return True, if restoring the preferences' default values should be proceeded, false * otherwise */ private boolean notifyOnRestoreDefaultValuesRequested() { boolean result = true; for (RestoreDefaultsListener listener : restoreDefaultsListeners) { result &= listener.onRestoreDefaultValuesRequested(this); } return result; } /** * Notifies all registered listeners, that the default value of a specific preference should be * restored. * * @param preference * The preference, whose default value should be restored, as an instance of the class * {@link Preference}. The preference may not be null * @param currentValue * The current value of the preference, whose default value should be restored, as an * instance of the class {@link Object} * @return True, if restoring the preference's default value should be proceeded, false * otherwise */ private boolean notifyOnRestoreDefaultValueRequested(@NonNull final Preference preference, final Object currentValue) { boolean result = true; for (RestoreDefaultsListener listener : restoreDefaultsListeners) { result &= listener.onRestoreDefaultValueRequested(this, preference, currentValue); } return result; } /** * Notifies all registered listeners, that the default value of a specific preference has been * be restored. * * @param preference * The preference, whose default value has been restored, as an instance of the class * {@link Preference}. The preference may not be null * @param oldValue * The old value of the preference, whose default value has been restored, as an * instance of the class {@link Object} * @param newValue * The new value of the preference, whose default value has been restored, as an * instance of the class {@link Object} */ private void notifyOnRestoredDefaultValue(@NonNull final Preference preference, final Object oldValue, final Object newValue) { for (RestoreDefaultsListener listener : restoreDefaultsListeners) { listener.onRestoredDefaultValue(this, preference, oldValue, newValue != null ? newValue : oldValue); } } /** * Adapts the visibility of the button bar. */ private void adaptButtonBarVisibility() { if (buttonBarParent != null) { buttonBarParent.setVisibility(showRestoreDefaultsButton ? View.VISIBLE : View.GONE); } } /** * Adapts the text of the button, which allows to restore the preferences' default values. */ private void adaptRestoreDefaultsButtonText() { if (restoreDefaultsButton != null) { restoreDefaultsButton.setText(restoreDefaultsButtonText); } } /** * Adapts the background of the button bar. */ private void adaptButtonBarBackground() { if (buttonBar != null) { ViewUtil.setBackground(buttonBar, buttonBarBackground); } } /** * Adapts the elevation of the button bar. */ private void adaptButtonBarElevation() { if (shadowView != null) { shadowView.setShadowElevation(buttonBarElevation); } } /** * Returns the frame layout, which contains the fragment's views. It is the root view of the * fragment. * * @return The frame layout, which contains the fragment's views, as an instance of the class * {@link FrameLayout} or null, if the fragment has not been created yet */ public final FrameLayout getFrameLayout() { return frameLayout; } /** * Returns the view group, which contains the button, which allows to restore the preferences' * default values. * * @return The view group, which contains the button, which allows to restore the preferences' * default values, as an instance of the class {@link ViewGroup} or null, if the button is not * shown or if the fragment has not been created yet */ public final ViewGroup getButtonBar() { return buttonBar; } /** * Returns the button, which allows to restore the preferences' default values. * * @return The button, which allows to restore the preferences' default values, as an instance * of the class {@link Button} or null, if the button is not shown */ public final Button getRestoreDefaultsButton() { return restoreDefaultsButton; } /** * Adds a new listener, which should be notified, when the preferences' default values should be * restored, to the fragment. * * @param listener * The listener, which should be added as an instance of the type {@link * RestoreDefaultsListener}. The listener may not be null */ public final void addRestoreDefaultsListener(@NonNull final RestoreDefaultsListener listener) { Condition.INSTANCE.ensureNotNull(listener, "The listener may not be null"); this.restoreDefaultsListeners.add(listener); } /** * Removes a specific listener, which should not be notified anymore, when the preferences' * default values should be restored, from the fragment. * * @param listener * The listener, which should be removed as an instance of the type {@link * RestoreDefaultsListener}. The listener may not be null */ public final void removeRestoreDefaultsListener( @NonNull final RestoreDefaultsListener listener) { Condition.INSTANCE.ensureNotNull(listener, "The listener may not be null"); this.restoreDefaultsListeners.remove(listener); } /** * Restores the default values of all preferences, which are contained by the fragment. */ public final void restoreDefaults() { SharedPreferences sharedPreferences = getPreferenceManager().getSharedPreferences(); if (getPreferenceScreen() != null) { restoreDefaults(getPreferenceScreen(), sharedPreferences); } } /** * Returns, whether the button, which allows to restore the preferences' default values, is * currently shown, or not. * * @return True, if the button, which allows to restore the preferences' default values, is * currently shown, false otherwise */ public final boolean isRestoreDefaultsButtonShown() { return restoreDefaultsButton != null; } /** * Shows or hides the button, which allows to restore the preferences' default values. * * @param show * True, if the button, which allows to restore the preferences' default values, should * be shown, false otherwise */ public final void showRestoreDefaultsButton(final boolean show) { this.showRestoreDefaultsButton = show; adaptButtonBarVisibility(); } /** * Returns the text of the button, which allows to restore the preferences' default values. * * @return The text of the button, which allows to restore the preferences' default values, as * an instance of the class {@link CharSequence} */ @NonNull public final CharSequence getRestoreDefaultsButtonText() { return restoreDefaultsButtonText; } /** * Sets the text of the button, which allows to restore the preferences' default values. The * text is only set, if the button is shown. * * @param resourceId * The resource id of the text, which should be set, as an {@link Integer} value. The * resource id must correspond to a valid string resource */ public final void setRestoreDefaultsButtonText(@StringRes final int resourceId) { setRestoreDefaultsButtonText(getText(resourceId)); } /** * Sets the text of the button, which allows to restore the preferences' default values. The * text is only set, if the button is shown. * * @param text * The text, which should be set, as an instance of the class {@link CharSequence}. The * text may neither be null, nor empty */ public final void setRestoreDefaultsButtonText(@NonNull final CharSequence text) { Condition.INSTANCE.ensureNotNull(text, "The text may not be null"); Condition.INSTANCE.ensureNotEmpty(text, "The text may not be empty"); this.restoreDefaultsButtonText = text; adaptRestoreDefaultsButtonText(); } /** * Returns the background of the view group, which contains the button, which allows to restore * the preferences' default values. * * @return The background of the view group, which contains the button, which allows to restore * the preferences' default values, as an instance of the class {@link Drawable} */ public final Drawable getButtonBarBackground() { return buttonBarBackground; } /** * Sets the background of the view group, which contains the button, which allows to restore the * preferences' default values. * * @param resourceId * The resource id of the background, which should be set, as an {@link Integer} value. * The resource id must correspond to a valid drawable resource */ public final void setButtonBarBackground(@DrawableRes final int resourceId) { setButtonBarBackground(ContextCompat.getDrawable(getActivity(), resourceId)); } /** * Sets the background color of the view group, which contains the button, which allows to * restore the preferences' default values. shown. * * @param color * The background color, which should be set, as an {@link Integer} value */ public final void setButtonBarBackgroundColor(@ColorInt final int color) { setButtonBarBackground(new ColorDrawable(color)); } /** * Sets the background of the view group, which contains the button, which allows to restore the * preferences' default values. * * @param background * The background, which should be set, as an instance of the class {@link Drawable} or * null, if no background should be set */ public final void setButtonBarBackground(@Nullable final Drawable background) { this.buttonBarBackground = background; adaptButtonBarBackground(); } /** * Returns the elevation of the view group, which contains the button, which allows to restore * the preferences' default values. * * @return The elevation in dp as an {@link Integer} value */ public final int getButtonBarElevation() { return buttonBarElevation; } /** * Sets the elevation of the view group, which contains the button, which allows to restore the * preferences' default values. * * @param elevation * The elevation, which should be set, in dp as an {@link Integer} value. The elevation * must be at least 1 and at maximum 16 */ public final void setButtonBarElevation(final int elevation) { Condition.INSTANCE.ensureAtLeast(elevation, 0, "The elevation must be at least 0"); Condition.INSTANCE.ensureAtMaximum(elevation, 16, "The elevation must be at maximum 16"); this.buttonBarElevation = elevation; adaptButtonBarElevation(); } @CallSuper @Override public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); obtainStyledAttributes(); handleArguments(); } @CallSuper @Override public void onActivityCreated(final Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); getListView().addOnScrollListener(new HideViewOnScrollAnimation(buttonBarParent, HideViewOnScrollAnimation.Direction.DOWN)); } @NonNull @CallSuper @Override public View onCreateView(final LayoutInflater inflater, final ViewGroup parent, final Bundle savedInstanceState) { View view = super.onCreateView(inflater, parent, savedInstanceState); View listContainer = view.findViewById(AndroidResources.ANDROID_R_LIST_CONTAINER); if (!(listContainer instanceof FrameLayout)) { throw new RuntimeException( "Fragment contains a view with id 'android.R.id.list_container' that is not a FrameLayout"); } frameLayout = (FrameLayout) listContainer; buttonBarParent = (ViewGroup) inflater .inflate(R.layout.restore_defaults_button_bar, frameLayout, false); frameLayout.addView(buttonBarParent); buttonBarParent.setVisibility(showRestoreDefaultsButton ? View.VISIBLE : View.GONE); buttonBar = buttonBarParent.findViewById(R.id.restore_defaults_button_bar); ViewUtil.setBackground(buttonBar, buttonBarBackground); restoreDefaultsButton = buttonBarParent.findViewById(R.id.restore_defaults_button); restoreDefaultsButton.setOnClickListener(createRestoreDefaultsListener()); restoreDefaultsButton.setText(restoreDefaultsButtonText); shadowView = buttonBarParent.findViewById(R.id.restore_defaults_button_bar_shadow_view); shadowView.setShadowElevation(buttonBarElevation); return view; } }