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