""" Android Text 组件 — TextUtils, SpannableString, Html, Layout 等 方法签名完全对齐 Android SDK """ import html as _html import re as _re import sys from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Type, Union from urllib.parse import quote_plus as _quote_plus, unquote_plus as _unquote_plus from .._core import ( Stub, AndroidOnlyError, android_context, resolve_android_path, to_android_path, ) # ═══════════════════════════════════════════════════════════════ # Typeface # ═══════════════════════════════════════════════════════════════ class Typeface(Stub): """Typeface — 字体""" NORMAL = 0 BOLD = 1 ITALIC = 2 BOLD_ITALIC = 3 DEFAULT: "Typeface" = None DEFAULT_BOLD: "Typeface" = None MONOSPACE: "Typeface" = None SANS_SERIF: "Typeface" = None SERIF: "Typeface" = None def __init__(self, family_name: str = "", style: int = NORMAL): super().__init__() self._family_name = family_name self._style = style self._weight = 400 if style in (self.NORMAL,) else 700 @staticmethod def defaultFromStyle(style: int) -> "Typeface": tf = Typeface("sans-serif", style) return tf @staticmethod def create(family_name: str, style: int) -> "Typeface": return Typeface(family_name, style) @staticmethod def createFromFile(path: str) -> "Typeface": return Typeface("custom") def getStyle(self) -> int: return self._style def getWeight(self) -> int: return self._weight def isBold(self) -> bool: return self._style in (self.BOLD, self.BOLD_ITALIC) def isItalic(self) -> bool: return self._style in (self.ITALIC, self.BOLD_ITALIC) def toString(self) -> str: return f"Typeface({self._family_name}, style={self._style})" # Initialize Typeface constants Typeface.DEFAULT = Typeface("sans-serif", Typeface.NORMAL) Typeface.DEFAULT_BOLD = Typeface("sans-serif", Typeface.BOLD) Typeface.MONOSPACE = Typeface("monospace", Typeface.NORMAL) Typeface.SANS_SERIF = Typeface("sans-serif", Typeface.NORMAL) Typeface.SERIF = Typeface("serif", Typeface.NORMAL) # ═══════════════════════════════════════════════════════════════ # TextPaint # ═══════════════════════════════════════════════════════════════ class TextPaint(Stub): """TextPaint — 文本绘制画笔""" def __init__(self, flags: int = 0): super().__init__() self.bgColor = 0 self.bgColorSet = False self.linkColor = 0xFF0000FF # blue self.underlineWidth = 0.0 self.baselineShift = 0 self.bulletRadius = 0.0 self.bulletGap = 0 self.bulletColor = 0 self.color = 0xFF000000 self.textSize = 14.0 self.typeface: Optional[Typeface] = Typeface.DEFAULT self.flags = flags self.antiAlias = True self.style = Typeface.NORMAL self.strokeWidth = 0.0 self.strokeCap = "butt" self.strokeJoin = "miter" self.shadowDx = 0.0 self.shadowDy = 0.0 self.shadowRadius = 0.0 self.shadowColor = 0 self.elevation = 0.0 self.letterSpacing = 0.0 self.fontFeatureSettings = "" self.fontVariationSettings = "" def setUnderlineText(self, underline: bool): if underline: self.underlineWidth = 1.0 else: self.underlineWidth = 0.0 def getColor(self) -> int: return self.color def setColor(self, color: int): self.color = color def setARGB(self, a: int, r: int, g: int, b: int): self.color = ((a & 0xFF) << 24) | ((r & 0xFF) << 16) | ((g & 0xFF) << 8) | (b & 0xFF) def getTextSize(self) -> float: return self.textSize def setTextSize(self, size: float): self.textSize = size def getTypeface(self) -> Optional[Typeface]: return self.typeface def setTypeface(self, tf: Typeface): self.typeface = tf def setAntiAlias(self, aa: bool): self.antiAlias = aa def measureText(self, text: str) -> float: return len(text) * self.textSize * 0.5 def toString(self) -> str: return f"TextPaint(size={self.textSize}, color={self.color})" # ═══════════════════════════════════════════════════════════════ # TextUtils # ═══════════════════════════════════════════════════════════════ class TextUtils(Stub): """TextUtils — 文本工具""" CAP_MODE_SENTENCES = 0 CAP_MODE_WORDS = 1 CAP_MODE_CHARACTERS = 2 @staticmethod def isEmpty(str_: Optional[str]) -> bool: return str_ is None or len(str_) == 0 @staticmethod def isBlank(str_: Optional[str]) -> bool: return str_ is None or str_.strip() == "" @staticmethod def equals(a: Optional[str], b: Optional[str]) -> bool: return a == b @staticmethod def join(delimiter: str, tokens: Sequence[str]) -> str: return delimiter.join(tokens) @staticmethod def split(text: str, delimiter: str) -> List[str]: if text is None or len(text) == 0: return [] return _re.split(delimiter, text) @staticmethod def htmlEncode(s: str) -> str: return _html.escape(s, quote=True) @staticmethod def htmlDecode(s: str) -> str: return _html.unescape(s) @staticmethod def concat(*texts: Any) -> str: parts = [] for t in texts: if t is not None: parts.append(str(t)) return "".join(parts) @staticmethod def indexOf(s: Any, needle: Any, start: int = 0) -> int: if s is None: return -1 s = str(s) needle = str(needle) if start < 0: start = 0 return s.find(needle, start) @staticmethod def lastIndexOf(s: Any, needle: Any, last: Optional[int] = None) -> int: if s is None: return -1 s = str(s) needle = str(needle) if last is None: return s.rfind(needle) if last < 0: return -1 return s.rfind(needle, 0, last + 1) @staticmethod def urlEncode(s: str) -> str: if s is None: return "" return _quote_plus(str(s)) @staticmethod def urlDecode(s: str) -> str: if s is None: return "" return _unquote_plus(str(s)) @staticmethod def getReverse(s: str, start: int, end: int) -> str: return s[start:end][::-1] @staticmethod def substring(text: str, start: int, end: int) -> str: return text[start:end] @staticmethod def delimitedListToStringArray(text: str, delimiter: str, delimiter2: str = "") -> List[str]: if not text: return [] return [t.strip() for t in text.split(delimiter)] @staticmethod def commaEllipsize(text: str, max_width: int, paint: Any, one_origin: bool = False, ellipsize: bool = False) -> str: if len(text) <= max_width: return text return text[:max_width] + "..." @staticmethod def ellipsize(text: str, paint: Any, avail: float, where: Any, ellipsis: str = "...") -> str: if paint and hasattr(paint, 'measureText'): text_width = paint.measureText(text) if text_width <= avail: return text max_chars = int(avail / 7) if avail else len(text) // 2 if max_chars >= len(text): return text if str(where) == "END": return text[:max_chars] + ellipsis elif str(where) == "START": return ellipsis + text[-max_chars:] elif str(where) == "MIDDLE": half = max_chars // 2 return text[:half] + ellipsis + text[-(max_chars - half):] return text[:max_chars] + ellipsis @staticmethod def isDigitsOnly(s: Any) -> bool: if s is None: return False s = str(s) if len(s) == 0: return False return s.isdigit() @staticmethod def isGraphical(s: str) -> bool: return bool(s and s.isprintable()) @staticmethod def capitalize(s: str) -> str: return s.capitalize() @staticmethod def trim(s: str) -> str: return s.strip() @staticmethod def getTrimmedLength(s: str) -> int: return len(s.strip()) @staticmethod def stringOrEmpty(s: Optional[str]) -> str: return s or "" @staticmethod def copySpansFrom(source: Any, start: int, end: int, kind: type, dest: Any, dest_offset: int = 0): pass @staticmethod def removeEmptySpans(spans: List, source: Any, kind: type) -> List: return [s for s in spans if s is not None] @staticmethod def expandTemplate(template: str, values: List[str]) -> str: result = template for i, v in enumerate(values): result = result.replace("$" + str(i + 1), v) return result # ═══════════════════════════════════════════════════════════════ # Span 相关接口和基类 # ═══════════════════════════════════════════════════════════════ class Spanned(Stub): """Spanned 接口 — 带标记的字符串""" SPAN_POINT_MARK = 0 SPAN_MARK_MARK = 1 SPAN_POINT_POINT = 2 SPAN_MARK_POINT = 3 SPAN_PARAGRAPH = 3 SPAN_INCLUSIVE_EXCLUSIVE = SPAN_MARK_POINT SPAN_INCLUSIVE_INCLUSIVE = SPAN_MARK_MARK SPAN_EXCLUSIVE_EXCLUSIVE = SPAN_POINT_POINT SPAN_EXCLUSIVE_INCLUSIVE = SPAN_POINT_MARK SPAN_COMPOSING = 256 SPAN_USER = 0x01000000 SPAN_USER_SHIFT = 24 SPAN_PRIORITY = 0x0000FF00 SPAN_PRIORITY_SHIFT = 8 SPAN_INTERMEDIATE = 0x00100000 def __init__(self, source: str = ""): super().__init__() self._source = source self._spans: Dict[Tuple[type, int], Tuple[int, int, int, Any]] = {} # (type, id) -> (start, end, flags, span_obj) def setSpan(self, span_obj: Any, start: int, end: int, flags: int): span_id = id(span_obj) span_type = type(span_obj) self._spans[(span_type, span_id)] = (start, end, flags, span_obj) def removeSpan(self, span_obj: Any): span_id = id(span_obj) span_type = type(span_obj) self._spans.pop((span_type, span_id), None) def getSpans(self, start: int, end: int, span_type: Optional[type] = None) -> List[Any]: collected = [] for (st, sid), (s, e, flags, obj) in self._spans.items(): if span_type is None or st == span_type: if s < end and e > start: collected.append((s, obj)) collected.sort(key=lambda t: t[0]) return [obj for _, obj in collected] def getSpanStart(self, span_obj: Any) -> int: span_id = id(span_obj) span_type = type(span_obj) data = self._spans.get((span_type, span_id)) return data[0] if data else -1 def getSpanEnd(self, span_obj: Any) -> int: span_id = id(span_obj) span_type = type(span_obj) data = self._spans.get((span_type, span_id)) return data[1] if data else -1 def getSpanFlags(self, span_obj: Any) -> int: span_id = id(span_obj) span_type = type(span_obj) data = self._spans.get((span_type, span_id)) return data[2] if data else 0 def nextSpanTransition(self, start: int, end: int, span_type: Optional[type] = None) -> int: result = end for (st, sid), (s, e, flags, obj) in self._spans.items(): if span_type is not None and st != span_type: continue if start < s < result: result = s if start < e < result: result = e return result def toString(self) -> str: return self._source def __len__(self) -> int: return len(self._source) def __getitem__(self, index): return self._source[index] def __str__(self): return self._source def length(self) -> int: return len(self._source) class Editable(Spanned): """Editable — 可编辑文本接口""" def __init__(self, source: str = ""): super().__init__(source) def append(self, text: Any) -> "Editable": self._source += str(text) return self def insert(self, where: int, text: Any): self._source = self._source[:where] + str(text) + self._source[where:] def replace(self, start: int, end: int, text: Any): self._source = self._source[:start] + str(text) + self._source[end:] def delete(self, start: int, end: int): self._source = self._source[:start] + self._source[end:] def clear(self): self._source = "" def clearSpans(self): self._spans.clear() def getChars(self, start: int, end: int, dest: List[str], destoff: int): for i, c in enumerate(self._source[start:end]): if destoff + i < len(dest): dest[destoff + i] = c # ═══════════════════════════════════════════════════════════════ # SpannableString # ═══════════════════════════════════════════════════════════════ class SpannableString(Spanned): """SpannableString — 不可变但可标记的字符串""" def __init__(self, source: str): super().__init__(source) self._source = source def setSpan(self, span_obj: Any, start: int, end: int, flags: int): super().setSpan(span_obj, start, end, flags) def removeSpan(self, span_obj: Any): super().removeSpan(span_obj) def toString(self) -> str: return self._source def __str__(self): return self._source @staticmethod def valueOf(source: str) -> "SpannableString": return SpannableString(source) def length(self) -> int: return len(self._source) def charAt(self, index: int) -> str: return self._source[index] def subSequence(self, start: int, end: int) -> str: return self._source[start:end] # ═══════════════════════════════════════════════════════════════ # SpannableStringBuilder # ═══════════════════════════════════════════════════════════════ class SpannableStringBuilder(Editable): """SpannableStringBuilder — 可变可标记字符串构建器""" def __init__(self, source: Any = None): if source is None: super().__init__("") elif isinstance(source, str): super().__init__(source) elif isinstance(source, Spanned): super().__init__(source.toString()) else: super().__init__(str(source)) def append(self, text: Any, start: Optional[int] = None, end: Optional[int] = None) -> "SpannableStringBuilder": if text is None: return self if start is None and end is None: self._source += str(text) else: s = 0 if start is None else start text_str = str(text) e = len(text_str) if end is None else end self._source += text_str[s:e] return self def insert(self, where: int, text: Any) -> "SpannableStringBuilder": super().insert(where, text) return self def replace(self, start: int, end: int, text: Any) -> "SpannableStringBuilder": super().replace(start, end, text) return self def delete(self, start: int, end: int) -> "SpannableStringBuilder": super().delete(start, end) return self def setSpan(self, span_obj: Any, start: int, end: int, flags: int): super().setSpan(span_obj, start, end, flags) def clear(self): super().clear() def clearSpans(self): super().clearSpans() def length(self) -> int: return len(self._source) def charAt(self, index: int) -> str: return self._source[index] if 0 <= index < len(self._source) else "" def subSequence(self, start: int, end: int) -> str: return self._source[start:end] def toString(self) -> str: return self._source def __str__(self): return self._source def __iadd__(self, other: Any) -> "SpannableStringBuilder": self.append(str(other)) return self # ═══════════════════════════════════════════════════════════════ # TextWatcher # ═══════════════════════════════════════════════════════════════ class TextWatcher(Stub): """TextWatcher — 文本变化监听器""" def beforeTextChanged(self, s: str, start: int, count: int, after: int): pass def onTextChanged(self, s: str, start: int, before: int, count: int): pass def afterTextChanged(self, s: Any): pass # ═══════════════════════════════════════════════════════════════ # InputFilter / LengthFilter # ═══════════════════════════════════════════════════════════════ class InputFilter(Stub): """InputFilter — 输入过滤器接口""" class LengthFilter(Stub): """LengthFilter — 长度限制过滤器""" def __init__(self, max_length: int): super().__init__() self._max_length = max_length def filter(self, source: str, start: int, end: int, dest: Spanned, dstart: int, dend: int) -> Optional[str]: keep = self._max_length - (len(dest.toString()) - (dend - dstart)) if keep <= 0: return "" if keep >= (end - start): return None # keep original return source[start:start + keep] def getMax(self) -> int: return self._max_length @staticmethod def allCaps() -> "InputFilter": f = InputFilter() f._transform = lambda s: s.upper() return f def filter(self, source: str, start: int, end: int, dest: Spanned, dstart: int, dend: int) -> Optional[str]: return None # ═══════════════════════════════════════════════════════════════ # Html # ═══════════════════════════════════════════════════════════════ class Html(Stub): """Html — HTML 文本处理""" FROM_HTML_MODE_COMPACT = 0 FROM_HTML_MODE_LEGACY = 1 FROM_HTML_OPTION_USE_CSS_COLORS = 256 FROM_HTML_SEPARATOR_LINE_BREAK_BLOCKQUOTE = 0x00000001 FROM_HTML_SEPARATOR_LINE_BREAK_DIV = 0x00000002 FROM_HTML_SEPARATOR_LINE_BREAK_HEADING = 0x00000004 FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM = 0x00000008 FROM_HTML_SEPARATOR_LINE_BREAK_LIST = 0x00000020 FROM_HTML_SEPARATOR_LINE_BREAK_TABLE = 0x00000040 TO_HTML_PARAGRAPH_LINES_INDIVIDUAL = 0x00000001 TO_HTML_PARAGRAPH_LINE_CONSECUTIVE = 0x00000002 _TAG_RE = _re.compile(r"<[^>]*>") _BR_RE = _re.compile(r"", _re.IGNORECASE) _P_RE = _re.compile(r"

