""" Android View Gesture 扩展组件 — GestureBuilder, GestureLibrary, GestureStore 等 方法签名完全对齐 Android SDK """ from __future__ import annotations import math import time import json import os import threading from typing import Any, Dict, List, Optional, Set, Tuple, Union from .._core import Stub, AndroidOnlyError from ..java.lang import RuntimeException # ═══════════════════════════════════════════════════════════════ # GesturePoint # ═══════════════════════════════════════════════════════════════ class GesturePoint(Stub): """GesturePoint — 手势点""" def __init__(self, x: float = 0.0, y: float = 0.0, pressure: float = 1.0, size: float = 1.0, timestamp: int = 0): super().__init__() self.x = x self.y = y self.pressure = pressure self.size = size self.timestamp = timestamp or int(time.time() * 1000) def getX(self) -> float: return self.x def getY(self) -> float: return self.y def getPressure(self) -> float: return self.pressure def getSize(self) -> float: return self.size def getTimestamp(self) -> int: return self.timestamp def distance(self, other: "GesturePoint") -> float: dx = self.x - other.x dy = self.y - other.y return math.sqrt(dx * dx + dy * dy) def toString(self) -> str: return (f"GesturePoint({self.x:.1f}, {self.y:.1f}, " f"p={self.pressure:.2f})") # ═══════════════════════════════════════════════════════════════ # GestureStroke # ═══════════════════════════════════════════════════════════════ class GestureStroke(Stub): """GestureStroke — 手势笔画""" def __init__(self, points: Optional[List[GesturePoint]] = None): super().__init__() self._points: List[GesturePoint] = list(points) if points else [] self._length: float = -1.0 self._bounding_box: Optional[Tuple[float, float, float, float]] = None self._straightness: Optional[float] = None self._lock = threading.Lock() def getPoints(self) -> List[GesturePoint]: return list(self._points) def getPointCount(self) -> int: return len(self._points) def getLength(self) -> float: with self._lock: if self._length < 0: self._length = self._compute_length() return self._length def _compute_length(self) -> float: length = 0.0 for i in range(1, len(self._points)): length += self._points[i].distance(self._points[i - 1]) return length def getBoundingBox(self) -> Tuple[float, float, float, float]: with self._lock: if self._bounding_box is None: self._bounding_box = self._compute_bounding_box() return self._bounding_box def _compute_bounding_box(self) -> Tuple[float, float, float, float]: if not self._points: return (0.0, 0.0, 0.0, 0.0) min_x = min(p.x for p in self._points) max_x = max(p.x for p in self._points) min_y = min(p.y for p in self._points) max_y = max(p.y for p in self._points) return (min_x, min_y, max_x, max_y) def getStraightness(self) -> float: with self._lock: if self._straightness is None: self._straightness = self._compute_straightness() return self._straightness def _compute_straightness(self) -> float: if len(self._points) < 2: return 1.0 dx = self._points[-1].x - self._points[0].x dy = self._points[-1].y - self._points[0].y straight_dist = math.sqrt(dx * dx + dy * dy) actual_dist = self.getLength() if actual_dist == 0: return 1.0 return straight_dist / actual_dist def computeOrientedBoundingBox(self) -> "OrientedBoundingBox": return OrientedBoundingBox.computeFromStroke(self) def addPoint(self, point: GesturePoint) -> None: with self._lock: self._points.append(point) self._length = -1.0 self._bounding_box = None def clear(self) -> None: with self._lock: self._points.clear() self._length = -1.0 self._bounding_box = None def toString(self) -> str: return f"GestureStroke(points={len(self._points)}, length={self.getLength():.1f})" # ═══════════════════════════════════════════════════════════════ # OrientedBoundingBox # ═══════════════════════════════════════════════════════════════ class OrientedBoundingBox(Stub): """OrientedBoundingBox — 定向边界框""" def __init__(self, center_x: float = 0.0, center_y: float = 0.0, width: float = 0.0, height: float = 0.0, orientation: float = 0.0): super().__init__() self.centerX = center_x self.centerY = center_y self.width = width self.height = height self.orientation = orientation @staticmethod def computeFromStroke(stroke: GestureStroke) -> "OrientedBoundingBox": points = stroke.getPoints() if len(points) < 2: return OrientedBoundingBox() sum_x = sum(p.x for p in points) sum_y = sum(p.y for p in points) n = len(points) center_x = sum_x / n center_y = sum_y / n variance_x = sum((p.x - center_x) ** 2 for p in points) / n variance_y = sum((p.y - center_y) ** 2 for p in points) / n covariance = sum((p.x - center_x) * (p.y - center_y) for p in points) / n if variance_x + variance_y == 0: return OrientedBoundingBox(center_x, center_y, 1, 1, 0) theta = 0.5 * math.atan2(2 * covariance, variance_x - variance_y) cos_t = math.cos(theta) sin_t = math.sin(theta) max_proj1 = max_proj2 = float("-inf") min_proj1 = min_proj2 = float("inf") for p in points: proj1 = (p.x - center_x) * cos_t + (p.y - center_y) * sin_t proj2 = -(p.x - center_x) * sin_t + (p.y - center_y) * cos_t max_proj1 = max(max_proj1, proj1) min_proj1 = min(min_proj1, proj1) max_proj2 = max(max_proj2, proj2) min_proj2 = min(min_proj2, proj2) width = max_proj1 - min_proj1 height = max_proj2 - min_proj2 orientation = math.degrees(theta) return OrientedBoundingBox(center_x, center_y, width, height, orientation) def getCenterX(self) -> float: return self.centerX def getCenterY(self) -> float: return self.centerY def getWidth(self) -> float: return self.width def getHeight(self) -> float: return self.height def getOrientation(self) -> float: return self.orientation def toString(self) -> str: return (f"OrientedBoundingBox(center=({self.centerX:.1f},{self.centerY:.1f}), " f"size=({self.width:.1f}x{self.height:.1f}), " f"angle={self.orientation:.1f})") # ═══════════════════════════════════════════════════════════════ # GestureUtils # ═══════════════════════════════════════════════════════════════ class GestureUtils(Stub): """GestureUtils — 手势工具""" MINIMUM_STROKE_LENGTH = 50.0 MAXIMUM_STROKE_LENGTH = 10000.0 @staticmethod def spatialSampling(gesture: "Gesture", num_samples: int = 16) -> List[float]: """空间采样 — 将手势转换为固定长度的向量""" result = [] strokes = gesture.getStrokes() if not strokes: return [0.0] * (num_samples * 2) all_points = [] for stroke in strokes: all_points.extend(stroke.getPoints()) if len(all_points) < 2: return [0.0] * (num_samples * 2) total_length = sum( all_points[i].distance(all_points[i - 1]) for i in range(1, len(all_points)) ) if total_length == 0: return [all_points[0].x, all_points[0].y] * num_samples step = total_length / (num_samples - 1) current_dist = 0.0 target_dist = 0.0 result.append(all_points[0].x) result.append(all_points[0].y) for i in range(1, len(all_points)): seg_length = all_points[i].distance(all_points[i - 1]) if seg_length == 0: continue current_dist += seg_length while target_dist < current_dist and len(result) < num_samples * 2: ratio = 1.0 - (current_dist - target_dist) / seg_length x = all_points[i - 1].x + ratio * (all_points[i].x - all_points[i - 1].x) y = all_points[i - 1].y + ratio * (all_points[i].y - all_points[i - 1].y) result.append(x) result.append(y) target_dist += step while len(result) < num_samples * 2: result.append(all_points[-1].x) result.append(all_points[-1].y) return result[:num_samples * 2] @staticmethod def temporalSampling(gesture: "Gesture", num_samples: int = 16) -> List[float]: return GestureUtils.spatialSampling(gesture, num_samples) @staticmethod def computeOrientedBoundingBox(stroke: GestureStroke) -> OrientedBoundingBox: return OrientedBoundingBox.computeFromStroke(stroke) @staticmethod def normalize(points: List[float]) -> List[float]: if not points: return [] xs = points[0::2] ys = points[1::2] min_x = min(xs) max_x = max(xs) min_y = min(ys) max_y = max(ys) range_x = max_x - min_x if max_x != min_x else 1 range_y = max_y - min_y if max_y != min_y else 1 normalized = [] for i in range(0, len(points), 2): normalized.append((points[i] - min_x) / range_x) normalized.append((points[i + 1] - min_y) / range_y) return normalized @staticmethod def scale(points: List[float], width: float, height: float) -> List[float]: normalized = GestureUtils.normalize(points) scaled = [] for i in range(0, len(normalized), 2): scaled.append(normalized[i] * width) scaled.append(normalized[i + 1] * height) return scaled @staticmethod def totalLength(points: List[float]) -> float: length = 0.0 for i in range(2, len(points), 2): dx = points[i] - points[i - 2] dy = points[i + 1] - points[i - 1] length += math.sqrt(dx * dx + dy * dy) return length @staticmethod def cosineDistance(vector1: List[float], vector2: List[float]) -> float: if len(vector1) != len(vector2) or not vector1: return 1.0 dot = sum(a * b for a, b in zip(vector1, vector2)) norm1 = math.sqrt(sum(a * a for a in vector1)) norm2 = math.sqrt(sum(b * b for b in vector2)) if norm1 == 0 or norm2 == 0: return 1.0 return 1.0 - dot / (norm1 * norm2) @staticmethod def euclideanDistance(vector1: List[float], vector2: List[float]) -> float: if len(vector1) != len(vector2): return float("inf") return math.sqrt(sum((a - b) ** 2 for a, b in zip(vector1, vector2))) # ═══════════════════════════════════════════════════════════════ # Gesture # ═══════════════════════════════════════════════════════════════ class Gesture(Stub): """Gesture — 手势""" def __init__(self): super().__init__() self._strokes: List[GestureStroke] = [] self._id: int = 0 self._name: str = "" self._lock = threading.Lock() def addStroke(self, stroke: GestureStroke) -> None: with self._lock: self._strokes.append(stroke) def getStrokes(self) -> List[GestureStroke]: return list(self._strokes) def getStrokeCount(self) -> int: return len(self._strokes) def getLength(self) -> float: total = 0.0 for stroke in self._strokes: total += stroke.getLength() return total def getBoundingBox(self) -> Tuple[float, float, float, float]: if not self._strokes: return (0, 0, 0, 0) min_x = min(s.getBoundingBox()[0] for s in self._strokes) min_y = min(s.getBoundingBox()[1] for s in self._strokes) max_x = max(s.getBoundingBox()[2] for s in self._strokes) max_y = max(s.getBoundingBox()[3] for s in self._strokes) return (min_x, min_y, max_x, max_y) def getID(self) -> int: return self._id def setID(self, id_val: int) -> None: self._id = id_val def getName(self) -> str: return self._name def setName(self, name: str) -> None: self._name = name def toJson(self) -> str: data = { "id": self._id, "name": self._name, "strokes": [] } for stroke in self._strokes: stroke_data = { "points": [ {"x": p.x, "y": p.y, "pressure": p.pressure, "size": p.size, "timestamp": p.timestamp} for p in stroke.getPoints() ] } data["strokes"].append(stroke_data) return json.dumps(data) @staticmethod def fromJson(json_str: str) -> "Gesture": data = json.loads(json_str) gesture = Gesture() gesture.setID(data.get("id", 0)) gesture.setName(data.get("name", "")) for stroke_data in data.get("strokes", []): points = [ GesturePoint(p["x"], p["y"], p.get("pressure", 1.0), p.get("size", 1.0), p.get("timestamp", 0)) for p in stroke_data.get("points", []) ] gesture.addStroke(GestureStroke(points)) return gesture def toString(self) -> str: return f"Gesture(name={self._name}, strokes={len(self._strokes)})" # ═══════════════════════════════════════════════════════════════ # Prediction # ═══════════════════════════════════════════════════════════════ class Prediction(Stub): """Prediction — 手势识别预测结果""" def __init__(self, name: str = "", score: float = 0.0): super().__init__() self.name = name self.score = score def getName(self) -> str: return self.name def getScore(self) -> float: return self.score def toString(self) -> str: return f"Prediction(name={self.name}, score={self.score:.3f})" # ═══════════════════════════════════════════════════════════════ # GestureLibrary / GestureStore # ═══════════════════════════════════════════════════════════════ class GestureLibrary(Stub): """GestureLibrary — 手势库扩展""" SEQUENCE_MINIMUM_LENGTH = 2 def __init__(self): super().__init__() self._gestures: Dict[str, List[Gesture]] = {} self._lock = threading.Lock() def getGestureEntries(self) -> Set[str]: with self._lock: return set(self._gestures.keys()) def getGestures(self, entry_name: str) -> List[Gesture]: with self._lock: return list(self._gestures.get(entry_name, [])) def recognize(self, gesture: Gesture) -> List[Prediction]: """识别手势,返回预测结果列表""" results: List[Prediction] = [] with self._lock: for name, gestures in self._gestures.items(): for g in gestures: score = self._compute_similarity(gesture, g) results.append(Prediction(name, score)) results.sort(key=lambda p: p.score, reverse=True) return results[:10] def _compute_similarity(self, g1: Gesture, g2: Gesture) -> float: """计算两个手势之间的相似度 (0.0 - 1.0)""" try: v1 = GestureUtils.spatialSampling(g1, 16) v2 = GestureUtils.spatialSampling(g2, 16) distance = GestureUtils.euclideanDistance(v1, v2) max_dist = math.sqrt(len(v1)) similarity = 1.0 - (distance / max_dist) return max(0.0, min(1.0, similarity)) except Exception: return 0.0 def addGesture(self, entry_name: str, gesture: Gesture) -> None: with self._lock: if entry_name not in self._gestures: self._gestures[entry_name] = [] gesture.setName(entry_name) self._gestures[entry_name].append(gesture) def removeGesture(self, entry_name: str, gesture: Gesture) -> None: with self._lock: if entry_name in self._gestures: if gesture in self._gestures[entry_name]: self._gestures[entry_name].remove(gesture) def removeEntry(self, entry_name: str) -> None: with self._lock: self._gestures.pop(entry_name, None) def getGestureCount(self) -> int: count = 0 with self._lock: for gestures in self._gestures.values(): count += len(gestures) return count def getEntryCount(self) -> int: return len(self._gestures) def save(self, path: str) -> bool: try: data = {} with self._lock: for name, gestures in self._gestures.items(): data[name] = [g.toJson() for g in gestures] with open(path, "w") as f: json.dump(data, f) return True except Exception: return False def load(self, path: str) -> bool: try: if not os.path.exists(path): return False with open(path, "r") as f: data = json.load(f) with self._lock: self._gestures.clear() for name, gesture_jsons in data.items(): self._gestures[name] = [ Gesture.fromJson(gj) for gj in gesture_jsons ] return True except Exception: return False def clear(self) -> None: with self._lock: self._gestures.clear() def isReadOnly(self) -> bool: return False def toString(self) -> str: return f"GestureLibrary(entries={len(self._gestures)})" class GestureStore(GestureLibrary): """GestureStore — 手势存储扩展""" SEQUENCE_TYPE_INVARIANT = 1 SEQUENCE_TYPE_SENSITIVE = 2 ORIENTATION_INVARIANT = 1 ORIENTATION_SENSITIVE = 2 def __init__(self): super().__init__() self._sequence_type = self.SEQUENCE_TYPE_SENSITIVE self._orientation_style = self.ORIENTATION_INVARIANT def setSequenceType(self, type_val: int) -> None: self._sequence_type = type_val def getSequenceType(self) -> int: return self._sequence_type def setOrientationStyle(self, style: int) -> None: self._orientation_style = style def getOrientationStyle(self) -> int: return self._orientation_style def toString(self) -> str: return f"GestureStore(entries={self.getEntryCount()})" class GestureBuilder(Stub): """GestureBuilder — 手势构建器""" MIN_LENGTH = 10.0 MAX_LENGTH = 500.0 def __init__(self): super().__init__() self._current_gesture: Optional[Gesture] = None self._current_stroke: Optional[GestureStroke] = None self._current_points: List[GesturePoint] = [] def newGesture(self) -> Gesture: gesture = Gesture() self._current_gesture = gesture self._current_stroke = None self._current_points = [] return gesture def startStroke(self, x: float, y: float, pressure: float = 1.0) -> None: self._current_points = [] point = GesturePoint(x, y, pressure, 1.0, int(time.time() * 1000)) self._current_points.append(point) def moveStroke(self, x: float, y: float, pressure: float = 1.0) -> None: point = GesturePoint(x, y, pressure, 1.0, int(time.time() * 1000)) self._current_points.append(point) def endStroke(self) -> GestureStroke: stroke = GestureStroke(self._current_points) self._current_stroke = stroke if self._current_gesture: self._current_gesture.addStroke(stroke) self._current_points = [] return stroke def cancelStroke(self) -> None: self._current_points = [] self._current_stroke = None def getCurrentGesture(self) -> Optional[Gesture]: return self._current_gesture def getCurrentStroke(self) -> Optional[GestureStroke]: return self._current_stroke def setOrientationStyle(self, style: int) -> None: pass def setSequenceType(self, type_val: int) -> None: pass def toString(self) -> str: return (f"GestureBuilder(gesture={'yes' if self._current_gesture else 'no'}, " f"strokePoints={len(self._current_points)})")