• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.car.settings.common;
18 
19 import android.content.Context;
20 import android.content.res.TypedArray;
21 import android.os.Parcel;
22 import android.os.Parcelable;
23 import android.util.AttributeSet;
24 import android.util.Log;
25 import android.view.KeyEvent;
26 import android.view.MotionEvent;
27 import android.view.View;
28 import android.widget.SeekBar;
29 import android.widget.TextView;
30 
31 import androidx.preference.PreferenceViewHolder;
32 
33 import com.android.car.settings.R;
34 import com.android.car.ui.preference.CarUiPreference;
35 import com.android.car.ui.utils.DirectManipulationHelper;
36 
37 /**
38  * Car Setting's own version of SeekBarPreference.
39  *
40  * The code is directly taken from androidx.preference.SeekBarPreference. However it has 1 main
41  * functionality difference. There is a new field which can enable continuous updates while the
42  * seek bar value is changing. This can be set programmatically by using the {@link
43  * #setContinuousUpdate() setContinuousUpdate} method.
44  */
45 public class SeekBarPreference extends CarUiPreference {
46 
47     private int mSeekBarValue;
48     private int mMin;
49     private int mMax;
50     private int mSeekBarIncrement;
51     private boolean mTrackingTouch;
52     private SeekBar mSeekBar;
53     private TextView mSeekBarValueTextView;
54     private boolean mAdjustable; // whether the seekbar should respond to the left/right keys
55     private boolean mShowSeekBarValue; // whether to show the seekbar value TextView next to the bar
56     private boolean mContinuousUpdate; // whether scrolling provides continuous calls to listener
57     private boolean mInDirectManipulationMode;
58 
59     private static final String TAG = "SeekBarPreference";
60 
61     /**
62      * Listener reacting to the SeekBar changing value by the user
63      */
64     private final SeekBar.OnSeekBarChangeListener mSeekBarChangeListener =
65             new SeekBar.OnSeekBarChangeListener() {
66                 @Override
67                 public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
68                     if (fromUser && (mContinuousUpdate || !mTrackingTouch)) {
69                         syncValueInternal(seekBar);
70                     }
71                 }
72 
73                 @Override
74                 public void onStartTrackingTouch(SeekBar seekBar) {
75                     mTrackingTouch = true;
76                 }
77 
78                 @Override
79                 public void onStopTrackingTouch(SeekBar seekBar) {
80                     mTrackingTouch = false;
81                     if (seekBar.getProgress() + mMin != mSeekBarValue) {
82                         syncValueInternal(seekBar);
83                     }
84                 }
85             };
86 
87     /**
88      * Listener reacting to the user pressing DPAD left/right keys if {@code
89      * adjustable} attribute is set to true; it transfers the key presses to the SeekBar
90      * to be handled accordingly. Also handles entering and exiting direct manipulation
91      * mode for rotary.
92      */
93     private final View.OnKeyListener mSeekBarKeyListener = new View.OnKeyListener() {
94         @Override
95         public boolean onKey(View v, int keyCode, KeyEvent event) {
96             // Don't allow events through if there is no SeekBar or we're in non-adjustable mode.
97             if (mSeekBar == null || !mAdjustable) {
98                 return false;
99             }
100 
101             // Consume nudge events in direct manipulation mode.
102             if (mInDirectManipulationMode
103                     && (keyCode == KeyEvent.KEYCODE_DPAD_LEFT
104                     || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT
105                     || keyCode == KeyEvent.KEYCODE_DPAD_UP
106                     || keyCode == KeyEvent.KEYCODE_DPAD_DOWN)) {
107                 return true;
108             }
109 
110             // Handle events to enter or exit direct manipulation mode.
111             if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
112                 if (event.getAction() == KeyEvent.ACTION_DOWN) {
113                     setInDirectManipulationMode(v, !mInDirectManipulationMode);
114                 }
115                 return true;
116             }
117             if (keyCode == KeyEvent.KEYCODE_BACK) {
118                 if (mInDirectManipulationMode) {
119                     if (event.getAction() == KeyEvent.ACTION_DOWN) {
120                         setInDirectManipulationMode(v, false);
121                     }
122                     return true;
123                 }
124             }
125 
126             // Don't propagate confirm keys to the SeekBar to prevent a ripple effect on the thumb.
127             if (KeyEvent.isConfirmKey(keyCode)) {
128                 return false;
129             }
130 
131             if (event.getAction() == KeyEvent.ACTION_DOWN) {
132                 return mSeekBar.onKeyDown(keyCode, event);
133             } else {
134                 return mSeekBar.onKeyUp(keyCode, event);
135             }
136         }
137     };
138 
139     /** Listener to exit rotary direct manipulation mode when the user switches to touch. */
140     private final View.OnFocusChangeListener mSeekBarFocusChangeListener =
141             (v, hasFocus) -> {
142                 if (!hasFocus && mInDirectManipulationMode && mSeekBar != null) {
143                     setInDirectManipulationMode(v, false);
144                 }
145             };
146 
147     /** Listener to handle rotate events from the rotary controller in direct manipulation mode. */
148     private final View.OnGenericMotionListener mSeekBarScrollListener = (v, event) -> {
149         if (!mInDirectManipulationMode || !mAdjustable || mSeekBar == null) {
150             return false;
151         }
152         int adjustment = Math.round(event.getAxisValue(MotionEvent.AXIS_SCROLL));
153         if (adjustment == 0) {
154             return false;
155         }
156         int count = Math.abs(adjustment);
157         int keyCode = adjustment < 0 ? KeyEvent.KEYCODE_DPAD_LEFT : KeyEvent.KEYCODE_DPAD_RIGHT;
158         KeyEvent downEvent = new KeyEvent(event.getDownTime(), event.getEventTime(),
159                 KeyEvent.ACTION_DOWN, keyCode, /* repeat= */ 0);
160         KeyEvent upEvent = new KeyEvent(event.getDownTime(), event.getEventTime(),
161                 KeyEvent.ACTION_UP, keyCode, /* repeat= */ 0);
162         for (int i = 0; i < count; i++) {
163             mSeekBar.onKeyDown(keyCode, downEvent);
164             mSeekBar.onKeyUp(keyCode, upEvent);
165         }
166         return true;
167     };
168 
SeekBarPreference( Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)169     public SeekBarPreference(
170             Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
171         super(context, attrs, defStyleAttr, defStyleRes);
172 
173         TypedArray a = context.obtainStyledAttributes(
174                 attrs, R.styleable.SeekBarPreference, defStyleAttr, defStyleRes);
175 
176         /**
177          * The ordering of these two statements are important. If we want to set max first, we need
178          * to perform the same steps by changing min/max to max/min as following:
179          * mMax = a.getInt(...) and setMin(...).
180          */
181         mMin = a.getInt(R.styleable.SeekBarPreference_min, 0);
182         setMax(a.getInt(R.styleable.SeekBarPreference_android_max, 100));
183         setSeekBarIncrement(a.getInt(R.styleable.SeekBarPreference_seekBarIncrement, 0));
184         mAdjustable = a.getBoolean(R.styleable.SeekBarPreference_adjustable, true);
185         mShowSeekBarValue = a.getBoolean(R.styleable.SeekBarPreference_showSeekBarValue, true);
186         a.recycle();
187     }
188 
SeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr)189     public SeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr) {
190         this(context, attrs, defStyleAttr, 0);
191     }
192 
SeekBarPreference(Context context, AttributeSet attrs)193     public SeekBarPreference(Context context, AttributeSet attrs) {
194         this(context, attrs, R.attr.seekBarPreferenceStyle);
195     }
196 
SeekBarPreference(Context context)197     public SeekBarPreference(Context context) {
198         this(context, null);
199     }
200 
201     @Override
onBindViewHolder(PreferenceViewHolder view)202     public void onBindViewHolder(PreferenceViewHolder view) {
203         super.onBindViewHolder(view);
204         view.itemView.setOnKeyListener(mSeekBarKeyListener);
205         view.itemView.setOnFocusChangeListener(mSeekBarFocusChangeListener);
206         view.itemView.setOnGenericMotionListener(mSeekBarScrollListener);
207         mSeekBar = (SeekBar) view.findViewById(R.id.seekbar);
208         mSeekBarValueTextView = (TextView) view.findViewById(R.id.seekbar_value);
209         if (mShowSeekBarValue) {
210             mSeekBarValueTextView.setVisibility(View.VISIBLE);
211         } else {
212             mSeekBarValueTextView.setVisibility(View.GONE);
213             mSeekBarValueTextView = null;
214         }
215 
216         if (mSeekBar == null) {
217             Log.e(TAG, "SeekBar view is null in onBindViewHolder.");
218             return;
219         }
220         mSeekBar.setOnSeekBarChangeListener(mSeekBarChangeListener);
221         mSeekBar.setMax(mMax - mMin);
222         // If the increment is not zero, use that. Otherwise, use the default mKeyProgressIncrement
223         // in AbsSeekBar when it's zero. This default increment value is set by AbsSeekBar
224         // after calling setMax. That's why it's important to call setKeyProgressIncrement after
225         // calling setMax() since setMax() can change the increment value.
226         if (mSeekBarIncrement != 0) {
227             mSeekBar.setKeyProgressIncrement(mSeekBarIncrement);
228         } else {
229             mSeekBarIncrement = mSeekBar.getKeyProgressIncrement();
230         }
231 
232         mSeekBar.setProgress(mSeekBarValue - mMin);
233         if (mSeekBarValueTextView != null) {
234             mSeekBarValueTextView.setText(String.valueOf(mSeekBarValue));
235         }
236         mSeekBar.setEnabled(isEnabled());
237     }
238 
239     @Override
onSetInitialValue(boolean restoreValue, Object defaultValue)240     protected void onSetInitialValue(boolean restoreValue, Object defaultValue) {
241         setValue(restoreValue ? getPersistedInt(mSeekBarValue)
242                 : (Integer) defaultValue);
243     }
244 
245     @Override
onGetDefaultValue(TypedArray a, int index)246     protected Object onGetDefaultValue(TypedArray a, int index) {
247         return a.getInt(index, 0);
248     }
249 
250     /** Setter for the minimum value allowed on seek bar. */
setMin(int min)251     public void setMin(int min) {
252         if (min > mMax) {
253             min = mMax;
254         }
255         if (min != mMin) {
256             mMin = min;
257             notifyChanged();
258         }
259     }
260 
261     /** Getter for the minimum value allowed on seek bar. */
getMin()262     public int getMin() {
263         return mMin;
264     }
265 
266     /** Setter for the maximum value allowed on seek bar. */
setMax(int max)267     public final void setMax(int max) {
268         if (max < mMin) {
269             max = mMin;
270         }
271         if (max != mMax) {
272             mMax = max;
273             notifyChanged();
274         }
275     }
276 
277     /**
278      * Returns the amount of increment change via each arrow key click. This value is derived
279      * from
280      * user's specified increment value if it's not zero. Otherwise, the default value is picked
281      * from the default mKeyProgressIncrement value in {@link android.widget.AbsSeekBar}.
282      *
283      * @return The amount of increment on the SeekBar performed after each user's arrow key press.
284      */
getSeekBarIncrement()285     public final int getSeekBarIncrement() {
286         return mSeekBarIncrement;
287     }
288 
289     /**
290      * Sets the increment amount on the SeekBar for each arrow key press.
291      *
292      * @param seekBarIncrement The amount to increment or decrement when the user presses an
293      *                         arrow key.
294      */
setSeekBarIncrement(int seekBarIncrement)295     public final void setSeekBarIncrement(int seekBarIncrement) {
296         if (seekBarIncrement != mSeekBarIncrement) {
297             mSeekBarIncrement = Math.min(mMax - mMin, Math.abs(seekBarIncrement));
298             notifyChanged();
299         }
300     }
301 
302     /** Getter for the maximum value allowed on seek bar. */
getMax()303     public int getMax() {
304         return mMax;
305     }
306 
307     /** Setter for the functionality which allows for changing the values via keyboard arrows. */
setAdjustable(boolean adjustable)308     public void setAdjustable(boolean adjustable) {
309         mAdjustable = adjustable;
310     }
311 
312     /** Getter for the functionality which allows for changing the values via keyboard arrows. */
isAdjustable()313     public boolean isAdjustable() {
314         return mAdjustable;
315     }
316 
317     /** Setter for the functionality which allows for continuous triggering of listener code. */
setContinuousUpdate(boolean continuousUpdate)318     public void setContinuousUpdate(boolean continuousUpdate) {
319         mContinuousUpdate = continuousUpdate;
320     }
321 
322     /** Setter for the whether the text should be visible. */
setShowSeekBarValue(boolean showSeekBarValue)323     public void setShowSeekBarValue(boolean showSeekBarValue) {
324         mShowSeekBarValue = showSeekBarValue;
325     }
326 
327     /** Setter for the current value of the seek bar. */
setValue(int seekBarValue)328     public void setValue(int seekBarValue) {
329         setValueInternal(seekBarValue, true);
330     }
331 
setValueInternal(int seekBarValue, boolean notifyChanged)332     private void setValueInternal(int seekBarValue, boolean notifyChanged) {
333         if (seekBarValue < mMin) {
334             seekBarValue = mMin;
335         }
336         if (seekBarValue > mMax) {
337             seekBarValue = mMax;
338         }
339 
340         if (seekBarValue != mSeekBarValue) {
341             mSeekBarValue = seekBarValue;
342             if (mSeekBarValueTextView != null) {
343                 mSeekBarValueTextView.setText(String.valueOf(mSeekBarValue));
344             }
345             persistInt(seekBarValue);
346             if (notifyChanged) {
347                 notifyChanged();
348             }
349         }
350     }
351 
352     /** Getter for the current value of the seek bar. */
getValue()353     public int getValue() {
354         return mSeekBarValue;
355     }
356 
357     /**
358      * Persist the seekBar's seekbar value if callChangeListener
359      * returns true, otherwise set the seekBar's value to the stored value
360      */
syncValueInternal(SeekBar seekBar)361     private void syncValueInternal(SeekBar seekBar) {
362         int seekBarValue = mMin + seekBar.getProgress();
363         if (seekBarValue != mSeekBarValue) {
364             if (callChangeListener(seekBarValue)) {
365                 setValueInternal(seekBarValue, false);
366             } else {
367                 seekBar.setProgress(mSeekBarValue - mMin);
368             }
369         }
370     }
371 
setInDirectManipulationMode(View view, boolean enable)372     private void setInDirectManipulationMode(View view, boolean enable) {
373         mInDirectManipulationMode = enable;
374         DirectManipulationHelper.enableDirectManipulationMode(mSeekBar, enable);
375         // The preference is highlighted when it's focused with one exception. In direct
376         // manipulation (DM) mode, the SeekBar's thumb is highlighted instead. In DM mode, the
377         // preference and SeekBar are selected. The preference's highlight is drawn when it's
378         // focused but not selected, while the SeekBar's thumb highlight is drawn when the SeekBar
379         // is selected.
380         view.setSelected(enable);
381         mSeekBar.setSelected(enable);
382     }
383 
384     @Override
onSaveInstanceState()385     protected Parcelable onSaveInstanceState() {
386         final Parcelable superState = super.onSaveInstanceState();
387         if (isPersistent()) {
388             // No need to save instance state since it's persistent
389             return superState;
390         }
391 
392         // Save the instance state
393         final SeekBarPreference.SavedState myState = new SeekBarPreference.SavedState(superState);
394         myState.mSeekBarValue = mSeekBarValue;
395         myState.mMin = mMin;
396         myState.mMax = mMax;
397         return myState;
398     }
399 
400     @Override
onRestoreInstanceState(Parcelable state)401     protected void onRestoreInstanceState(Parcelable state) {
402         if (!state.getClass().equals(SeekBarPreference.SavedState.class)) {
403             // Didn't save state for us in onSaveInstanceState
404             super.onRestoreInstanceState(state);
405             return;
406         }
407 
408         // Restore the instance state
409         SeekBarPreference.SavedState myState = (SeekBarPreference.SavedState) state;
410         super.onRestoreInstanceState(myState.getSuperState());
411         mSeekBarValue = myState.mSeekBarValue;
412         mMin = myState.mMin;
413         mMax = myState.mMax;
414         notifyChanged();
415     }
416 
417     /**
418      * SavedState, a subclass of {@link BaseSavedState}, will store the state
419      * of MyPreference, a subclass of Preference.
420      * <p>
421      * It is important to always call through to super methods.
422      */
423     private static class SavedState extends BaseSavedState {
424         int mSeekBarValue;
425         int mMin;
426         int mMax;
427 
SavedState(Parcel source)428         SavedState(Parcel source) {
429             super(source);
430 
431             // Restore the click counter
432             mSeekBarValue = source.readInt();
433             mMin = source.readInt();
434             mMax = source.readInt();
435         }
436 
437         @Override
writeToParcel(Parcel dest, int flags)438         public void writeToParcel(Parcel dest, int flags) {
439             super.writeToParcel(dest, flags);
440 
441             // Save the click counter
442             dest.writeInt(mSeekBarValue);
443             dest.writeInt(mMin);
444             dest.writeInt(mMax);
445         }
446 
SavedState(Parcelable superState)447         SavedState(Parcelable superState) {
448             super(superState);
449         }
450 
451         @SuppressWarnings("unused")
452         public static final Parcelable.Creator<SeekBarPreference.SavedState> CREATOR =
453                 new Parcelable.Creator<SeekBarPreference.SavedState>() {
454                     @Override
455                     public SeekBarPreference.SavedState createFromParcel(Parcel in) {
456                         return new SeekBarPreference.SavedState(in);
457                     }
458 
459                     @Override
460                     public SeekBarPreference.SavedState[] newArray(int size) {
461                         return new SeekBarPreference
462                                 .SavedState[size];
463                     }
464                 };
465     }
466 }
467