From 66d26993a2f09b0f0fe53c34d78b04b26a0a7428 Mon Sep 17 00:00:00 2001 From: Arnold Loubriat Date: Sun, 20 Sep 2026 16:17:22 +0200 Subject: [PATCH] fix: Use an integer range type on Android when the range is integral TalkBack reads a `RANGE_TYPE_FLOAT` value verbatim, so a slider at 60 was announced as "60.0" (spoken as "sixty point zero"). Expose `RANGE_TYPE_INT` instead when the current, minimum, and maximum values and the step, if any, are all integral, which matches what the framework's own `SeekBar` does. --- adapters/android/src/node.rs | 77 ++++++++++++++++++++++-------------- adapters/android/src/util.rs | 1 + 2 files changed, 48 insertions(+), 30 deletions(-) diff --git a/adapters/android/src/node.rs b/adapters/android/src/node.rs index d2e480843..212142b99 100644 --- a/adapters/android/src/node.rs +++ b/adapters/android/src/node.rs @@ -70,6 +70,29 @@ impl NodeWrapper<'_> { self.0.is_selected().unwrap_or(false) } + /// Returns the current, minimum, and maximum values to expose + /// through `RangeInfo`, if this node has a numeric value + /// but no textual one. + fn range(&self) -> Option<(f64, f64, f64)> { + if self.0.data().value().is_some() { + return None; + } + let current = self.0.numeric_value()?; + let min = self.0.min_numeric_value()?; + let max = self.0.max_numeric_value()?; + Some((current, min, max)) + } + + fn range_type(&self, current: f64, min: f64, max: f64) -> jint { + let is_integral = |value: f64| value.fract() == 0.0; + let step_is_integral = self.0.numeric_value_step().is_none_or(is_integral); + if is_integral(current) && is_integral(min) && is_integral(max) && step_is_integral { + RANGE_TYPE_INT + } else { + RANGE_TYPE_FLOAT + } + } + pub(crate) fn content_description(&self) -> Option { if self.0.label_comes_from_value() { self.0.value() @@ -387,38 +410,32 @@ impl NodeWrapper<'_> { add_action(env, node_info, ACTION_SCROLL_FORWARD); } - if self.0.data().value().is_none() { - if let (Some(current), Some(min), Some(max)) = ( - self.0.numeric_value(), - self.0.min_numeric_value(), - self.0.max_numeric_value(), - ) { - let range_info_class = env - .find_class("android/view/accessibility/AccessibilityNodeInfo$RangeInfo") - .unwrap(); - let range_info = env - .call_static_method( - &range_info_class, - "obtain", - "(IFFF)Landroid/view/accessibility/AccessibilityNodeInfo$RangeInfo;", - &[ - RANGE_TYPE_FLOAT.into(), - (min as f32).into(), - (max as f32).into(), - (current as f32).into(), - ], - ) - .unwrap() - .l() - .unwrap(); - env.call_method( - node_info, - "setRangeInfo", - "(Landroid/view/accessibility/AccessibilityNodeInfo$RangeInfo;)V", - &[(&range_info).into()], + if let Some((current, min, max)) = self.range() { + let range_info_class = env + .find_class("android/view/accessibility/AccessibilityNodeInfo$RangeInfo") + .unwrap(); + let range_info = env + .call_static_method( + &range_info_class, + "obtain", + "(IFFF)Landroid/view/accessibility/AccessibilityNodeInfo$RangeInfo;", + &[ + self.range_type(current, min, max).into(), + (min as f32).into(), + (max as f32).into(), + (current as f32).into(), + ], ) + .unwrap() + .l() .unwrap(); - } + env.call_method( + node_info, + "setRangeInfo", + "(Landroid/view/accessibility/AccessibilityNodeInfo$RangeInfo;)V", + &[(&range_info).into()], + ) + .unwrap(); } let live = match self.0.live() { diff --git a/adapters/android/src/util.rs b/adapters/android/src/util.rs index 61b0022ec..257b5d38c 100644 --- a/adapters/android/src/util.rs +++ b/adapters/android/src/util.rs @@ -61,6 +61,7 @@ pub(crate) const MOVEMENT_GRANULARITY_WORD: jint = 1 << 1; pub(crate) const MOVEMENT_GRANULARITY_LINE: jint = 1 << 2; pub(crate) const MOVEMENT_GRANULARITY_PARAGRAPH: jint = 1 << 3; +pub(crate) const RANGE_TYPE_INT: jint = 0; pub(crate) const RANGE_TYPE_FLOAT: jint = 1; #[derive(Debug, Default)]