| /* |
| * Copyright 2018 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package androidx.preference; |
| |
| import android.content.Context; |
| import android.content.res.TypedArray; |
| import android.os.Parcel; |
| import android.os.Parcelable; |
| import android.util.AttributeSet; |
| import android.util.Log; |
| import android.view.KeyEvent; |
| import android.view.View; |
| import android.widget.SeekBar; |
| import android.widget.SeekBar.OnSeekBarChangeListener; |
| import android.widget.TextView; |
| |
| /** |
| * Preference based on android.preference.SeekBarPreference but uses support v7 preference as base. |
| * It contains a title and a seekbar and an optional seekbar value TextView. The actual preference |
| * layout is customizable by setting {@code android:layout} on the preference widget layout or |
| * {@code seekBarPreferenceStyle} attribute. |
| * The seekbar within the preference can be defined adjustable or not by setting {@code |
| * adjustable} attribute. If adjustable, the preference will be responsive to DPAD left/right keys. |
| * Otherwise, it skips those keys. |
| * The seekbar value view can be shown or disabled by setting {@code showSeekBarValue} attribute |
| * to true or false, respectively. |
| * Other SeekBar specific attributes (e.g. {@code title, summary, defaultValue, min, max}) can be |
| * set directly on the preference widget layout. |
| */ |
| public class SeekBarPreference extends Preference { |
| |
| private int mSeekBarValue; |
| private int mMin; |
| private int mMax; |
| private int mSeekBarIncrement; |
| private boolean mTrackingTouch; |
| private SeekBar mSeekBar; |
| private TextView mSeekBarValueTextView; |
| private boolean mAdjustable; // whether the seekbar should respond to the left/right keys |
| private boolean mShowSeekBarValue; // whether to show the seekbar value TextView next to the bar |
| |
| private static final String TAG = "SeekBarPreference"; |
| |
| /** |
| * Listener reacting to the SeekBar changing value by the user |
| */ |
| private OnSeekBarChangeListener mSeekBarChangeListener = new OnSeekBarChangeListener() { |
| @Override |
| public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { |
| if (fromUser && !mTrackingTouch) { |
| syncValueInternal(seekBar); |
| } |
| } |
| |
| @Override |
| public void onStartTrackingTouch(SeekBar seekBar) { |
| mTrackingTouch = true; |
| } |
| |
| @Override |
| public void onStopTrackingTouch(SeekBar seekBar) { |
| mTrackingTouch = false; |
| if (seekBar.getProgress() + mMin != mSeekBarValue) { |
| syncValueInternal(seekBar); |
| } |
| } |
| }; |
| |
| /** |
| * Listener reacting to the user pressing DPAD left/right keys if {@code |
| * adjustable} attribute is set to true; it transfers the key presses to the SeekBar |
| * to be handled accordingly. |
| */ |
| private View.OnKeyListener mSeekBarKeyListener = new View.OnKeyListener() { |
| @Override |
| public boolean onKey(View v, int keyCode, KeyEvent event) { |
| if (event.getAction() != KeyEvent.ACTION_DOWN) { |
| return false; |
| } |
| |
| if (!mAdjustable && (keyCode == KeyEvent.KEYCODE_DPAD_LEFT |
| || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT)) { |
| // Right or left keys are pressed when in non-adjustable mode; Skip the keys. |
| return false; |
| } |
| |
| // We don't want to propagate the click keys down to the seekbar view since it will |
| // create the ripple effect for the thumb. |
| if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER) { |
| return false; |
| } |
| |
| if (mSeekBar == null) { |
| Log.e(TAG, "SeekBar view is null and hence cannot be adjusted."); |
| return false; |
| } |
| return mSeekBar.onKeyDown(keyCode, event); |
| } |
| }; |
| |
| public SeekBarPreference( |
| Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { |
| super(context, attrs, defStyleAttr, defStyleRes); |
| |
| TypedArray a = context.obtainStyledAttributes( |
| attrs, R.styleable.SeekBarPreference, defStyleAttr, defStyleRes); |
| |
| /** |
| * The ordering of these two statements are important. If we want to set max first, we need |
| * to perform the same steps by changing min/max to max/min as following: |
| * mMax = a.getInt(...) and setMin(...). |
| */ |
| mMin = a.getInt(R.styleable.SeekBarPreference_min, 0); |
| setMax(a.getInt(R.styleable.SeekBarPreference_android_max, 100)); |
| setSeekBarIncrement(a.getInt(R.styleable.SeekBarPreference_seekBarIncrement, 0)); |
| mAdjustable = a.getBoolean(R.styleable.SeekBarPreference_adjustable, true); |
| mShowSeekBarValue = a.getBoolean(R.styleable.SeekBarPreference_showSeekBarValue, true); |
| a.recycle(); |
| } |
| |
| public SeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr) { |
| this(context, attrs, defStyleAttr, 0); |
| } |
| |
| public SeekBarPreference(Context context, AttributeSet attrs) { |
| this(context, attrs, R.attr.seekBarPreferenceStyle); |
| } |
| |
| public SeekBarPreference(Context context) { |
| this(context, null); |
| } |
| |
| @Override |
| public void onBindViewHolder(PreferenceViewHolder view) { |
| super.onBindViewHolder(view); |
| view.itemView.setOnKeyListener(mSeekBarKeyListener); |
| mSeekBar = (SeekBar) view.findViewById(R.id.seekbar); |
| mSeekBarValueTextView = (TextView) view.findViewById(R.id.seekbar_value); |
| if (mShowSeekBarValue) { |
| mSeekBarValueTextView.setVisibility(View.VISIBLE); |
| } else { |
| mSeekBarValueTextView.setVisibility(View.GONE); |
| mSeekBarValueTextView = null; |
| } |
| |
| if (mSeekBar == null) { |
| Log.e(TAG, "SeekBar view is null in onBindViewHolder."); |
| return; |
| } |
| mSeekBar.setOnSeekBarChangeListener(mSeekBarChangeListener); |
| mSeekBar.setMax(mMax - mMin); |
| // If the increment is not zero, use that. Otherwise, use the default mKeyProgressIncrement |
| // in AbsSeekBar when it's zero. This default increment value is set by AbsSeekBar |
| // after calling setMax. That's why it's important to call setKeyProgressIncrement after |
| // calling setMax() since setMax() can change the increment value. |
| if (mSeekBarIncrement != 0) { |
| mSeekBar.setKeyProgressIncrement(mSeekBarIncrement); |
| } else { |
| mSeekBarIncrement = mSeekBar.getKeyProgressIncrement(); |
| } |
| |
| mSeekBar.setProgress(mSeekBarValue - mMin); |
| if (mSeekBarValueTextView != null) { |
| mSeekBarValueTextView.setText(String.valueOf(mSeekBarValue)); |
| } |
| mSeekBar.setEnabled(isEnabled()); |
| } |
| |
| @Override |
| protected void onSetInitialValue(boolean restoreValue, Object defaultValue) { |
| setValue(restoreValue ? getPersistedInt(mSeekBarValue) |
| : (Integer) defaultValue); |
| } |
| |
| @Override |
| protected Object onGetDefaultValue(TypedArray a, int index) { |
| return a.getInt(index, 0); |
| } |
| |
| public void setMin(int min) { |
| if (min > mMax) { |
| min = mMax; |
| } |
| if (min != mMin) { |
| mMin = min; |
| notifyChanged(); |
| } |
| } |
| |
| public int getMin() { |
| return mMin; |
| } |
| |
| public final void setMax(int max) { |
| if (max < mMin) { |
| max = mMin; |
| } |
| if (max != mMax) { |
| mMax = max; |
| notifyChanged(); |
| } |
| } |
| |
| /** |
| * Returns the amount of increment change via each arrow key click. This value is derived from |
| * user's specified increment value if it's not zero. Otherwise, the default value is picked |
| * from the default mKeyProgressIncrement value in {@link android.widget.AbsSeekBar}. |
| * @return The amount of increment on the SeekBar performed after each user's arrow key press. |
| */ |
| public final int getSeekBarIncrement() { |
| return mSeekBarIncrement; |
| } |
| |
| /** |
| * Sets the increment amount on the SeekBar for each arrow key press. |
| * @param seekBarIncrement The amount to increment or decrement when the user presses an |
| * arrow key. |
| */ |
| public final void setSeekBarIncrement(int seekBarIncrement) { |
| if (seekBarIncrement != mSeekBarIncrement) { |
| mSeekBarIncrement = Math.min(mMax - mMin, Math.abs(seekBarIncrement)); |
| notifyChanged(); |
| } |
| } |
| |
| public int getMax() { |
| return mMax; |
| } |
| |
| public void setAdjustable(boolean adjustable) { |
| mAdjustable = adjustable; |
| } |
| |
| public boolean isAdjustable() { |
| return mAdjustable; |
| } |
| |
| public void setValue(int seekBarValue) { |
| setValueInternal(seekBarValue, true); |
| } |
| |
| private void setValueInternal(int seekBarValue, boolean notifyChanged) { |
| if (seekBarValue < mMin) { |
| seekBarValue = mMin; |
| } |
| if (seekBarValue > mMax) { |
| seekBarValue = mMax; |
| } |
| |
| if (seekBarValue != mSeekBarValue) { |
| mSeekBarValue = seekBarValue; |
| if (mSeekBarValueTextView != null) { |
| mSeekBarValueTextView.setText(String.valueOf(mSeekBarValue)); |
| } |
| persistInt(seekBarValue); |
| if (notifyChanged) { |
| notifyChanged(); |
| } |
| } |
| } |
| |
| public int getValue() { |
| return mSeekBarValue; |
| } |
| |
| /** |
| * Persist the seekBar's seekbar value if callChangeListener |
| * returns true, otherwise set the seekBar's value to the stored value |
| */ |
| private void syncValueInternal(SeekBar seekBar) { |
| int seekBarValue = mMin + seekBar.getProgress(); |
| if (seekBarValue != mSeekBarValue) { |
| if (callChangeListener(seekBarValue)) { |
| setValueInternal(seekBarValue, false); |
| } else { |
| seekBar.setProgress(mSeekBarValue - mMin); |
| } |
| } |
| } |
| |
| @Override |
| protected Parcelable onSaveInstanceState() { |
| final Parcelable superState = super.onSaveInstanceState(); |
| if (isPersistent()) { |
| // No need to save instance state since it's persistent |
| return superState; |
| } |
| |
| // Save the instance state |
| final SavedState myState = new SavedState(superState); |
| myState.seekBarValue = mSeekBarValue; |
| myState.min = mMin; |
| myState.max = mMax; |
| return myState; |
| } |
| |
| @Override |
| protected void onRestoreInstanceState(Parcelable state) { |
| if (!state.getClass().equals(SavedState.class)) { |
| // Didn't save state for us in onSaveInstanceState |
| super.onRestoreInstanceState(state); |
| return; |
| } |
| |
| // Restore the instance state |
| SavedState myState = (SavedState) state; |
| super.onRestoreInstanceState(myState.getSuperState()); |
| mSeekBarValue = myState.seekBarValue; |
| mMin = myState.min; |
| mMax = myState.max; |
| notifyChanged(); |
| } |
| |
| /** |
| * SavedState, a subclass of {@link BaseSavedState}, will store the state |
| * of MyPreference, a subclass of Preference. |
| * <p> |
| * It is important to always call through to super methods. |
| */ |
| private static class SavedState extends BaseSavedState { |
| int seekBarValue; |
| int min; |
| int max; |
| |
| public SavedState(Parcel source) { |
| super(source); |
| |
| // Restore the click counter |
| seekBarValue = source.readInt(); |
| min = source.readInt(); |
| max = source.readInt(); |
| } |
| |
| @Override |
| public void writeToParcel(Parcel dest, int flags) { |
| super.writeToParcel(dest, flags); |
| |
| // Save the click counter |
| dest.writeInt(seekBarValue); |
| dest.writeInt(min); |
| dest.writeInt(max); |
| } |
| |
| public SavedState(Parcelable superState) { |
| super(superState); |
| } |
| |
| @SuppressWarnings("unused") |
| public static final Parcelable.Creator<SavedState> CREATOR = |
| new Parcelable.Creator<SavedState>() { |
| @Override |
| public SavedState createFromParcel(Parcel in) { |
| return new SavedState(in); |
| } |
| |
| @Override |
| public SavedState[] newArray(int size) { |
| return new SavedState[size]; |
| } |
| }; |
| } |
| } |