""" Android Graphics Text 组件 — TextRunShaper, LineBreaker, MeasuredText, PositionedGlyphs, TextLayout 等 方法签名完全对齐 Android SDK (android.graphics.text 包) """ from __future__ import annotations import math from typing import Any, Callable, Dict, List, Optional, Set, Tuple from .._core import Stub, AndroidOnlyError from ..java.lang import RuntimeException # ═══════════════════════════════════════════════════════════════ # PositionedGlyphs # ═══════════════════════════════════════════════════════════════ class PositionedGlyphs(Stub): """PositionedGlyphs — 定位字形""" def __init__(self): super().__init__() self._glyph_ids: List[int] = [] self._positions_x: List[float] = [] self._positions_y: List[float] = [] self._advances_x: List[float] = [] self._advances_y: List[float] = [] self._width: float = 0.0 self._height: float = 0.0 self._ascent: float = 0.0 self._descent: float = 0.0 self._glyph_count: int = 0 def getGlyphCount(self) -> int: return self._glyph_count def getGlyphId(self, index: int) -> int: return self._glyph_ids[index] if 0 <= index < len(self._glyph_ids) else 0 def getGlyphX(self, index: int) -> float: return self._positions_x[index] if 0 <= index < len(self._positions_x) else 0.0 def getGlyphY(self, index: int) -> float: return self._positions_y[index] if 0 <= index < len(self._positions_y) else 0.0 def getGlyphAdvanceX(self, index: int) -> float: return self._advances_x[index] if 0 <= index < len(self._advances_x) else 0.0 def getGlyphAdvanceY(self, index: int) -> float: return self._advances_y[index] if 0 <= index < len(self._advances_y) else 0.0 def getWidth(self) -> float: return self._width def getHeight(self) -> float: return self._height def getAscent(self) -> float: return self._ascent def getDescent(self) -> float: return self._descent def addGlyph(self, glyph_id: int, x: float, y: float, advance_x: float, advance_y: float): self._glyph_ids.append(glyph_id) self._positions_x.append(x) self._positions_y.append(y) self._advances_x.append(advance_x) self._advances_y.append(advance_y) self._glyph_count += 1 def setBounds(self, width: float, height: float): self._width = width self._height = height def setMetrics(self, ascent: float, descent: float): self._ascent = ascent self._descent = descent def clear(self): self._glyph_ids.clear() self._positions_x.clear() self._positions_y.clear() self._advances_x.clear() self._advances_y.clear() self._glyph_count = 0 self._width = 0.0 self._height = 0.0 self._ascent = 0.0 self._descent = 0.0 def toString(self) -> str: return f"PositionedGlyphs(count={self._glyph_count}, w={self._width:.1f}, h={self._height:.1f})" # ═══════════════════════════════════════════════════════════════ # TextRunShaper # ═══════════════════════════════════════════════════════════════ class TextRunShaper(Stub): """TextRunShaper — 文本运行重塑器""" def __init__(self): super().__init__() @staticmethod def shapeTextRun(text: str, start: int, count: int, context_start: int, context_count: int, x_offset: float, y_offset: float, is_rtl: bool, paint: Any) -> PositionedGlyphs: """重塑文本运行""" glyphs = PositionedGlyphs() for i in range(count): if start + i < len(text): char_code = ord(text[start + i]) glyphs.addGlyph(char_code, x_offset + i * 12.0, y_offset, 12.0, 0.0) glyphs.setBounds(count * 12.0, 16.0) glyphs.setMetrics(-12.0, 3.0) return glyphs @staticmethod def shapeTextRunWithFont(text: str, start: int, count: int, context_start: int, context_count: int, x_offset: float, y_offset: float, is_rtl: bool, paint: Any, font: Any) -> PositionedGlyphs: """使用指定字体重塑文本运行""" return TextRunShaper.shapeTextRun(text, start, count, context_start, context_count, x_offset, y_offset, is_rtl, paint) @staticmethod def shapeTextRunOnPath(text: str, start: int, count: int, path: Any, paint: Any) -> PositionedGlyphs: """沿路径重塑文本运行""" glyphs = PositionedGlyphs() for i in range(count): if start + i < len(text): char_code = ord(text[start + i]) glyphs.addGlyph(char_code, i * 12.0, 0.0, 12.0, 0.0) glyphs.setBounds(count * 12.0, 16.0) glyphs.setMetrics(-12.0, 3.0) return glyphs @staticmethod def getGlyphBounds(glyphs: PositionedGlyphs, index: int) -> Tuple[float, float, float, float]: """获取字形边界""" x = glyphs.getGlyphX(index) y = glyphs.getGlyphY(index) return (x, y, x + 12.0, y + 16.0) # ═══════════════════════════════════════════════════════════════ # LineBreaker # ═══════════════════════════════════════════════════════════════ class LineBreaker(Stub): """LineBreaker — 换行器""" BREAK_STRATEGY_SIMPLE = 0 BREAK_STRATEGY_HIGH_QUALITY = 1 BREAK_STRATEGY_BALANCED = 2 HYPHENATION_FREQUENCY_NONE = 0 HYPHENATION_FREQUENCY_NORMAL = 1 HYPHENATION_FREQUENCY_FULL = 2 WORD_BREAK_NORMAL = 0 WORD_BREAK_BREAK_ALL = 1 WORD_BREAK_KEEP_ALL = 2 TAB_STRATEGY_NONE = 0 TAB_STRATEGY_ORIGIN = 1 TAB_STRATEGY_DESTINATION = 2 JUSTIFICATION_MODE_NONE = 0 JUSTIFICATION_MODE_INTER_WORD = 1 JUSTIFICATION_MODE_CHARACTER = 2 LINE_LIMIT_UNLIMITED = 0 class Result(Stub): """LineBreaker.Result — 换行结果""" def __init__(self): super().__init__() self._line_counts: int = 0 self._line_breaks: List[Tuple[int, float]] = [] self._line_widths: List[float] = [] self._line_ascent: List[float] = [] self._line_descent: List[float] = [] def getLineCount(self) -> int: return self._line_counts def getLineBreakOffset(self, line_index: int) -> int: if 0 <= line_index < len(self._line_breaks): return self._line_breaks[line_index][0] return 0 def getLineWidth(self, line_index: int) -> float: if 0 <= line_index < len(self._line_widths): return self._line_widths[line_index] return 0.0 def getLineAscent(self, line_index: int) -> float: if 0 <= line_index < len(self._line_ascent): return self._line_ascent[line_index] return 0.0 def getLineDescent(self, line_index: int) -> float: if 0 <= line_index < len(self._line_descent): return self._line_descent[line_index] return 0.0 def addLineBreak(self, offset: int, width: float, ascent: float, descent: float): self._line_breaks.append((offset, width)) self._line_widths.append(width) self._line_ascent.append(ascent) self._line_descent.append(descent) self._line_counts += 1 def toString(self) -> str: return f"LineBreaker.Result(lines={self._line_counts})" def __init__(self, strategy: int = BREAK_STRATEGY_SIMPLE, hyphenation: int = HYPHENATION_FREQUENCY_NONE): super().__init__() self._strategy = strategy self._hyphenation_frequency = hyphenation self._justification_mode: int = LineBreaker.JUSTIFICATION_MODE_NONE self._word_break: int = LineBreaker.WORD_BREAK_NORMAL self._tab_strategy: int = LineBreaker.TAB_STRATEGY_NONE self._line_limit: int = LineBreaker.LINE_LIMIT_UNLIMITED def getStrategy(self) -> int: return self._strategy def setStrategy(self, strategy: int): self._strategy = strategy def getHyphenationFrequency(self) -> int: return self._hyphenation_frequency def setHyphenationFrequency(self, freq: int): self._hyphenation_frequency = freq def setJustificationMode(self, mode: int): self._justification_mode = mode def getJustificationMode(self) -> int: return self._justification_mode def setWordBreak(self, word_break: int): self._word_break = word_break def getWordBreak(self) -> int: return self._word_break def setTabStrategy(self, strategy: int): self._tab_strategy = strategy def getTabStrategy(self) -> int: return self._tab_strategy def setLineLimit(self, limit: int): self._line_limit = limit def getLineLimit(self) -> int: return self._line_limit def computeLineBreaks(self, text: str, width: float, paint: Any = None) -> Result: """计算换行""" result = LineBreaker.Result() if not text: return result words = text.split(" ") line_start = 0 line_width = 0.0 char_width = 12.0 # 粗略估计 for word in words: word_len = len(word) * char_width if word_len + line_width > width and len(text[line_start:text.index(word)]) > 0: break_offset = text.index(word, line_start) if line_start > 0 else len(word) result.addLineBreak(break_offset, line_width, -12.0, 3.0) line_start = break_offset line_width = word_len else: line_width += word_len + char_width if line_start < len(text): result.addLineBreak(len(text), line_width, -12.0, 3.0) return result def computeLineBreaksForMeasuredText(self, measured_text: MeasuredText, width: float) -> Result: """对度量文本计算换行""" return self.computeLineBreaks(measured_text.getText(), width) def toString(self) -> str: return f"LineBreaker(strategy={self._strategy}, hyphen={self._hyphenation_frequency})" # ═══════════════════════════════════════════════════════════════ # MeasuredText # ═══════════════════════════════════════════════════════════════ class MeasuredText(Stub): """MeasuredText — 度量文本""" def __init__(self, text: str = ""): super().__init__() self._text = text self._widths: List[float] = [] self._ascent: float = 0.0 self._descent: float = 0.0 self._height: float = 0.0 self._glyph_count: int = 0 self._char_spacing: float = 0.0 self._word_spacing: float = 0.0 self._scale_x: float = 1.0 self._advances: List[float] = [] self._clusters: List[int] = [] for c in text: self._widths.append(12.0) self._advances.append(12.0) self._clusters.append(1) def getText(self) -> str: return self._text def getWidth(self, start: int = 0, end: int = -1) -> float: if end < 0: end = len(self._widths) return sum(self._widths[start:end]) def getAscent(self) -> float: return self._ascent def getDescent(self) -> float: return self._descent def getHeight(self) -> float: return self._height def getCharWidthAt(self, index: int) -> float: if 0 <= index < len(self._widths): return self._widths[index] return 0.0 def setWidths(self, widths: List[float]): self._widths = list(widths) def setMetrics(self, ascent: float, descent: float): self._ascent = ascent self._descent = descent self._height = ascent - descent def getAdvance(self, index: int) -> float: if 0 <= index < len(self._advances): return self._advances[index] return 0.0 def getCluster(self, index: int) -> int: if 0 <= index < len(self._clusters): return self._clusters[index] return 1 def getGlyphCount(self) -> int: return self._glyph_count def setGlyphCount(self, count: int): self._glyph_count = count def getCharSpacing(self) -> float: return self._char_spacing def getWordSpacing(self) -> float: return self._word_spacing def getScaleX(self) -> float: return self._scale_x def toString(self) -> str: return f"MeasuredText(text={self._text[:20]}, w={self.getWidth():.1f})" # ═══════════════════════════════════════════════════════════════ # TextLayout # ═══════════════════════════════════════════════════════════════ class TextLayout(Stub): """TextLayout — 文本布局""" DIRECTION_LEFT_TO_RIGHT = 0 DIRECTION_RIGHT_TO_LEFT = 1 ALIGN_NORMAL = 0 ALIGN_CENTER = 1 ALIGN_OPPOSITE = 2 TEXT_DIRECTION_FIRST_STRONG_LTR = 0 TEXT_DIRECTION_FIRST_STRONG_RTL = 1 TEXT_DIRECTION_ANY_LTR = 2 TEXT_DIRECTION_ANY_RTL = 3 TEXT_DIRECTION_LTR = 4 TEXT_DIRECTION_RTL = 5 TEXT_DIRECTION_LOCALE = 6 TEXT_DIRECTION_FIRST_STRONG = 7 def __init__(self, text: str = "", width: float = 0.0, paint: Any = None): super().__init__() self._text = text self._width = width self._paint = paint self._lines: List[TextLayout.LineInfo] = [] self._alignment: int = TextLayout.ALIGN_NORMAL self._direction: int = TextLayout.DIRECTION_LEFT_TO_RIGHT self._text_direction: int = TextLayout.TEXT_DIRECTION_FIRST_STRONG_LTR self._max_lines: int = 0 self._include_padding: bool = True self._spacing_mult: float = 1.0 self._spacing_add: float = 0.0 self._ellipsized_width: float = 0.0 self._is_ellipsized: bool = False def getText(self) -> str: return self._text def getWidth(self) -> float: return self._width def getHeight(self) -> float: if not self._lines: return 0.0 return sum(l.getHeight() for l in self._lines) def getLineCount(self) -> int: return len(self._lines) def getLineInfo(self, line_index: int) -> Optional[TextLayout.LineInfo]: if 0 <= line_index < len(self._lines): return self._lines[line_index] return None def getLineStart(self, line_index: int) -> int: if 0 <= line_index < len(self._lines): return self._lines[line_index].getStart() return 0 def getLineEnd(self, line_index: int) -> int: if 0 <= line_index < len(self._lines): return self._lines[line_index].getEnd() return 0 def getLineMax(self, line_index: int) -> float: if 0 <= line_index < len(self._lines): return self._lines[line_index].getMax() return 0.0 def getLineAscent(self, line_index: int) -> float: if 0 <= line_index < len(self._lines): return self._lines[line_index].getAscent() return 0.0 def getLineDescent(self, line_index: int) -> float: if 0 <= line_index < len(self._lines): return self._lines[line_index].getDescent() return 0.0 def getMaxLines(self) -> int: return self._max_lines def setMaxLines(self, max_lines: int): self._max_lines = max_lines def getAlignment(self) -> int: return self._alignment def setAlignment(self, alignment: int): self._alignment = alignment def getDirection(self) -> int: return self._direction def setDirection(self, direction: int): self._direction = direction def getTextDirection(self) -> int: return self._text_direction def setTextDirection(self, direction: int): self._text_direction = direction def setIncludePadding(self, include: bool): self._include_padding = include def isIncludePadding(self) -> bool: return self._include_padding def setSpacingMultiplier(self, mult: float): self._spacing_mult = mult def getSpacingMultiplier(self) -> float: return self._spacing_mult def setSpacingAdd(self, add: float): self._spacing_add = add def getSpacingAdd(self) -> float: return self._spacing_add def isEllipsized(self) -> bool: return self._is_ellipsized def setEllipsizedWidth(self, width: float): self._ellipsized_width = width def getEllipsizedWidth(self) -> float: return self._ellipsized_width def addLine(self, start: int, end: int, max_width: float, ascent: float, descent: float): line = TextLayout.LineInfo(start, end, max_width, ascent, descent) self._lines.append(line) def clearLines(self): self._lines.clear() def buildSimpleLayout(self, char_width: float = 12.0, line_height: float = 16.0): """构建简单布局""" self._lines.clear() if not self._text: return chars_per_line = int(self._width / char_width) if char_width > 0 else 10 if chars_per_line <= 0: chars_per_line = 10 start = 0 while start < len(self._text): end = min(start + chars_per_line, len(self._text)) line_text = self._text[start:end] line_width = len(line_text) * char_width self.addLine(start, end, line_width, -12.0, 3.0) start = end def toString(self) -> str: return f"TextLayout(lines={len(self._lines)}, w={self._width:.1f})" # ═══════════════════════════════════════════════════════ # TextLayout.LineInfo # ═══════════════════════════════════════════════════════ class LineInfo(Stub): """TextLayout.LineInfo — 行信息""" def __init__(self, start: int = 0, end: int = 0, max_width: float = 0.0, ascent: float = 0.0, descent: float = 0.0): super().__init__() self._start = start self._end = end self._max_width = max_width self._ascent = ascent self._descent = descent self._height = ascent - descent if descent > 0 else 16.0 self._direction: int = TextLayout.DIRECTION_LEFT_TO_RIGHT self._is_ellipsized: bool = False def getStart(self) -> int: return self._start def getEnd(self) -> int: return self._end def getMax(self) -> float: return self._max_width def getAscent(self) -> float: return self._ascent def getDescent(self) -> float: return self._descent def getHeight(self) -> float: return self._height def getDirection(self) -> int: return self._direction def setDirection(self, direction: int): self._direction = direction def isEllipsized(self) -> bool: return self._is_ellipsized def setEllipsized(self, ellipsized: bool): self._is_ellipsized = ellipsized def toString(self) -> str: return f"LineInfo({self._start}-{self._end}, w={self._max_width:.1f})" # ═══════════════════════════════════════════════════════════════ # FontShaper # ═══════════════════════════════════════════════════════════════ class FontShaper(Stub): """FontShaper — 字体重塑器""" def __init__(self, font: Any = None): super().__init__() self._font = font self._variations: Dict[str, float] = {} self._features: Dict[str, int] = {} self._is_ready: bool = True def getFont(self) -> Any: return self._font def setFont(self, font: Any): self._font = font def setVariation(self, tag: str, value: float): self._variations[tag] = value def getVariation(self, tag: str) -> Optional[float]: return self._variations.get(tag) def getVariations(self) -> Dict[str, float]: return dict(self._variations) def setFeature(self, tag: str, value: int): self._features[tag] = value def getFeature(self, tag: str) -> int: return self._features.get(tag, 0) def getFeatures(self) -> Dict[str, int]: return dict(self._features) def shapeText(self, text: str, paint: Any = None) -> PositionedGlyphs: """重塑文本""" glyphs = PositionedGlyphs() for i, c in enumerate(text): glyphs.addGlyph(ord(c), i * 12.0, 0.0, 12.0, 0.0) glyphs.setBounds(len(text) * 12.0, 16.0) glyphs.setMetrics(-12.0, 3.0) return glyphs def shapeTextRun(self, text: str, start: int, count: int, context_start: int, context_count: int, is_rtl: bool, paint: Any) -> PositionedGlyphs: """重塑文本运行""" glyphs = PositionedGlyphs() for i in range(count): idx = start + i if idx < len(text): glyphs.addGlyph(ord(text[idx]), i * 12.0, 0.0, 12.0, 0.0) glyphs.setBounds(count * 12.0, 16.0) glyphs.setMetrics(-12.0, 3.0) return glyphs def getGlyphMetrics(self, glyph_id: int) -> Tuple[float, float, float, float]: """获取字形度量""" return (0.0, 0.0, 12.0, 16.0) def isReady(self) -> bool: return self._is_ready def toString(self) -> str: return f"FontShaper(variations={len(self._variations)})" # ═══════════════════════════════════════════════════════════════ # TextRunShaperResult (helper) # ═══════════════════════════════════════════════════════════════ class TextRunShaperResult(Stub): """TextRunShaperResult — 文本运行重塑结果""" def __init__(self): super().__init__() self._glyphs: PositionedGlyphs = PositionedGlyphs() self._char_offset: int = 0 self._char_count: int = 0 self._is_rtl: bool = False self._font_size: float = 12.0 def getGlyphs(self) -> PositionedGlyphs: return self._glyphs def setGlyphs(self, glyphs: PositionedGlyphs): self._glyphs = glyphs def getCharOffset(self) -> int: return self._char_offset def setCharOffset(self, offset: int): self._char_offset = offset def getCharCount(self) -> int: return self._char_count def setCharCount(self, count: int): self._char_count = count def isRtl(self) -> bool: return self._is_rtl def setRtl(self, rtl: bool): self._is_rtl = rtl def toString(self) -> str: return f"TextRunShaperResult(glyphs={self._glyphs.getGlyphCount()})" # ═══════════════════════════════════════════════════════════════ # TextShaperUtils # ═══════════════════════════════════════════════════════════════ class TextShaperUtils(Stub): """TextShaperUtils — 文本重塑工具类""" @staticmethod def estimateCharWidth(paint: Any = None) -> float: return 12.0 @staticmethod def estimateLineHeight(paint: Any = None) -> float: return 16.0 @staticmethod def measureText(text: str, paint: Any = None) -> float: return len(text) * 12.0 @staticmethod def wordWrap(text: str, max_width: float, char_width: float = 12.0) -> List[str]: lines = [] chars_per_line = int(max_width / char_width) if char_width > 0 else 10 if chars_per_line <= 0: chars_per_line = 10 words = text.split(" ") current_line = "" for word in words: if len(current_line) + len(word) + 1 <= chars_per_line: if current_line: current_line += " " current_line += word else: if current_line: lines.append(current_line) current_line = word if current_line: lines.append(current_line) return lines @staticmethod def truncateText(text: str, max_width: float, char_width: float = 12.0) -> str: max_chars = int(max_width / char_width) if max_chars <= 0: return "" if len(text) <= max_chars: return text return text[:max_chars - 1] + "..." @staticmethod def isRtlCharacter(c: str) -> bool: cp = ord(c) return (0x0590 <= cp <= 0x05FF or 0x0600 <= cp <= 0x06FF or 0x0700 <= cp <= 0x074F or 0x07C0 <= cp <= 0x07FF or 0xFB1D <= cp <= 0xFDFF or 0xFE70 <= cp <= 0xFEFF) @staticmethod def getTextDirection(text: str) -> int: for c in text[:100]: if TextShaperUtils.isRtlCharacter(c): return TextLayout.TEXT_DIRECTION_FIRST_STRONG_RTL return TextLayout.TEXT_DIRECTION_FIRST_STRONG_LTR