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 }