Skip to content

Commit 089372f

Browse files
Material Design Teamdsn5ft
authored andcommitted
[TimePicker][A11y] Add keyboard support to clock input mode
Enables focus on the `ClockFaceView` and implements `onKeyDown` to allow users to navigate between clock values using DPAD keys. DPAD_RIGHT/UP increments the selected value, while DPAD_LEFT/DOWN decrements it. PiperOrigin-RevId: 796951982
1 parent 7818901 commit 089372f

File tree

2 files changed

+85
-2
lines changed

2 files changed

+85
-2
lines changed

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

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import android.content.res.ColorStateList;
3030
import android.content.res.Resources;
3131
import android.content.res.TypedArray;
32+
import android.graphics.Outline;
3233
import android.graphics.RadialGradient;
3334
import android.graphics.Rect;
3435
import android.graphics.RectF;
@@ -39,9 +40,11 @@
3940
import android.util.AttributeSet;
4041
import android.util.DisplayMetrics;
4142
import android.util.SparseArray;
43+
import android.view.KeyEvent;
4244
import android.view.LayoutInflater;
4345
import android.view.MotionEvent;
4446
import android.view.View;
47+
import android.view.ViewOutlineProvider;
4548
import android.view.ViewTreeObserver.OnPreDrawListener;
4649
import android.view.accessibility.AccessibilityNodeInfo;
4750
import android.widget.TextView;
@@ -92,6 +95,8 @@ class ClockFaceView extends RadialViewGroup implements OnRotateListener {
9295

9396
private final ColorStateList textColor;
9497

98+
private OnEnterKeyPressedListener onEnterKeyPressedListener;
99+
95100
public ClockFaceView(@NonNull Context context) {
96101
this(context, null);
97102
}
@@ -150,8 +155,18 @@ public boolean onPreDraw() {
150155
}
151156
});
152157

153-
setFocusable(false);
154158
a.recycle();
159+
160+
setOutlineProvider(
161+
new ViewOutlineProvider() {
162+
@Override
163+
public void getOutline(View view, Outline outline) {
164+
outline.setOval(0, 0, view.getWidth(), view.getHeight());
165+
}
166+
});
167+
setFocusable(true);
168+
setClipToOutline(true);
169+
155170
valueAccessibilityDelegate =
156171
new AccessibilityDelegateCompat() {
157172
@Override
@@ -167,7 +182,7 @@ public void onInitializeAccessibilityNodeInfo(
167182
CollectionItemInfoCompat.obtain(
168183
/* rowIndex= */ 0,
169184
/* rowSpan= */ 1,
170-
/* columnIndex =*/ index,
185+
/* columnIndex= */ index,
171186
/* columnSpan= */ 1,
172187
/* heading= */ false,
173188
/* selected= */ host.isSelected()));
@@ -375,6 +390,57 @@ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
375390
super.onMeasure(spec, spec);
376391
}
377392

393+
private int getSelectedIndex() {
394+
for (int i = 0; i < textViewPool.size(); i++) {
395+
TextView textView = textViewPool.valueAt(i);
396+
if (textView.isSelected()) {
397+
return (int) textView.getTag(R.id.material_value_index);
398+
}
399+
}
400+
return -1;
401+
}
402+
403+
@Override
404+
public boolean onKeyDown(int keyCode, KeyEvent event) {
405+
int selectedIndex = getSelectedIndex();
406+
if (!isShown() || selectedIndex == -1) {
407+
return super.onKeyDown(keyCode, event);
408+
}
409+
410+
int nextIndex;
411+
switch (keyCode) {
412+
case KeyEvent.KEYCODE_DPAD_RIGHT:
413+
case KeyEvent.KEYCODE_DPAD_UP:
414+
nextIndex = (selectedIndex + 1) % values.length;
415+
break;
416+
case KeyEvent.KEYCODE_DPAD_LEFT:
417+
case KeyEvent.KEYCODE_DPAD_DOWN:
418+
nextIndex = (selectedIndex - 1 + values.length) % values.length;
419+
break;
420+
case KeyEvent.KEYCODE_ENTER:
421+
case KeyEvent.KEYCODE_DPAD_CENTER:
422+
if (onEnterKeyPressedListener != null) {
423+
onEnterKeyPressedListener.onEnterKeyPressed();
424+
}
425+
return true;
426+
default:
427+
return super.onKeyDown(keyCode, event);
428+
}
429+
430+
if (nextIndex != selectedIndex) {
431+
int level = (nextIndex / INITIAL_CAPACITY) + LEVEL_1;
432+
if (level != getCurrentLevel()) {
433+
setCurrentLevel(level);
434+
}
435+
436+
float rotation = (nextIndex % INITIAL_CAPACITY) * (360f / INITIAL_CAPACITY);
437+
setHandRotation(rotation);
438+
return true;
439+
}
440+
441+
return super.onKeyDown(keyCode, event);
442+
}
443+
378444
private static float max3(float a, float b, float c) {
379445
return max(max(a, b), c);
380446
}
@@ -387,4 +453,14 @@ int getCurrentLevel() {
387453
void setCurrentLevel(@Level int level) {
388454
clockHandView.setCurrentLevel(level);
389455
}
456+
457+
public void setOnEnterKeyPressedListener(OnEnterKeyPressedListener onEnterKeyPressedListener) {
458+
this.onEnterKeyPressedListener = onEnterKeyPressedListener;
459+
}
460+
461+
/** Listener interface for enter key press events on the clock face. */
462+
interface OnEnterKeyPressedListener {
463+
/** Called when the enter key is pressed. */
464+
void onEnterKeyPressed();
465+
}
390466
}

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,13 @@ public TimePickerView(Context context, @Nullable AttributeSet attrs, int defStyl
123123
hourView = findViewById(R.id.material_hour_tv);
124124
clockHandView = findViewById(R.id.material_clock_hand);
125125

126+
clockFace.setOnEnterKeyPressedListener(
127+
() -> {
128+
if (hourView.isChecked() && onSelectionChangeListener != null) {
129+
onSelectionChangeListener.onSelectionChanged(MINUTE);
130+
}
131+
});
132+
126133
setupDoubleTap();
127134

128135
setUpDisplay();

0 commit comments

Comments
 (0)