Adding settings screen for font size. Also factored out common
components from screen magnification settings screen.

Change-Id: I1828c7c0246f14493903172f2da7419fdc8e5f09
diff --git a/res/layout/font_size_activity.xml b/res/layout/font_size_activity.xml
new file mode 100644
index 0000000..9072d38
--- /dev/null
+++ b/res/layout/font_size_activity.xml
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+     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.
+-->
+
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <LinearLayout
+        android:orientation="vertical"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:paddingTop="16dp"
+        android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+        android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
+
+        <FrameLayout
+            android:layout_width="match_parent"
+            android:layout_height="@dimen/font_size_preview_height"
+            android:background="?android:attr/colorBackgroundFloating"
+            android:elevation="2dp">
+
+            <view class="com.android.settings.TouchBlockingFrameLayout"
+                android:id="@+id/preview_frame"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent" />
+
+            <TextView
+                android:id="@+id/current_label"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center_horizontal|bottom"
+                android:padding="8dp"
+                android:theme="@android:style/Theme.Material"
+                android:background="?android:attr/colorBackgroundFloating"
+                android:textAppearance="@android:style/TextAppearance.Material.Caption"
+                android:textAllCaps="true"
+                android:elevation="2dp" />
+        </FrameLayout>
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:paddingTop="8dp">
+
+            <ImageView
+                android:id="@+id/smaller"
+                android:layout_width="48dp"
+                android:layout_height="48dp"
+                android:background="?android:attr/selectableItemBackgroundBorderless"
+                android:src="@drawable/ic_remove_24dp"
+                android:tint="?android:attr/colorControlNormal"
+                android:tintMode="src_in"
+                android:scaleType="center"
+                android:focusable="true"
+                android:contentDescription="@string/font_size_make_smaller_desc" />
+
+            <SeekBar
+                android:id="@+id/seek_bar"
+                android:layout_width="0dp"
+                android:layout_height="48dp"
+                android:layout_weight="1"
+                style="@android:style/Widget.Material.SeekBar.Discrete"/>
+
+            <ImageView
+                android:id="@+id/larger"
+                android:layout_width="48dp"
+                android:layout_height="48dp"
+                android:background="?android:attr/selectableItemBackgroundBorderless"
+                android:src="@drawable/ic_add_24dp"
+                android:tint="?android:attr/colorControlNormal"
+                android:tintMode="src_in"
+                android:scaleType="center"
+                android:focusable="true"
+                android:contentDescription="@string/font_size_make_larger_desc" />
+        </LinearLayout>
+    </LinearLayout>
+</ScrollView>
diff --git a/res/layout/font_size_preview.xml b/res/layout/font_size_preview.xml
new file mode 100644
index 0000000..1e3f69a
--- /dev/null
+++ b/res/layout/font_size_preview.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+     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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <TextView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="@string/font_size_preview_text_title"
+        android:textAppearance="@android:style/TextAppearance.Material.Title"/>
+
+    <TextView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="@string/font_size_preview_text_subhead"
+        android:textAppearance="@android:style/TextAppearance.Material.Subhead"/>
+
+    <TextView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="@string/font_size_preview_text_body"
+        android:textAppearance="@android:style/TextAppearance.Material.Body1"/>
+</LinearLayout>
diff --git a/res/layout/screen_zoom_activity.xml b/res/layout/screen_zoom_activity.xml
index 81ce102..a00e7bd 100644
--- a/res/layout/screen_zoom_activity.xml
+++ b/res/layout/screen_zoom_activity.xml
@@ -23,7 +23,7 @@
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:paddingTop="16dp"
-        android:paddingLeft="?android:attr/listPreferredItemPaddingStart"
+        android:paddingStart="?android:attr/listPreferredItemPaddingStart"
         android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
 
         <TextView
@@ -39,13 +39,13 @@
             android:background="?android:attr/colorBackgroundFloating"
             android:elevation="2dp">
 
-            <com.android.settings.display.TouchBlockingFrameLayout
+            <view class="com.android.settings.TouchBlockingFrameLayout"
                 android:id="@+id/preview_frame"
                 android:layout_width="match_parent"
                 android:layout_height="match_parent" />
 
             <TextView
-                android:id="@+id/current_density"
+                android:id="@+id/current_label"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:layout_gravity="center_horizontal|bottom"
@@ -78,7 +78,8 @@
                 android:id="@+id/seek_bar"
                 android:layout_width="0dp"
                 android:layout_height="48dp"
