1 /*
2  * Copyright (C) 2015 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 
17 package com.android.deskclock;
18 
19 import android.annotation.TargetApi;
20 import android.content.Context;
21 import android.content.res.TypedArray;
22 import android.graphics.Color;
23 import android.graphics.drawable.Drawable;
24 import android.os.Build;
25 import android.os.Parcel;
26 import android.os.Parcelable;
27 import android.util.AttributeSet;
28 import android.widget.NumberPicker;
29 
30 import java.lang.reflect.Field;
31 
32 /**
33  * Subclass of NumberPicker that allows customizing divider color and saves/restores its value
34  * across device rotations.
35  */
36 public class NumberPickerCompat extends NumberPicker implements NumberPicker.OnValueChangeListener {
37 
38     private static Field sSelectionDivider;
39     private static boolean sTrySelectionDivider = true;
40 
41     private final Runnable mAnnounceValueRunnable = new Runnable() {
42         @Override
43         public void run() {
44             if (mOnAnnounceValueChangedListener != null) {
45                 final int value = getValue();
46                 final String[] displayedValues = getDisplayedValues();
47                 final String displayedValue =
48                         displayedValues == null ? null : displayedValues[value];
49                 mOnAnnounceValueChangedListener.onAnnounceValueChanged(
50                         NumberPickerCompat.this, value, displayedValue);
51             }
52         }
53     };
54     private OnValueChangeListener mOnValueChangedListener;
55     private OnAnnounceValueChangedListener mOnAnnounceValueChangedListener;
56 
NumberPickerCompat(Context context)57     public NumberPickerCompat(Context context) {
58         this(context, null /* attrs */);
59     }
60 
NumberPickerCompat(Context context, AttributeSet attrs)61     public NumberPickerCompat(Context context, AttributeSet attrs) {
62         super(context, attrs);
63         tintSelectionDivider(context);
64         super.setOnValueChangedListener(this);
65     }
66 
NumberPickerCompat(Context context, AttributeSet attrs, int defStyleAttr)67     public NumberPickerCompat(Context context, AttributeSet attrs, int defStyleAttr) {
68         super(context, attrs, defStyleAttr);
69         tintSelectionDivider(context);
70         super.setOnValueChangedListener(this);
71     }
72 
73     @TargetApi(Build.VERSION_CODES.LOLLIPOP)
tintSelectionDivider(Context context)74     private void tintSelectionDivider(Context context) {
75         // Accent color in KK will stay system blue, so leave divider color matching.
76         // The divider is correctly tinted to controlColorNormal in M.
77 
78         if (Utils.isLOrLMR1() && sTrySelectionDivider) {
79             final TypedArray a = context.obtainStyledAttributes(
80                     new int[] { android.R.attr.colorControlNormal });
81              // White is default color if colorControlNormal is not defined.
82             final int color = a.getColor(0, Color.WHITE);
83             a.recycle();
84 
85             try {
86                 if (sSelectionDivider == null) {
87                     sSelectionDivider = NumberPicker.class.getDeclaredField("mSelectionDivider");
88                     sSelectionDivider.setAccessible(true);
89                 }
90                 final Drawable selectionDivider = (Drawable) sSelectionDivider.get(this);
91                 if (selectionDivider != null) {
92                     // setTint is API21+, but this will only be called in API21
93                     selectionDivider.setTint(color);
94                 }
95             } catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException e) {
96                 LogUtils.e("Unable to set selection divider", e);
97                 sTrySelectionDivider = false;
98             }
99         }
100     }
101 
102     /**
103      * @return the state of this NumberPicker including the currently selected value
104      */
105     @Override
onSaveInstanceState()106     protected Parcelable onSaveInstanceState() {
107         return new State(super.onSaveInstanceState(), getValue());
108     }
109 
110     /**
111      * @param state the state of this NumberPicker including the value to select
112      */
113     @Override
onRestoreInstanceState(Parcelable state)114     protected void onRestoreInstanceState(Parcelable state) {
115         final State instanceState = (State) state;
116         super.onRestoreInstanceState(instanceState.getSuperState());
117         setValue(instanceState.mValue);
118     }
119 
120     @Override
setOnValueChangedListener(OnValueChangeListener onValueChangedListener)121     public void setOnValueChangedListener(OnValueChangeListener onValueChangedListener) {
122         mOnValueChangedListener = onValueChangedListener;
123     }
124 
125     @Override
onValueChange(NumberPicker picker, int oldVal, int newVal)126     public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
127         if (mOnValueChangedListener != null) {
128             mOnValueChangedListener.onValueChange(picker, oldVal, newVal);
129         }
130 
131         // Wait till we reach a value to prevent TalkBack from announcing every intermediate value
132         // when scrolling fast.
133         removeCallbacks(mAnnounceValueRunnable);
134         postDelayed(mAnnounceValueRunnable, 200L);
135     }
136 
137     /**
138      * Register a callback to be invoked whenever a value change should be announced.
139      */
setOnAnnounceValueChangedListener(OnAnnounceValueChangedListener listener)140     public void setOnAnnounceValueChangedListener(OnAnnounceValueChangedListener listener) {
141         mOnAnnounceValueChangedListener = listener;
142     }
143 
144     /**
145      * The state of this NumberPicker including the selected value. Used to preserve values across
146      * device rotation.
147      */
148     private static final class State extends BaseSavedState {
149 
150         private final int mValue;
151 
State(Parcel source)152         public State(Parcel source) {
153             super(source);
154             mValue = source.readInt();
155         }
156 
State(Parcelable superState, int value)157         public State(Parcelable superState, int value) {
158             super(superState);
159             mValue = value;
160         }
161 
162         @Override
writeToParcel(Parcel dest, int flags)163         public void writeToParcel(Parcel dest, int flags) {
164             super.writeToParcel(dest, flags);
165             dest.writeInt(mValue);
166         }
167 
168         public static final Parcelable.Creator<State> CREATOR =
169                 new Parcelable.Creator<State>() {
170                     public State createFromParcel(Parcel in) { return new State(in); }
171                     public State[] newArray(int size) { return new State[size]; }
172                 };
173     }
174 
175     /**
176      * Interface for a callback to be invoked when a value change should be announced for
177      * accessibility.
178      */
179     public interface OnAnnounceValueChangedListener {
180         /**
181          * Called when a value change should be announced.
182          * @param picker The number picker whose value changed.
183          * @param value The new value.
184          * @param displayedValue The text displayed for the value, or null if the value itself
185          *     is displayed.
186          */
onAnnounceValueChanged(NumberPicker picker, int value, String displayedValue)187         void onAnnounceValueChanged(NumberPicker picker, int value, String displayedValue);
188     }
189 }