Java tutorial
/* * Copyright 2014 - 2017 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.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.TypedArray; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Bundle; import android.preference.Preference; import android.preference.PreferenceGroup; import android.support.annotation.ColorInt; import android.support.annotation.DrawableRes; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.StringRes; import android.support.annotation.XmlRes; import android.support.v4.content.ContextCompat; import android.text.TextUtils; import android.view.Gravity; 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 android.widget.LinearLayout; import android.widget.ListView; import java.util.LinkedHashSet; import java.util.Set; import de.mrapp.android.preference.activity.animation.HideViewOnScrollAnimation; import de.mrapp.android.preference.activity.animation.HideViewOnScrollAnimation.Direction; import de.mrapp.android.preference.activity.decorator.PreferenceDecorator; import de.mrapp.android.util.ViewUtil; import de.mrapp.android.util.view.ElevationShadowView; import static de.mrapp.android.util.Condition.ensureNotNull; 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 android.preference.PreferenceFragment { /** * 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"; /** * The default elevation of the button bar in dp. */ private static final int DEFAULT_BUTTON_BAR_ELEVATION = 2; /** * The fragment's parent parentView. */ private LinearLayout parentView; /** * The list view, which contains the fragment's preferences. */ private ListView listView; /** * 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 view group, which contains the button, which allows to restore the * preferences' default values. */ private ViewGroup buttonBarParent; /** * The view group, which contains the button, which allows to restore the preferences' default * values. */ 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; /** * The elevation of the button bar in dp. */ private int buttonBarElevation; /** * A set, which contains the listeners, which should be notified, when the preferences' default * values should be restored. */ private Set<RestoreDefaultsListener> restoreDefaultsListeners = new LinkedHashSet<>(); /** * Initializes the list view, which is used to show the fragment's preferences. */ private void initializeListView() { listView = (ListView) parentView.findViewById(android.R.id.list); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { FrameLayout listContainer = (FrameLayout) parentView.findViewById(android.R.id.list_container); listContainer.removeView(listView); } else { parentView.removeView(listView); } frameLayout = new FrameLayout(getActivity()); frameLayout.setId(R.id.preference_fragment_frame_layout); parentView.addView(frameLayout, listView.getLayoutParams()); frameLayout.addView(listView, FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT); int paddingTop = getResources().getDimensionPixelSize(R.dimen.list_view_padding_top); listView.setPadding(0, paddingTop, 0, 0); } /** * Inflates the view group, which contains the button, which allows to restore the preferences' * default values. */ private void inflateRestoreDefaultsButtonBar() { if (buttonBarParent == null) { LayoutInflater layoutInflater = getActivity().getLayoutInflater(); buttonBarParent = (ViewGroup) layoutInflater.inflate(R.layout.restore_defaults_button_bar, frameLayout, false); buttonBar = (ViewGroup) buttonBarParent.findViewById(R.id.restore_defaults_button_bar); restoreDefaultsButton = (Button) buttonBarParent.findViewById(R.id.restore_defaults_button); restoreDefaultsButton.setOnClickListener(createRestoreDefaultsListener()); shadowView = (ElevationShadowView) buttonBarParent .findViewById(R.id.restore_defaults_button_bar_shadow_view); obtainStyledAttributes(); } } /** * Obtains all relevant attributes from the activity's current theme. */ private void obtainStyledAttributes() { int theme = obtainTheme(); if (theme != 0) { obtainButtonBarBackground(theme); obtainButtonBarElevation(theme); } } /** * Obtains the resource id of the activity's current theme. * * @return The resource id of the acitivty's current theme as an {@link Integer} value or 0, if * an error occurred while obtaining the theme */ private int obtainTheme() { try { String packageName = getActivity().getClass().getPackage().getName(); PackageInfo packageInfo = getActivity().getPackageManager().getPackageInfo(packageName, PackageManager.GET_META_DATA); return packageInfo.applicationInfo.theme; } catch (NameNotFoundException e) { return 0; } } /** * Obtains the background of the button bar from a specific theme. * * @param theme * The resource id of the theme, the background should be obtained from, as an {@link * Integer} value */ private void obtainButtonBarBackground(final int theme) { TypedArray typedArray = getActivity().getTheme().obtainStyledAttributes(theme, new int[] { R.attr.restoreDefaultsButtonBarBackground }); int color = typedArray.getColor(0, 0); if (color != 0) { setButtonBarBackgroundColor(color); } else { int resourceId = typedArray.getResourceId(0, 0); if (resourceId != 0) { setButtonBarBackground(resourceId); } } } /** * Obtains the elevation of the button bar from a specific theme. * * @param theme * The resource id of the theme, the navigation width should be obtained from, as an * {@link Integer} value */ private void obtainButtonBarElevation(final int theme) { TypedArray typedArray = getActivity().getTheme().obtainStyledAttributes(theme, new int[] { R.attr.restoreDefaultsButtonBarElevation }); int elevation = pixelsToDp(getActivity(), typedArray.getDimensionPixelSize(0, 0)); if (elevation != 0) { this.buttonBarElevation = elevation; } } /** * 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(); } } }; } /** * Adds the view group, which contains the button, which allows to restore the preferences' * default values, to the fragment. */ private void addRestoreDefaultsButtonBar() { if (frameLayout != null && buttonBarParent != null) { FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.BOTTOM); frameLayout.addView(buttonBarParent, layoutParams); listView.setOnScrollListener(new HideViewOnScrollAnimation(buttonBarParent, Direction.DOWN)); setButtonBarElevation(buttonBarElevation); } } /** * Removes the view group, which contains the button, which allows to restore the preferences' * default values, from the fragment. */ private void removeRestoreDefaultsButtonBar() { if (frameLayout != null && buttonBarParent != null) { frameLayout.removeView(buttonBarParent); } } /** * 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); } } } } /** * Applies Material style on all preferences, which are contained by a specific preference * group, and on the group itself. * * @param preferenceGroup * The preference group, at whose preferences the Material style should be applied on, * as an instance of the class {@link PreferenceGroup}. The preference group may not be * null * @param decorator * The decorator, which should be used to apply the Material style, as an instance of * the class {@link PreferenceDecorator}. The decorator may not be null */ private void applyMaterialStyle(@NonNull final PreferenceGroup preferenceGroup, @NonNull final PreferenceDecorator decorator) { for (int i = 0; i < preferenceGroup.getPreferenceCount(); i++) { Preference preference = preferenceGroup.getPreference(i); if (preference instanceof PreferenceGroup) { decorator.applyDecorator(preference); applyMaterialStyle((PreferenceGroup) preference, decorator); } else { decorator.applyDecorator(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); } } /** * 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. */ private void handleShowRestoreDefaultsButtonArgument() { boolean showButton = getArguments().getBoolean(EXTRA_SHOW_RESTORE_DEFAULTS_BUTTON, false); if (showButton) { showRestoreDefaultsButton(true); } } /** * 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. */ private void handleRestoreDefaultsButtonTextArgument() { CharSequence buttonText = getCharSequenceFromArguments(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 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 String name) { CharSequence charSequence = getArguments().getCharSequence(name); if (charSequence == null) { int resourceId = getArguments().getInt(name, 0); if (resourceId != 0) { charSequence = getText(resourceId); } } return charSequence; } /** * 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); } } /** * 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) { 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) { ensureNotNull(listener, "The listener may not be null"); this.restoreDefaultsListeners.remove(listener); } /** * 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) { if (show) { inflateRestoreDefaultsButtonBar(); addRestoreDefaultsButtonBar(); } else { removeRestoreDefaultsButtonBar(); listView.setOnScrollListener(null); buttonBarParent = null; buttonBar = null; restoreDefaultsButton = null; } } /** * 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 */ 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 */ public final ViewGroup getButtonBar() { return buttonBar; } /** * 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} or null, if the * button is not shown or no background is set */ public final Drawable getButtonBarBackground() { if (getButtonBar() != null) { return buttonBar.getBackground(); } return null; } /** * Sets the background of the view group, which contains the button, which allows to restore the * preferences' default values. The background is only set, if the button is shown. * * @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 * @return True, if the background has been set, false otherwise */ public final boolean setButtonBarBackground(@DrawableRes final int resourceId) { return 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. The background color is only set, if the button is * shown. * * @param color * The background color, which should be set, as an {@link Integer} value * @return True, if the background color has been set, false otherwise */ public final boolean setButtonBarBackgroundColor(@ColorInt final int color) { return setButtonBarBackground(new ColorDrawable(color)); } /** * Sets the background of the view group, which contains the button, which allows to restore the * preferences' default values. The background is only set, if the button is shown. * * @param drawable * The background, which should be set, as an instance of the class {@link Drawable} or * null, if no background should be set * @return True, if the background has been set, false otherwise */ public final boolean setButtonBarBackground(@Nullable final Drawable drawable) { if (getButtonBar() != null) { ViewUtil.setBackground(getButtonBar(), drawable); return true; } return false; } /** * 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 or -1, if the button is not shown */ public final int getButtonBarElevation() { if (isRestoreDefaultsButtonShown()) { return buttonBarElevation; } return -1; } /** * Sets the elevation of the view group, which contains the button, which allows to restore the * preferences' default values. The elevation is only set when the button is shown. * * @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 * @return True, if the elevation has been set, false otherwise */ public final boolean setButtonBarElevation(final int elevation) { if (isRestoreDefaultsButtonShown()) { buttonBarElevation = elevation; shadowView.setShadowElevation(elevation); return true; } return false; } /** * 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; } /** * 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} or null, if the button is not shown */ public final CharSequence getRestoreDefaultsButtonText() { if (restoreDefaultsButton != null) { return restoreDefaultsButton.getText(); } return null; } /** * 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 not be null * @return True, if the text has been set, false otherwise */ public final boolean setRestoreDefaultsButtonText(@NonNull final CharSequence text) { ensureNotNull(text, "The text may not be null"); if (restoreDefaultsButton != null) { restoreDefaultsButton.setText(text); return true; } return false; } /** * 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 * @return True, if the text has been set, false otherwise */ public final boolean setRestoreDefaultsButtonText(@StringRes final int resourceId) { return setRestoreDefaultsButtonText(getText(resourceId)); } @Override public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); this.buttonBarElevation = DEFAULT_BUTTON_BAR_ELEVATION; if (getArguments() != null) { handleShowRestoreDefaultsButtonArgument(); handleRestoreDefaultsButtonTextArgument(); } } @Override public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) { parentView = (LinearLayout) super.onCreateView(inflater, container, savedInstanceState); if (savedInstanceState == null) { initializeListView(); addRestoreDefaultsButtonBar(); } return parentView; } @Override public final void addPreferencesFromResource(@XmlRes final int resourceId) { super.addPreferencesFromResource(resourceId); PreferenceDecorator decorator = new PreferenceDecorator(getActivity()); if (getPreferenceScreen() != null) { applyMaterialStyle(getPreferenceScreen(), decorator); } } }