Files
Android-pyApi/android_pyapi/android/graphics.py
T
shuiyu1123 8c3bd3c415 feat: 大幅增强原生行为 — 修复P0 Bug + 补全核心API
P0 Bug 修复:
- Canvas.drawBitmap 重复定义导致重载丢失 → 合并为*args重载
- String.hashCode() 不可达的第二return → 修复为单return
- File.compareTo() 调用不存在的__cmp__ → 改为显式比较
- HashMap.remove() 逻辑错误 → 区分单参(返回旧值)/双参(返回boolean)

P1 核心行为增强:
- Handler: sendMessage/sendMessageDelayed 现在真正入队异步执行
- Looper: loop() 实现真正的消息循环(按when时间取出到期消息)
- AsyncTask: execute() 在后台线程池执行,完成后主线程回调
- Bundle: 新增12个数组类型put/get方法
- Math.round: 改为Java语义(floor(a+0.5))而非Python银行家舍入
- Thread: currentThread()缓存对象, 新增join/getState/getId/getPriority
- String: 新增strip/repeat/isBlank/lines/subSequence/getChars/chars/formatted/translateEscapes

P2 缺失API补全:
- 新增21个Java标准Exception类 (NullPointerException等)
- View: 新增39个事件处理/监听器/位置尺寸方法
- Activity: 新增33个生命周期/权限/UI方法
- Context: 新增22个文件流/权限/资源/主题方法
- Canvas: 支持 Canvas(Bitmap) 构造函数重载
- __init__.py: __all__ 补全Exception体系和Activity类
2026-06-24 15:04:17 +00:00

2834 lines
98 KiB
Python

