1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.settings.display;
18 
19 import android.content.Context;
20 import android.content.res.Configuration;
21 import android.os.Bundle;
22 import android.os.SystemClock;
23 import android.view.Choreographer;
24 import android.view.LayoutInflater;
25 import android.view.View;
26 import android.view.ViewGroup;
27 import android.view.accessibility.AccessibilityEvent;
28 import android.widget.SeekBar;
29 import android.widget.SeekBar.OnSeekBarChangeListener;
30 import android.widget.TextView;
31 
32 import androidx.viewpager.widget.ViewPager;
33 import androidx.viewpager.widget.ViewPager.OnPageChangeListener;
34 
35 import com.android.settings.R;
36 import com.android.settings.SettingsPreferenceFragment;
37 import com.android.settings.widget.DotsPageIndicator;
38 import com.android.settings.widget.LabeledSeekBar;
39 
40 /**
41  * Preference fragment shows a preview and a seek bar to adjust a specific settings.
42  */
43 public abstract class PreviewSeekBarPreferenceFragment extends SettingsPreferenceFragment {
44 
45     /** List of entries corresponding the settings being set. */
46     protected String[] mEntries;
47 
48     /** Index of the entry corresponding to initial value of the settings. */
49     protected int mInitialIndex;
50 
51     /** Index of the entry corresponding to current value of the settings. */
52     protected int mCurrentIndex;
53 
54     private ViewPager mPreviewPager;
55     private PreviewPagerAdapter mPreviewPagerAdapter;
56     private DotsPageIndicator mPageIndicator;
57 
58     private TextView mLabel;
59     private LabeledSeekBar mSeekBar;
60     private View mLarger;
61     private View mSmaller;
62 
63     private static final long MIN_COMMIT_INTERVAL_MS = 800;
64     private long mLastCommitTime;
65 
66     private class onPreviewSeekBarChangeListener implements OnSeekBarChangeListener {
67         private static final long CHANGE_BY_SEEKBAR_DELAY_MS = 100;
68         private static final long CHANGE_BY_BUTTON_DELAY_MS = 300;
69 
70         private boolean mSeekByTouch;
71         private boolean mIsChanged;
72         private long mCommitDelayMs;
73 
74         private final Choreographer.FrameCallback mCommit = f -> {
75             commit();
76             mLastCommitTime = SystemClock.elapsedRealtime();
77         };
78 
79         @Override
onProgressChanged(SeekBar seekBar, int progress, boolean fromUser)80         public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
81             if (mCurrentIndex == progress) {
82                 mIsChanged = false;
83                 return;
84             }
85             mIsChanged = true;
86             setPreviewLayer(progress, false);
87             if (mSeekByTouch) {
88                 mCommitDelayMs = CHANGE_BY_SEEKBAR_DELAY_MS;
89             } else {
90                 mCommitDelayMs = CHANGE_BY_BUTTON_DELAY_MS;
91                 commitOnNextFrame();
92             }
93         }
94 
95         @Override
onStartTrackingTouch(SeekBar seekBar)96         public void onStartTrackingTouch(SeekBar seekBar) {
97             mSeekByTouch = true;
98         }
99 
100         @Override
onStopTrackingTouch(SeekBar seekBar)101         public void onStopTrackingTouch(SeekBar seekBar) {
102             mSeekByTouch = false;
103             if (!mIsChanged) {
104                 return;
105             }
106             if (mPreviewPagerAdapter.isAnimating()) {
107                 mPreviewPagerAdapter.setAnimationEndAction(this::commitOnNextFrame);
108             } else {
109                 commitOnNextFrame();
110             }
111         }
112 
commitOnNextFrame()113         private void commitOnNextFrame() {
114             if (SystemClock.elapsedRealtime() - mLastCommitTime < MIN_COMMIT_INTERVAL_MS) {
115                 mCommitDelayMs += MIN_COMMIT_INTERVAL_MS;
116             }
117             final Choreographer choreographer = Choreographer.getInstance();
118             choreographer.removeFrameCallback(mCommit);
119             choreographer.postFrameCallbackDelayed(mCommit, mCommitDelayMs);
120         }
121     }
122 
123     @Override
onSaveInstanceState(Bundle outState)124     public void onSaveInstanceState(Bundle outState) {
125         super.onSaveInstanceState(outState);
126         outState.putLong("mLastCommitTime", mLastCommitTime);
127     }
128 
129     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)130     public View onCreateView(LayoutInflater inflater, ViewGroup container,
131             Bundle savedInstanceState) {
132         if (savedInstanceState != null) {
133             mLastCommitTime = savedInstanceState.getLong("mLastCommitTime");
134         }
135         final View root = super.onCreateView(inflater, container, savedInstanceState);
136         final ViewGroup listContainer = root.findViewById(android.R.id.list_container);
137         listContainer.removeAllViews();
138 
139         final View content = inflater.inflate(getActivityLayoutResId(), listContainer, false);
140         listContainer.addView(content);
141 
142         mLabel = content.findViewById(R.id.current_label);
143 
144         // The maximum SeekBar value always needs to be non-zero. If there's
145         // only one available value, we'll handle this by disabling the
146         // seek bar.
147         final int max = Math.max(1, mEntries.length - 1);
148 
149         mSeekBar = content.findViewById(R.id.seek_bar);
150         mSeekBar.setLabels(mEntries);
151         mSeekBar.setMax(max);
152 
153         mSmaller = content.findViewById(R.id.smaller);
154         mSmaller.setOnClickListener(v -> {
155             final int progress = mSeekBar.getProgress();
156             if (progress > 0) {
157                 mSeekBar.setProgress(progress - 1, true);
158             }
159         });
160 
161         mLarger = content.findViewById(R.id.larger);
162         mLarger.setOnClickListener(v -> {
163             final int progress = mSeekBar.getProgress();
164             if (progress < mSeekBar.getMax()) {
165                 mSeekBar.setProgress(progress + 1, true);
166             }
167         });
168 
169         if (mEntries.length == 1) {
170             // The larger and smaller buttons will be disabled when we call
171             // setPreviewLayer() later in this method.
172             mSeekBar.setEnabled(false);
173         }
174 
175         final Context context = getContext();
176         final Configuration origConfig = context.getResources().getConfiguration();
177         final boolean isLayoutRtl = origConfig.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
178         Configuration[] configurations = new Configuration[mEntries.length];
179         for (int i = 0; i < mEntries.length; ++i) {
180             configurations[i] = createConfig(origConfig, i);
181         }
182 
183         final int[] previews = getPreviewSampleResIds();
184         mPreviewPager = content.findViewById(R.id.preview_pager);
185         mPreviewPagerAdapter = new PreviewPagerAdapter(context, isLayoutRtl,
186                 previews, configurations);
187         mPreviewPager.setAdapter(mPreviewPagerAdapter);
188         mPreviewPager.setCurrentItem(isLayoutRtl ? previews.length - 1 : 0);
189         mPreviewPager.addOnPageChangeListener(mPreviewPageChangeListener);
190 
191         mPageIndicator = content.findViewById(R.id.page_indicator);
192         if (previews.length > 1) {
193             mPageIndicator.setViewPager(mPreviewPager);
194             mPageIndicator.setVisibility(View.VISIBLE);
195             mPageIndicator.setOnPageChangeListener(mPageIndicatorPageChangeListener);
196         } else {
197             mPageIndicator.setVisibility(View.GONE);
198         }
199 
200         setPreviewLayer(mInitialIndex, false);
201         return root;
202     }
203 
204     @Override
onStart()205     public void onStart() {
206         super.onStart();
207         // Set SeekBar listener here to avoid onProgressChanged() is called
208         // during onRestoreInstanceState().
209         mSeekBar.setProgress(mCurrentIndex);
210         mSeekBar.setOnSeekBarChangeListener(new onPreviewSeekBarChangeListener());
211     }
212 
213     @Override
onStop()214     public void onStop() {
215         super.onStop();
216         mSeekBar.setOnSeekBarChangeListener(null);
217     }
218 
219     /** Resource id of the layout for this preference fragment. */
getActivityLayoutResId()220     protected abstract int getActivityLayoutResId();
221 
222     /** Resource id of the layout that defines the contents inside preview screen. */
getPreviewSampleResIds()223     protected abstract int[] getPreviewSampleResIds();
224 
225     /**
226      * Creates new configuration based on the current position of the SeekBar.
227      */
createConfig(Configuration origConfig, int index)228     protected abstract Configuration createConfig(Configuration origConfig, int index);
229 
230     /**
231      * Persists the selected value and sends a configuration change.
232      */
commit()233     protected abstract void commit();
234 
setPreviewLayer(int index, boolean animate)235     private void setPreviewLayer(int index, boolean animate) {
236         mLabel.setText(mEntries[index]);
237         mSmaller.setEnabled(index > 0);
238         mLarger.setEnabled(index < mEntries.length - 1);
239         setPagerIndicatorContentDescription(mPreviewPager.getCurrentItem());
240         mPreviewPagerAdapter.setPreviewLayer(index, mCurrentIndex,
241                 mPreviewPager.getCurrentItem(), animate);
242 
243         mCurrentIndex = index;
244     }
245 
246     private void setPagerIndicatorContentDescription(int position) {
247         mPageIndicator.setContentDescription(
248                 getString(R.string.preview_page_indicator_content_description,
249                         position + 1, getPreviewSampleResIds().length));
250     }
251 
252     private OnPageChangeListener mPreviewPageChangeListener = new OnPageChangeListener() {
253         @Override
254         public void onPageScrollStateChanged(int state) {
255             // Do nothing.
256         }
257 
258         @Override
259         public void onPageScrolled(int position, float positionOffset,
260                 int positionOffsetPixels) {
261             // Do nothing.
262         }
263 
264         @Override
265         public void onPageSelected(int position) {
266             mPreviewPager.sendAccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT);
267         }
268     };
269 
270     private OnPageChangeListener mPageIndicatorPageChangeListener = new OnPageChangeListener() {
271         @Override
272         public void onPageScrollStateChanged(int state) {
273             // Do nothing.
274         }
275 
276         @Override
277         public void onPageScrolled(int position, float positionOffset,
278                 int positionOffsetPixels) {
279             // Do nothing.
280         }
281 
282         @Override
283         public void onPageSelected(int position) {
284             setPagerIndicatorContentDescription(position);
285         }
286     };
287 }