-                android:layout_weight="1" />
+                android:layout_weight="1"
+                style="@android:style/Widget.Material.SeekBar.Discrete"/>
 
             <ImageView
                 android:id="@+id/larger"
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index f7c3437..792582a 100755
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -270,4 +270,7 @@
     <!-- Accessibility Settings -->
     <dimen name="accessibility_layout_margin_start_end">24dp</dimen>
     <dimen name="accessibility_button_preference_padding_top_bottom">18dp</dimen>
+
+    <!-- Accessibility, Font size -->
+    <dimen name="font_size_preview_height">240dp</dimen>
 </resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 398dc9b..b6edd1d 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -154,8 +154,16 @@
     <!-- choice for the font size spinner -->
     <string name="large_font">Large</string>
 
+    <!-- Description for the button that makes interface elements smaller. [CHAR_LIMIT=NONE] -->
+    <string name="font_size_make_smaller_desc">Make smaller</string>
+    <!-- Description for the button that makes interface elements larger. [CHAR_LIMIT=NONE] -->
+    <string name="font_size_make_larger_desc">Make larger</string>
+
     <!-- Do not translate. label for font size preview.  Does not need to be translated. -->
     <string name="font_size_preview_text">Servez à ce monsieur une bière et des kiwis.</string>
+    <string name="font_size_preview_text_title" translatable="false">Twenty Thousand Leagues Under The Sea</string>
+    <string name="font_size_preview_text_subhead" translatable="false">Chapter 23: The Coral Kingdom</string>
+    <string name="font_size_preview_text_body" translatable="false">The next day I woke with my head singularly clear. To my great surprise, I was in my own room. My companions, no doubt, had been reinstated in their cabin, without having perceived it any more than I. Of what had passed during the night they were as ignorant as I was, and to penetrate this mystery I only reckoned upon the chances of the future.</string>
     <!-- Button. Chosen when they want to save the chosen text size. -->
     <string name="font_size_save">OK</string>
 
diff --git a/res/xml/accessibility_settings.xml b/res/xml/accessibility_settings.xml
index 591bdad..42537b0 100644
--- a/res/xml/accessibility_settings.xml
+++ b/res/xml/accessibility_settings.xml
@@ -44,6 +44,11 @@
             android:title="@string/accessibility_screen_magnification_title"/>
 
         <PreferenceScreen
+                android:fragment="com.android.settings.accessibility.ToggleFontSizePreferenceFragment"
+                android:key="font_size_preference_screen"
+                android:title="@string/title_font_size"/>
+
+        <PreferenceScreen
             android:fragment="com.android.settings.accessibility.ToggleAutoclickPreferenceFragment"
             android:key="autoclick_preference_screen"
             android:title="@string/accessibility_autoclick_preference_title"/>
diff --git a/res/xml/accessibility_settings_for_setup_wizard.xml b/res/xml/accessibility_settings_for_setup_wizard.xml
index 742b897..f35d708 100644
--- a/res/xml/accessibility_settings_for_setup_wizard.xml
+++ b/res/xml/accessibility_settings_for_setup_wizard.xml
@@ -30,7 +30,7 @@
         android:layout="@layout/preference_button"
         android:title="@string/title_font_size" />
 
-    <com.android.settings.DisplayDensityPreference
+    <com.android.settings.display.ScreenZoomPreference
         android:key="force_density_preference"
         android:layout="@layout/preference_button"
         settings:keywords="@string/screen_zoom_keywords"
diff --git a/src/com/android/settings/InstrumentedFragment.java b/src/com/android/settings/InstrumentedFragment.java
index cc0014d..2d39961 100644
--- a/src/com/android/settings/InstrumentedFragment.java
+++ b/src/com/android/settings/InstrumentedFragment.java
@@ -33,6 +33,7 @@
     public static final int CONFIGURE_NOTIFICATION = UNDECLARED + 3;
     public static final int CONFIGURE_WIFI = UNDECLARED + 4;
     public static final int DISPLAY_SCREEN_ZOOM = UNDECLARED + 5;