|]*>", _re.IGNORECASE) @staticmethod def fromHtml(html_text: str, flags: int = 0) -> str: """将 HTML 转为纯文本""" text = html_text text = Html._BR_RE.sub("\n", text) text = Html._P_RE.sub("\n\n", text) text = Html._TAG_RE.sub("", text) text = _html.unescape(text) return text.strip() @staticmethod def toHtml(text: str, option: int = 0) -> str: """将纯文本转为 HTML""" paragraphs = text.split("\n") html_parts = [] for p in paragraphs: escaped = _html.escape(p) html_parts.append(f"

{escaped}

") return "".join(html_parts) @staticmethod def fromHtml(html_text: str, image_getter: Any, tag_handler: Any) -> str: return Html.fromHtml(html_text) @staticmethod def toHtmlWithOptions(text: str, options: int) -> str: return Html.toHtml(text) class ImageGetter: def getDrawable(self, source: str): return None class TagHandler: def handleTag(self, opening: bool, tag: str, output: Editable, xml_reader: Any): pass # ═══════════════════════════════════════════════════════════════ # CharacterStyle / 各 Span 实现 # ═══════════════════════════════════════════════════════════════ class CharacterStyle(Stub): """CharacterStyle — 字符样式基类""" def underLine(self): pass def updateDrawState(self, tp: TextPaint): pass def getUnderline(self) -> bool: return False @staticmethod def wrap(cs: "CharacterStyle") -> "CharacterStyle": return cs class StyleSpan(CharacterStyle): """StyleSpan — 字体样式""" def __init__(self, style: int): super().__init__() self._style = style def getStyle(self) -> int: return self._style def updateDrawState(self, tp: TextPaint): tp.style = self._style class TypefaceSpan(CharacterStyle): """TypefaceSpan — 字体族""" def __init__(self, family: str): super().__init__() self._family = family def getFamily(self) -> str: return self._family def updateDrawState(self, tp: TextPaint): if self._family == "monospace": tp.typeface = Typeface.MONOSPACE elif self._family == "serif": tp.typeface = Typeface.SERIF else: tp.typeface = Typeface.DEFAULT class RelativeSizeSpan(CharacterStyle): """RelativeSizeSpan — 相对大小""" def __init__(self, size: float): super().__init__() self._size = size def getSizeChange(self) -> float: return self._size def updateDrawState(self, tp: TextPaint): tp.textSize *= self._size class ForegroundColorSpan(CharacterStyle): """ForegroundColorSpan — 前景色""" def __init__(self, color: int): super().__init__() self._color = color def getForegroundColor(self) -> int: return self._color def updateDrawState(self, tp: TextPaint): tp.color = self._color class BackgroundColorSpan(CharacterStyle): """BackgroundColorSpan — 背景色""" def __init__(self, color: int): super().__init__() self._color = color def getBackgroundColor(self) -> int: return self._color def updateDrawState(self, tp: TextPaint): tp.bgColor = self._color tp.bgColorSet = True class UnderlineSpan(CharacterStyle): """UnderlineSpan — 下划线""" def updateDrawState(self, tp: TextPaint): tp.setUnderlineText(True) class StrikethroughSpan(CharacterStyle): """StrikethroughSpan — 删除线""" def updateDrawState(self, tp: TextPaint): pass # desktop 无直接等价 class SubscriptSpan(CharacterStyle): """SubscriptSpan — 下标""" def updateDrawState(self, tp: TextPaint): tp.baselineShift = -tp.textSize / 4 class SuperscriptSpan(CharacterStyle): """SuperscriptSpan — 上标""" def updateDrawState(self, tp: TextPaint): tp.baselineShift = tp.textSize / 2 class ImageSpan(CharacterStyle): """ImageSpan — 图片""" ALIGN_BOTTOM = 0 ALIGN_BASELINE = 1 ALIGN_CENTER = 2 ALIGN_TOP = 3 def __init__(self, drawable: Any, source: str = "", align: int = ALIGN_BOTTOM): super().__init__() self._drawable = drawable self._source = source self._align = align def getDrawable(self): return self._drawable def getSource(self) -> str: return self._source def getAlignment(self) -> int: return self._align class ClickableSpan(CharacterStyle): """ClickableSpan — 可点击文本""" def __init__(self): super().__init__() def onClick(self, view: Any): pass def updateDrawState(self, tp: TextPaint): tp.color = tp.linkColor tp.setUnderlineText(True) class URLSpan(ClickableSpan): """URLSpan — URL 链接""" def __init__(self, url: str): super().__init__() self._url = url def getURL(self) -> str: return self._url def onClick(self, view: Any): sys.stderr.write(f"[URLSpan] onClick: {self._url}\n") def updateDrawState(self, tp: TextPaint): super().updateDrawState(tp) # ═══════════════════════════════════════════════════════════════ # NoCopySpan / ParagraphStyle 接口 # ═══════════════════════════════════════════════════════════════ class NoCopySpan(Stub): """NoCopySpan — 标记不在拷贝时复制的 span""" pass class NoCopySpanConcise(NoCopySpan): """NoCopySpan.Concrete — 具体实现""" pass class ParagraphStyle(Stub): """ParagraphStyle — 段落样式接口""" pass # ═══════════════════════════════════════════════════════════════ # Ellipsize / TruncateAt 枚举 # ═══════════════════════════════════════════════════════════════ class Ellipsize(Stub): """Ellipsize 常量""" END = "END" MIDDLE = "MIDDLE" MARQUEE = "MARQUEE" START = "START" NONE = "NONE" class TruncateAt(Stub): """TruncateAt 常量""" END = "END" MIDDLE = "MIDDLE" MARQUEE = "MARQUEE" START = "START" # ═══════════════════════════════════════════════════════════════ # Align # ═══════════════════════════════════════════════════════════════ class Align(Stub): """Layout.Alignment 枚举""" ALIGN_NORMAL = 0 ALIGN_OPPOSITE = 1 ALIGN_CENTER = 2 # ═══════════════════════════════════════════════════════════════ # TextDirectionHeuristic / TextDirectionHeuristics # ═══════════════════════════════════════════════════════════════ class TextDirectionHeuristic(Stub): """TextDirectionHeuristic — 文本方向启发式""" pass class TextDirectionHeuristics(Stub): """TextDirectionHeuristics — 文本方向启发式常量""" FIRSTSTRONG_LTR = TextDirectionHeuristic() FIRSTSTRONG_RTL = TextDirectionHeuristic() ANYRTL_LTR = TextDirectionHeuristic() LTR = TextDirectionHeuristic() RTL = TextDirectionHeuristic() LOCALE = TextDirectionHeuristic() # ═══════════════════════════════════════════════════════════════ # LoginFilter # ═══════════════════════════════════════════════════════════════ class LoginFilter(InputFilter): """LoginFilter — 登录名输入过滤器""" def __init__(self, append_ok: bool = True): super().__init__() self._append_ok = append_ok def isAllowed(self, c: str) -> bool: return c.isalnum() or c in "@._-" def filter(self, source: str, start: int, end: int, dest: Spanned, dstart: int, dend: int) -> Optional[str]: filtered = [] for c in source[start:end]: if self.isAllowed(c): filtered.append(c) elif self._append_ok: pass result = "".join(filtered) return result if result != source else None class UsernameFilterGMail(InputFilter): def filter(self, source, start, end, dest, dstart, dend): result = [] for c in source[start:end]: if c.isalnum() or c in "@._-": result.append(c) return "".join(result) or None class PasswordFilterGMail(InputFilter): def filter(self, source, start, end, dest, dstart, dend): return None # ═══════════════════════════════════════════════════════════════ # Method / MovementMethod # ═══════════════════════════════════════════════════════════════ class Method(Stub): """Method — 输入法接口""" pass class MovementMethod(Stub): """MovementMethod — 移动方式接口""" pass # ═══════════════════════════════════════════════════════════════ # Layout / StaticLayout / DynamicLayout / BoringLayout # ═══════════════════════════════════════════════════════════════ class Layout(Stub): """Layout — 布局基类""" Alignment = Align 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 JUSTIFICATION_MODE_NONE = 0 JUSTIFICATION_MODE_INTER_WORD = 1 def __init__(self, text: str, paint: TextPaint, width: int, alignment: int, spacing_mult: float, spacing_add: float): super().__init__() self._text = text self._paint = paint self._width = width self._alignment = alignment self._spacing_mult = spacing_mult self._spacing_add = spacing_add self._line_count = max(1, len(text) // max(1, width // 10) + 1) def getLineCount(self) -> int: return self._line_count def getLineBounds(self, line: int, bounds: Optional[Any]) -> int: line_height = int(self._paint.textSize * self._spacing_mult + self._spacing_add) return line * line_height def getPrimaryHorizontal(self, offset: int) -> float: line = 0 return (offset % max(1, self._width)) * self._paint.measureText("x") def getSecondaryHorizontal(self, offset: int) -> float: return 0.0 def getLineTop(self, line: int) -> int: line_height = int(self._paint.textSize * self._spacing_mult + self._spacing_add) return line * line_height def getLineBottom(self, line: int) -> int: line_height = int(self._paint.textSize * self._spacing_mult + self._spacing_add) return (line + 1) * line_height def getLineStart(self, line: int) -> int: chars_per_line = max(1, self._width // max(1, int(self._paint.measureText("x")))) return line * chars_per_line def getLineEnd(self, line: int) -> int: chars_per_line = max(1, self._width // max(1, int(self._paint.measureText("x")))) return min((line + 1) * chars_per_line, len(self._text)) def getText(self) -> str: return self._text def getPaint(self) -> TextPaint: return self._paint def getWidth(self) -> int: return self._width def getAlignment(self) -> int: return self._alignment def getSpacingMultiplier(self) -> float: return self._spacing_mult def getSpacingAdd(self) -> float: return self._spacing_add def isRtlCharAt(self, offset: int) -> bool: return False def getParagraphDirection(self, line: int) -> int: return 1 def draw(self, canvas: Any): pass class StaticLayout(Layout): """StaticLayout — 静态布局""" def __init__(self, source: str, paint: TextPaint, width: int, alignment: int, spacing_mult: float, spacing_add: float, include_pad: bool): super().__init__(source, paint, width, alignment, spacing_mult, spacing_add) self._include_pad = include_pad class DynamicLayout(Layout): """DynamicLayout — 动态布局""" def __init__(self, base: Any, paint: TextPaint, width: int, alignment: int, spacing_mult: float, spacing_add: float, include_pad: bool, ellipsize: Any = None, ellipsize_width: int = 0): super().__init__(str(base), paint, width, alignment, spacing_mult, spacing_add) self._include_pad = include_pad class BoringLayout(Layout): """BoringLayout — 单行布局""" def __init__(self, source: str, paint: TextPaint, width: int, alignment: int, spacing_mult: float, spacing_add: float, include_pad: bool, ellipsize: Any = None, ellipsize_width: int = 0): super().__init__(source, paint, width, alignment, spacing_mult, spacing_add) self._include_pad = include_pad self._line_count = 1 if source else 0 class Metrics(Stub): """Metrics for BoringLayout""" width = 0 height = 0 @staticmethod def isBoring(text: str, paint: TextPaint) -> Optional["BoringLayout.Metrics"]: m = BoringLayout.Metrics() m.width = int(paint.measureText(text)) m.height = int(paint.textSize) return m if text else None @staticmethod def make(source: str, paint: TextPaint, width: int, alignment: int, spacing_mult: float, spacing_add: float, metrics: "BoringLayout.Metrics", include_pad: bool) -> "BoringLayout": return BoringLayout(source, paint, width, alignment, spacing_mult, spacing_add, include_pad) class ClipRectangle(Stub): """ClipRectangle — 裁剪矩形""" def __init__(self, left: int = 0, top: int = 0, right: int = 0, bottom: int = 0): super().__init__() self.left = left self.top = top self.right = right self.bottom = bottom # ═══════════════════════════════════════════════════════════════ # Spannable (接口别名) # ═══════════════════════════════════════════════════════════════ class Spannable(Spanned): """Spannable — 可标记接口""" pass # ═══════════════════════════════════════════════════════════════ # __all__ # ═══════════════════════════════════════════════════════════════ __all__ = [ "TextUtils", "SpannableString", "SpannableStringBuilder", "Spannable", "Editable", "Spanned", "TextWatcher", "InputFilter", "Html", "Layout", "StaticLayout", "DynamicLayout", "BoringLayout", "ClipRectangle", "TextPaint", "TextDirectionHeuristic", "TextDirectionHeuristics", "Align", "Ellipsize", "TruncateAt", "LoginFilter", "Method", "MovementMethod", "NoCopySpan", "ParagraphStyle", "CharacterStyle", "StyleSpan", "TypefaceSpan", "RelativeSizeSpan", "ForegroundColorSpan", "BackgroundColorSpan", "UnderlineSpan", "StrikethroughSpan", "SubscriptSpan", "SuperscriptSpan", "ImageSpan", "ClickableSpan", "URLSpan", "Typeface", ]