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