+    public static final int ACCESSIBILITY_FONT_SIZE = UNDECLARED + 6;
 
     /**
      * Declare the view of this category.
diff --git a/src/com/android/settings/PreviewSeekBarPreferenceFragment.java b/src/com/android/settings/PreviewSeekBarPreferenceFragment.java
new file mode 100644
index 0000000..7a9efd3
--- /dev/null
+++ b/src/com/android/settings/PreviewSeekBarPreferenceFragment.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * 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 com.android.settings;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.widget.FrameLayout;
+import android.widget.SeekBar;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+import android.widget.TextView;
+
+
+/**
+ * Preference fragment shows a preview and a seek bar to adjust a specific settings.
+ */
+public abstract class PreviewSeekBarPreferenceFragment extends SettingsPreferenceFragment {
+
+    /** List of entries corresponding the settings being set. */
+    protected String[] mEntries;
+
+    /** Index of the entry corresponding to initial value of the settings. */
+    protected int mInitialIndex;
+
+    /** Index of the entry corresponding to current value of the settings. */
+    protected int mCurrentIndex;
+
+    /** Resource id of the layout for this preference fragment. */
+    protected int mActivityLayoutResId;
+
+    /** Resource id of the layout that defines the contents instide preview screen. */
+    protected int mPreviewSampleResId;
+
+    /** Duration to use when cross-fading between previews. */
+    private static final long CROSS_FADE_DURATION_MS = 400;
+
+    /** Interpolator to use when cross-fading between previews. */
+    private static final Interpolator FADE_IN_INTERPOLATOR = new DecelerateInterpolator();
+
+    /** Interpolator to use when cross-fading between previews. */
+    private static final Interpolator FADE_OUT_INTERPOLATOR = new AccelerateInterpolator();
+
+    private ViewGroup mPreviewFrame;
+    private TextView mLabel;
+    private View mLarger;
+    private View mSmaller;
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+                             Bundle savedInstanceState) {
+        final View root = super.onCreateView(inflater, container, savedInstanceState);
+        final ViewGroup listContainer = (ViewGroup) root.findViewById(android.R.id.list_container);
+        listContainer.removeAllViews();
+
+        final View content = inflater.inflate(mActivityLayoutResId, listContainer, false);
+        listContainer.addView(content);
+
+        mLabel = (TextView) content.findViewById(R.id.current_label);
+
+        // The maximum SeekBar value always needs to be non-zero. If there's
+        // only one available value, we'll handle this by disabling the
+        // seek bar.
+        final int max = Math.max(1, mEntries.length - 1);
+
+        final SeekBar seekBar = (SeekBar) content.findViewById(R.id.seek_bar);
+        seekBar.setMax(max);
+        seekBar.setProgress(mInitialIndex);
+        seekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
+            @Override
+            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+                setPreviewLayer(progress, true);
+            }
+
+            @Override
+            public void onStartTrackingTouch(SeekBar seekBar) {}
+
+            @Override
+            public void onStopTrackingTouch(SeekBar seekBar) {}
+        });
+
+        mSmaller = content.findViewById(R.id.smaller);
+        mSmaller.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                final int progress = seekBar.getProgress();
+                if (progress > 0) {
+                    seekBar.setProgress(progress - 1, true);
+                }
+            }
+        });
+
+        mLarger = content.findViewById(R.id.larger);
+        mLarger.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                final int progress = seekBar.getProgress();
+                if (progress < seekBar.getMax()) {
+                    seekBar.setProgress(progress + 1, true);
+                }
+            }
+        });
+
+        if (mEntries.length == 1) {
+            // The larger and smaller buttons will be disabled when we call
+            // setPreviewLayer() later in this method.
+            seekBar.setEnabled(false);
+        }
+
+        mPreviewFrame = (FrameLayout) content.findViewById(R.id.preview_frame);
+
+        // Populate the sample layouts.
+        final Context context = getContext();
+        final Configuration origConfig = context.getResources().getConfiguration();
+        for (int i = 0; i < mEntries.length; ++i) {
+            final Configuration config = createConfig(origConfig, i);
+
+            // Create a new configuration for the specified value. It won't
+            // have any theme set, so manually apply the current theme.
+            final Context configContext = context.createConfigurationContext(config);
+            configContext.setTheme(context.getThemeResId());
+
+            final LayoutInflater configInflater = LayoutInflater.from(configContext);
+            final View sampleView = configInflater.inflate(mPreviewSampleResId, mPreviewFrame, false);
+            sampleView.setAlpha(0);
+            sampleView.setVisibility(View.INVISIBLE);
+
+            mPreviewFrame.addView(sampleView);
+        }
+
+        setPreviewLayer(mInitialIndex, false);
+
+        return root;
+    }
+
+    @Override
+    public void onDetach() {
+        super.onDetach();
+
+        // This will commit the change SLIGHTLY after the activity has
+        // finished, which could be considered a feature or a bug...
+        commit();
+    }
+
+    /**
+     * Creates new configuration based on the current position of the SeekBar.
+     */
+    protected abstract Configuration createConfig(Configuration origConfig, int index);
+
+    private void setPreviewLayer(int index, boolean animate) {
+        mLabel.setText(mEntries[index]);
+
+        if (mCurrentIndex >= 0) {
+            final View lastLayer = mPreviewFrame.getChildAt(mCurrentIndex);
+            if (animate) {
+                lastLayer.animate()
+                        .alpha(0)
+                        .setInterpolator(FADE_OUT_INTERPOLATOR)
+                        .setDuration(CROSS_FADE_DURATION_MS)
+                        .withEndAction(new Runnable() {
+                            @Override
+                            public void run() {
+                                lastLayer.setVisibility(View.INVISIBLE);
+                            }
+                        });
+            } else {
+                lastLayer.setAlpha(0);
+                lastLayer.setVisibility(View.INVISIBLE);
+            }
+        }
+
+        final View nextLayer = mPreviewFrame.getChildAt(index);
+        if (animate) {
+            nextLayer.animate()
+                    .alpha(1)
+                    .setInterpolator(FADE_IN_INTERPOLATOR)
+                    .setDuration(CROSS_FADE_DURATION_MS)
+                    .withStartAction(new Runnable() {
+                        @Override
+                        public void run() {
+                            nextLayer.setVisibility(View.VISIBLE);
+                        }
+                    });
+        } else {
+            nextLayer.setVisibility(View.VISIBLE);
+            nextLayer.setAlpha(1);
+        }
+
+        mSmaller.setEnabled(index > 0);
+        mLarger.setEnabled(index < mEntries.length - 1);
+
+        mCurrentIndex = index;
+    }
+
+    /**
+     * Persists the selected value and sends a configuration change.
+     */
+    protected abstract void commit();
+}
diff --git a/src/com/android/settings/display/TouchBlockingFrameLayout.java b/src/com/android/settings/TouchBlockingFrameLayout.java
similarity index 96%
rename from src/com/android/settings/display/TouchBlockingFrameLayout.java
rename to src/com/android/settings/TouchBlockingFrameLayout.java
index 3f5483d..8b36635 100644
--- a/src/com/android/settings/display/TouchBlockingFrameLayout.java
+++ b/src/com/android/settings/TouchBlockingFrameLayout.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.settings.display;
+package com.android.settings;
 
 import android.annotation.Nullable;
 import android.content.Context;
