"""
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 .._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]:
return text.split(delimiter)
@staticmethod
def htmlEncode(s: str) -> str:
return _html.escape(s, quote=True)
@staticmethod
def htmlDecode(s: str) -> str:
return _html.unescape(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: str) -> bool:
return s.isdigit() if s else False
@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, Any]] = {} # (type, id) -> (start, end, 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, 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]:
result = []
for (st, sid), (s, e, obj) in self._spans.items():
if span_type is None or st == span_type:
if s < end and e > start:
result.append(obj)
return result
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 0 if data is None else 0
def nextSpanTransition(self, start: int, end: int, span_type: Optional[type] = None) -> int:
return end
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) -> "SpannableStringBuilder":
super().append(text)
return self
def append(self, text: Any, start: int, end: int) -> "SpannableStringBuilder":
if isinstance(text, str):
self._source += text[start:end]
else:
self._source += str(text)[start:end]
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", ]