1106 lines
36 KiB
Python
1106 lines
36 KiB
Python
"""
|
|
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"<br\s*/?\s*>", _re.IGNORECASE)
|
|
_P_RE = _re.compile(r"</p>|<p[^>]*>", _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"<p>{escaped}</p>")
|
|
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",
|
|
] |