diff --git a/src/com/android/settings/accessibility/ToggleFontSizePreferenceFragment.java b/src/com/android/settings/accessibility/ToggleFontSizePreferenceFragment.java
new file mode 100644
index 0000000..8b9e27b
--- /dev/null
+++ b/src/com/android/settings/accessibility/ToggleFontSizePreferenceFragment.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * 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 com.android.settings.accessibility;
+
+import com.android.settings.InstrumentedFragment;
+import com.android.settings.R;
+import com.android.settings.PreviewSeekBarPreferenceFragment;
+
+import android.annotation.Nullable;
+import android.app.ActivityManagerNative;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.util.Log;
+
+/**
+ * Preference fragment used to control font size.
+ */
+public class ToggleFontSizePreferenceFragment extends PreviewSeekBarPreferenceFragment {
+    private static final String LOG_TAG = "ToggleFontSizePreferenceFragment";
+
+    private float[] mValues;
+
+    @Override
+    public void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        mActivityLayoutResId = R.layout.font_size_activity;
+        mPreviewSampleResId = R.layout.font_size_preview;
+
+        Resources res = getContext().getResources();
+        // Mark the appropriate item in the preferences list.
+        final Configuration origConfig = res.getConfiguration();
+        mEntries = res.getStringArray(R.array.entries_font_size);
+        final String[] strEntryValues = res.getStringArray(R.array.entryvalues_font_size);
+        mInitialIndex = floatToIndex(origConfig.fontScale, strEntryValues);
+        mValues = new float[strEntryValues.length];
+        for (int i = 0; i < strEntryValues.length; ++i) {
+            mValues[i] = Float.parseFloat(strEntryValues[i]);
+        }
+    }
+
+    @Override
+    protected Configuration createConfig(Configuration origConfig, int index) {
+        // Populate the sample layouts.
+        final Configuration config = new Configuration(origConfig);
+        config.fontScale = mValues[index];
+        return config;
+    }
+
+    /**
+     * Persists the selected font size and sends a configuration change.
+     */
+    @Override
+    protected void commit() {
+        Configuration config = getContext().getResources().getConfiguration();
+        config.fontScale = mValues[mCurrentIndex];
+        try {
+            ActivityManagerNative.getDefault().updatePersistentConfiguration(config);
+        } catch (RemoteException e) {
+            Log.w(LOG_TAG, "Unable to save font size setting");
+        }
+    }
+
+    @Override
+    protected int getMetricsCategory() {
+        return InstrumentedFragment.ACCESSIBILITY_FONT_SIZE;
+    }
+
+    private int floatToIndex(float val, String[] indices) {
+        float lastVal = Float.parseFloat(indices[0]);
+        for (int i=1; i<indices.length; i++) {
+            float thisVal = Float.parseFloat(indices[i]);
+            if (val < (lastVal + (thisVal-lastVal)*.5f)) {
+                return i-1;
+            }
+            lastVal = thisVal;
+        }
+        return indices.length-1;
+    }
+
+}
\ No newline at end of file
diff --git a/src/com/android/settings/display/ScreenZoomSettings.java b/src/com/android/settings/display/ScreenZoomSettings.java
index 1ddcaab..7e8ac22 100644
--- a/src/com/android/settings/display/ScreenZoomSettings.java
+++ b/src/com/android/settings/display/ScreenZoomSettings.java
@@ -18,7 +18,7 @@
 
 import com.android.settings.InstrumentedFragment;
 import com.android.settings.R;
