fix: 收窄颜色方向识别范围 (#264)
This commit is contained in:
@@ -194,6 +194,45 @@ def _heading_bounds(root: ET.Element) -> Optional[Bounds]:
|
|||||||
return fuzzy_match or package_fallback
|
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(
|
def _target_scrollable_ancestor(
|
||||||
root: ET.Element,
|
root: ET.Element,
|
||||||
target: str,
|
target: str,
|
||||||
@@ -237,6 +276,19 @@ def _horizontal_color_region(
|
|||||||
|
|
||||||
width = bounds[2] - bounds[0]
|
width = bounds[2] - bounds[0]
|
||||||
height = bounds[3] - bounds[1]
|
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:
|
if heading is not None and screen is not None:
|
||||||
distance_below_heading = bounds[1] - heading[3]
|
distance_below_heading = bounds[1] - heading[3]
|
||||||
@@ -659,14 +711,17 @@ def _color_scroll_direction(
|
|||||||
) -> Optional[str]:
|
) -> 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] = []
|
option_bounds: list[Bounds] = []
|
||||||
for node in root.iter("node"):
|
for node in nodes:
|
||||||
if node.get("clickable") != "true" or not _is_available(node):
|
if node.get("clickable") != "true" or not _is_available(node):
|
||||||
continue
|
continue
|
||||||
bounds = _parse_bounds(node.get("bounds", ""))
|
bounds = _parse_bounds(node.get("bounds", ""))
|
||||||
if bounds is None or not _center_in(bounds, region):
|
if bounds is None or not _center_in(bounds, region):
|
||||||
continue
|
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
|
continue
|
||||||
option_bounds.append(bounds)
|
option_bounds.append(bounds)
|
||||||
|
|
||||||
@@ -784,7 +839,7 @@ def select_color(
|
|||||||
if direction == "vertical":
|
if direction == "vertical":
|
||||||
# Adapter 正常会先把颜色标题恢复到顶部;独立调用若标题已经
|
# Adapter 正常会先把颜色标题恢复到顶部;独立调用若标题已经
|
||||||
# 滚出视口,则先有界回到起点,随后只向下扫描一次。
|
# 滚出视口,则先有界回到起点,随后只向下扫描一次。
|
||||||
if _heading_bounds(root) is None:
|
if not _color_heading_in_region(root, region):
|
||||||
previous_signature = _visible_signature(root, region)
|
previous_signature = _visible_signature(root, region)
|
||||||
unchanged_count = 0
|
unchanged_count = 0
|
||||||
for _ in range(max_swipes):
|
for _ in range(max_swipes):
|
||||||
@@ -792,7 +847,7 @@ def select_color(
|
|||||||
time.sleep(action_delay)
|
time.sleep(action_delay)
|
||||||
root = _parse_xml(device.dump_hierarchy())
|
root = _parse_xml(device.dump_hierarchy())
|
||||||
region = _target_scrollable_ancestor(root, target_color) or region
|
region = _target_scrollable_ancestor(root, target_color) or region
|
||||||
if _heading_bounds(root) is not None:
|
if _color_heading_in_region(root, region):
|
||||||
break
|
break
|
||||||
signature = _visible_signature(root, region)
|
signature = _visible_signature(root, region)
|
||||||
unchanged_count = (
|
unchanged_count = (
|
||||||
|
|||||||
@@ -60,10 +60,12 @@ class ScrollColorDevice:
|
|||||||
direction: str,
|
direction: str,
|
||||||
target: str = "目标白色",
|
target: str = "目标白色",
|
||||||
include_target: bool = True,
|
include_target: bool = True,
|
||||||
|
polluted: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.direction = direction
|
self.direction = direction
|
||||||
self.target = target
|
self.target = target
|
||||||
self.include_target = include_target
|
self.include_target = include_target
|
||||||
|
self.polluted = polluted
|
||||||
self.page = 0
|
self.page = 0
|
||||||
self.selected = False
|
self.selected = False
|
||||||
self.clicks = []
|
self.clicks = []
|
||||||
@@ -104,15 +106,24 @@ class ScrollColorDevice:
|
|||||||
container = ''.join(options)
|
container = ''.join(options)
|
||||||
|
|
||||||
summary = f"已选:{self.target}" if self.selected else "请选择:颜色 尺码"
|
summary = f"已选:{self.target}" if self.selected else "请选择:颜色 尺码"
|
||||||
return f'''<hierarchy>
|
panel = f'''<node class="android.widget.ScrollView" scrollable="true"
|
||||||
<node bounds="[0,0][1080,2340]">
|
|
||||||
<node class="android.widget.ScrollView" scrollable="true"
|
|
||||||
bounds="[0,945][1080,2079]">
|
bounds="[0,945][1080,2079]">
|
||||||
<node text="{summary}" bounds="[36,950][800,1000]" />
|
<node text="{summary}" bounds="[36,950][800,1000]" />
|
||||||
<node text="颜色 (14)" bounds="[36,971][216,1024]" />
|
<node text="颜色 (14)" bounds="[36,971][216,1024]" />
|
||||||
{container}
|
{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>
|
</node>
|
||||||
|
<node text="悬浮操作" clickable="true"
|
||||||
|
bounds="[936,980][1080,1124]" />'''
|
||||||
|
return f'''<hierarchy>
|
||||||
|
<node bounds="[0,0][1080,2340]">
|
||||||
|
{panel}
|
||||||
|
</node>
|
||||||
</hierarchy>'''
|
</hierarchy>'''
|
||||||
|
|
||||||
def click(self, x: int, y: int) -> None:
|
def click(self, x: int, y: int) -> None:
|
||||||
@@ -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):
|
def test_horizontal_color_list_keeps_horizontal_scan(self):
|
||||||
device = ScrollColorDevice("horizontal")
|
device = ScrollColorDevice("horizontal")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user