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