-import com.android.settings.SettingsPreferenceFragment;
+import com.android.settings.PreviewSeekBarPreferenceFragment;
 import com.android.settings.search.BaseSearchIndexProvider;
 import com.android.settings.search.Indexable;
 import com.android.settings.search.SearchIndexableRaw;
@@ -29,17 +29,6 @@
 import android.content.res.Resources;
 import android.os.Bundle;
 import android.view.Display;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.view.ViewGroup;
-import android.view.animation.AccelerateInterpolator;
-import android.view.animation.DecelerateInterpolator;
-import android.view.animation.Interpolator;
-import android.widget.FrameLayout;
-import android.widget.SeekBar;
-import android.widget.SeekBar.OnSeekBarChangeListener;
-import android.widget.TextView;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -47,32 +36,17 @@
 /**
  * Preference fragment used to control screen zoom.
  */
-public class ScreenZoomSettings extends SettingsPreferenceFragment implements Indexable {
-    /** Duration to use when cross-fading between previews. */
-    private static final long CROSS_FADE_DURATION_MS = 400;
+public class ScreenZoomSettings extends PreviewSeekBarPreferenceFragment implements Indexable {
 
-    /** Interpolator to use when cross-fading between previews. */
-    private static final Interpolator FADE_IN_INTERPOLATOR = new DecelerateInterpolator();
-
-    /** Interpolator to use when cross-fading between previews. */
-    private static final Interpolator FADE_OUT_INTERPOLATOR = new AccelerateInterpolator();
-
-    private ViewGroup mPreviewFrame;
-    private TextView mLabel;
-    private View mLarger;
-    private View mSmaller;
-
-    private String[] mEntries;
-    private int[] mValues;
     private int mNormalDensity;
-    private int mInitialIndex;
-
-    private int mCurrentIndex;
+    private int[] mValues;
 
     @Override
     public void onCreate(@Nullable Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
 
+        mActivityLayoutResId = R.layout.screen_zoom_activity;
+        mPreviewSampleResId = R.layout.screen_zoom_preview;
         final DisplayDensityUtils density = new DisplayDensityUtils(getContext());
 
         final int initialIndex = density.getCurrentIndex();
@@ -94,152 +68,18 @@
     }
 
     @Override
-    public View onCreateView(LayoutInflater inflater, ViewGroup container,
-            Bundle savedInstanceState) {
-        final View root = super.onCreateView(inflater, container, savedInstanceState);
-        final ViewGroup list_container = (ViewGroup) root.findViewById(android.R.id.list_container);
-        list_container.removeAllViews();
-
-        final View content = inflater.inflate(R.layout.screen_zoom_activity, list_container, false);
-        list_container.addView(content);
-
-        mLabel = (TextView) content.findViewById(R.id.current_density);
-
-        // The maximum SeekBar value always needs to be non-zero. If there's
-        // only one available zoom level, we'll handle this by disabling the
-        // seek bar.
-        final int max = Math.max(1, mValues.length - 1);
-
-        final SeekBar seekBar = (SeekBar) content.findViewById(R.id.seek_bar);
-        seekBar.setMax(max);
-        seekBar.setProgress(mInitialIndex);
-        seekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
-            @Override
-            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
-                setPreviewLayer(progress, true);
-            }
-
-            @Override
-            public void onStartTrackingTouch(SeekBar seekBar) {}
-
-            @Override
-            public void onStopTrackingTouch(SeekBar seekBar) {}
-        });
-
-        mSmaller = content.findViewById(R.id.smaller);
-        mSmaller.setOnClickListener(new OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                final int progress = seekBar.getProgress();
-                if (progress > 0) {
-                    seekBar.setProgress(progress - 1, true);
-                }
-            }
-        });
-
-        mLarger = content.findViewById(R.id.larger);
-        mLarger.setOnClickListener(new OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                final int progress = seekBar.getProgress();
-                if (progress < seekBar.getMax()) {
-                    seekBar.setProgress(progress + 1, true);
-                }
-            }
-        });
-
-        if (mValues.length == 1) {
-            // The larger and smaller buttons will be disabled when we call
-            // setPreviewLayer() later in this method.
-            seekBar.setEnabled(false);
-        }
-
-        mPreviewFrame = (FrameLayout) content.findViewById(R.id.preview_frame);
-
+    protected Configuration createConfig(Configuration origConfig, int index) {
         // Populate the sample layouts.
-        final Context context = getContext();
-        final Configuration origConfig = context.getResources().getConfiguration();
-        for (int mValue : mValues) {
-            final Configuration config = new Configuration(origConfig);
-            config.densityDpi = mValue;
-
-            // Create a new configuration for the specified density. It won't
-            // have any theme set, so manually apply the current theme.
-            final Context configContext = context.createConfigurationContext(config);
-            configContext.setTheme(context.getThemeResId());
-
-            final LayoutInflater configInflater = LayoutInflater.from(configContext);
-            final View sampleView = configInflater.inflate(
-                    R.layout.screen_zoom_preview, mPreviewFrame, false);
-            sampleView.setAlpha(0);
-            sampleView.setVisibility(View.INVISIBLE);
-
-            mPreviewFrame.addView(sampleView);
-        }
-
-        setPreviewLayer(mInitialIndex, false);
-
-        return root;
-    }
-
-    @Override
-    public void onDetach() {
-        super.onDetach();
-
-        // This will adjust the density SLIGHTLY after the activity has
-        // finished, which could be considered a feature or a bug...
-        commit();
-    }
-
-    private void setPreviewLayer(int index, boolean animate) {
-        mLabel.setText(mEntries[index]);
-
-        if (mCurrentIndex >= 0) {
-            final View lastLayer = mPreviewFrame.getChildAt(mCurrentIndex);
-            if (animate) {
-                lastLayer.animate()
-                        .alpha(0)
-                        .setInterpolator(FADE_OUT_INTERPOLATOR)
-                        .setDuration(CROSS_FADE_DURATION_MS)
-                        .withEndAction(new Runnable() {
-                            @Override
-                            public void run() {
-                                lastLayer.setVisibility(View.INVISIBLE);
-                            }
-                        });
-            } else {
-                lastLayer.setAlpha(0);
-                lastLayer.setVisibility(View.INVISIBLE);
-            }
-        }
-
-        final View nextLayer = mPreviewFrame.getChildAt(index);
-        if (animate) {
-            nextLayer.animate()
-                    .alpha(1)
-                    .setInterpolator(FADE_IN_INTERPOLATOR)
-                    .setDuration(CROSS_FADE_DURATION_MS)
-                    .withStartAction(new Runnable() {
-                @Override
-                public void run() {
-                    nextLayer.setVisibility(View.VISIBLE);
-                }
-            });
-        } else {
-            nextLayer.setVisibility(View.VISIBLE);
-            nextLayer.setAlpha(1);
-        }
-
-        mSmaller.setEnabled(index > 0);
-        mLarger.setEnabled(index < mEntries.length - 1);
-
-        mCurrentIndex = index;
+        final Configuration config = new Configuration(origConfig);
+        config.densityDpi = mValues[index];
+        return config;
     }
 
     /**
      * Persists the selected density and sends a configuration change.
      */
-    private void commit() {
+    @Override
+    protected void commit() {
         final int densityDpi = mValues[mCurrentIndex];
         if (densityDpi == mNormalDensity) {
             DisplayDensityUtils.clearForcedDisplayDensity(Display.DEFAULT_DISPLAY);