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