/* * Copyright (C) 2016 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 android.support.v7.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. *

* 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 CREATOR = new Parcelable.Creator() { @Override public SavedState createFromParcel(Parcel in) { return new SavedState(in); } @Override public SavedState[] newArray(int size) { return new SavedState[size]; } }; } }