"""
Android Graphics 组件 — Bitmap, Canvas, Paint, Color, Path, Rect 等
方法签名完全对齐 Android SDK
"""
import array
import math
import struct
import io
import os
from enum import IntEnum
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
from .._core import Stub, AndroidOnlyError, android_context, HAS_PIL, resolve_android_path
# Re-export Typeface from text module
from .text import Typeface
# ═══════════════════════════════════════════════════════════════
# Color
# ═══════════════════════════════════════════════════════════════
class Color(Stub):
"""Color — 颜色工具类"""
BLACK = 0xFF000000
DKGRAY = 0xFF444444
GRAY = 0xFF888888
LTGRAY = 0xFFCCCCCC
WHITE = 0xFFFFFFFF
RED = 0xFFFF0000
GREEN = 0xFF00FF00
BLUE = 0xFF0000FF
YELLOW = 0xFFFFFF00
CYAN = 0xFF00FFFF
MAGENTA = 0xFFFF00FF
TRANSPARENT = 0
@staticmethod
def alpha(color: int) -> int:
return (color >> 24) & 0xFF
@staticmethod
def red(color: int) -> int:
return (color >> 16) & 0xFF
@staticmethod
def green(color: int) -> int:
return (color >> 8) & 0xFF
@staticmethod
def blue(color: int) -> int:
return color & 0xFF
@staticmethod
def rgb(r: int, g: int, b: int) -> int:
return 0xFF000000 | ((r & 0xFF) << 16) | ((g & 0xFF) << 8) | (b & 0xFF)
@staticmethod
def argb(a: int, r: int, g: int, b: int) -> int:
return ((a & 0xFF) << 24) | ((r & 0xFF) << 16) | ((g & 0xFF) << 8) | (b & 0xFF)
@staticmethod
def parseColor(colorString: str) -> int:
s = colorString.strip()
if s.startswith("#"):
s = s[1:]
if len(s) == 3:
s = "".join(c * 2 for c in s)
if len(s) == 6:
s = "FF" + s
return int(s, 16) & 0xFFFFFFFF
if s.startswith("0x"):
return int(s, 16) & 0xFFFFFFFF
name_map = {
"black": Color.BLACK, "white": Color.WHITE, "red": Color.RED,
"green": Color.GREEN, "blue": Color.BLUE, "yellow": Color.YELLOW,
"cyan": Color.CYAN, "magenta": Color.MAGENTA, "transparent": Color.TRANSPARENT,
"dkgray": Color.DKGRAY, "gray": Color.GRAY, "ltgray": Color.LTGRAY,
"darkgray": Color.DKGRAY, "lightgray": Color.LTGRAY,
}
return name_map.get(s.lower(), Color.BLACK)
@staticmethod
def RGBToHSV(red: int, green: int, blue: int, hsv: List[float]):
r, g, b = red / 255.0, green / 255.0, blue / 255.0
mx = max(r, g, b)
mn = min(r, g, b)
df = mx - mn
if df == 0:
h = 0.0
elif mx == r:
h = ((g - b) / df) % 6.0
elif mx == g:
h = (b - r) / df + 2.0
else:
h = (r - g) / df + 4.0
h = h * 60.0
if h < 0:
h += 360.0
s = 0.0 if mx == 0 else (df / mx)
v = mx
hsv[0] = h
hsv[1] = s
hsv[2] = v
@staticmethod
def HSVToColor(hsv: List[float]) -> int:
return Color.HSVToColor(255, hsv)
@staticmethod
def HSVToColor(alpha: int, hsv: List[float]) -> int:
h, s, v = hsv[0], hsv[1], hsv[2]
hi = int(h / 60.0) % 6
f = (h / 60.0) - hi
p = v * (1.0 - s)
q = v * (1.0 - f * s)
t = v * (1.0 - (1.0 - f) * s)
if hi == 0:
r, g, b = v, t, p
elif hi == 1:
r, g, b = q, v, p
elif hi == 2:
r, g, b = p, v, t
elif hi == 3:
r, g, b = p, q, v
elif hi == 4:
r, g, b = t, p, v
else:
r, g, b = v, p, q
return Color.argb(alpha, int(r * 255 + 0.5), int(g * 255 + 0.5), int(b * 255 + 0.5))
@staticmethod
def ColorToHSV(color: int, hsv: List[float]):
Color.RGBToHSV(Color.red(color), Color.green(color), Color.blue(color), hsv)
@staticmethod
def getHue(color: int) -> float:
hsv = [0.0, 0.0, 0.0]
Color.ColorToHSV(color, hsv)
return hsv[0]
@staticmethod
def getSaturation(color: int) -> float:
hsv = [0.0, 0.0, 0.0]
Color.ColorToHSV(color, hsv)
return hsv[1]
@staticmethod
def getBrightness(color: int) -> float:
hsv = [0.0, 0.0, 0.0]
Color.ColorToHSV(color, hsv)
return hsv[2]
@staticmethod
def HSLToColor(hsl: List[float], alpha: float = 1.0) -> int:
h, s, l = hsl[0] / 360.0, hsl[1], hsl[2]
if s == 0:
r = g = b = l
else:
def hue2rgb(p, q, t):
if t < 0:
t += 1
if t > 1:
t -= 1
if t < 1.0 / 6.0:
return p + (q - p) * 6.0 * t
if t < 1.0 / 2.0:
return q
if t < 2.0 / 3.0:
return p + (q - p) * (2.0 / 3.0 - t) * 6.0
return p
q = l * (1 + s) if l < 0.5 else l + s - l * s
p = 2 * l - q
r = hue2rgb(p, q, h + 1.0 / 3.0)
g = hue2rgb(p, q, h)
b = hue2rgb(p, q, h - 1.0 / 3.0)
a = max(0, min(255, int(alpha * 255)))
return Color.argb(a, int(r * 255), int(g * 255), int(b * 255))
@staticmethod
def ColorToHSL(color: int, hsl: List[float]):
r, g, b = Color.red(color) / 255.0, Color.green(color) / 255.0, Color.blue(color) / 255.0
mx = max(r, g, b)
mn = min(r, g, b)
l = (mx + mn) / 2.0
if mx == mn:
h = 0.0
s = 0.0
else:
d = mx - mn
s = d / (1 - abs(2 * l - 1))
if mx == r:
h = ((g - b) / d) % 6.0
elif mx == g:
h = (b - r) / d + 2.0
else:
h = (r - g) / d + 4.0
h = h * 60.0
if h < 0:
h += 360.0
hsl[0] = h
hsl[1] = s
hsl[2] = l
# ═══════════════════════════════════════════════════════════════
# Rect
# ═══════════════════════════════════════════════════════════════
class Rect(Stub):
"""Rect — 矩形 (int 坐标)"""
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
def width(self) -> int:
return self.right - self.left
def height(self) -> int:
return self.bottom - self.top
def centerX(self) -> int:
return (self.left + self.right) // 2
def centerY(self) -> int:
return (self.top + self.bottom) // 2
def exactCenterX(self) -> float:
return (self.left + self.right) * 0.5
def exactCenterY(self) -> float:
return (self.top + self.bottom) * 0.5
def set(self, left: int, top: int, right: int, bottom: int):
self.left, self.top, self.right, self.bottom = left, top, right, bottom
def offset(self, dx: int, dy: int):
self.left += dx
self.top += dy
self.right += dx
self.bottom += dy
def offsetTo(self, newLeft: int, newTop: int):
self.right += newLeft - self.left
self.bottom += newTop - self.top
self.left = newLeft
self.top = newTop
def inset(self, dx: int, dy: int):
self.left += dx
self.top += dy
self.right -= dx
self.bottom -= dy
def contains(self, *args) -> bool:
if len(args) == 2:
x, y = args
return (self.left < self.right and self.top < self.bottom
and x >= self.left and x < self.right and y >= self.top and y < self.bottom)
elif len(args) == 4:
left, top, right, bottom = args
return (self.left < self.right and self.top < self.bottom
and self.left <= left and self.top <= top
and self.right >= right and self.bottom >= bottom)
return False
def intersect(self, left: int, top: int, right: int, bottom: int) -> bool:
nL = max(self.left, left)
nT = max(self.top, top)
nR = min(self.right, right)
nB = min(self.bottom, bottom)
if nL < nR and nT < nB:
self.left, self.top, self.right, self.bottom = nL, nT, nR, nB
return True
return False
def intersects(self, left: int, top: int, right: int, bottom: int) -> bool:
return self.left < right and self.right > left \
and self.top < bottom and self.bottom > top
@staticmethod
def intersects(a: "Rect", b: "Rect") -> bool:
return a.left < b.right and a.right > b.left \
and a.top < b.bottom and a.bottom > b.top
def union(self, left: int, top: int, right: int, bottom: int):
if self.isEmpty():
self.set(left, top, right, bottom)
else:
self.left = min(self.left, left)
self.top = min(self.top, top)
self.right = max(self.right, right)
self.bottom = max(self.bottom, bottom)
def sort(self):
if self.left > self.right:
self.left, self.right = self.right, self.left
if self.top > self.bottom:
self.top, self.bottom = self.bottom, self.top
def isEmpty(self) -> bool:
return self.left >= self.right or self.top >= self.bottom
def setEmpty(self):
self.left = self.top = self.right = self.bottom = 0
def toShortString(self) -> str:
return f"[{self.left},{self.top}][{self.right},{self.bottom}]"
def flattenToString(self) -> str:
return f"{self.left} {self.top} {self.right} {self.bottom}"
@staticmethod
def unflattenFromString(s: str) -> "Rect":
parts = s.split()
if len(parts) >= 4:
return Rect(int(parts[0]), int(parts[1]), int(parts[2]), int(parts[3]))
return Rect()
def toString(self) -> str:
return f"Rect({self.left}, {self.top}, {self.right}, {self.bottom})"
# ═══════════════════════════════════════════════════════════════
# RectF
# ═══════════════════════════════════════════════════════════════
class RectF(Stub):
"""RectF — 矩形 (float 坐标)"""
def __init__(self, left: float = 0.0, top: float = 0.0, right: float = 0.0, bottom: float = 0.0):
super().__init__()
self.left = left
self.top = top
self.right = right
self.bottom = bottom
def width(self) -> float:
return self.right - self.left
def height(self) -> float:
return self.bottom - self.top
def centerX(self) -> float:
return (self.left + self.right) * 0.5
def centerY(self) -> float:
return (self.top + self.bottom) * 0.5
def set(self, left: float, top: float, right: float, bottom: float):
self.left, self.top, self.right, self.bottom = left, top, right, bottom
def offset(self, dx: float, dy: float):
self.left += dx
self.top += dy
self.right += dx
self.bottom += dy
def offsetTo(self, newLeft: float, newTop: float):
self.right += newLeft - self.left
self.bottom += newTop - self.top
self.left = newLeft
self.top = newTop
def inset(self, dx: float, dy: float):
self.left += dx
self.top += dy
self.right -= dx
self.bottom -= dy
def contains(self, x: float, y: float) -> bool:
return self.left < self.right and self.top < self.bottom \
and x >= self.left and x < self.right and y >= self.top and y < self.bottom
def intersect(self, left: float, top: float, right: float, bottom: float) -> bool:
nL = max(self.left, left)
nT = max(self.top, top)
nR = min(self.right, right)
nB = min(self.bottom, bottom)
if nL < nR and nT < nB:
self.left, self.top, self.right, self.bottom = nL, nT, nR, nB
return True
return False
def intersects(self, left: float, top: float, right: float, bottom: float) -> bool:
return self.left < right and self.right > left \
and self.top < bottom and self.bottom > top
@staticmethod
def intersects(a: "RectF", b: "RectF") -> bool:
return a.left < b.right and a.right > b.left \
and a.top < b.bottom and a.bottom > b.top
def union(self, left: float, top: float, right: float, bottom: float):
if self.isEmpty():
self.set(left, top, right, bottom)
else:
self.left = min(self.left, left)
self.top = min(self.top, top)
self.right = max(self.right, right)
self.bottom = max(self.bottom, bottom)
def round(self, r: Rect):
r.left = int(self.left + 0.5)
r.top = int(self.top + 0.5)
r.right = int(self.right + 0.5)
r.bottom = int(self.bottom + 0.5)
def roundOut(self, r: Rect):
r.left = int(math.floor(self.left))
r.top = int(math.floor(self.top))
r.right = int(math.ceil(self.right))
r.bottom = int(math.ceil(self.bottom))
def sort(self):
if self.left > self.right:
self.left, self.right = self.right, self.left
if self.top > self.bottom:
self.top, self.bottom = self.bottom, self.top
def isEmpty(self) -> bool:
return self.left >= self.right or self.top >= self.bottom
def setEmpty(self):
self.left = self.top = self.right = self.bottom = 0.0
def toShortString(self) -> str:
return f"[{self.left:.1f},{self.top:.1f}][{self.right:.1f},{self.bottom:.1f}]"
def toString(self) -> str:
return f"RectF({self.left}, {self.top}, {self.right}, {self.bottom})"
# ═══════════════════════════════════════════════════════════════
# Point / PointF
# ═══════════════════════════════════════════════════════════════
class Point(Stub):
"""Point — 整数坐标点"""
def __init__(self, x: int = 0, y: int = 0):
super().__init__()
self.x = x
self.y = y
def set(self, x: int, y: int):
self.x, self.y = x, y
def offset(self, dx: int, dy: int):
self.x += dx
self.y += dy
def equals(self, other) -> bool:
if isinstance(other, Point):
return self.x == other.x and self.y == other.y
return False
def hashCode(self) -> int:
return self.x * 32713 + self.y
def toString(self) -> str:
return f"Point({self.x}, {self.y})"
class PointF(Stub):
"""PointF — 浮点坐标点"""
def __init__(self, x: float = 0.0, y: float = 0.0):
super().__init__()
self.x = x
self.y = y
def set(self, x: float, y: float):
self.x, self.y = x, y
def offset(self, dx: float, dy: float):
self.x += dx
self.y += dy
def equals(self, other) -> bool:
if isinstance(other, PointF):
return self.x == other.x and self.y == other.y
return False
def hashCode(self) -> int:
return int(self.x * 32713 + self.y)
def toString(self) -> str:
return f"PointF({self.x}, {self.y})"
# ═══════════════════════════════════════════════════════════════
# Path
# ═══════════════════════════════════════════════════════════════
class Path(Stub):
"""Path — 路径"""
class Direction:
CW = 0
CCW = 1
class FillType:
WINDING = 0
EVEN_ODD = 1
INVERSE_WINDING = 2
INVERSE_EVEN_ODD = 3
class Op:
DIFFERENCE = 0
INTERSECT = 1
UNION = 2
XOR = 3
REVERSE_DIFFERENCE = 4
def __init__(self):
super().__init__()
self._segments: List[dict] = []
self._fill_type = Path.FillType.WINDING
self._start_x = 0.0
self._start_y = 0.0
self._current_x = 0.0
self._current_y = 0.0
def moveTo(self, x: float, y: float):
self._segments.append({"type": "move", "x": x, "y": y})
self._current_x = x
self._current_y = y
def lineTo(self, x: float, y: float):
self._segments.append({"type": "line", "x": x, "y": y})
self._current_x = x
self._current_y = y
def quadTo(self, x1: float, y1: float, x2: float, y2: float):
self._segments.append({"type": "quad", "x1": x1, "y1": y1, "x2": x2, "y2": y2})
self._current_x = x2
self._current_y = y2
def cubicTo(self, x1: float, y1: float, x2: float, y2: float, x3: float, y3: float):
self._segments.append({"type": "cubic", "x1": x1, "y1": y1, "x2": x2, "y2": y2, "x3": x3, "y3": y3})
self._current_x = x3
self._current_y = y3
def rMoveTo(self, dx: float, dy: float):
self.moveTo(self._current_x + dx, self._current_y + dy)
def rLineTo(self, dx: float, dy: float):
self.lineTo(self._current_x + dx, self._current_y + dy)
def rQuadTo(self, dx1: float, dy1: float, dx2: float, dy2: float):
self.quadTo(self._current_x + dx1, self._current_y + dy1,
self._current_x + dx2, self._current_y + dy2)
def rCubicTo(self, dx1: float, dy1: float, dx2: float, dy2: float, dx3: float, dy3: float):
self.cubicTo(self._current_x + dx1, self._current_y + dy1,
self._current_x + dx2, self._current_y + dy2,
self._current_x + dx3, self._current_y + dy3)
def addRect(self, left: float, top: float, right: float, bottom: float,
dir: int = Direction.CW):
self.moveTo(left, top)
self.lineTo(right, top)
self.lineTo(right, bottom)
self.lineTo(left, bottom)
self.close()
def addRoundRect(self, rect: Union["RectF", tuple], rx: float, ry: float,
dir: int = Direction.CW):
if isinstance(rect, RectF):
l, t, r, b = rect.left, rect.top, rect.right, rect.bottom
else:
l, t, r, b = rect
self.moveTo(l + rx, t)
self.lineTo(r - rx, t)
self.quadTo(r, t, r, t + ry)
self.lineTo(r, b - ry)
self.quadTo(r, b, r - rx, b)
self.lineTo(l + rx, b)
self.quadTo(l, b, l, b - ry)
self.lineTo(l, t + ry)
self.quadTo(l, t, l + rx, t)
self.close()
def addCircle(self, x: float, y: float, radius: float, dir: int = Direction.CW):
k = 0.5522847498
self.moveTo(x + radius, y)
self.cubicTo(x + radius, y + radius * k, x + radius * k, y + radius, x, y + radius)
self.cubicTo(x - radius * k, y + radius, x - radius, y + radius * k, x - radius, y)
self.cubicTo(x - radius, y - radius * k, x - radius * k, y - radius, x, y - radius)
self.cubicTo(x + radius * k, y - radius, x + radius, y - radius * k, x + radius, y)
self.close()
def addOval(self, rect: Union["RectF", tuple], dir: int = Direction.CW):
if isinstance(rect, RectF):
l, t, r, b = rect.left, rect.top, rect.right, rect.bottom
else:
l, t, r, b = rect
cx = (l + r) / 2.0
cy = (t + b) / 2.0
rx = (r - l) / 2.0
ry = (b - t) / 2.0
kx = rx * 0.5522847498
ky = ry * 0.5522847498
self.moveTo(cx + rx, cy)
self.cubicTo(cx + rx, cy + ky, cx + kx, cy + ry, cx, cy + ry)
self.cubicTo(cx - kx, cy + ry, cx - rx, cy + ky, cx - rx, cy)
self.cubicTo(cx - rx, cy - ky, cx - kx, cy - ry, cx, cy - ry)
self.cubicTo(cx + kx, cy - ry, cx + rx, cy - ky, cx + rx, cy)
self.close()
def addArc(self, rect: Union["RectF", tuple], startAngle: float, sweepAngle: float):
if isinstance(rect, RectF):
l, t, r, b = rect.left, rect.top, rect.right, rect.bottom
else:
l, t, r, b = rect
cx = (l + r) / 2.0
cy = (t + b) / 2.0
rx = (r - l) / 2.0
ry = (b - t) / 2.0
start_rad = math.radians(startAngle)
end_rad = math.radians(startAngle + sweepAngle)
segments = 4
da = (end_rad - start_rad) / segments
for i in range(segments + 1):
a = start_rad + i * da
px = cx + rx * math.cos(a)
py = cy + ry * math.sin(a)
if i == 0:
self.moveTo(px, py)
else:
self.lineTo(px, py)
def addPath(self, path: "Path", dx: float = 0.0, dy: float = 0.0):
for seg in path._segments:
s = dict(seg)
if "x" in s:
s["x"] = s["x"] + dx
if "y" in s:
s["y"] = s["y"] + dy
if "x1" in s:
s["x1"] = s["x1"] + dx
if "y1" in s:
s["y1"] = s["y1"] + dy
if "x2" in s:
s["x2"] = s["x2"] + dx
if "y2" in s:
s["y2"] = s["y2"] + dy
if "x3" in s:
s["x3"] = s["x3"] + dx
if "y3" in s:
s["y3"] = s["y3"] + dy
self._segments.append(s)
self._current_x = path._current_x + dx
self._current_y = path._current_y + dy
def arcTo(self, rect: Union["RectF", tuple], startAngle: float, sweepAngle: float,
forceMoveTo: bool = False):
if isinstance(rect, RectF):
l, t, r, b = rect.left, rect.top, rect.right, rect.bottom
else:
l, t, r, b = rect
cx = (l + r) / 2.0
cy = (t + b) / 2.0
rx = (r - l) / 2.0
ry = (b - t) / 2.0
start_rad = math.radians(startAngle)
end_rad = math.radians(startAngle + sweepAngle)
segments = 4
da = (end_rad - start_rad) / segments
for i in range(segments + 1):
a = start_rad + i * da
px = cx + rx * math.cos(a)
py = cy + ry * math.sin(a)
if i == 0 and forceMoveTo:
self.moveTo(px, py)
elif i == 0:
self.lineTo(px, py)
else:
self.lineTo(px, py)
def close(self):
self._segments.append({"type": "close"})
def reset(self):
self._segments.clear()
self._current_x = 0.0
self._current_y = 0.0
def rewind(self):
self._segments.clear()
self._current_x = 0.0
self._current_y = 0.0
def set(self, src: "Path"):
self._segments = list(src._segments)
self._fill_type = src._fill_type
self._current_x = src._current_x
self._current_y = src._current_y
def computeBounds(self, exact: bool = False) -> RectF:
if not self._segments:
return RectF(0, 0, 0, 0)
min_x = float("inf")
min_y = float("inf")
max_x = float("-inf")
max_y = float("-inf")
for seg in self._segments:
for key in ("x", "x1", "x2", "x3"):
if key in seg:
if seg[key] < min_x:
min_x = seg[key]
if seg[key] > max_x:
max_x = seg[key]
for key in ("y", "y1", "y2", "y3"):
if key in seg:
if seg[key] < min_y:
min_y = seg[key]
if seg[key] > max_y:
max_y = seg[key]
if min_x == float("inf"):
return RectF(0, 0, 0, 0)
return RectF(min_x, min_y, max_x, max_y)
def incReserve(self, extraPtCount: int):
pass
def op(self, other: "Path", op: int) -> bool:
return False
def isRect(self) -> bool:
if len(self._segments) != 5:
return False
types = [s["type"] for s in self._segments]
return types == ["move", "line", "line", "line", "close"]
def isConvex(self) -> bool:
return True
def setFillType(self, ft: int):
self._fill_type = ft
def getFillType(self) -> int:
return self._fill_type
def toggleReverseFillType(self):
mapping = {
Path.FillType.WINDING: Path.FillType.INVERSE_WINDING,
Path.FillType.EVEN_ODD: Path.FillType.INVERSE_EVEN_ODD,
Path.FillType.INVERSE_WINDING: Path.FillType.WINDING,
Path.FillType.INVERSE_EVEN_ODD: Path.FillType.EVEN_ODD,
}
self._fill_type = mapping.get(self._fill_type, Path.FillType.WINDING)
# ═══════════════════════════════════════════════════════════════
# PathMeasure
# ═══════════════════════════════════════════════════════════════
class PathMeasure(Stub):
"""PathMeasure — 路径测量"""
def __init__(self, path: Path = None, forceClosed: bool = False):
super().__init__()
self._path = path
self._force_closed = forceClosed
def setPath(self, path: Path, forceClosed: bool):
self._path = path
self._force_closed = forceClosed
def getLength(self) -> float:
if not self._path:
return 0.0
bounds = self._path.computeBounds()
return math.sqrt(bounds.width() ** 2 + bounds.height() ** 2)
def getPosTan(self, distance: float, pos: List[float], tan: List[float]) -> bool:
if not self._path:
return False
if len(pos) >= 2:
pos[0] = 0.0
pos[1] = 0.0
if len(tan) >= 2:
tan[0] = 1.0
tan[1] = 0.0
return True
def getMatrix(self, distance: float, matrix: "Matrix", flags: int) -> bool:
return False
def getSegment(self, startD: float, stopD: float, dst: Path, startWithMoveTo: bool) -> bool:
return False
def isClosed(self) -> bool:
return self._force_closed
def nextContour(self) -> bool:
return False
# ═══════════════════════════════════════════════════════════════
# PorterDuff
# ═══════════════════════════════════════════════════════════════
class PorterDuff(Stub):
"""PorterDuff — 图形混合模式"""
class Mode:
CLEAR = 0
SRC = 1
DST = 2
SRC_OVER = 3
DST_OVER = 4
SRC_IN = 5
DST_IN = 6
SRC_OUT = 7
DST_OUT = 8
SRC_ATOP = 9
DST_ATOP = 10
XOR = 11
ADD = 12
MULTIPLY = 13
SCREEN = 14
OVERLAY = 15
DARKEN = 16
LIGHTEN = 17
# ═══════════════════════════════════════════════════════════════
# ClipOp
# ═══════════════════════════════════════════════════════════════
class ClipOp(Stub):
"""ClipOp — 裁剪操作"""
DIFFERENCE = 0
INTERSECT = 1
REPLACE = 2
# ═══════════════════════════════════════════════════════════════
# BlendMode
# ═══════════════════════════════════════════════════════════════
class BlendMode(Stub):
"""BlendMode — Android 29+ 混合模式"""
CLEAR = 0
SRC = 1
DST = 2
SRC_OVER = 3
DST_OVER = 4
SRC_IN = 5
DST_IN = 6
SRC_OUT = 7
DST_OUT = 8
SRC_ATOP = 9
DST_ATOP = 10
XOR = 11
PLUS = 12
MODULATE = 13
SCREEN = 14
OVERLAY = 15
DARKEN = 16
LIGHTEN = 17
# ═══════════════════════════════════════════════════════════════
# BlendModeColorFilter
# ═══════════════════════════════════════════════════════════════
class BlendModeColorFilter(Stub):
"""BlendModeColorFilter — 混合模式颜色过滤器"""
def __init__(self, color: int, mode):
super().__init__()
self._color = color
self._mode = mode
def getColor(self) -> int:
return self._color
def getMode(self):
return self._mode
# ═══════════════════════════════════════════════════════════════
# ColorFilter
# ═══════════════════════════════════════════════════════════════
class ColorFilter(Stub):
"""ColorFilter — 颜色过滤器的抽象基类"""
pass
# ═══════════════════════════════════════════════════════════════
# PorterDuffColorFilter
# ═══════════════════════════════════════════════════════════════
class PorterDuffColorFilter(ColorFilter):
"""PorterDuffColorFilter — PorterDuff 颜色过滤器"""
def __init__(self, color: int, mode):
super().__init__()
self._color = color
self._mode = mode
def getColor(self) -> int:
return self._color
def getMode(self):
return self._mode
# ═══════════════════════════════════════════════════════════════
# LightingColorFilter
# ═══════════════════════════════════════════════════════════════
class LightingColorFilter(ColorFilter):
"""LightingColorFilter — 光照颜色过滤器"""
def __init__(self, mul: int, add: int):
super().__init__()
self._mul = mul
self._add = add
def getMultiply(self) -> int:
return self._mul
def getAdd(self) -> int:
return self._add
# ═══════════════════════════════════════════════════════════════
# ComposeColorFilter
# ═══════════════════════════════════════════════════════════════
class ComposeColorFilter(ColorFilter):
"""ComposeColorFilter — 组合颜色过滤器"""
def __init__(self, cf1: ColorFilter, cf2: ColorFilter):
super().__init__()
self._cf1 = cf1
self._cf2 = cf2
def getColorFilters(self) -> Tuple[ColorFilter, ColorFilter]:
return (self._cf1, self._cf2)
# ═══════════════════════════════════════════════════════════════
# ColorMatrix
# ═══════════════════════════════════════════════════════════════
class ColorMatrix(Stub):
"""ColorMatrix — 4x5 颜色变换矩阵"""
def __init__(self, src: "ColorMatrix" = None):
super().__init__()
if src:
self._array = list(src._array)
else:
self._array = [1, 0, 0, 0, 0,
0, 1, 0, 0, 0,
0, 0, 1, 0, 0,
0, 0, 0, 1, 0]
def getArray(self) -> List[float]:
return self._array
def set(self, src: "ColorMatrix"):
self._array = list(src._array)
def reset(self):
self._array = [1, 0, 0, 0, 0,
0, 1, 0, 0, 0,
0, 0, 1, 0, 0,
0, 0, 0, 1, 0]
def setSaturation(self, sat: float):
R_lum = 0.213
G_lum = 0.715
B_lum = 0.072
one_minus = 1.0 - sat
self._array = [
R_lum * one_minus + sat, G_lum * one_minus, B_lum * one_minus, 0, 0,
R_lum * one_minus, G_lum * one_minus + sat, B_lum * one_minus, 0, 0,
R_lum * one_minus, G_lum * one_minus, B_lum * one_minus + sat, 0, 0,
0, 0, 0, 1, 0,
]
def setRotate(self, axis: int, degrees: float):
rad = math.radians(degrees)
c = math.cos(rad)
s = math.sin(rad)
self.reset()
if axis == 0:
self._array[5] = c
self._array[6] = s
self._array[9] = -s
self._array[10] = c
elif axis == 1:
self._array[0] = c
self._array[2] = -s
self._array[8] = s
self._array[10] = c
elif axis == 2:
self._array[0] = c
self._array[1] = s
self._array[4] = -s
self._array[5] = c
def setConcat(self, a: "ColorMatrix", b: "ColorMatrix"):
a_arr = a._array
b_arr = b._array
result = [0.0] * 20
for i in range(4):
for j in range(5):
s = 0.0
for k in range(4):
s += a_arr[i * 5 + k] * b_arr[k * 5 + j]
result[i * 5 + j] = s
self._array = result
def preConcat(self, other: "ColorMatrix"):
new = ColorMatrix()
new.setConcat(self, other)
self._array = new._array
def postConcat(self, other: "ColorMatrix"):
new = ColorMatrix()
new.setConcat(other, self)
self._array = new._array
def setScale(self, rScale: float, gScale: float, bScale: float, aScale: float):
self._array = [
rScale, 0, 0, 0, 0,
0, gScale, 0, 0, 0,
0, 0, bScale, 0, 0,
0, 0, 0, aScale, 0,
]
# ═══════════════════════════════════════════════════════════════
# ColorMatrixColorFilter
# ═══════════════════════════════════════════════════════════════
class ColorMatrixColorFilter(ColorFilter):
"""ColorMatrixColorFilter — 颜色矩阵过滤器"""
def __init__(self, matrix: ColorMatrix):
super().__init__()
self._matrix = matrix
def getMatrix(self) -> ColorMatrix:
return self._matrix
# ═══════════════════════════════════════════════════════════════
# Matrix
# ═══════════════════════════════════════════════════════════════
class Matrix(Stub):
"""Matrix — 3x3 变换矩阵"""
IDENTITY_MATRIX: "Matrix" = None
MPERSP_0 = 6
MPERSP_1 = 7
MPERSP_2 = 8
MSCALE_X = 0
MSCALE_Y = 4
MSKEW_X = 1
MSKEW_Y = 3
MTRANS_X = 2
MTRANS_Y = 5
def __init__(self, src: "Matrix" = None):
super().__init__()
if src:
self._values = list(src._values)
else:
self._values = [1, 0, 0,
0, 1, 0,
0, 0, 1]
def getValues(self) -> List[float]:
return self._values
def setValues(self, values: List[float]):
self._values = list(values)
def reset(self):
self._values = [1, 0, 0, 0, 1, 0, 0, 0, 1]
def isIdentity(self) -> bool:
return self._values == [1, 0, 0, 0, 1, 0, 0, 0, 1]
def invert(self, inverse: "Matrix") -> bool:
v = self._values
det = v[0] * (v[4] * v[8] - v[5] * v[7]) \
- v[1] * (v[3] * v[8] - v[5] * v[6]) \
+ v[2] * (v[3] * v[7] - v[4] * v[6])
if abs(det) < 1e-10:
return False
inv_det = 1.0 / det
inv = [
(v[4] * v[8] - v[5] * v[7]) * inv_det,
(v[2] * v[7] - v[1] * v[8]) * inv_det,
(v[1] * v[5] - v[2] * v[4]) * inv_det,
(v[5] * v[6] - v[3] * v[8]) * inv_det,
(v[0] * v[8] - v[2] * v[6]) * inv_det,
(v[2] * v[3] - v[0] * v[5]) * inv_det,
(v[3] * v[7] - v[4] * v[6]) * inv_det,
(v[1] * v[6] - v[0] * v[7]) * inv_det,
(v[0] * v[4] - v[1] * v[3]) * inv_det,
]
inverse._values = inv
return True
def setTranslate(self, dx: float, dy: float):
self._values = [1, 0, dx, 0, 1, dy, 0, 0, 1]
def setScale(self, sx: float, sy: float, px: float = 0.0, py: float = 0.0):
self._values = [sx, 0, px - sx * px, 0, sy, py - sy * py, 0, 0, 1]
def setRotate(self, degrees: float, px: float = 0.0, py: float = 0.0):
rad = math.radians(degrees)
c = math.cos(rad)
s = math.sin(rad)
tx = px - px * c + py * s
ty = py - px * s - py * c
self._values = [c, -s, tx, s, c, ty, 0, 0, 1]
def setSkew(self, kx: float, ky: float, px: float = 0.0, py: float = 0.0):
self._values = [1, kx, -kx * px, ky, 1, -ky * py, 0, 0, 1]
def setConcat(self, a: "Matrix", b: "Matrix"):
va, vb = a._values, b._values
self._values = [
va[0] * vb[0] + va[1] * vb[3] + va[2] * vb[6],
va[0] * vb[1] + va[1] * vb[4] + va[2] * vb[7],
va[0] * vb[2] + va[1] * vb[5] + va[2] * vb[8],
va[3] * vb[0] + va[4] * vb[3] + va[5] * vb[6],
va[3] * vb[1] + va[4] * vb[4] + va[5] * vb[7],
va[3] * vb[2] + va[4] * vb[5] + va[5] * vb[8],
va[6] * vb[0] + va[7] * vb[3] + va[8] * vb[6],
va[6] * vb[1] + va[7] * vb[4] + va[8] * vb[7],
va[6] * vb[2] + va[7] * vb[5] + va[8] * vb[8],
]
def preConcat(self, other: "Matrix"):
new = Matrix()
new.setConcat(self, other)
self._values = new._values
def postConcat(self, other: "Matrix"):
new = Matrix()
new.setConcat(other, self)
self._values = new._values
def preTranslate(self, dx: float, dy: float):
m = Matrix()
m.setTranslate(dx, dy)
self.preConcat(m)
def preScale(self, sx: float, sy: float, px: float = 0.0, py: float = 0.0):
m = Matrix()
m.setScale(sx, sy, px, py)
self.preConcat(m)
def preRotate(self, degrees: float, px: float = 0.0, py: float = 0.0):
m = Matrix()
m.setRotate(degrees, px, py)
self.preConcat(m)
def preSkew(self, kx: float, ky: float, px: float = 0.0, py: float = 0.0):
m = Matrix()
m.setSkew(kx, ky, px, py)
self.preConcat(m)
def postTranslate(self, dx: float, dy: float):
m = Matrix()
m.setTranslate(dx, dy)
self.postConcat(m)
def postScale(self, sx: float, sy: float, px: float = 0.0, py: float = 0.0):
m = Matrix()
m.setScale(sx, sy, px, py)
self.postConcat(m)
def postRotate(self, degrees: float, px: float = 0.0, py: float = 0.0):
m = Matrix()
m.setRotate(degrees, px, py)
self.postConcat(m)
def postSkew(self, kx: float, ky: float, px: float = 0.0, py: float = 0.0):
m = Matrix()
m.setSkew(kx, ky, px, py)
self.postConcat(m)
def setRectToRect(self, src: RectF, dst: RectF, stf: int) -> bool:
if src.isEmpty() or dst.isEmpty():
return False
sx = dst.width() / src.width()
sy = dst.height() / src.height()
tx = dst.left - src.left * sx
ty = dst.top - src.top * sy
self._values = [sx, 0, tx, 0, sy, ty, 0, 0, 1]
return True
def mapRect(self, src: RectF, dst: RectF = None) -> RectF:
pts = [src.left, src.top, src.right, src.bottom]
self.mapPoints(pts)
if dst:
dst.set(pts[0], pts[1], pts[2], pts[3])
return dst
return RectF(pts[0], pts[1], pts[2], pts[3])
def mapPoints(self, pts: List[float], offset: int = 0, count: int = -1):
if count < 0:
count = len(pts) - offset
v = self._values
for i in range(offset, offset + count, 2):
x = pts[i]
y = pts[i + 1]
pts[i] = x * v[0] + y * v[1] + v[2]
pts[i + 1] = x * v[3] + y * v[4] + v[5]
def mapVectors(self, vecs: List[float]):
v = self._values
for i in range(0, len(vecs), 2):
x = vecs[i]
y = vecs[i + 1]
vecs[i] = x * v[0] + y * v[1]
vecs[i + 1] = x * v[3] + y * v[4]
def mapRadius(self, radius: float) -> float:
v = self._values
sx = math.sqrt(v[0] * v[0] + v[3] * v[3])
sy = math.sqrt(v[1] * v[1] + v[4] * v[4])
return radius * max(sx, sy)
def toString(self) -> str:
return f"Matrix({self._values})"
Matrix.IDENTITY_MATRIX = Matrix()
# ═══════════════════════════════════════════════════════════════
# Shader
# ═══════════════════════════════════════════════════════════════
class Shader(Stub):
"""Shader — 着色器基类"""
class TileMode:
CLAMP = 0
REPEAT = 1
MIRROR = 2
def __init__(self):
super().__init__()
self._local_matrix = None
def setLocalMatrix(self, matrix: Matrix):
self._local_matrix = matrix
def getLocalMatrix(self) -> Optional[Matrix]:
return self._local_matrix
# ═══════════════════════════════════════════════════════════════
# LinearGradient
# ═══════════════════════════════════════════════════════════════
class LinearGradient(Shader):
"""LinearGradient — 线性渐变"""
def __init__(self, x0: float, y0: float, x1: float, y1: float,
colors: Sequence[int], positions: Sequence[float],
tile: int = Shader.TileMode.CLAMP):
super().__init__()
self._x0, self._y0 = x0, y0
self._x1, self._y1 = x1, y1
self._colors = list(colors)
self._positions = list(positions) if positions else None
self._tile = tile
# ═══════════════════════════════════════════════════════════════
# RadialGradient
# ═══════════════════════════════════════════════════════════════
class RadialGradient(Shader):
"""RadialGradient — 径向渐变"""
CENTER_COLOR = 0
EDGE_COLOR = 1
def __init__(self, x: float, y: float, radius: float,
colors: Sequence[int], positions: Sequence[float],
tile: int = Shader.TileMode.CLAMP,
centerColor: int = Shader.TileMode.CLAMP):
"""centerColor is ignored, kept for API compatibility"""
super().__init__()
self._x, self._y = x, y
self._radius = radius
self._colors = list(colors)
self._positions = list(positions) if positions else None
self._tile = tile
def getX(self) -> float:
return self._x
def getY(self) -> float:
return self._y
def getRadius(self) -> float:
return self._radius
# ═══════════════════════════════════════════════════════════════
# SweepGradient
# ═══════════════════════════════════════════════════════════════
class SweepGradient(Shader):
"""SweepGradient — 扫描渐变"""
def __init__(self, x: float, y: float,
colors: Sequence[int], positions: Sequence[float]):
super().__init__()
self._x, self._y = x, y
self._colors = list(colors)
self._positions = list(positions) if positions else None
def getX(self) -> float:
return self._x
def getY(self) -> float:
return self._y
# ═══════════════════════════════════════════════════════════════
# BitmapShader
# ═══════════════════════════════════════════════════════════════
class BitmapShader(Shader):
"""BitmapShader — 位图着色器"""
def __init__(self, bitmap: "Bitmap", tileX: int, tileY: int):
super().__init__()
self._bitmap = bitmap
self._tile_x = tileX
self._tile_y = tileY
def getBitmap(self) -> "Bitmap":
return self._bitmap
# ═══════════════════════════════════════════════════════════════
# ComposeShader
# ═══════════════════════════════════════════════════════════════
class ComposeShader(Shader):
"""ComposeShader — 组合着色器"""
def __init__(self, shaderA: Shader, shaderB: Shader, mode):
super().__init__()
self._shader_a = shaderA
self._shader_b = shaderB
self._mode = mode
# ═══════════════════════════════════════════════════════════════
# MaskFilter / BlurMaskFilter / EmbossMaskFilter
# ═══════════════════════════════════════════════════════════════
class MaskFilter(Stub):
"""MaskFilter — 遮罩过滤器基类"""
pass
class BlurMaskFilter(MaskFilter):
"""BlurMaskFilter — 模糊遮罩过滤器"""
NORMAL = 0
SOLID = 1
OUTER = 2
INNER = 3
def __init__(self, radius: float, style: int):
super().__init__()
self._radius = radius
self._style = style
def getRadius(self) -> float:
return self._radius
def getStyle(self) -> int:
return self._style
class EmbossMaskFilter(MaskFilter):
"""EmbossMaskFilter — 浮雕遮罩过滤器"""
def __init__(self, direction: List[float], ambient: float, specular: float, blurRadius: float):
super().__init__()
self._direction = list(direction)
self._ambient = ambient
self._specular = specular
self._blur_radius = blurRadius
# ═══════════════════════════════════════════════════════════════
# PathEffect hierarchy
# ═══════════════════════════════════════════════════════════════
class PathEffect(Stub):
"""PathEffect — 路径效果基类"""
pass
class ComposePathEffect(PathEffect):
"""ComposePathEffect — 组合路径效果"""
def __init__(self, outerpe: PathEffect, innerpe: PathEffect):
super().__init__()
self._outer = outerpe
self._inner = innerpe
class CornerPathEffect(PathEffect):
"""CornerPathEffect — 圆角路径效果"""
def __init__(self, radius: float):
super().__init__()
self._radius = radius
class DashPathEffect(PathEffect):
"""DashPathEffect — 虚线路径效果"""
def __init__(self, intervals: List[float], phase: float):
super().__init__()
self._intervals = list(intervals)
self._phase = phase
class DiscretePathEffect(PathEffect):
"""DiscretePathEffect — 离散路径效果"""
def __init__(self, segmentLength: float, deviation: float):
super().__init__()
self._segment_length = segmentLength
self._deviation = deviation
class PathDashPathEffect(PathEffect):
"""PathDashPathEffect — 路径虚线效果"""
ROTATE = 0
MORPH = 1
TRANSLATE = 2
def __init__(self, shape: Path, advance: float, phase: float, style: int):
super().__init__()
self._shape = shape
self._advance = advance
self._phase = phase
self._style = style
class SumPathEffect(PathEffect):
"""SumPathEffect — 求和路径效果"""
def __init__(self, first: PathEffect, second: PathEffect):
super().__init__()
self._first = first
self._second = second
# ═══════════════════════════════════════════════════════════════
# Xfermode hierarchy
# ═══════════════════════════════════════════════════════════════
class Xfermode(Stub):
"""Xfermode — 传输模式基类"""
pass
class PorterDuffXfermode(Xfermode):
"""PorterDuffXfermode — PorterDuff 传输模式"""
def __init__(self, mode):
super().__init__()
self._mode = mode
def getMode(self):
return self._mode
class AvoidXfermode(Xfermode):
"""AvoidXfermode — deprecated, kept for API compatibility"""
TARGET = 0
AVOID = 1
def __init__(self, opColor: int, tolerance: int, mode: int):
super().__init__()
self._op_color = opColor
self._tolerance = tolerance
self._mode = mode
class PixelXorXfermode(Xfermode):
"""PixelXorXfermode — deprecated, kept for API compatibility"""
def __init__(self, opColor: int):
super().__init__()
self._op_color = opColor
# ═══════════════════════════════════════════════════════════════
# DrawFilter
# ═══════════════════════════════════════════════════════════════
class DrawFilter(Stub):
"""DrawFilter — 绘制过滤器基类"""
pass
class PaintFlagsDrawFilter(DrawFilter):
"""PaintFlagsDrawFilter — 画笔标志绘制过滤器"""
def __init__(self, clearBits: int, setBits: int):
super().__init__()
self._clear_bits = clearBits
self._set_bits = setBits
# ═══════════════════════════════════════════════════════════════
# Camera
# ═══════════════════════════════════════════════════════════════
class Camera(Stub):
"""Camera — 3D 相机变换"""
def __init__(self):
super().__init__()
self._location_x = 0.0
self._location_y = 0.0
self._location_z = -8.0
def save(self):
pass
def restore(self):
pass
def translate(self, x: float, y: float, z: float):
self._location_x += x
self._location_y += y
self._location_z += z
def rotateX(self, deg: float):
pass
def rotateY(self, deg: float):
pass
def rotateZ(self, deg: float):
pass
def rotate(self, x: float, y: float, z: float):
pass
def getMatrix(self, matrix: Matrix):
matrix.reset()
def applyToCanvas(self, canvas: "Canvas"):
pass
def setLocation(self, x: float, y: float, z: float):
self._location_x = x
self._location_y = y
self._location_z = z
def dotWithNormal(self, dx: float, dy: float, dz: float) -> float:
return 0.0
# ═══════════════════════════════════════════════════════════════
# LayerRasterizer / Rasterizer
# ═══════════════════════════════════════════════════════════════
class Rasterizer(Stub):
"""Rasterizer — 光栅化基类 (deprecated)"""
pass
class LayerRasterizer(Rasterizer):
"""LayerRasterizer — 分层光栅化 (deprecated)"""
def __init__(self):
super().__init__()
def addLayer(self, paint: "Paint", dx: float = 0.0, dy: float = 0.0):
pass
# ═══════════════════════════════════════════════════════════════
# NinePatch
# ═══════════════════════════════════════════════════════════════
class NinePatch(Stub):
"""NinePatch — 九宫格图片"""
def __init__(self, bitmap: "Bitmap", chunk: bytes):
super().__init__()
self._bitmap = bitmap
self._chunk = chunk if chunk else b""
def getBitmap(self) -> "Bitmap":
return self._bitmap
def getWidth(self) -> int:
return self._bitmap.getWidth() if self._bitmap else 0
def getHeight(self) -> int:
return self._bitmap.getHeight() if self._bitmap else 0
@staticmethod
def isNinePatchChunk(chunk: bytes) -> bool:
return bool(chunk and len(chunk) > 0)
# ═══════════════════════════════════════════════════════════════
# Picture / PictureDrawable
# ═══════════════════════════════════════════════════════════════
class Picture(Stub):
"""Picture — 图画录制与回放"""
def __init__(self):
super().__init__()
self._recorded = []
def beginRecording(self, width: int, height: int) -> "Canvas":
canvas = Canvas(width, height)
self._canvas = canvas
return canvas
def endRecording(self):
self._canvas = None
def draw(self, canvas: "Canvas"):
for cmd in self._recorded:
pass
def getWidth(self) -> int:
return self._canvas.getWidth() if hasattr(self, "_canvas") and self._canvas else 0
def getHeight(self) -> int:
return self._canvas.getHeight() if hasattr(self, "_canvas") and self._canvas else 0
def createFromStream(self, stream) -> "Picture":
return Picture()
def writeToStream(self, stream) -> bool:
return True
class PictureDrawable(Stub):
"""PictureDrawable — 图画 Drawable"""
def __init__(self, picture: Picture):
super().__init__()
self._picture = picture
def getPicture(self) -> Picture:
return self._picture
def setPicture(self, picture: Picture):
self._picture = picture
def draw(self, canvas: "Canvas"):
pass
# ═══════════════════════════════════════════════════════════════
# SurfaceTexture
# ═══════════════════════════════════════════════════════════════
class SurfaceTexture(Stub):
"""SurfaceTexture — 表面纹理"""
def __init__(self, texName: int = 0):
super().__init__()
self._tex_name = texName
self._timestamp = 0
def getTimestamp(self) -> int:
return self._timestamp
def updateTexImage(self):
self._timestamp = int(time.time() * 1e9)
def release(self):
pass
def setDefaultBufferSize(self, width: int, height: int):
pass
def attachToGLContext(self, texName: int):
pass
def detachFromGLContext(self):
pass
# ═══════════════════════════════════════════════════════════════
# Region / RegionIterator
# ═══════════════════════════════════════════════════════════════
class Region(Stub):
"""Region — 区域"""
OP_DIFFERENCE = 0
OP_INTERSECT = 1
OP_UNION = 2
OP_XOR = 3
OP_REVERSE_DIFFERENCE = 4
OP_REPLACE = 5
def __init__(self, *args):
super().__init__()
if len(args) == 4:
self._rect = Rect(args[0], args[1], args[2], args[3])
elif len(args) == 1 and isinstance(args[0], Rect):
self._rect = Rect(args[0].left, args[0].top, args[0].right, args[0].bottom)
else:
self._rect = Rect()
def set(self, *args) -> bool:
if len(args) == 4:
self._rect.set(args[0], args[1], args[2], args[3])
elif len(args) == 1 and isinstance(args[0], Rect):
r = args[0]
self._rect.set(r.left, r.top, r.right, r.bottom)
return True
def getBounds(self, r: Rect = None) -> Rect:
if r:
r.set(self._rect.left, self._rect.top, self._rect.right, self._rect.bottom)
return r
return Rect(self._rect.left, self._rect.top, self._rect.right, self._rect.bottom)
def isEmpty(self) -> bool:
return self._rect.isEmpty()
def contains(self, x: int, y: int) -> bool:
return self._rect.contains(x, y)
def op(self, *args) -> bool:
return False
def isRect(self) -> bool:
return True
def isComplex(self) -> bool:
return False
def toString(self) -> str:
return f"Region({self._rect})"
class RegionIterator(Stub):
"""RegionIterator — 区域迭代器"""
def __init__(self, region: Region):
super().__init__()
self._region = region
self._done = False
def next(self, r: Rect) -> bool:
if self._done:
return False
r.set(self._region._rect.left, self._region._rect.top,
self._region._rect.right, self._region._rect.bottom)
self._done = True
return True
# ═══════════════════════════════════════════════════════════════
# Paint
# ═══════════════════════════════════════════════════════════════
class Paint(Stub):
"""Paint — 画笔"""
class Style:
FILL = 0
STROKE = 1
FILL_AND_STROKE = 2
class Cap:
BUTT = 0
ROUND = 1
SQUARE = 2
class Join:
MITER = 0
ROUND = 1
BEVEL = 2
class Align:
LEFT = 0
CENTER = 1
RIGHT = 2
ANTI_ALIAS_FLAG = 0x01
FILTER_BITMAP_FLAG = 0x02
DITHER_FLAG = 0x04
UNDERLINE_TEXT_FLAG = 0x08
STRIKE_THRU_TEXT_FLAG = 0x10
FAKE_BOLD_TEXT_FLAG = 0x20
LINEAR_TEXT_FLAG = 0x40
SUBPIXEL_TEXT_FLAG = 0x80
DEV_KERN_TEXT_FLAG = 0x100
ALIGN_LEFT = 0
ALIGN_CENTER = 1
ALIGN_RIGHT = 2
def __init__(self, flags: int = 0):
super().__init__()
self._flags = flags
self._color = 0xFF000000
self._alpha = 255
self._style = Paint.Style.FILL
self._stroke_width = 0.0
self._text_size = 12.0
self._typeface = None
self._anti_alias = bool(flags & Paint.ANTI_ALIAS_FLAG)
self._color_filter = None
self._mask_filter = None
self._shader = None
self._path_effect = None
self._xfermode = None
self._filter_bitmap = bool(flags & Paint.FILTER_BITMAP_FLAG)
self._dither = bool(flags & Paint.DITHER_FLAG)
self._subpixel_text = bool(flags & Paint.SUBPIXEL_TEXT_FLAG)
self._underline_text = bool(flags & Paint.UNDERLINE_TEXT_FLAG)
self._strike_thru_text = bool(flags & Paint.STRIKE_THRU_TEXT_FLAG)
self._text_align = Paint.Align.LEFT
self._text_scale_x = 1.0
self._text_skew_x = 0.0
self._shadow_layer = None
def setColor(self, color: int):
self._color = color
self._alpha = (color >> 24) & 0xFF
def getColor(self) -> int:
return self._color
def setARGB(self, a: int, r: int, g: int, b: int):
self.setColor(Color.argb(a, r, g, b))
def setAlpha(self, alpha: int):
self._alpha = max(0, min(255, alpha))
self._color = (self._color & 0x00FFFFFF) | (self._alpha << 24)
def getAlpha(self) -> int:
return self._alpha
def setStyle(self, style: int):
self._style = style
def getStyle(self) -> int:
return self._style
def setStrokeWidth(self, width: float):
self._stroke_width = width
def getStrokeWidth(self) -> float:
return self._stroke_width
def setTextSize(self, textSize: float):
self._text_size = textSize
def getTextSize(self) -> float:
return self._text_size
def setTypeface(self, typeface: Optional[Typeface]):
self._typeface = typeface
def getTypeface(self) -> Optional[Typeface]:
return self._typeface
def setAntiAlias(self, aa: bool):
self._anti_alias = aa
def isAntiAlias(self) -> bool:
return self._anti_alias
def setColorFilter(self, filter: Optional[ColorFilter]):
self._color_filter = filter
def getColorFilter(self) -> Optional[ColorFilter]:
return self._color_filter
def setMaskFilter(self, maskfilter: Optional[MaskFilter]):
self._mask_filter = maskfilter
def getMaskFilter(self) -> Optional[MaskFilter]:
return self._mask_filter
def setShader(self, shader: Optional[Shader]):
self._shader = shader
def getShader(self) -> Optional[Shader]:
return self._shader
def setPathEffect(self, effect: Optional[PathEffect]):
self._path_effect = effect
def getPathEffect(self) -> Optional[PathEffect]:
return self._path_effect
def setXfermode(self, xfermode: Optional[Xfermode]):
self._xfermode = xfermode
def getXfermode(self) -> Optional[Xfermode]:
return self._xfermode
def setFlags(self, flags: int):
self._flags = flags
def getFlags(self) -> int:
return self._flags
def setFilterBitmap(self, filter: bool):
self._filter_bitmap = filter
def isFilterBitmap(self) -> bool:
return self._filter_bitmap
def setDither(self, dither: bool):
self._dither = dither
def isDither(self) -> bool:
return self._dither
def setSubpixelText(self, subpixel: bool):
self._subpixel_text = subpixel
def isSubpixelText(self) -> bool:
return self._subpixel_text
def setUnderlineText(self, underline: bool):
self._underline_text = underline
def isUnderlineText(self) -> bool:
return self._underline_text
def setStrikeThruText(self, strike: bool):
self._strike_thru_text = strike
def isStrikeThruText(self) -> bool:
return self._strike_thru_text
def setTextScaleX(self, scaleX: float):
self._text_scale_x = scaleX
def getTextScaleX(self) -> float:
return self._text_scale_x
def setTextSkewX(self, skewX: float):
self._text_skew_x = skewX
def getTextSkewX(self) -> float:
return self._text_skew_x
def setTextAlign(self, align: int):
self._text_align = align
def getTextAlign(self) -> int:
return self._text_align
def setTextLocales(self, locales):
pass
def setShadowLayer(self, radius: float, dx: float, dy: float, color: int):
self._shadow_layer = (radius, dx, dy, color)
def clearShadowLayer(self):
self._shadow_layer = None
def measureText(self, text: str) -> float:
return len(text) * self._text_size * 0.5
def breakText(self, text: str, maxWidth: float, measuredWidth: List[float]) -> int:
width = 0.0
count = 0
for i, ch in enumerate(text):
cw = self._text_size * 0.5
if width + cw > maxWidth:
break
width += cw
count += 1
measuredWidth[0] = width
return count
def getTextWidths(self, text: str, widths: List[float]) -> int:
cw = self._text_size * 0.5
for i, ch in enumerate(text):
widths[i] = cw
return len(text)
def getTextPath(self, text: str, start: int, end: int, x: float, y: float, path: Path):
path.moveTo(x, y)
for i in range(start, end):
path.lineTo(x + (i - start + 1) * self._text_size * 0.3, y)
def getFontMetrics(self) -> Dict[str, float]:
size = self._text_size
return {
"ascent": -size * 0.8,
"descent": size * 0.2,
"top": -size * 0.95,
"bottom": size * 0.25,
"leading": 0.0,
}
def getFontMetricsInt(self) -> Dict[str, int]:
fm = self.getFontMetrics()
return {k: int(v) for k, v in fm.items()}
def ascent(self) -> float:
return -self._text_size * 0.8
def descent(self) -> float:
return self._text_size * 0.2
# ═══════════════════════════════════════════════════════════════
# Bitmap
# ═══════════════════════════════════════════════════════════════
class Bitmap(Stub):
"""Bitmap — 位图"""
class Config:
ALPHA_8 = "ALPHA_8"
ARGB_4444 = "ARGB_4444"
ARGB_8888 = "ARGB_8888"
RGB_565 = "RGB_565"
RGBA_F16 = "RGBA_F16"
class CompressFormat:
JPEG = "JPEG"
PNG = "PNG"
WEBP = "WEBP"
WEBP_LOSSY = "WEBP_LOSSY"
WEBP_LOSSLESS = "WEBP_LOSSLESS"
def __init__(self, width: int, height: int, config: str = Config.ARGB_8888):
super().__init__()
self._width = width
self._height = height
self._config = config
self._recycled = False
self._has_alpha = config in (Bitmap.Config.ARGB_8888, Bitmap.Config.ARGB_4444,
Bitmap.Config.ALPHA_8, Bitmap.Config.RGBA_F16)
self._generation_id = 1
if HAS_PIL:
from PIL import Image
if config == Bitmap.Config.ALPHA_8:
mode = "L"
elif config == Bitmap.Config.RGB_565:
mode = "RGB"
else:
mode = "RGBA"
self._image = Image.new(mode, (width, height), (0, 0, 0, 0))
else:
bpp = 4 if config == Bitmap.Config.ARGB_8888 else (
2 if config in (Bitmap.Config.ARGB_4444, Bitmap.Config.RGB_565) else 1
)
self._pixels = bytearray(width * height * bpp)
@staticmethod
def createBitmap(width, height=None, config=None):
if isinstance(width, Bitmap):
src = width
dst = Bitmap(src._width, src._height, src._config)
if HAS_PIL and hasattr(src, "_image"):
from PIL import Image
dst._image = src._image.copy()
elif hasattr(src, "_pixels"):
dst._pixels = bytearray(src._pixels)
return dst
return Bitmap(width, height, config)
@staticmethod
def createScaledBitmap(src: "Bitmap", dstWidth: int, dstHeight: int,
filter: bool = True) -> "Bitmap":
if HAS_PIL and hasattr(src, "_image"):
from PIL import Image
dst = Bitmap(dstWidth, dstHeight, src._config)
dst._image = src._image.resize((dstWidth, dstHeight), Image.LANCZOS if filter else Image.NEAREST)
return dst
dst = Bitmap(dstWidth, dstHeight, src._config)
return dst
def copy(self, config: str, isMutable: bool) -> "Bitmap":
dst = Bitmap(self._width, self._height, config)
if HAS_PIL and hasattr(self, "_image"):
from PIL import Image
if config != self._config:
mode_map = {Bitmap.Config.ARGB_8888: "RGBA", Bitmap.Config.RGB_565: "RGB",
Bitmap.Config.ALPHA_8: "L"}
dst._image = self._image.convert(mode_map.get(config, "RGBA"))
else:
dst._image = self._image.copy()
elif hasattr(self, "_pixels"):
dst._pixels = bytearray(self._pixels)
return dst
def compress(self, format: str, quality: int, stream) -> bool:
if self._recycled:
return False
if HAS_PIL and hasattr(self, "_image"):
pil_fmt = format.lower()
if pil_fmt == "jpeg":
pil_fmt = "jpeg"
elif pil_fmt == "webp":
pil_fmt = "webp"
else:
pil_fmt = "png"
if pil_fmt == "jpeg" and self._image.mode == "RGBA":
self._image = self._image.convert("RGB")
self._image.save(stream, format=pil_fmt.upper(), quality=quality)
return True
return False
def recycle(self):
self._recycled = True
if HAS_PIL and hasattr(self, "_image"):
self._image = None
if hasattr(self, "_pixels"):
self._pixels = None
def isRecycled(self) -> bool:
return self._recycled
def getWidth(self) -> int:
return self._width
def getHeight(self) -> int:
return self._height
def getConfig(self) -> str:
return self._config
def getRowBytes(self) -> int:
bpp = {"ALPHA_8": 1, "ARGB_4444": 2, "ARGB_8888": 4, "RGB_565": 2, "RGBA_F16": 8}
return self._width * bpp.get(self._config, 4)
def getByteCount(self) -> int:
return self.getRowBytes() * self._height
def getAllocationByteCount(self) -> int:
return self.getByteCount()
def getPixel(self, x: int, y: int) -> int:
if self._recycled:
return 0
if HAS_PIL and hasattr(self, "_image"):
p = self._image.getpixel((x, y))
if self._image.mode == "RGBA":
return Color.argb(p[3], p[0], p[1], p[2])
elif self._image.mode == "RGB":
return Color.rgb(p[0], p[1], p[2])
elif self._image.mode == "L":
return Color.argb(255, p, p, p)
return 0
def getPixels(self, pixels: List[int], offset: int, stride: int,
x: int, y: int, width: int, height: int):
if self._recycled:
return
for row in range(height):
for col in range(width):
px = x + col
py = y + row
idx = offset + row * stride + col
if 0 <= idx < len(pixels) and 0 <= px < self._width and 0 <= py < self._height:
pixels[idx] = self.getPixel(px, py)
def setPixel(self, x: int, y: int, color: int):
if self._recycled:
return
if HAS_PIL and hasattr(self, "_image"):
if self._image.mode == "RGBA":
self._image.putpixel((x, y), (
Color.red(color), Color.green(color),
Color.blue(color), Color.alpha(color)
))
elif self._image.mode == "RGB":
self._image.putpixel((x, y), (
Color.red(color), Color.green(color), Color.blue(color)
))
elif self._image.mode == "L":
self._image.putpixel((x, y), Color.red(color))
else:
pass
def setPixels(self, pixels: List[int], offset: int, stride: int,
x: int, y: int, width: int, height: int):
for row in range(height):
for col in range(width):
idx = offset + row * stride + col
if idx < len(pixels):
self.setPixel(x + col, y + row, pixels[idx])
def eraseColor(self, color: int):
if self._recycled:
return
if HAS_PIL and hasattr(self, "_image"):
from PIL import Image
w, h = self._image.size
if self._image.mode == "RGBA":
self._image = Image.new("RGBA", (w, h),
(Color.red(color), Color.green(color),
Color.blue(color), Color.alpha(color)))
elif self._image.mode == "RGB":
self._image = Image.new("RGB", (w, h),
(Color.red(color), Color.green(color), Color.blue(color)))
def extractAlpha(self, paint: Paint = None) -> "Bitmap":
return Bitmap(self._width, self._height, Bitmap.Config.ALPHA_8)
def sameAs(self, other: "Bitmap") -> bool:
if not isinstance(other, Bitmap):
return False
if self._width != other._width or self._height != other._height:
return False
return True
def prepareToDraw(self):
pass
def hasAlpha(self) -> bool:
return self._has_alpha
def setHasAlpha(self, hasAlpha: bool):
self._has_alpha = hasAlpha
def getGenerationId(self) -> int:
return self._generation_id
def describeContents(self) -> int:
return 0
def writeToParcel(self, dest, flags: int):
pass
def copyPixelsToBuffer(self, dst):
if HAS_PIL and hasattr(self, "_image"):
buf = self._image.tobytes()
dst.write(buf)
def copyPixelsFromBuffer(self, src):
if HAS_PIL and hasattr(self, "_image"):
data = src.read()
import io as _io
buf = _io.BytesIO(data)
from PIL import Image
self._image = Image.open(buf)
self._image = self._image.resize((self._width, self._height))
def asMutable(self) -> "Bitmap":
return Bitmap.createBitmap(self)
def toString(self) -> str:
return f"Bitmap({self._width}x{self._height}, config={self._config})"
# ═══════════════════════════════════════════════════════════════
# BitmapFactory
# ═══════════════════════════════════════════════════════════════
class BitmapFactory(Stub):
"""BitmapFactory — 位图工厂"""
class Options(Stub):
"""BitmapFactory.Options — 解码选项"""
def __init__(self):
super().__init__()
self.inJustDecodeBounds = False
self.outWidth = 0
self.outHeight = 0
self.inSampleSize = 1
self.inPreferredConfig = Bitmap.Config.ARGB_8888
self.inDensity = 0
self.inTargetDensity = 0
self.inScaled = True
self.inMutable = False
self.outMimeType = ""
self.inPurgeable = False
self.inInputShareable = False
@staticmethod
def decodeFile(pathName: str, opts: Options = None) -> Optional[Bitmap]:
resolved = resolve_android_path(pathName)
if not os.path.exists(resolved):
return None
if HAS_PIL:
from PIL import Image
try:
img = Image.open(resolved)
if opts and opts.inJustDecodeBounds:
opts.outWidth, opts.outHeight = img.size
opts.outMimeType = f"image/{img.format.lower() if img.format else 'png'}"
return None
w, h = img.size
bm = Bitmap(w, h, opts.inPreferredConfig if opts else Bitmap.Config.ARGB_8888)
if img.mode != "RGBA":
img = img.convert("RGBA")
bm._image = img
return bm
except Exception:
return None
return Bitmap(100, 100, Bitmap.Config.ARGB_8888)
@staticmethod
def decodeResource(res, resId: int, opts: Options = None) -> Optional[Bitmap]:
return Bitmap(100, 100, Bitmap.Config.ARGB_8888)
@staticmethod
def decodeStream(stream, outPadding: "Rect" = None, opts: Options = None) -> Optional[Bitmap]:
if stream is None:
return None
if HAS_PIL:
from PIL import Image
try:
img = Image.open(stream)
if opts and opts.inJustDecodeBounds:
opts.outWidth, opts.outHeight = img.size
return None
w, h = img.size
bm = Bitmap(w, h, opts.inPreferredConfig if opts else Bitmap.Config.ARGB_8888)
if img.mode != "RGBA":
img = img.convert("RGBA")
bm._image = img
return bm
except Exception:
return None
return Bitmap(100, 100, Bitmap.Config.ARGB_8888)
@staticmethod
def decodeByteArray(data: bytes, offset: int, length: int, opts: Options = None) -> Optional[Bitmap]:
if not data:
return None
if HAS_PIL:
from PIL import Image
import io as _io
try:
buf = _io.BytesIO(data[offset:offset + length])
img = Image.open(buf)
if opts and opts.inJustDecodeBounds:
opts.outWidth, opts.outHeight = img.size
return None
w, h = img.size
bm = Bitmap(w, h, opts.inPreferredConfig if opts else Bitmap.Config.ARGB_8888)
if img.mode != "RGBA":
img = img.convert("RGBA")
bm._image = img
return bm
except Exception:
return None
return Bitmap(100, 100, Bitmap.Config.ARGB_8888)
@staticmethod
def decodeResourceStream(res, value, stream, pad: "Rect", opts: Options = None) -> Optional[Bitmap]:
return BitmapFactory.decodeStream(stream, pad, opts)
# ═══════════════════════════════════════════════════════════════
# Canvas
# ═══════════════════════════════════════════════════════════════
class Canvas(Stub):
"""Canvas — 画布"""
ALL_SAVE_FLAG = 0x1F
def __init__(self, width=0, height=0):
super().__init__()
# 支持 Canvas(Bitmap) 重载
if isinstance(width, Bitmap):
bitmap = width
self._bitmap = bitmap
self._width = bitmap.getWidth()
self._height = bitmap.getHeight()
if HAS_PIL:
from PIL import Image, ImageDraw
self._image = bitmap._image if hasattr(bitmap, '_image') and bitmap._image else None
self._draw = ImageDraw.Draw(self._image) if self._image else None
else:
self._pixels = None
self._save_count = 0
self._matrix_stack = [Matrix()]
self._clip_stack = [Rect(0, 0, self._width, self._height)]
self._matrix = Matrix()
return
if HAS_PIL:
from PIL import Image, ImageDraw
self._width = width
self._height = height
if width > 0 and height > 0:
self._image = Image.new("RGBA", (width, height), (0, 0, 0, 0))
self._draw = ImageDraw.Draw(self._image)
else:
self._image = None
self._draw = None
else:
self._width = width
self._height = height
self._pixels = None
self._bitmap = None
self._save_count = 0
self._matrix_stack = [Matrix()]
self._clip_stack = [Rect(0, 0, width, height)]
self._matrix = Matrix()
def setBitmap(self, bitmap: Bitmap):
self._bitmap = bitmap
self._width = bitmap.getWidth()
self._height = bitmap.getHeight()
if HAS_PIL:
from PIL import ImageDraw
if hasattr(bitmap, "_image") and bitmap._image:
self._image = bitmap._image
self._draw = ImageDraw.Draw(self._image)
def getBitmap(self) -> Optional[Bitmap]:
return self._bitmap
def getWidth(self) -> int:
return self._width
def getHeight(self) -> int:
return self._height
def save(self) -> int:
self._save_count += 1
self._matrix_stack.append(Matrix(self._matrix))
self._clip_stack.append(Rect(
self._clip_stack[-1].left, self._clip_stack[-1].top,
self._clip_stack[-1].right, self._clip_stack[-1].bottom
))
return self._save_count
def restore(self):
if len(self._matrix_stack) > 1:
self._matrix = self._matrix_stack.pop()
self._clip_stack.pop()
self._save_count -= 1
def saveLayer(self, bounds: RectF, paint: Paint = None, saveFlags: int = ALL_SAVE_FLAG) -> int:
return self.save()
def getSaveCount(self) -> int:
return self._save_count
def restoreToCount(self, saveCount: int):
while self._save_count > saveCount:
self.restore()
def translate(self, dx: float, dy: float):
self._matrix.postTranslate(dx, dy)
def scale(self, sx: float, sy: float, px: float = 0.0, py: float = 0.0):
self._matrix.postScale(sx, sy, px, py)
def rotate(self, degrees: float, px: float = 0.0, py: float = 0.0):
self._matrix.postRotate(degrees, px, py)
def skew(self, sx: float, sy: float):
self._matrix.postSkew(sx, sy)
def concat(self, matrix: Matrix):
self._matrix.postConcat(matrix)
def setMatrix(self, matrix: Matrix):
self._matrix = Matrix(matrix)
def getMatrix(self, ctm: Matrix = None) -> Matrix:
if ctm:
ctm._values = list(self._matrix._values)
return ctm
return Matrix(self._matrix)
def getLocalTransform(self, transform: Matrix = None) -> Matrix:
return self.getMatrix(transform)
def getDeviceTransform(self, transform: Matrix = None) -> Matrix:
if transform:
transform.reset()
return transform
return Matrix()
def clipRect(self, left: float, top: float, right: float, bottom: float,
op: int = ClipOp.INTERSECT):
r = self._clip_stack[-1]
if op == ClipOp.INTERSECT:
self._clip_stack[-1] = Rect(
max(r.left, int(left)), max(r.top, int(top)),
min(r.right, int(right)), min(r.bottom, int(bottom))
)
elif op == ClipOp.DIFFERENCE:
pass
def clipPath(self, path: Path, op: int = ClipOp.INTERSECT):
bounds = path.computeBounds()
self.clipRect(bounds.left, bounds.top, bounds.right, bounds.bottom, op)
def clipRegion(self, region: Region, op: int = ClipOp.INTERSECT):
r = region._rect
self.clipRect(float(r.left), float(r.top), float(r.right), float(r.bottom), op)
def getClipBounds(self, bounds: Rect = None) -> Rect:
r = self._clip_stack[-1]
if bounds:
bounds.set(r.left, r.top, r.right, r.bottom)
return bounds
return Rect(r.left, r.top, r.right, r.bottom)
def quickReject(self, *args) -> bool:
return False
def drawColor(self, color: int):
if HAS_PIL and self._draw is not None:
r, g, b = Color.red(color), Color.green(color), Color.blue(color)
a = Color.alpha(color)
self._draw.rectangle([0, 0, self._width, self._height],
fill=(r, g, b, a))
def drawARGB(self, a: int, r: int, g: int, b: int):
self.drawColor(Color.argb(a, r, g, b))
def drawRGB(self, r: int, g: int, b: int):
self.drawARGB(255, r, g, b)
def drawPaint(self, paint: Paint):
self.drawColor(paint.getColor())
def drawPoint(self, x: float, y: float, paint: Paint):
if HAS_PIL and self._draw is not None:
c = paint.getColor()
self._draw.point([(x, y)], fill=(
Color.red(c), Color.green(c), Color.blue(c), Color.alpha(c)
))
def drawPoints(self, pts: List[float], offset: int = 0, count: int = -1, paint: Paint = None):
if paint is None:
paint = Paint()
if count < 0:
count = len(pts) - offset
for i in range(offset, offset + count, 2):
self.drawPoint(pts[i], pts[i + 1], paint)
def drawLine(self, startX: float, startY: float, stopX: float, stopY: float, paint: Paint):
if HAS_PIL and self._draw is not None:
c = paint.getColor()
w = paint.getStrokeWidth() if paint.getStyle() != Paint.Style.FILL else 1
self._draw.line([(startX, startY), (stopX, stopY)],
fill=(Color.red(c), Color.green(c), Color.blue(c), Color.alpha(c)),
width=max(1, int(w)))
def drawLines(self, pts: List[float], offset: int = 0, count: int = -1, paint: Paint = None):
if paint is None:
paint = Paint()
if count < 0:
count = len(pts) - offset
for i in range(offset, offset + count, 4) if count >= 4 else []:
self.drawLine(pts[i], pts[i + 1], pts[i + 2], pts[i + 3], paint)
def drawRect(self, rect: Union[Rect, RectF, tuple], paint: Paint):
if isinstance(rect, Rect):
l, t, r, b = rect.left, rect.top, rect.right, rect.bottom
elif isinstance(rect, RectF):
l, t, r, b = rect.left, rect.top, rect.right, rect.bottom
else:
l, t, r, b = rect
if HAS_PIL and self._draw is not None:
c = paint.getColor()
fill_color = (Color.red(c), Color.green(c), Color.blue(c), Color.alpha(c))
if paint.getStyle() == Paint.Style.FILL:
self._draw.rectangle([l, t, r, b], fill=fill_color)
elif paint.getStyle() == Paint.Style.STROKE:
w = max(1, int(paint.getStrokeWidth()))
self._draw.rectangle([l, t, r, b], outline=fill_color, width=w)
else:
self._draw.rectangle([l, t, r, b], fill=fill_color)
def drawRoundRect(self, rect: Union[RectF, tuple], rx: float, ry: float, paint: Paint):
if isinstance(rect, RectF):
l, t, r, b = rect.left, rect.top, rect.right, rect.bottom
else:
l, t, r, b = rect
if HAS_PIL and self._draw is not None:
c = paint.getColor()
fill_color = (Color.red(c), Color.green(c), Color.blue(c), Color.alpha(c))
self._draw.rounded_rectangle(
[l, t, r, b], radius=rx,
fill=fill_color if paint.getStyle() != Paint.Style.STROKE else None,
outline=fill_color if paint.getStyle() != Paint.Style.FILL else None,
width=max(1, int(paint.getStrokeWidth())) if paint.getStyle() != Paint.Style.FILL else 1
)
def drawCircle(self, cx: float, cy: float, radius: float, paint: Paint):
if HAS_PIL and self._draw is not None:
c = paint.getColor()
fill_color = (Color.red(c), Color.green(c), Color.blue(c), Color.alpha(c))
bbox = [cx - radius, cy - radius, cx + radius, cy + radius]
if paint.getStyle() == Paint.Style.FILL:
self._draw.ellipse(bbox, fill=fill_color)
elif paint.getStyle() == Paint.Style.STROKE:
w = max(1, int(paint.getStrokeWidth()))
self._draw.ellipse(bbox, outline=fill_color, width=w)
else:
self._draw.ellipse(bbox, fill=fill_color)
def drawOval(self, rect: Union[RectF, tuple], paint: Paint):
if isinstance(rect, RectF):
l, t, r, b = rect.left, rect.top, rect.right, rect.bottom
else:
l, t, r, b = rect
if HAS_PIL and self._draw is not None:
c = paint.getColor()
fill_color = (Color.red(c), Color.green(c), Color.blue(c), Color.alpha(c))
self._draw.ellipse([l, t, r, b], fill=fill_color)
def drawArc(self, rect: Union[RectF, tuple], startAngle: float, sweepAngle: float,
useCenter: bool, paint: Paint):
if isinstance(rect, RectF):
l, t, r, b = rect.left, rect.top, rect.right, rect.bottom
else:
l, t, r, b = rect
if HAS_PIL and self._draw is not None:
c = paint.getColor()
fill_color = (Color.red(c), Color.green(c), Color.blue(c), Color.alpha(c))
if not HAS_PIL:
return
from PIL import ImageDraw
self._draw.pieslice(
[l, t, r, b], start=startAngle, end=startAngle + sweepAngle,
fill=fill_color if useCenter or paint.getStyle() != Paint.Style.STROKE else None,
outline=fill_color if paint.getStyle() != Paint.Style.FILL else None,
width=max(1, int(paint.getStrokeWidth())) if paint.getStyle() != Paint.Style.FILL else 1
)
def drawPath(self, path: Path, paint: Paint):
if HAS_PIL and self._draw is not None:
pil_path = []
for seg in path._segments:
if seg["type"] == "move":
pil_path.append(("M", seg["x"], seg["y"]))
elif seg["type"] == "line":
pil_path.append(("L", seg["x"], seg["y"]))
elif seg["type"] == "close":
pil_path.append(("Z",))
elif seg["type"] in ("quad", "cubic"):
pass
if pil_path:
from PIL import ImageDraw
c = paint.getColor()
fill_color = (Color.red(c), Color.green(c), Color.blue(c), Color.alpha(c))
try:
if paint.getStyle() == Paint.Style.FILL:
self._draw.polygon(
[(s[1], s[2]) for s in pil_path if s[0] in ("M", "L")],
fill=fill_color
)
else:
w = max(1, int(paint.getStrokeWidth()))
if len(pil_path) > 1:
pts = [(s[1], s[2]) for s in pil_path if s[0] in ("M", "L")]
for i in range(len(pts) - 1):
self._draw.line([pts[i], pts[i + 1]], fill=fill_color, width=w)
except Exception:
pass
def drawBitmap(self, *args):
"""drawBitmap 重载: (bitmap,left,top,paint) / (bitmap,src,dst,paint) / (bitmap,matrix,paint)"""
if len(args) == 4:
bitmap, a, b, paint = args
if isinstance(a, (int, float)) and isinstance(b, (int, float)):
if HAS_PIL and self._draw is not None and hasattr(bitmap, "_image") and bitmap._image:
self._image.paste(bitmap._image, (int(a), int(b)))
elif HAS_PIL and self._image is not None:
pass
else:
src, dst = a, b
if HAS_PIL and self._image is not None and hasattr(bitmap, "_image") and bitmap._image:
if src:
cropped = bitmap._image.crop((src.left, src.top, src.right, src.bottom))
resized = cropped.resize((int(dst.width()), int(dst.height())))
self._image.paste(resized, (int(dst.left), int(dst.top)))
else:
resized = bitmap._image.resize((int(dst.width()), int(dst.height())))
self._image.paste(resized, (int(dst.left), int(dst.top)))
elif len(args) == 3:
bitmap, matrix, paint = args
if HAS_PIL and self._image is not None and hasattr(bitmap, "_image") and bitmap._image:
try:
from PIL import Image
inv = Matrix()
if hasattr(matrix, "invert") and matrix.invert(inv):
v = inv.getValues()
elif hasattr(matrix, "getValues"):
v = matrix.getValues()
else:
v = [1, 0, 0, 0, 1, 0, 0, 0, 1]
transformed = bitmap._image.transform(
bitmap._image.size, Image.AFFINE,
(v[0], v[1], v[2], v[3], v[4], v[5]))
self._image.paste(transformed, (0, 0))
except Exception:
pass
def drawText(self, text: str, x: float, y: float, paint: Paint):
if HAS_PIL and self._draw is not None:
c = paint.getColor()
fill_color = (Color.red(c), Color.green(c), Color.blue(c), Color.alpha(c))
self._draw.text((x, y), text, fill=fill_color)
def drawTextOnPath(self, text: str, path: Path, hOffset: float, vOffset: float, paint: Paint):
pass
def drawPosText(self, text: str, pos: List[float], paint: Paint):
if HAS_PIL and self._draw is not None:
c = paint.getColor()
fill_color = (Color.red(c), Color.green(c), Color.blue(c), Color.alpha(c))
for i, ch in enumerate(text):
if i * 2 + 1 < len(pos):
self._draw.text((pos[i * 2], pos[i * 2 + 1]), ch, fill=fill_color)
def drawPicture(self, picture: Picture, dst: Union[Rect, RectF] = None):
pass
def drawPatch(self, patch: NinePatch, dst: Rect, paint: Paint = None):
pass
def drawDoubleRoundRect(self, outer: RectF, outerRx: float, outerRy: float,
inner: RectF, innerRx: float, innerRy: float, paint: Paint):
pass
def drawBitmapMesh(self, bitmap: Bitmap, meshWidth: int, meshHeight: int,
verts: List[float], vertOffset: int, colors: List[int],
colorOffset: int, paint: Paint = None):
pass
def drawVertices(self, mode: int, vertexCount: int, verts: List[float],
vertOffset: int, texs: List[float], texOffset: int,
colors: List[int], colorOffset: int, indices: List[int],
indexOffset: int, indexCount: int, paint: Paint):
pass
def isHardwareAccelerated(self) -> bool:
return False
def isOpaque(self) -> bool:
return False
def setDensity(self, density: int):
pass
def getDensity(self) -> int:
return 0
def toString(self) -> str:
return f"Canvas({self._width}x{self._height})"
# ═══════════════════════════════════════════════════════════════
# __all__
# ═══════════════════════════════════════════════════════════════
__all__ = [
# Core
"Bitmap", "BitmapFactory", "Canvas", "Paint", "Color", "ColorFilter",
"PorterDuffColorFilter", "LightingColorFilter", "ComposeColorFilter",
"ColorMatrix", "ColorMatrixColorFilter", "Path", "PathMeasure",
"Rect", "RectF", "Point", "PointF", "Typeface", "Matrix",
# Shader hierarchy
"Shader", "LinearGradient", "RadialGradient", "SweepGradient",
"BitmapShader", "ComposeShader",
# MaskFilter hierarchy
"MaskFilter", "BlurMaskFilter", "EmbossMaskFilter",
# PorterDuff
"PorterDuff", "PorterDuffXfermode", "Xfermode",
"AvoidXfermode", "PixelXorXfermode",
# DrawFilter
"DrawFilter", "PaintFlagsDrawFilter",
# PathEffect hierarchy
"PathEffect", "SumPathEffect", "CornerPathEffect", "DashPathEffect",
"DiscretePathEffect", "PathDashPathEffect", "ComposePathEffect",
# Misc
"Camera", "SurfaceTexture", "Region", "RegionIterator",
"NinePatch", "Picture", "PictureDrawable",
"Rasterizer", "LayerRasterizer",
# BlendMode
"BlendMode", "BlendModeColorFilter",
"ClipOp",
]