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