fix: 收窄颜色方向识别范围 (#264)

This commit is contained in:
chengma
2026-08-18 11:43:09 +08:00
parent be73e8d827
commit 2e40bc67ef
2 changed files with 93 additions and 7 deletions
+59 -4
View File
@@ -194,6 +194,45 @@ def _heading_bounds(root: ET.Element) -> Optional[Bounds]:
return fuzzy_match or package_fallback
def _scrollable_region_node(
root: ET.Element,
region: Bounds,
) -> Optional[ET.Element]:
"""返回边界与目标区域一致的最内层可滚动容器。"""
parents = _parent_map(root)
candidates = [
node
for node in root.iter("node")
if node.get("scrollable") == "true"
and _is_available(node)
and _parse_bounds(node.get("bounds", "")) == region
]
if not candidates:
return None
def depth(node: ET.Element) -> int:
result = 0
current = parents.get(node)
while current is not None:
result += 1
current = parents.get(current)
return result
return max(candidates, key=depth)
def _color_heading_in_region(root: ET.Element, region: Bounds) -> bool:
"""只在当前颜色滚动容器内寻找标题,忽略背后商品页文字。"""
container = _scrollable_region_node(root, region)
if container is not None:
return _heading_bounds(container) is not None
heading = _heading_bounds(root)
return heading is not None and _center_in(heading, region)
def _target_scrollable_ancestor(
root: ET.Element,
target: str,
@@ -237,6 +276,19 @@ def _horizontal_color_region(
width = bounds[2] - bounds[0]
height = bounds[3] - bounds[1]
contained_heading = _heading_bounds(node)
if (
contained_heading is not None
and screen is not None
and _center_in(contained_heading, bounds)
and width >= screen[2] * 0.60
):
# 纵向长列表的颜色标题位于滚动容器内部;页面背后的同名
# 文本不能参与定位。面积较小的内层容器优先。
score = 50_000.0 - width * height / 1_000_000
candidates.append((score, bounds))
continue
if heading is not None and screen is not None:
distance_below_heading = bounds[1] - heading[3]
@@ -659,14 +711,17 @@ def _color_scroll_direction(
) -> Optional[str]:
"""判断颜色容器的主要滚动方向;证据不足时不猜测。"""
container = _scrollable_region_node(root, region)
nodes = container.iter("node") if container is not None else root.iter("node")
option_bounds: list[Bounds] = []
for node in root.iter("node"):
for node in nodes:
if node.get("clickable") != "true" or not _is_available(node):
continue
bounds = _parse_bounds(node.get("bounds", ""))
if bounds is None or not _center_in(bounds, region):
continue
if _intersection_area(bounds, region) <= 0:
area = (bounds[2] - bounds[0]) * (bounds[3] - bounds[1])
if area <= 0 or _intersection_area(bounds, region) != area:
continue
option_bounds.append(bounds)
@@ -784,7 +839,7 @@ def select_color(
if direction == "vertical":
# Adapter 正常会先把颜色标题恢复到顶部;独立调用若标题已经
# 滚出视口,则先有界回到起点,随后只向下扫描一次。
if _heading_bounds(root) is None:
if not _color_heading_in_region(root, region):
previous_signature = _visible_signature(root, region)
unchanged_count = 0
for _ in range(max_swipes):
@@ -792,7 +847,7 @@ def select_color(
time.sleep(action_delay)
root = _parse_xml(device.dump_hierarchy())
region = _target_scrollable_ancestor(root, target_color) or region
if _heading_bounds(root) is not None:
if _color_heading_in_region(root, region):
break
signature = _visible_signature(root, region)
unchanged_count = (
+34 -3
View File
@@ -60,10 +60,12 @@ class ScrollColorDevice:
direction: str,
target: str = "目标白色",
include_target: bool = True,
polluted: bool = False,
) -> None:
self.direction = direction
self.target = target
self.include_target = include_target
self.polluted = polluted
self.page = 0
self.selected = False
self.clicks = []
@@ -104,14 +106,23 @@ class ScrollColorDevice:
container = ''.join(options)
summary = f"已选:{self.target}" if self.selected else "请选择:颜色 尺码"
return f'''<hierarchy>
<node bounds="[0,0][1080,2340]">
<node class="android.widget.ScrollView" scrollable="true"
panel = f'''<node class="android.widget.ScrollView" scrollable="true"
bounds="[0,945][1080,2079]">
<node text="{summary}" bounds="[36,950][800,1000]" />
<node text="颜色 (14)" bounds="[36,971][216,1024]" />
{container}
</node>'''
if self.polluted:
panel = f'''<node clickable="true" bounds="[0,120][1080,2328]">
<node clickable="true" bounds="[0,366][1080,2328]">
{panel}
</node>
</node>
<node text="悬浮操作" clickable="true"
bounds="[936,980][1080,1124]" />'''
return f'''<hierarchy>
<node bounds="[0,0][1080,2340]">
{panel}
</node>
</hierarchy>'''
@@ -332,6 +343,26 @@ class StaticColorSelectionTest(unittest.TestCase):
)
)
def test_external_clickables_do_not_pollute_vertical_direction(self):
device = ScrollColorDevice("vertical", polluted=True)
selected = select_color(
device,
device.dump_hierarchy(),
device.target,
action_delay=0.001,
)
self.assertTrue(selected)
self.assertEqual(len(device.clicks), 1)
self.assertTrue(device.swipes)
self.assertTrue(
all(
abs(y2 - y1) > abs(x2 - x1)
for x1, y1, x2, y2, _duration in device.swipes
)
)
def test_horizontal_color_list_keeps_horizontal_scan(self):
device = ScrollColorDevice("horizontal")