• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2011 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.widget;
18 
19 import static android.view.HapticFeedbackConstants.CLOCK_TICK;
20 
21 import android.content.Context;
22 import android.content.res.TypedArray;
23 import android.os.Parcel;
24 import android.os.Parcelable;
25 import android.text.TextUtils;
26 import android.util.AttributeSet;
27 import android.view.KeyEvent;
28 import android.view.View;
29 import android.view.accessibility.AccessibilityNodeInfo;
30 import android.widget.SeekBar;
31 import android.widget.SeekBar.OnSeekBarChangeListener;
32 
33 import androidx.core.content.res.TypedArrayUtils;
34 import androidx.preference.PreferenceViewHolder;
35 
36 import com.android.settingslib.RestrictedPreference;
37 
38 /**
39  * Based on android.preference.SeekBarPreference, but uses support preference as base.
40  */
41 public class SeekBarPreference extends RestrictedPreference
42         implements OnSeekBarChangeListener, View.OnKeyListener {
43 
44     public static final int HAPTIC_FEEDBACK_MODE_NONE = 0;
45     public static final int HAPTIC_FEEDBACK_MODE_ON_TICKS = 1;
46     public static final int HAPTIC_FEEDBACK_MODE_ON_ENDS = 2;
47 
48     private int mProgress;
49     private int mMax;
50     private int mMin;
51     private boolean mTrackingTouch;
52 
53     private boolean mContinuousUpdates;
54     private int mHapticFeedbackMode = HAPTIC_FEEDBACK_MODE_NONE;
55     private int mDefaultProgress = -1;
56 
57     private SeekBar mSeekBar;
58     private boolean mShouldBlink;
59     private int mAccessibilityRangeInfoType = AccessibilityNodeInfo.RangeInfo.RANGE_TYPE_INT;
60     private CharSequence mOverrideSeekBarStateDescription;
61     private CharSequence mSeekBarContentDescription;
62     private CharSequence mSeekBarStateDescription;
63 
SeekBarPreference( Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)64     public SeekBarPreference(
65             Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
66         super(context, attrs, defStyleAttr, defStyleRes);
67 
68         TypedArray a = context.obtainStyledAttributes(
69                 attrs, com.android.internal.R.styleable.ProgressBar, defStyleAttr, defStyleRes);
70         setMax(a.getInt(com.android.internal.R.styleable.ProgressBar_max, mMax));
71         setMin(a.getInt(com.android.internal.R.styleable.ProgressBar_min, mMin));
72         a.recycle();
73 
74         a = context.obtainStyledAttributes(attrs,
75                 com.android.internal.R.styleable.SeekBarPreference, defStyleAttr, defStyleRes);
76         final int layoutResId = a.getResourceId(
77                 com.android.internal.R.styleable.SeekBarPreference_layout,
78                 com.android.internal.R.layout.preference_widget_seekbar);
79         a.recycle();
80 
81         a = context.obtainStyledAttributes(
82                 attrs, com.android.internal.R.styleable.Preference, defStyleAttr, defStyleRes);
83         final boolean isSelectable = a.getBoolean(
84                 com.android.settings.R.styleable.Preference_android_selectable, false);
85         setSelectable(isSelectable);
86         a.recycle();
87 
88         setLayoutResource(layoutResId);
89     }
90 
SeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr)91     public SeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr) {
92         this(context, attrs, defStyleAttr, 0);
93     }
94 
SeekBarPreference(Context context, AttributeSet attrs)95     public SeekBarPreference(Context context, AttributeSet attrs) {
96         this(context, attrs, TypedArrayUtils.getAttr(context,
97                         androidx.preference.R.attr.seekBarPreferenceStyle,
98                         com.android.internal.R.attr.seekBarPreferenceStyle));
99     }
100 
SeekBarPreference(Context context)101     public SeekBarPreference(Context context) {
102         this(context, null);
103     }
104 
setShouldBlink(boolean shouldBlink)105     public void setShouldBlink(boolean shouldBlink) {
106         mShouldBlink = shouldBlink;
107         notifyChanged();
108     }
109 
110     @Override
isSelectable()111     public boolean isSelectable() {
112         if(isDisabledByAdmin()) {
113             return true;
114         } else {
115             return super.isSelectable();
116         }
117     }
118 
119     @Override
onBindViewHolder(PreferenceViewHolder view)120     public void onBindViewHolder(PreferenceViewHolder view) {
121         super.onBindViewHolder(view);
122         view.itemView.setOnKeyListener(this);
123         mSeekBar = (SeekBar) view.findViewById(
124                 com.android.internal.R.id.seekbar);
125         mSeekBar.setOnSeekBarChangeListener(this);
126         mSeekBar.setMax(mMax);
127         mSeekBar.setMin(mMin);
128         mSeekBar.setProgress(mProgress);
129         mSeekBar.setEnabled(isEnabled());
130         final CharSequence title = getTitle();
131         if (!TextUtils.isEmpty(mSeekBarContentDescription)) {
132             mSeekBar.setContentDescription(mSeekBarContentDescription);
133         } else if (!TextUtils.isEmpty(title)) {
134             mSeekBar.setContentDescription(title);
135         }
136         if (!TextUtils.isEmpty(mSeekBarStateDescription)) {
137             mSeekBar.setStateDescription(mSeekBarStateDescription);
138         }
139         if (mSeekBar instanceof DefaultIndicatorSeekBar) {
140             ((DefaultIndicatorSeekBar) mSeekBar).setDefaultProgress(mDefaultProgress);
141         }
142         if (mShouldBlink) {
143             View v = view.itemView;
144             v.post(() -> {
145                 if (v.getBackground() != null) {
146                     final int centerX = v.getWidth() / 2;
147                     final int centerY = v.getHeight() / 2;
148                     v.getBackground().setHotspot(centerX, centerY);
149                 }
150                 v.setPressed(true);
151                 v.setPressed(false);
152                 mShouldBlink = false;
153             });
154         }
155         mSeekBar.setAccessibilityDelegate(new View.AccessibilityDelegate() {
156             @Override
157             public void onInitializeAccessibilityNodeInfo(View view, AccessibilityNodeInfo info) {
158                 super.onInitializeAccessibilityNodeInfo(view, info);
159                 // Update the range info with the correct type
160                 AccessibilityNodeInfo.RangeInfo rangeInfo = info.getRangeInfo();
161                 if (rangeInfo != null) {
162                     info.setRangeInfo(AccessibilityNodeInfo.RangeInfo.obtain(
163                                     mAccessibilityRangeInfoType, rangeInfo.getMin(),
164                                     rangeInfo.getMax(), rangeInfo.getCurrent()));
165                 }
166                 if (mOverrideSeekBarStateDescription != null) {
167                     info.setStateDescription(mOverrideSeekBarStateDescription);
168                 }
169             }
170         });
171     }
172 
173     @Override
getSummary()174     public CharSequence getSummary() {
175         return null;
176     }
177 
178     @Override
onSetInitialValue(boolean restoreValue, Object defaultValue)179     protected void onSetInitialValue(boolean restoreValue, Object defaultValue) {
180         setProgress(restoreValue ? getPersistedInt(mProgress)
181                 : (Integer) defaultValue);
182     }
183 
184     @Override
onGetDefaultValue(TypedArray a, int index)185     protected Object onGetDefaultValue(TypedArray a, int index) {
186         return a.getInt(index, 0);
187     }
188 
189     @Override
onKey(View v, int keyCode, KeyEvent event)190     public boolean onKey(View v, int keyCode, KeyEvent event) {
191         if (event.getAction() != KeyEvent.ACTION_DOWN) {
192             return false;
193         }
194 
195         SeekBar seekBar = (SeekBar) v.findViewById(com.android.internal.R.id.seekbar);
196         if (seekBar == null) {
197             return false;
198         }
199         return seekBar.onKeyDown(keyCode, event);
200     }
201 
setMax(int max)202     public void setMax(int max) {
203         if (max != mMax) {
204             mMax = max;
205             notifyChanged();
206         }
207     }
208 
setMin(int min)209     public void setMin(int min) {
210         if (min != mMin) {
211             mMin = min;
212             notifyChanged();
213         }
214     }
215 
getMax()216     public int getMax() {
217         return mMax;
218     }
219 
getMin()220     public int getMin() {
221         return mMin;
222     }
223 
setProgress(int progress)224     public void setProgress(int progress) {
225         setProgress(progress, true);
226     }
227 
228     /**
229      * Sets the progress point to draw a single tick mark representing a default value.
230      */
setDefaultProgress(int defaultProgress)231     public void setDefaultProgress(int defaultProgress) {
232         if (mDefaultProgress != defaultProgress) {
233             mDefaultProgress = defaultProgress;
234             if (mSeekBar instanceof DefaultIndicatorSeekBar) {
235                 ((DefaultIndicatorSeekBar) mSeekBar).setDefaultProgress(mDefaultProgress);
236             }
237         }
238     }
239 
240     /**
241      * When {@code continuousUpdates} is true, update the persisted setting immediately as the thumb
242      * is dragged along the SeekBar. Otherwise, only update the value of the setting when the thumb
243      * is dropped.
244      */
setContinuousUpdates(boolean continuousUpdates)245     public void setContinuousUpdates(boolean continuousUpdates) {
246         mContinuousUpdates = continuousUpdates;
247     }
248 
249     /**
250      * Sets the haptic feedback mode. HAPTIC_FEEDBACK_MODE_ON_TICKS means to perform haptic feedback
251      * as the SeekBar's progress is updated; HAPTIC_FEEDBACK_MODE_ON_ENDS means to perform haptic
252      * feedback as the SeekBar's progress value is equal to the min/max value.
253      *
254      * @param hapticFeedbackMode the haptic feedback mode.
255      */
setHapticFeedbackMode(int hapticFeedbackMode)256     public void setHapticFeedbackMode(int hapticFeedbackMode) {
257         mHapticFeedbackMode = hapticFeedbackMode;
258     }
259 
setProgress(int progress, boolean notifyChanged)260     private void setProgress(int progress, boolean notifyChanged) {
261         if (progress > mMax) {
262             progress = mMax;
263         }
264         if (progress < mMin) {
265             progress = mMin;
266         }
267         if (progress != mProgress) {
268             mProgress = progress;
269             persistInt(progress);
270             if (notifyChanged) {
271                 notifyChanged();
272             }
273         }
274     }
275 
getProgress()276     public int getProgress() {
277         return mProgress;
278     }
279 
280     /**
281      * Persist the seekBar's progress value if callChangeListener
282      * returns true, otherwise set the seekBar's progress to the stored value
283      */
syncProgress(SeekBar seekBar)284     void syncProgress(SeekBar seekBar) {
285         int progress = seekBar.getProgress();
286         if (progress != mProgress) {
287             if (callChangeListener(progress)) {
288                 setProgress(progress, false);
289                 switch (mHapticFeedbackMode) {
290                     case HAPTIC_FEEDBACK_MODE_ON_TICKS:
291                         seekBar.performHapticFeedback(CLOCK_TICK);
292                         break;
293                     case HAPTIC_FEEDBACK_MODE_ON_ENDS:
294                         if (progress == mMax || progress == mMin) {
295                             seekBar.performHapticFeedback(CLOCK_TICK);
296                         }
297                         break;
298                 }
299             } else {
300                 seekBar.setProgress(mProgress);
301             }
302         }
303     }
304 
305     @Override
onProgressChanged(SeekBar seekBar, int progress, boolean fromUser)306     public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
307         if (fromUser && (mContinuousUpdates || !mTrackingTouch)) {
308             syncProgress(seekBar);
309         }
310     }
311 
312     @Override
onStartTrackingTouch(SeekBar seekBar)313     public void onStartTrackingTouch(SeekBar seekBar) {
314         mTrackingTouch = true;
315     }
316 
317     @Override
onStopTrackingTouch(SeekBar seekBar)318     public void onStopTrackingTouch(SeekBar seekBar) {
319         mTrackingTouch = false;
320         if (seekBar.getProgress() != mProgress) {
321             syncProgress(seekBar);
322         }
323     }
324 
325     /**
326      * Specify the type of range this seek bar represents.
327      *
328      * @param rangeInfoType The type of range to be shared with accessibility
329      *
330      * @see android.view.accessibility.AccessibilityNodeInfo.RangeInfo
331      */
setAccessibilityRangeInfoType(int rangeInfoType)332     public void setAccessibilityRangeInfoType(int rangeInfoType) {
333         mAccessibilityRangeInfoType = rangeInfoType;
334     }
335 
setSeekBarContentDescription(CharSequence contentDescription)336     public void setSeekBarContentDescription(CharSequence contentDescription) {
337         mSeekBarContentDescription = contentDescription;
338         if (mSeekBar != null) {
339             mSeekBar.setContentDescription(contentDescription);
340         }
341     }
342 
343     /**
344      * Specify the state description for this seek bar represents.
345      *
346      * @param stateDescription the state description of seek bar
347      */
setSeekBarStateDescription(CharSequence stateDescription)348     public void setSeekBarStateDescription(CharSequence stateDescription) {
349         mSeekBarStateDescription = stateDescription;
350         if (mSeekBar != null) {
351             mSeekBar.setStateDescription(stateDescription);
352         }
353     }
354 
355     /**
356      * Overrides the state description of {@link SeekBar} with given content.
357      */
overrideSeekBarStateDescription(CharSequence stateDescription)358     public void overrideSeekBarStateDescription(CharSequence stateDescription) {
359         mOverrideSeekBarStateDescription = stateDescription;
360     }
361 
362     @Override
onSaveInstanceState()363     protected Parcelable onSaveInstanceState() {
364         /*
365          * Suppose a client uses this preference type without persisting. We
366          * must save the instance state so it is able to, for example, survive
367          * orientation changes.
368          */
369 
370         final Parcelable superState = super.onSaveInstanceState();
371         if (isPersistent()) {
372             // No need to save instance state since it's persistent
373             return superState;
374         }
375 
376         // Save the instance state
377         final SavedState myState = new SavedState(superState);
378         myState.progress = mProgress;
379         myState.max = mMax;
380         myState.min = mMin;
381         return myState;
382     }
383 
384     @Override
onRestoreInstanceState(Parcelable state)385     protected void onRestoreInstanceState(Parcelable state) {
386         if (!state.getClass().equals(SavedState.class)) {
387             // Didn't save state for us in onSaveInstanceState
388             super.onRestoreInstanceState(state);
389             return;
390         }
391 
392         // Restore the instance state
393         SavedState myState = (SavedState) state;
394         super.onRestoreInstanceState(myState.getSuperState());
395         mProgress = myState.progress;
396         mMax = myState.max;
397         mMin = myState.min;
398         notifyChanged();
399     }
400 
401     /**
402      * SavedState, a subclass of {@link BaseSavedState}, will store the state
403      * of MyPreference, a subclass of Preference.
404      * <p>
405      * It is important to always call through to super methods.
406      */
407     private static class SavedState extends BaseSavedState {
408         int progress;
409         int max;
410         int min;
411 
SavedState(Parcel source)412         public SavedState(Parcel source) {
413             super(source);
414 
415             // Restore the click counter
416             progress = source.readInt();
417             max = source.readInt();
418             min = source.readInt();
419         }
420 
421         @Override
writeToParcel(Parcel dest, int flags)422         public void writeToParcel(Parcel dest, int flags) {
423             super.writeToParcel(dest, flags);
424 
425             // Save the click counter
426             dest.writeInt(progress);
427             dest.writeInt(max);
428             dest.writeInt(min);
429         }
430 
SavedState(Parcelable superState)431         public SavedState(Parcelable superState) {
432             super(superState);
433         }
434 
435         @SuppressWarnings("unused")
436         public static final Parcelable.Creator<SavedState> CREATOR =
437                 new Parcelable.Creator<SavedState>() {
438             public SavedState createFromParcel(Parcel in) {
439                 return new SavedState(in);
440             }
441 
442             public SavedState[] newArray(int size) {
443                 return new SavedState[size];
444             }
445         };
446     }
447 }
448