1 /* 2 * Copyright (C) 2019 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.developeroptions.widget; 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.text.TextUtils; 24 import android.util.AttributeSet; 25 import android.view.KeyEvent; 26 import android.view.View; 27 import android.view.accessibility.AccessibilityNodeInfo; 28 import android.widget.SeekBar; 29 import android.widget.SeekBar.OnSeekBarChangeListener; 30 31 import androidx.core.content.res.TypedArrayUtils; 32 import androidx.preference.PreferenceViewHolder; 33 34 import com.android.settingslib.RestrictedPreference; 35 36 /** 37 * Based on android.preference.SeekBarPreference, but uses support preference as base. 38 */ 39 public class SeekBarPreference extends RestrictedPreference 40 implements OnSeekBarChangeListener, View.OnKeyListener { 41 42 private int mProgress; 43 private int mMax; 44 private int mMin; 45 private boolean mTrackingTouch; 46 47 private boolean mContinuousUpdates; 48 private int mDefaultProgress = -1; 49 50 private SeekBar mSeekBar; 51 private boolean mShouldBlink; 52 private int mAccessibilityRangeInfoType = AccessibilityNodeInfo.RangeInfo.RANGE_TYPE_INT; 53 private CharSequence mSeekBarContentDescription; 54 SeekBarPreference( Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)55 public SeekBarPreference( 56 Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 57 super(context, attrs, defStyleAttr, defStyleRes); 58 59 TypedArray a = context.obtainStyledAttributes( 60 attrs, com.android.internal.R.styleable.ProgressBar, defStyleAttr, defStyleRes); 61 setMax(a.getInt(com.android.internal.R.styleable.ProgressBar_max, mMax)); 62 setMin(a.getInt(com.android.internal.R.styleable.ProgressBar_min, mMin)); 63 a.recycle(); 64 65 a = context.obtainStyledAttributes(attrs, 66 com.android.internal.R.styleable.SeekBarPreference, defStyleAttr, defStyleRes); 67 final int layoutResId = a.getResourceId( 68 com.android.internal.R.styleable.SeekBarPreference_layout, 69 com.android.internal.R.layout.preference_widget_seekbar); 70 a.recycle(); 71 72 setLayoutResource(layoutResId); 73 } 74 SeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr)75 public SeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr) { 76 this(context, attrs, defStyleAttr, 0); 77 } 78 SeekBarPreference(Context context, AttributeSet attrs)79 public SeekBarPreference(Context context, AttributeSet attrs) { 80 this(context, attrs, TypedArrayUtils.getAttr(context, 81 androidx.preference.R.attr.seekBarPreferenceStyle, 82 com.android.internal.R.attr.seekBarPreferenceStyle)); 83 } 84 SeekBarPreference(Context context)85 public SeekBarPreference(Context context) { 86 this(context, null); 87 } 88 setShouldBlink(boolean shouldBlink)89 public void setShouldBlink(boolean shouldBlink) { 90 mShouldBlink = shouldBlink; 91 notifyChanged(); 92 } 93 94 @Override isSelectable()95 public boolean isSelectable() { 96 return false; 97 } 98 99 @Override onBindViewHolder(PreferenceViewHolder view)100 public void onBindViewHolder(PreferenceViewHolder view) { 101 super.onBindViewHolder(view); 102 view.itemView.setOnKeyListener(this); 103 mSeekBar = (SeekBar) view.findViewById( 104 com.android.internal.R.id.seekbar); 105 mSeekBar.setOnSeekBarChangeListener(this); 106 mSeekBar.setMax(mMax); 107 mSeekBar.setMin(mMin); 108 mSeekBar.setProgress(mProgress); 109 mSeekBar.setEnabled(isEnabled()); 110 final CharSequence title = getTitle(); 111 if (!TextUtils.isEmpty(mSeekBarContentDescription)) { 112 mSeekBar.setContentDescription(mSeekBarContentDescription); 113 } else if (!TextUtils.isEmpty(title)) { 114 mSeekBar.setContentDescription(title); 115 } 116 if (mSeekBar instanceof DefaultIndicatorSeekBar) { 117 ((DefaultIndicatorSeekBar) mSeekBar).setDefaultProgress(mDefaultProgress); 118 } 119 if (mShouldBlink) { 120 View v = view.itemView; 121 v.post(() -> { 122 if (v.getBackground() != null) { 123 final int centerX = v.getWidth() / 2; 124 final int centerY = v.getHeight() / 2; 125 v.getBackground().setHotspot(centerX, centerY); 126 } 127 v.setPressed(true); 128 v.setPressed(false); 129 mShouldBlink = false; 130 }); 131 } 132 mSeekBar.setAccessibilityDelegate(new View.AccessibilityDelegate() { 133 @Override 134 public void onInitializeAccessibilityNodeInfo(View view, AccessibilityNodeInfo info) { 135 super.onInitializeAccessibilityNodeInfo(view, info); 136 // Update the range info with the correct type 137 AccessibilityNodeInfo.RangeInfo rangeInfo = info.getRangeInfo(); 138 if (rangeInfo != null) { 139 info.setRangeInfo(AccessibilityNodeInfo.RangeInfo.obtain( 140 mAccessibilityRangeInfoType, rangeInfo.getMin(), 141 rangeInfo.getMax(), rangeInfo.getCurrent())); 142 } 143 } 144 }); 145 } 146 147 @Override getSummary()148 public CharSequence getSummary() { 149 return null; 150 } 151 152 @Override onSetInitialValue(boolean restoreValue, Object defaultValue)153 protected void onSetInitialValue(boolean restoreValue, Object defaultValue) { 154 setProgress(restoreValue ? getPersistedInt(mProgress) 155 : (Integer) defaultValue); 156 } 157 158 @Override onGetDefaultValue(TypedArray a, int index)159 protected Object onGetDefaultValue(TypedArray a, int index) { 160 return a.getInt(index, 0); 161 } 162 163 @Override onKey(View v, int keyCode, KeyEvent event)164 public boolean onKey(View v, int keyCode, KeyEvent event) { 165 if (event.getAction() != KeyEvent.ACTION_DOWN) { 166 return false; 167 } 168 169 SeekBar seekBar = (SeekBar) v.findViewById(com.android.internal.R.id.seekbar); 170 if (seekBar == null) { 171 return false; 172 } 173 return seekBar.onKeyDown(keyCode, event); 174 } 175 setMax(int max)176 public void setMax(int max) { 177 if (max != mMax) { 178 mMax = max; 179 notifyChanged(); 180 } 181 } 182 setMin(int min)183 public void setMin(int min) { 184 if (min != mMin) { 185 mMin = min; 186 notifyChanged(); 187 } 188 } 189 getMax()190 public int getMax() { 191 return mMax; 192 } 193 getMin()194 public int getMin() { 195 return mMin; 196 } 197 setProgress(int progress)198 public void setProgress(int progress) { 199 setProgress(progress, true); 200 } 201 202 /** 203 * Sets the progress point to draw a single tick mark representing a default value. 204 */ setDefaultProgress(int defaultProgress)205 public void setDefaultProgress(int defaultProgress) { 206 if (mDefaultProgress != defaultProgress) { 207 mDefaultProgress = defaultProgress; 208 if (mSeekBar instanceof DefaultIndicatorSeekBar) { 209 ((DefaultIndicatorSeekBar) mSeekBar).setDefaultProgress(mDefaultProgress); 210 } 211 } 212 } 213 214 /** 215 * When {@code continuousUpdates} is true, update the persisted setting immediately as the thumb 216 * is dragged along the SeekBar. Otherwise, only update the value of the setting when the thumb 217 * is dropped. 218 */ setContinuousUpdates(boolean continuousUpdates)219 public void setContinuousUpdates(boolean continuousUpdates) { 220 mContinuousUpdates = continuousUpdates; 221 } 222 setProgress(int progress, boolean notifyChanged)223 private void setProgress(int progress, boolean notifyChanged) { 224 if (progress > mMax) { 225 progress = mMax; 226 } 227 if (progress < mMin) { 228 progress = mMin; 229 } 230 if (progress != mProgress) { 231 mProgress = progress; 232 persistInt(progress); 233 if (notifyChanged) { 234 notifyChanged(); 235 } 236 } 237 } 238 getProgress()239 public int getProgress() { 240 return mProgress; 241 } 242 243 /** 244 * Persist the seekBar's progress value if callChangeListener 245 * returns true, otherwise set the seekBar's progress to the stored value 246 */ syncProgress(SeekBar seekBar)247 void syncProgress(SeekBar seekBar) { 248 int progress = seekBar.getProgress(); 249 if (progress != mProgress) { 250 if (callChangeListener(progress)) { 251 setProgress(progress, false); 252 } else { 253 seekBar.setProgress(mProgress); 254 } 255 } 256 } 257 258 @Override onProgressChanged(SeekBar seekBar, int progress, boolean fromUser)259 public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 260 if (fromUser && (mContinuousUpdates || !mTrackingTouch)) { 261 syncProgress(seekBar); 262 } 263 } 264 265 @Override onStartTrackingTouch(SeekBar seekBar)266 public void onStartTrackingTouch(SeekBar seekBar) { 267 mTrackingTouch = true; 268 } 269 270 @Override onStopTrackingTouch(SeekBar seekBar)271 public void onStopTrackingTouch(SeekBar seekBar) { 272 mTrackingTouch = false; 273 if (seekBar.getProgress() != mProgress) { 274 syncProgress(seekBar); 275 } 276 } 277 278 /** 279 * Specify the type of range this seek bar represents. 280 * 281 * @param rangeInfoType The type of range to be shared with accessibility 282 * 283 * @see android.view.accessibility.AccessibilityNodeInfo.RangeInfo 284 */ setAccessibilityRangeInfoType(int rangeInfoType)285 public void setAccessibilityRangeInfoType(int rangeInfoType) { 286 mAccessibilityRangeInfoType = rangeInfoType; 287 } 288 setSeekBarContentDescription(CharSequence contentDescription)289 public void setSeekBarContentDescription(CharSequence contentDescription) { 290 mSeekBarContentDescription = contentDescription; 291 if (mSeekBar != null) { 292 mSeekBar.setContentDescription(contentDescription); 293 } 294 } 295 296 @Override onSaveInstanceState()297 protected Parcelable onSaveInstanceState() { 298 /* 299 * Suppose a client uses this preference type without persisting. We 300 * must save the instance state so it is able to, for example, survive 301 * orientation changes. 302 */ 303 304 final Parcelable superState = super.onSaveInstanceState(); 305 if (isPersistent()) { 306 // No need to save instance state since it's persistent 307 return superState; 308 } 309 310 // Save the instance state 311 final SavedState myState = new SavedState(superState); 312 myState.progress = mProgress; 313 myState.max = mMax; 314 myState.min = mMin; 315 return myState; 316 } 317 318 @Override onRestoreInstanceState(Parcelable state)319 protected void onRestoreInstanceState(Parcelable state) { 320 if (!state.getClass().equals(SavedState.class)) { 321 // Didn't save state for us in onSaveInstanceState 322 super.onRestoreInstanceState(state); 323 return; 324 } 325 326 // Restore the instance state 327 SavedState myState = (SavedState) state; 328 super.onRestoreInstanceState(myState.getSuperState()); 329 mProgress = myState.progress; 330 mMax = myState.max; 331 mMin = myState.min; 332 notifyChanged(); 333 } 334 335 /** 336 * SavedState, a subclass of {@link BaseSavedState}, will store the state 337 * of MyPreference, a subclass of Preference. 338 * <p> 339 * It is important to always call through to super methods. 340 */ 341 private static class SavedState extends BaseSavedState { 342 int progress; 343 int max; 344 int min; 345 SavedState(Parcel source)346 public SavedState(Parcel source) { 347 super(source); 348 349 // Restore the click counter 350 progress = source.readInt(); 351 max = source.readInt(); 352 min = source.readInt(); 353 } 354 355 @Override writeToParcel(Parcel dest, int flags)356 public void writeToParcel(Parcel dest, int flags) { 357 super.writeToParcel(dest, flags); 358 359 // Save the click counter 360 dest.writeInt(progress); 361 dest.writeInt(max); 362 dest.writeInt(min); 363 } 364 SavedState(Parcelable superState)365 public SavedState(Parcelable superState) { 366 super(superState); 367 } 368 369 @SuppressWarnings("unused") 370 public static final Parcelable.Creator<SavedState> CREATOR = 371 new Parcelable.Creator<SavedState>() { 372 public SavedState createFromParcel(Parcel in) { 373 return new SavedState(in); 374 } 375 376 public SavedState[] newArray(int size) { 377 return new SavedState[size]; 378 } 379 }; 380 } 381 } 382