Skip to content

Commit 62f193a

Browse files
authored
feat: Scrolling on Android (#586)
1 parent e639c0e commit 62f193a

File tree

4 files changed

+170
-6
lines changed

4 files changed

+170
-6
lines changed

platforms/android/src/adapter.rs

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
use accesskit::{
1212
Action, ActionData, ActionHandler, ActionRequest, ActivationHandler, Node as NodeData, NodeId,
13-
Point, Role, TextSelection, Tree as TreeData, TreeUpdate,
13+
Orientation, Point, Role, ScrollUnit, TextSelection, Tree as TreeData, TreeUpdate,
1414
};
1515
use accesskit_consumer::{FilterResult, Node, TextPosition, Tree, TreeChangeHandler};
1616
use jni::{
@@ -21,7 +21,7 @@ use jni::{
2121

2222
use crate::{
2323
action::{PlatformAction, PlatformActionInner},
24-
event::{QueuedEvent, QueuedEvents},
24+
event::{QueuedEvent, QueuedEvents, ScrollDimension},
2525
filters::filter,
2626
node::{add_action, NodeWrapper},
2727
util::*,
@@ -113,6 +113,34 @@ impl TreeChangeHandler for AdapterChangeHandler<'_> {
113113
}
114114
}
115115
}
116+
let scroll_x = old_wrapper
117+
.scroll_x()
118+
.zip(new_wrapper.scroll_x())
119+
.and_then(|(old, new)| {
120+
(new != old).then(|| ScrollDimension {
121+
current: new,
122+
delta: new - old,
123+
max: new_wrapper.max_scroll_x(),
124+
})
125+
});
126+
let scroll_y = old_wrapper
127+
.scroll_y()
128+
.zip(new_wrapper.scroll_y())
129+
.and_then(|(old, new)| {
130+
(new != old).then(|| ScrollDimension {
131+
current: new,
132+
delta: new - old,
133+
max: new_wrapper.max_scroll_y(),
134+
})
135+
});
136+
if scroll_x.is_some() || scroll_y.is_some() {
137+
let id = self.node_id_map.get_or_create_java_id(new_node);
138+
self.events.push(QueuedEvent::Scrolled {
139+
virtual_view_id: id,
140+
x: scroll_x,
141+
y: scroll_y,
142+
});
143+
}
116144
// TODO: other events
117145
}
118146

