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