Files
Android-pyApi/android_pyapi/android/text.py
T
shuiyu1123 fbb8263b13 feat: 第三轮深度完善 — SQLite/JSON/网络/媒体/文本 真实实现
SQLite真实数据库:
- SQLiteDatabase: execSQL/insert/insertOrThrow/replace/update/delete/query 用sqlite3
- 修复getVersion/setVersion用PRAGMA user_version
- 修复getMaximumSize/getPageSize/setMaximumSize/setPageSize用PRAGMA
- SQLiteStatement: execute/simpleQueryForLong/simpleQueryForString/execQuery
- SQLiteOpenHelper: onCreate/onUpgrade/onDowngrade回调正确触发
- 修复SQLiteCursor.moveToPosition越界处理
- 修复ContentValues值对提取bug

JSON真实实现:
- JSONObject: isNull/append/putOnce/putOpt/getNames
- JSONArray: 完整put/get/opt/length/toString

网络真实实现:
- NetworkInterface: getByName/getNetworkInterfaces/getHardwareAddress 用/sys/class/net+fcntl.ioctl
- InterfaceAddress: 新增类
- ConnectivityManager: getActiveNetwork/getAllNetworks 用LinuxNetwork
- WifiManager: getScanResults用nmcli, setWifiEnabled用nmcli/rfkill
- BluetoothAdapter: getAddress/getName 用bluetoothctl

媒体真实实现:
- MediaPlayer: prepare检查文件, getCurrentPosition实时计算, getDuration/getVideoWidth用ffprobe
- MediaRecorder: setVideoSize/setVideoFrameRate/setVideoEncodingBitRate配置存储
- MediaMetadataRetriever: extractMetadata用ffprobe, getFrameAtTime用ffmpeg, getEmbeddedPicture用ffmpeg

文本真实实现:
- TextUtils: split用re.split, concat/indexOf/lastIndexOf/urlEncode/urlDecode
- SpannableString: setSpan/getSpans/getSpanFlags/nextSpanTransition 真实实现
- SpannableStringBuilder: 修复append重载bug

Provider增强:
- Settings: putBoolean/getBoolean
- MediaStore: getContentUri/VOLUME常量
- ContactsContract: 补全常量和CommonDataKinds

Util增强:
- Log: w/e/wtf支持Throwable参数, 修复wtf重复定义bug
- TypedValue: 修复complexToDimension/complexToFloat/getFloat实现

修复:
- __init__.py: 补全NetworkInterface/InetAddress/MediaMetadataRetriever/SpannableStringBuilder导出

统计: 221模块, 3666类, 31005方法, 183529行
2026-06-25 00:40:59 +00:00

1167 lines
38 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 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"<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",
]