@@ -373,6 +401,41 @@ impl Adapter {
373401
target,
374402
data: None,
375403
},
404+
ACTION_SCROLL_BACKWARD | ACTION_SCROLL_FORWARD => ActionRequest {
405+
action: {
406+
let node = tree_state.node_by_id(target).unwrap();
407+
if let Some(orientation) = node.orientation() {
408+
match orientation {
409+
Orientation::Horizontal => {
410+
if action == ACTION_SCROLL_BACKWARD {
411+
Action::ScrollLeft
412+
} else {
413+
Action::ScrollRight
414+
}
415+
}
416+
Orientation::Vertical => {
417+
if action == ACTION_SCROLL_BACKWARD {
418+
Action::ScrollUp
419+
} else {
420+
Action::ScrollDown
421+
}
422+
}
423+
}
424+
} else if action == ACTION_SCROLL_BACKWARD {
425+
if node.supports_action(Action::ScrollUp) {
426+
Action::ScrollUp
427+
} else {
428+
Action::ScrollLeft
429+
}
430+
} else if node.supports_action(Action::ScrollDown) {
431+
Action::ScrollDown
432+
} else {
433+
Action::ScrollRight
434+
}
435+
},
436+
target,
437+
data: Some(ActionData::ScrollUnit(ScrollUnit::Page)),
438+
},
376439
ACTION_ACCESSIBILITY_FOCUS => {
377440
self.accessibility_focus = Some(virtual_view_id);
378441
events.push(QueuedEvent::InvalidateHost);

platforms/android/src/event.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,45 @@ fn send_text_traversed(
227227
send_completed_event(env, host, event);
228228
}
229229

230+
pub(crate) struct ScrollDimension {
231+
pub(crate) current: jint,
232+
pub(crate) delta: jint,
233+
pub(crate) max: Option<jint>,
234+
}
235+
236+
fn send_scrolled(
237+
env: &mut JNIEnv,
238+
host: &JObject,
239+
virtual_view_id: jint,
240+
x: Option<ScrollDimension>,
241+
y: Option<ScrollDimension>,
242+
) {
243+
let event = new_event(env, host, virtual_view_id, EVENT_VIEW_SCROLLED);
244+
env.call_method(&event, "setScrollable", "(Z)V", &[true.into()])
245+
.unwrap();
246+
if let Some(x) = x {
247+
env.call_method(&event, "setScrollX", "(I)V", &[x.current.into()])
248+
.unwrap();
249+
env.call_method(&event, "setScrollDeltaX", "(I)V", &[x.delta.into()])
250+
.unwrap();
251+
if let Some(max) = x.max {
252+
env.call_method(&event, "setMaxScrollX", "(I)V", &[max.into()])
253+
.unwrap();
254+
}
255+
}
256+
if let Some(y) = y {
257+
env.call_method(&event, "setScrollY", "(I)V", &[y.current.into()])
258+
.unwrap();
259+
env.call_method(&event, "setScrollDeltaY", "(I)V", &[y.delta.into()])
260+
.unwrap();
261+
if let Some(max) = y.max {
262+
env.call_method(&event, "setMaxScrollY", "(I)V", &[max.into()])
263+
.unwrap();
264+
}
265+
}
266+
send_completed_event(env, host, event);
267+
}
268+
230269
pub(crate) enum QueuedEvent {
231270
Simple {
232271
virtual_view_id: jint,
@@ -253,6 +292,11 @@ pub(crate) enum QueuedEvent {
253292
segment_start: jint,
254293
segment_end: jint,
255294
},
295+
Scrolled {
296+
virtual_view_id: jint,
297+
x: Option<ScrollDimension>,
298+
y: Option<ScrollDimension>,
299+
},
256300
InvalidateHost,
257301
}
258302

@@ -314,6 +358,13 @@ impl QueuedEvents {
314358
segment_end,
315359
);
316360
}
361+
QueuedEvent::Scrolled {
362+
virtual_view_id,
363+
x,
364+
y,
365+
} => {
366+
send_scrolled(env, host, virtual_view_id, x, y);
367+
}
317368
QueuedEvent::InvalidateHost => {
318369
env.call_method(host, "invalidate", "()V", &[]).unwrap();
319370
}

platforms/android/src/node.rs

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
// Use of this source code is governed by a BSD-style license that can be
99
// found in the LICENSE.chromium file.
1010

11-
use accesskit::{Live, Role, Toggled};
11+
use accesskit::{Action, Live, Role, Toggled};
1212
use accesskit_consumer::Node;
1313
use jni::{objects::JObject, sys::jint, JNIEnv};
1414

@@ -36,7 +36,7 @@ impl NodeWrapper<'_> {
3636
}
3737

3838
fn is_focusable(&self) -> bool {
39-
self.0.is_focusable()
39+
self.0.is_focusable() && self.0.role() != Role::ScrollView
4040
}
4141

4242
fn is_focused(&self) -> bool {
@@ -59,6 +59,13 @@ impl NodeWrapper<'_> {
5959
}
6060
}
6161

