Skip to content

Commit 84cec15

Browse files
ymariandsn5ft
authored andcommitted
[TimePicker] Fixed Talback support
PiperOrigin-RevId: 322589365
1 parent aca5307 commit 84cec15

15 files changed

+189
-26
lines changed

lib/java/com/google/android/material/timepicker/ChipTextInputComboView.java

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import com.google.android.material.R;
2020

2121
import android.content.Context;
22+
import androidx.core.view.AccessibilityDelegateCompat;
23+
import androidx.core.view.ViewCompat;
2224
import android.text.Editable;
2325
import android.text.InputFilter;
2426
import android.text.TextUtils;
@@ -28,6 +30,7 @@
2830
import android.widget.Checkable;
2931
import android.widget.EditText;
3032
import android.widget.FrameLayout;
33+
import android.widget.TextView;
3134
import androidx.annotation.NonNull;
3235
import androidx.annotation.Nullable;
3336
import com.google.android.material.chip.Chip;
@@ -45,6 +48,7 @@ class ChipTextInputComboView extends FrameLayout implements Checkable {
4548
private final TextInputLayout textInputLayout;
4649
private final EditText editText;
4750
private TextWatcher watcher;
51+
private TextView label;
4852

4953
public ChipTextInputComboView(@NonNull Context context) {
5054
this(context, null);
@@ -66,6 +70,7 @@ public ChipTextInputComboView(
6670
editText.addTextChangedListener(watcher);
6771
addView(chip);
6872
addView(textInputLayout);
73+
label = findViewById(R.id.material_label);
6974
}
7075

7176
@Override
@@ -111,11 +116,7 @@ public void setTag(int key, Object tag) {
111116
}
112117

113118
public void setHelperText(CharSequence helperText) {
114-
textInputLayout.setHelperText(helperText);
115-
}
116-
117-
public void setError(String error) {
118-
textInputLayout.setError(error);
119+
label.setText(helperText);
119120
}
120121

121122
public void setCursorVisible(boolean visible) {
@@ -133,6 +134,10 @@ public TextInputLayout getTextInput() {
133134
return textInputLayout;
134135
}
135136

137+
public void setChipDelegate(AccessibilityDelegateCompat clickActionDelegate) {
138+
ViewCompat.setAccessibilityDelegate(chip, clickActionDelegate);
139+
}
140+
136141
private class HintSetterTextWatcher extends TextWatcherAdapter {
137142

138143
private static final String DEFAULT_HINT = "00";
@@ -141,6 +146,8 @@ public void afterTextChanged(Editable editable) {
141146
if (TextUtils.isEmpty(editable)) {
142147
setText(DEFAULT_HINT);
143148
return;
149+
} else {
150+
textInputLayout.getEditText().setHint(null);
144151
}
145152

146153
chip.setText(editable.toString());
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright (C) 2020 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.google.android.material.timepicker;
18+
19+
import android.content.Context;
20+
import androidx.core.view.AccessibilityDelegateCompat;
21+
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
22+
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat;
23+
import android.view.View;
24+
25+
class ClickActionDelegate extends AccessibilityDelegateCompat {
26+
private final AccessibilityActionCompat clickAction;
27+
28+
public ClickActionDelegate(Context context, int resId) {
29+
clickAction =
30+
new AccessibilityActionCompat(
31+
AccessibilityNodeInfoCompat.ACTION_CLICK, context.getString(resId));
32+
}
33+
34+
@Override
35+
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
36+
super.onInitializeAccessibilityNodeInfo(host, info);
37+
info.addAction(clickAction);
38+
}
39+
}

lib/java/com/google/android/material/timepicker/ClockFaceView.java

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import com.google.android.material.R;
2020

21+
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionInfoCompat.SELECTION_MODE_SINGLE;
2122
import static java.lang.Math.abs;
2223
import static java.lang.Math.max;
2324

@@ -30,16 +31,24 @@
3031
import android.graphics.Rect;
3132
import android.graphics.RectF;
3233
import android.graphics.Shader.TileMode;
34+
import androidx.core.view.AccessibilityDelegateCompat;
35+
import androidx.core.view.ViewCompat;
36+
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
37+
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionInfoCompat;
38+
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionItemInfoCompat;
3339
import androidx.appcompat.content.res.AppCompatResources;
3440
import android.util.AttributeSet;
3541
import android.util.SparseArray;
3642
import android.view.LayoutInflater;
43+
import android.view.View;
3744
import android.view.ViewTreeObserver.OnPreDrawListener;
45+
import android.view.accessibility.AccessibilityNodeInfo;
3846
import android.widget.TextView;
3947
import androidx.annotation.ColorInt;
4048
import androidx.annotation.FloatRange;
4149
import androidx.annotation.NonNull;
4250
import androidx.annotation.Nullable;
51+
import androidx.annotation.StringRes;
4352
import com.google.android.material.color.MaterialColors;
4453
import com.google.android.material.timepicker.ClockHandView.OnRotateListener;
4554

@@ -58,6 +67,8 @@ public class ClockFaceView extends RadialViewGroup implements OnRotateListener {
5867
private final RectF scratch = new RectF();
5968

6069
private final SparseArray<TextView> textViewPool = new SparseArray<>();
70+
private final AccessibilityDelegateCompat valueAccessibilityDelegate;
71+
6172
private final int[] gradientColors;
6273
private final float[] gradientPositions = new float[] {0f, 0.9f, 1f};
6374
private final int clockHandPadding;
@@ -112,19 +123,42 @@ public boolean onPreDraw() {
112123
return true;
113124
}
114125
});
126+
127+
setFocusable(true);
115128
a.recycle();
129+
valueAccessibilityDelegate =
130+
new AccessibilityDelegateCompat() {
131+
@Override
132+
public void onInitializeAccessibilityNodeInfo(
133+
View host, @NonNull AccessibilityNodeInfoCompat info) {
134+
super.onInitializeAccessibilityNodeInfo(host, info);
135+
int index = (int) host.getTag(R.id.material_value_index);
136+
if (index > 0) {
137+
info.setTraversalAfter(textViewPool.get(index - 1));
138+
}
139+
140+
info.setCollectionItemInfo(
141+
CollectionItemInfoCompat.obtain(
142+
/* rowIndex= */ 0,
143+
/* rowSpan= */ 1,
144+
/* columnIndex =*/ index,
145+
/* columnSpan= */ 1,
146+
/* heading= */ false,
147+
/* selected= */ host.isSelected()));
148+
}
149+
};
116150
}
117151

118152
/**
119153
* Sets the list of values that will be shown in the clock face. The first value will be shown in
120154
* the 12 O'Clock position, subsequent values will be evenly distributed after.
121155
*/
122-
public void setValues(String[] values) {
156+
public void setValues(String[] values, @StringRes int contentDescription) {
123157
this.values = values;
124-
updateTextViews();
158+
updateTextViews(contentDescription);
125159
}
126160

127-
private void updateTextViews() {
161+
private void updateTextViews(@StringRes int contentDescription) {
128162
LayoutInflater inflater = LayoutInflater.from(getContext());
129163
for (int i = 0; i < max(values.length, textViewPool.size()); ++i) {
130164
TextView textView = textViewPool.get(i);
@@ -140,11 +174,28 @@ private void updateTextViews() {
140174
textViewPool.put(i, textView);
141175
}
142176

143-
textView.setTextColor(textColor);
144177
textView.setText(values[i]);
178+
textView.setTag(R.id.material_value_index, i);
179+
ViewCompat.setAccessibilityDelegate(textView, valueAccessibilityDelegate);
180+
181+
textView.setTextColor(textColor);
182+
Resources res = getResources();
183+
textView.setContentDescription(res.getString(contentDescription, values[i]));
145184
}
146185
}
147186

187+
@Override
188+
public void onInitializeAccessibilityNodeInfo(@NonNull AccessibilityNodeInfo info) {
189+
super.onInitializeAccessibilityNodeInfo(info);
190+
AccessibilityNodeInfoCompat infoCompat = AccessibilityNodeInfoCompat.wrap(info);
191+
infoCompat.setCollectionInfo(
192+
CollectionInfoCompat.obtain(
193+
/* rowCount= */ 1,
194+
/* columnCount= */ values.length,
195+
/* hierarchical= */ false,
196+
SELECTION_MODE_SINGLE));
197+
}
198+
148199
@Override
149200
public void setRadius(int radius) {
150201
if (radius != getRadius()) {
@@ -169,7 +220,9 @@ private void findIntersectingTextView() {
169220
for (int i = 0; i < textViewPool.size(); ++i) {
170221
TextView tv = textViewPool.get(i);
171222
tv.getDrawingRect(textViewRect);
223+
textViewRect.offset(tv.getPaddingLeft(), getPaddingTop());
172224
offsetDescendantRectToMyCoords(tv, textViewRect);
225+
173226
scratch.set(textViewRect);
174227
RadialGradient radialGradient = getGradientForTextView(selectorBox, scratch);
175228
tv.getPaint().setShader(radialGradient);

lib/java/com/google/android/material/timepicker/ClockHandView.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import android.graphics.Canvas;
3131
import android.graphics.Paint;
3232
import android.graphics.RectF;
33+
import androidx.core.view.ViewCompat;
3334
import android.util.AttributeSet;
3435
import android.util.Pair;
3536
import android.view.MotionEvent;
@@ -107,6 +108,7 @@ public ClockHandView(Context context, @Nullable AttributeSet attrs, int defStyle
107108
setHandRotation(0);
108109

109110
scaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
111+
ViewCompat.setImportantForAccessibility(this, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO);
110112
a.recycle();
111113
}
112114

@@ -265,8 +267,8 @@ public void setCircleRadius(@Dimension int circleRadius) {
265267
invalidate();
266268
}
267269

268-
@SuppressLint("ClickableViewAccessibility")
269270
@Override
271+
@SuppressLint("ClickableViewAccessibility")
270272
public boolean onTouchEvent(MotionEvent event) {
271273
int action = event.getActionMasked();
272274
boolean forceSelection = false;

lib/java/com/google/android/material/timepicker/TimePickerClockPresenter.java

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,20 @@
1616

1717
package com.google.android.material.timepicker;
1818

19+
import com.google.android.material.R;
20+
1921
import static android.view.HapticFeedbackConstants.CLOCK_TICK;
2022
import static android.view.HapticFeedbackConstants.VIRTUAL_KEY;
2123
import static android.view.View.GONE;
24+
import static androidx.core.content.ContextCompat.getSystemService;
2225
import static com.google.android.material.timepicker.TimeFormat.CLOCK_12H;
2326
import static com.google.android.material.timepicker.TimeFormat.CLOCK_24H;
2427
import static java.util.Calendar.MINUTE;
2528

2629
import android.os.Build.VERSION;
2730
import android.os.Build.VERSION_CODES;
2831
import android.view.View;
32+
import android.view.accessibility.AccessibilityManager;
2933
import com.google.android.material.timepicker.ClockHandView.OnActionUpListener;
3034
import com.google.android.material.timepicker.ClockHandView.OnRotateListener;
3135
import com.google.android.material.timepicker.TimePickerControls.ActiveSelection;
@@ -154,9 +158,15 @@ void setSelection(@ActiveSelection int selection, boolean animate) {
154158
// Don't animate hours since we are going to auto switch to the minute selection.
155159
timePickerView.setAnimateOnTouchUp(isMinute);
156160
time.selection = selection;
157-
timePickerView.setValues(isMinute ? MINUTE_CLOCK_VALUES : getHourClockValues());
161+
timePickerView.setValues(
162+
isMinute ? MINUTE_CLOCK_VALUES : getHourClockValues(),
163+
isMinute ? R.string.material_minute_suffix : R.string.material_hour_suffix);
158164
timePickerView.setHandRotation(isMinute ? minuteRotation : hourRotation, animate);
159165
timePickerView.setActiveSelection(selection);
166+
timePickerView.setMinuteHourDelegate(
167+
new ClickActionDelegate(timePickerView.getContext(), R.string.material_hour_selection));
168+
timePickerView.setHourClickDelegate(
169+
new ClickActionDelegate(timePickerView.getContext(), R.string.material_minute_selection));
160170
}
161171

162172
@Override
@@ -169,7 +179,13 @@ public void onActionUp(float rotation, boolean moveInEventStream) {
169179
// Snap to the closest hour before animating to the position the minute selection is on.
170180
timePickerView.setHandRotation(hourRotation, /* animate= */ false);
171181
// Automatically move to minutes once the user finishes choosing the hour.
172-
setSelection(MINUTE, /* animate= */ true);
182+
183+
AccessibilityManager am =
184+
getSystemService(timePickerView.getContext(), AccessibilityManager.class);
185+
boolean isExploreByTouchEnabled = am.isTouchExplorationEnabled();
186+
if (!isExploreByTouchEnabled) {
187+
setSelection(MINUTE, /* animate= */ true);
188+
}
173189
} else {
174190
int rotationInt = Math.round(rotation);
175191
if (!moveInEventStream) {

lib/java/com/google/android/material/timepicker/TimePickerControls.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import androidx.annotation.IntDef;
2020
import androidx.annotation.IntRange;
21+
import androidx.annotation.StringRes;
2122
import java.lang.annotation.Retention;
2223
import java.lang.annotation.RetentionPolicy;
2324
import java.util.Calendar;
@@ -46,7 +47,7 @@ interface TimePickerControls {
4647
void setActiveSelection(@ActiveSelection int selection);
4748

4849
/** Set the values in the clock face. */
49-
void setValues(String[] clockValues);
50+
void setValues(String[] clockValues, @StringRes int contentDescription);
5051

5152
void setHandRotation(float rotation);
5253
}

lib/java/com/google/android/material/timepicker/TimePickerTextInputPresenter.java

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828

2929
import android.content.Context;
3030
import android.content.res.Resources;
31+
import android.os.Build.VERSION;
32+
import android.os.Build.VERSION_CODES;
3133
import android.text.Editable;
3234
import android.text.TextUtils;
3335
import android.text.TextWatcher;
@@ -36,6 +38,7 @@
3638
import android.view.inputmethod.InputMethodManager;
3739
import android.widget.EditText;
3840
import android.widget.LinearLayout;
41+
import android.widget.TextView;
3942
import com.google.android.material.button.MaterialButtonToggleGroup;
4043
import com.google.android.material.button.MaterialButtonToggleGroup.OnButtonCheckedListener;
4144
import com.google.android.material.internal.TextWatcherAdapter;
@@ -89,12 +92,15 @@ public void afterTextChanged(Editable s) {
8992
public TimePickerTextInputPresenter(LinearLayout timePickerView, TimeModel time) {
9093
this.timePickerView = timePickerView;
9194
this.time = time;
92-
9395
Resources res = timePickerView.getResources();
9496
minuteTextInput = timePickerView.findViewById(R.id.material_minute_text_input);
9597
hourTextInput = timePickerView.findViewById(R.id.material_hour_text_input);
96-
hourTextInput.setHelperText(res.getString(R.string.material_timepicker_hour));
97-
minuteTextInput.setHelperText(res.getString(R.string.material_timepicker_minute));
98+
99+
TextView minuteLabel = minuteTextInput.findViewById(R.id.material_label);
100+
TextView hourLabel = hourTextInput.findViewById(R.id.material_label);
101+
102+
minuteLabel.setText(res.getString(R.string.material_timepicker_minute));
103+
hourLabel.setText(res.getString(R.string.material_timepicker_hour));
98104
minuteTextInput.setTag(R.id.selection_type, MINUTE);
99105
hourTextInput.setTag(R.id.selection_type, HOUR);
100106

@@ -117,7 +123,17 @@ public void onClick(View v) {
117123

118124
hourEditText = hourTextInput.getTextInput().getEditText();
119125
minuteEditText = minuteTextInput.getTextInput().getEditText();
126+
127+
if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) {
128+
minuteLabel.setLabelFor(minuteEditText.getId());
129+
hourLabel.setLabelFor(hourEditText.getId());
130+
}
131+
120132
controller = new TimePickerTextInputKeyController(hourTextInput, minuteTextInput, time);
133+
hourTextInput.setChipDelegate(
134+
new ClickActionDelegate(timePickerView.getContext(), R.string.material_hour_selection));
135+
minuteTextInput.setChipDelegate(
136+
new ClickActionDelegate(timePickerView.getContext(), R.string.material_minute_selection));
121137

122138
initialize();
123139
}

0 commit comments

Comments
 (0)