blob: 308cbd4d893ecc9674d1ea40d6a849665492944a [file] [log] [blame]
/*
* Copyright (C) 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 com.android.car.settings.common;
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.TextView;
import androidx.preference.PreferenceViewHolder;
import com.android.car.settings.R;
import com.android.car.ui.preference.CarUiPreference;
/**
* Car Setting's own version of SeekBarPreference.
*
* The code is directly taken from androidx.preference.SeekBarPreference. However it has 1 main
* functionality difference. There is a new field which can enable continuous updates while the
* seek bar value is changing. This can be set programmatically by using the {@link
* #setContinuousUpdate() setContinuousUpdate} method.
*/
public class SeekBarPreference extends CarUiPreference {
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 boolean mContinuousUpdate; // whether scrolling provides continuous calls to listener
private static final String TAG = "SeekBarPreference";
/**
* Listener reacting to the SeekBar changing value by the user
*/
private SeekBar.OnSeekBarChangeListener mSeekBarChangeListener =
new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (fromUser && (mContinuousUpdate || !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);
}
/** Setter for the minimum value allowed on seek bar. */
public void setMin(int min) {
if (min > mMax) {
min = mMax;
}
if (min != mMin) {
mMin = min;
notifyChanged();
}
}
/** Getter for the minimum value allowed on seek bar. */
public int getMin() {
return mMin;
}
/** Setter for the maximum value allowed on seek bar. */
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();
}
}
/** Getter for the maximum value allowed on seek bar. */
public int getMax() {
return mMax;
}
/** Setter for the functionality which allows for changing the values via keyboard arrows. */
public void setAdjustable(boolean adjustable) {
mAdjustable = adjustable;
}
/** Getter for the functionality which allows for changing the values via keyboard arrows. */
public boolean isAdjustable() {
return mAdjustable;
}
/** Setter for the functionality which allows for continuous triggering of listener code. */
public void setContinuousUpdate(boolean continuousUpdate) {
mContinuousUpdate = continuousUpdate;
}
/** Setter for the whether the text should be visible. */
public void setShowSeekBarValue(boolean showSeekBarValue) {
mShowSeekBarValue = showSeekBarValue;
}
/** Setter for the current value of the seek bar. */
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();
}
}
}
/** Getter for the current value of the seek bar. */
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 SeekBarPreference.SavedState myState = new SeekBarPreference.SavedState(superState);
myState.mSeekBarValue = mSeekBarValue;
myState.mMin = mMin;
myState.mMax = mMax;
return myState;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
if (!state.getClass().equals(SeekBarPreference.SavedState.class)) {
// Didn't save state for us in onSaveInstanceState
super.onRestoreInstanceState(state);
return;
}
// Restore the instance state
SeekBarPreference.SavedState myState = (SeekBarPreference.SavedState) state;
super.onRestoreInstanceState(myState.getSuperState());
mSeekBarValue = myState.mSeekBarValue;
mMin = myState.mMin;
mMax = myState.mMax;
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 mSeekBarValue;
int mMin;
int mMax;
SavedState(Parcel source) {
super(source);
// Restore the click counter
mSeekBarValue = source.readInt();
mMin = source.readInt();
mMax = source.readInt();
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
// Save the click counter
dest.writeInt(mSeekBarValue);
dest.writeInt(mMin);
dest.writeInt(mMax);
}
SavedState(Parcelable superState) {
super(superState);
}
@SuppressWarnings("unused")
public static final Parcelable.Creator<SeekBarPreference.SavedState> CREATOR =
new Parcelable.Creator<SeekBarPreference.SavedState>() {
@Override
public SeekBarPreference.SavedState createFromParcel(Parcel in) {
return new SeekBarPreference.SavedState(in);
}
@Override
public SeekBarPreference.SavedState[] newArray(int size) {
return new SeekBarPreference
.SavedState[size];
}
};
}
}