62+
fn is_scrollable(&self) -> bool {
63+
self.0.supports_action(Action::ScrollDown)
64+
|| self.0.supports_action(Action::ScrollLeft)
65+
|| self.0.supports_action(Action::ScrollRight)
66+
|| self.0.supports_action(Action::ScrollUp)
67+
}
68+
6269
fn is_selected(&self) -> bool {
6370
match self.0.role() {
6471
// https://www.w3.org/TR/core-aam-1.1/#mapping_state-property_table
@@ -131,7 +138,9 @@ impl NodeWrapper<'_> {
131138
Role::Meter | Role::ProgressIndicator => "android.widget.ProgressBar",
132139
Role::TabList => "android.widget.TabWidget",
133140
Role::Grid | Role::Table | Role::TreeGrid => "android.widget.GridView",
134-
Role::DescriptionList | Role::List | Role::ListBox => "android.widget.ListView",
141+
Role::DescriptionList | Role::List | Role::ListBox | Role::ScrollView => {
142+
"android.widget.ListView"
143+
}
135144
Role::Dialog => "android.app.Dialog",
136145
Role::RootWebArea => "android.webkit.WebView",
137146
Role::MenuItem | Role::MenuItemCheckBox | Role::MenuItemRadio => {
@@ -142,6 +151,30 @@ impl NodeWrapper<'_> {
142151
}
143152
}
144153

154+
pub(crate) fn scroll_x(&self) -> Option<jint> {
155+
self.0
156+
.scroll_x()
157+
.map(|value| (value - self.0.scroll_x_min().unwrap_or(0.0)) as jint)
158+
}
159+
160+
pub(crate) fn max_scroll_x(&self) -> Option<jint> {
161+
self.0
162+
.scroll_x_max()
163+
.map(|value| (value - self.0.scroll_x_min().unwrap_or(0.0)) as jint)
164+
}
165+
166+
pub(crate) fn scroll_y(&self) -> Option<jint> {
167+
self.0
168+
.scroll_y()
169+
.map(|value| (value - self.0.scroll_y_min().unwrap_or(0.0)) as jint)
170+
}
171+
172+
pub(crate) fn max_scroll_y(&self) -> Option<jint> {
173+
self.0
174+
.scroll_y_max()
175+
.map(|value| (value - self.0.scroll_y_min().unwrap_or(0.0)) as jint)
176+
}
177+
145178
pub(crate) fn populate_node_info(
146179
&self,
147180
env: &mut JNIEnv,
@@ -240,6 +273,13 @@ impl NodeWrapper<'_> {
240273
&[self.is_password().into()],
241274
)
242275
.unwrap();
276+
env.call_method(
277+
node_info,
278+
"setScrollable",
279+
"(Z)V",
280+
&[self.is_scrollable().into()],
281+
)
282+
.unwrap();
243283
env.call_method(
244284
node_info,
245285
"setSelected",
@@ -290,7 +330,7 @@ impl NodeWrapper<'_> {
290330
)
291331
.unwrap();
292332

293-
let can_focus = self.0.is_focusable() && !self.0.is_focused();
333+
let can_focus = self.is_focusable() && !self.0.is_focused();
294334
if self.0.is_clickable() || can_focus {
295335
add_action(env, node_info, ACTION_CLICK);
296336
}
@@ -313,6 +353,13 @@ impl NodeWrapper<'_> {
313353
)
314354
.unwrap();
315355
}
356+
if self.0.supports_action(Action::ScrollLeft) || self.0.supports_action(Action::ScrollUp) {
357+
add_action(env, node_info, ACTION_SCROLL_BACKWARD);
358+
}
359+
if self.0.supports_action(Action::ScrollRight) || self.0.supports_action(Action::ScrollDown)
360+
{
361+
add_action(env, node_info, ACTION_SCROLL_FORWARD);
362+
}
316363

317364
let live = match self.0.live() {
318365
Live::Off => LIVE_REGION_NONE,

platforms/android/src/util.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ pub(crate) const ACTION_ACCESSIBILITY_FOCUS: jint = 1 << 6;
1414
pub(crate) const ACTION_CLEAR_ACCESSIBILITY_FOCUS: jint = 1 << 7;
1515
pub(crate) const ACTION_NEXT_AT_MOVEMENT_GRANULARITY: jint = 1 << 8;
1616
pub(crate) const ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY: jint = 1 << 9;
17+
pub(crate) const ACTION_SCROLL_FORWARD: jint = 1 << 12;
18+
pub(crate) const ACTION_SCROLL_BACKWARD: jint = 1 << 13;
1719
pub(crate) const ACTION_SET_SELECTION: jint = 1 << 17;
1820

1921
pub(crate) const ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT: &str =
@@ -30,6 +32,7 @@ pub(crate) const EVENT_VIEW_FOCUSED: jint = 1 << 3;
3032
pub(crate) const EVENT_VIEW_TEXT_CHANGED: jint = 1 << 4;
3133
pub(crate) const EVENT_VIEW_HOVER_ENTER: jint = 1 << 7;
3234
pub(crate) const EVENT_VIEW_HOVER_EXIT: jint = 1 << 8;
35+
pub(crate) const EVENT_VIEW_SCROLLED: jint = 1 << 12;
3336
pub(crate) const EVENT_VIEW_TEXT_SELECTION_CHANGED: jint = 1 << 13;
3437
pub(crate) const EVENT_VIEW_ACCESSIBILITY_FOCUSED: jint = 1 << 15;
3538
pub(crate) const EVENT_VIEW_ACCESSIBILITY_FOCUS_CLEARED: jint = 1 << 16;

0 commit comments

Comments
 (0)