Files
Android-pyApi/android_pyapi/android/graphics.py
T
shuiyu1123 4235f8692c feat: 深度完善 — 存根转真实实现、UI类方法补全、Java集合/IO增强
存根转真实实现:
- ContentResolver: query/insert/update/delete对file://URI真实文件操作, getType用mimetypes
- Context: getResources/getAssets/getString/getColor 委托真实Resources
- Bitmap.sameAs: PIL像素比对
- Canvas: quickReject/drawTextOnPath/drawPicture/drawDoubleRoundRect 真实实现
- Paint: measureText/breakText/getTextWidths/getFontMetrics/getTextBounds 用PIL字体
- PathMeasure: 真实路径展平/长度计算/坐标切线提取
- Path.op/Region.op: 布尔运算
- Camera: 3D变换矩阵计算
- SystemClock.setCurrentTimeMillis: date -s命令
- Environment.isExternalStorageRemovable: /proc/mounts检查
- Process.myUid: os.getuid()
- PowerManager.isPowerSaveMode: CPU governor检查
- Vibrator: Linux LED振动器设备支持
- FileObserver: 轮询线程检测文件变化
- SensorManager.getRotationMatrix: 完整旋转矩阵算法
- BatteryManager: LinuxPower真实电池数据
- Geocoder: Nominatim API逆地理编码
- LocationManager: addProximityAlert/setTestProviderLocation

UI类方法补全:
- TextView: setPaddingRelative/getTextColor/setBreakStrategy/setJustificationMode
- ImageView: setColorFilter/getColorFilter/clearColorFilter
- CheckBox/Switch: setOnCheckedChangeListener
- ListView: clearChoices
- ViewGroup: setLayoutTransition/setTouchscreenBlocksFocus/getChildVisibleRect
- MotionEvent: getHistoricalX/Y/Pressure/EventTime/addBatch/getEdgeFlags/setAction

Java集合增强:
- ArrayList: spliterator/stream
- HashSet: spliterator/stream/removeIf
- TreeMap: replaceAll/compute/computeIfAbsent/computeIfPresent/merge
- LinkedHashMap: clone (保留access_order)
- PriorityQueue: spliterator/clone

Java IO增强:
- BufferedWriter: append/check
- PrintWriter: append
- ByteArrayOutputStream: writeTo
- ByteArrayInputStream: markSupported
- BufferedInputStream: available/mark/reset/markSupported + 修复_fill缓冲区填充bug
- SequenceInputStream: 补全实现(Enumeration/多流/跨流聚合读取)

修复:
- ViewGroup.addView: 合并重复定义为智能参数分派
- view_ext.ViewGroup: 继承view.ViewGroup
- view_ext.KeyEvent: 继承view.KeyEvent
- hardware_sensor_ext.SensorManager: 继承hardware.SensorManager
- app_fragment: 使用java.lang.IllegalStateException
- content_intent.Intent: setData/getScheme正确从Uri提取scheme
- __init__.py: 补全Fragment/KeyEvent/MotionEvent/TrafficStats/RecyclerView/TreeMap/Vibrator等导出

统计: 221模块, 3662类, 30952方法, 182302行
2026-06-25 00:17:50 +00:00

3455 lines
123 KiB
Python

"""
Android Graphics 组件 — Bitmap, Canvas, Paint, Color, Path, Rect 等
方法签名完全对齐 Android SDK
"""
import array
import math
import struct
import io
import os
import time
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:
"""对两个路径执行布尔运算。基于包围盒的近似实现。"""
if other is None:
return False
self_bounds = self.computeBounds()
other_bounds = other.computeBounds()
if op == Path.Op.UNION:
# 合并:将 other 的段追加到 self
self.addPath(other)
return True
if op == Path.Op.INTERSECT:
# 交集:仅当包围盒相交时保留相交区域
if not RectF.intersects(self_bounds, other_bounds):
self.reset()
return False
# 用相交包围盒作为近似结果
inter = RectF(self_bounds.left, self_bounds.top,
self_bounds.right, self_bounds.bottom)
inter.intersect(other_bounds.left, other_bounds.top,
other_bounds.right, other_bounds.bottom)
self.reset()
self.addRect(inter.left, inter.top, inter.right, inter.bottom)
return True
if op == Path.Op.DIFFERENCE:
# 差集:若包围盒不相交则 self 不变,否则标记(近似)
if not RectF.intersects(self_bounds, other_bounds):
return True
# 近似:无法精确计算,保留 self 不变
return True
if op == Path.Op.REVERSE_DIFFERENCE:
# 反向差集:用 other 替换 self
self.set(other)
return True
if op == Path.Op.XOR:
# 异或:合并两者
self.addPath(other)
return True
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
self._contours: List[List[Tuple[float, float, float]]] = []
self._contour_lengths: List[float] = []
self._total_length: float = 0.0
if path is not None:
self._flatten()
def _flatten(self):
"""将路径展平为线段列表,计算累积长度。"""
self._contours = []
self._contour_lengths = []
self._total_length = 0.0
if not self._path:
return
segs = self._path._segments
cur_x = 0.0
cur_y = 0.0
start_x = 0.0
start_y = 0.0
contour: List[Tuple[float, float, float]] = []
contour_len = 0.0
for seg in segs:
t = seg.get("type")
if t == "move":
if contour:
self._contours.append(contour)
self._contour_lengths.append(contour_len)
self._total_length += contour_len
cur_x = seg.get("x", 0.0)
cur_y = seg.get("y", 0.0)
start_x = cur_x
start_y = cur_y
contour = [(cur_x, cur_y, 0.0)]
contour_len = 0.0
elif t == "line":
nx = seg.get("x", cur_x)
ny = seg.get("y", cur_y)
d = math.hypot(nx - cur_x, ny - cur_y)
contour_len += d
contour.append((nx, ny, contour_len))
cur_x = nx
cur_y = ny
elif t == "quad":
x1, y1 = seg.get("x1", cur_x), seg.get("y1", cur_y)
x2, y2 = seg.get("x2", cur_x), seg.get("y2", cur_y)
steps = 16
prev_x, prev_y = cur_x, cur_y
for i in range(1, steps + 1):
tt = i / steps
mt = 1 - tt
nx = mt * mt * cur_x + 2 * mt * tt * x1 + tt * tt * x2
ny = mt * mt * cur_y + 2 * mt * tt * y1 + tt * tt * y2
d = math.hypot(nx - prev_x, ny - prev_y)
contour_len += d
contour.append((nx, ny, contour_len))
prev_x, prev_y = nx, ny
cur_x = x2
cur_y = y2
elif t == "cubic":
x1, y1 = seg.get("x1", cur_x), seg.get("y1", cur_y)
x2, y2 = seg.get("x2", cur_x), seg.get("y2", cur_y)
x3, y3 = seg.get("x3", cur_x), seg.get("y3", cur_y)
steps = 24
prev_x, prev_y = cur_x, cur_y
for i in range(1, steps + 1):
tt = i / steps
mt = 1 - tt
nx = mt ** 3 * cur_x + 3 * mt ** 2 * tt * x1 + 3 * mt * tt ** 2 * x2 + tt ** 3 * x3
ny = mt ** 3 * cur_y + 3 * mt ** 2 * tt * y1 + 3 * mt * tt ** 2 * y2 + tt ** 3 * y3
d = math.hypot(nx - prev_x, ny - prev_y)
contour_len += d
contour.append((nx, ny, contour_len))
prev_x, prev_y = nx, ny
cur_x = x3
cur_y = y3
elif t == "close":
if contour:
d = math.hypot(start_x - cur_x, start_y - cur_y)
if d > 1e-10:
contour_len += d
contour.append((start_x, start_y, contour_len))
cur_x = start_x
cur_y = start_y
if contour:
if self._force_closed and segs and segs[-1].get("type") != "close":
d = math.hypot(start_x - cur_x, start_y - cur_y)
if d > 1e-10:
contour_len += d
contour.append((start_x, start_y, contour_len))
self._contours.append(contour)
self._contour_lengths.append(contour_len)
self._total_length += contour_len
def setPath(self, path: Path, forceClosed: bool):
self._path = path
self._force_closed = forceClosed
self._contours = []
self._contour_lengths = []
self._total_length = 0.0
if path is not None:
self._flatten()
def getLength(self) -> float:
"""返回当前轮廓的长度。"""
if self._contour_lengths:
return self._contour_lengths[0]
return 0.0
def getPosTan(self, distance: float, pos: List[float], tan: List[float]) -> bool:
"""获取指定距离处的坐标和切线方向。"""
if not self._contours:
return False
contour = self._contours[0]
length = self._contour_lengths[0]
if length <= 0:
return False
# 钳制距离
d = max(0.0, min(distance, length))
# 二分查找
lo, hi = 0, len(contour) - 1
while lo < hi:
mid = (lo + hi) // 2
if contour[mid][2] < d:
lo = mid + 1
else:
hi = mid
idx = max(1, lo)
px, py, pd = contour[idx - 1]
nx, ny, nd = contour[idx]
seg_len = nd - pd
if seg_len < 1e-10:
t = 0.0
else:
t = (d - pd) / seg_len
x = px + (nx - px) * t
y = py + (ny - py) * t
dx = nx - px
dy = ny - py
dl = math.hypot(dx, dy)
if dl < 1e-10:
tx, ty = 1.0, 0.0
else:
tx, ty = dx / dl, dy / dl
if len(pos) >= 2:
pos[0] = x
pos[1] = y
if len(tan) >= 2:
tan[0] = tx
tan[1] = ty
return True
def getMatrix(self, distance: float, matrix: "Matrix", flags: int) -> bool:
"""获取指定距离处的变换矩阵(平移+旋转)。"""
pos = [0.0, 0.0]
tan = [0.0, 0.0]
if not self.getPosTan(distance, pos, tan):
return False
angle = math.degrees(math.atan2(tan[1], tan[0]))
matrix.reset()
matrix.postRotate(angle)
matrix.postTranslate(pos[0], pos[1])
return True
def getSegment(self, startD: float, stopD: float, dst: Path, startWithMoveTo: bool) -> bool:
"""提取路径中 [startD, stopD] 区间的子路径。"""
if not self._contours:
return False
contour = self._contours[0]
length = self._contour_lengths[0]
if length <= 0:
return False
startD = max(0.0, min(startD, length))
stopD = max(0.0, min(stopD, length))
if startD >= stopD:
return False
first = True
for i in range(1, len(contour)):
px, py, pd = contour[i - 1]
nx, ny, nd = contour[i]
if nd < startD:
continue
if pd > stopD:
break
seg_len = nd - pd
if seg_len < 1e-10:
continue
# 计算线段两端在 [startD, stopD] 内的参数
t0 = max(0.0, (startD - pd) / seg_len)
t1 = min(1.0, (stopD - pd) / seg_len)
x0 = px + (nx - px) * t0
y0 = py + (ny - py) * t0
x1 = px + (nx - px) * t1
y1 = py + (ny - py) * t1
if first and startWithMoveTo:
dst.moveTo(x0, y0)
else:
dst.lineTo(x0, y0)
dst.lineTo(x1, y1)
first = False
return True
def isClosed(self) -> bool:
if not self._path or not self._path._segments:
return self._force_closed
if self._force_closed:
return True
return any(s.get("type") == "close" for s in self._path._segments)
def nextContour(self) -> bool:
"""移动到下一个轮廓。"""
if len(self._contours) > 1:
self._contours.pop(0)
self._contour_lengths.pop(0)
return True
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
self._rot_x = 0.0
self._rot_y = 0.0
self._rot_z = 0.0
self._trans_x = 0.0
self._trans_y = 0.0
self._trans_z = 0.0
self._stack: List[Tuple[float, float, float, float, float, float, float, float, float]] = []
def save(self):
"""保存当前变换状态到栈。"""
self._stack.append((self._rot_x, self._rot_y, self._rot_z,
self._trans_x, self._trans_y, self._trans_z,
self._location_x, self._location_y, self._location_z))
def restore(self):
"""从栈恢复上次保存的变换状态。"""
if self._stack:
(self._rot_x, self._rot_y, self._rot_z,
self._trans_x, self._trans_y, self._trans_z,
self._location_x, self._location_y, self._location_z) = self._stack.pop()
def translate(self, x: float, y: float, z: float):
self._trans_x += x
self._trans_y += y
self._trans_z += z
def rotateX(self, deg: float):
"""绕 X 轴旋转(累积角度)。"""
self._rot_x += deg
def rotateY(self, deg: float):
"""绕 Y 轴旋转(累积角度)。"""
self._rot_y += deg
def rotateZ(self, deg: float):
"""绕 Z 轴旋转(累积角度)。"""
self._rot_z += deg
def rotate(self, x: float, y: float, z: float):
"""同时绕 X/Y/Z 轴旋转。"""
self._rot_x += x
self._rot_y += y
self._rot_z += z
def _compute_matrix(self) -> List[float]:
"""计算 4x4 变换矩阵并投影为 3x3 仿射矩阵。"""
rx = math.radians(self._rot_x)
ry = math.radians(self._rot_y)
rz = math.radians(self._rot_z)
cx, sx = math.cos(rx), math.sin(rx)
cy, sy = math.cos(ry), math.sin(ry)
cz, sz = math.cos(rz), math.sin(rz)
# 旋转矩阵 R = Rz * Ry * Rx
r00 = cz * cy
r01 = cz * sy * sx - sz * cx
r02 = cz * sy * cx + sz * sx
r10 = sz * cy
r11 = sz * sy * sx + cz * cx
r12 = sz * sy * cx - cz * sx
r20 = -sy
r21 = cy * sx
r22 = cy * cx
# 应用平移
tx = self._trans_x
ty = self._trans_y
tz = self._trans_z
# 相机位置(z 越负越远)
cam_z = self._location_z
# 透视投影:z' = z + cam_z,当 cam_z != 0 时做透视除法
# 简化为:将 3D 点投影到 2D
# 点 (x,y,z) -> (x*r00+y*r01+z*r02+tx, x*r10+y*r11+z*r12+ty, x*r20+y*r21+z*r22+tz)
# 透视:screen_x = x' * d / (d - z'), screen_y = y' * d / (d - z')
d = -cam_z # 相机到原点距离
if abs(d) < 1e-10:
# 正交投影
return [r00, r01, tx, r10, r11, ty, 0.0, 0.0, 1.0]
# 透视投影矩阵(近似):将 z 影响缩放到 x,y
# m[0]=r00, m[1]=r01, m[2]=tx, m[3]=r10, m[4]=r11, m[5]=ty
# 透视项:m[6]=r20/d, m[7]=r21/d, m[8]=1 - tz/d
return [r00, r01, tx,
r10, r11, ty,
r20 / d, r21 / d, 1.0 - tz / d]
def getMatrix(self, matrix: Matrix):
"""计算变换矩阵并写入给定的 Matrix。"""
matrix._values = self._compute_matrix()
def applyToCanvas(self, canvas: "Canvas"):
"""将相机变换应用到 Canvas。"""
m = Matrix()
self.getMatrix(m)
if hasattr(canvas, "concat"):
canvas.concat(m)
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:
"""计算向量与变换后 Z 轴法线的点积。"""
rx = math.radians(self._rot_x)
ry = math.radians(self._rot_y)
rz = math.radians(self._rot_z)
cy, sy = math.cos(ry), math.sin(ry)
cx, sx = math.cos(rx), math.sin(rx)
# 变换后的 Z 轴法线
nz_x = sy
nz_y = -cy * sx
nz_z = cy * cx
return dx * nz_x + dy * nz_y + dz * nz_z
# ═══════════════════════════════════════════════════════════════
# 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"):
"""回放录制的绘制命令到指定 Canvas。"""
for cmd in self._recorded:
method = cmd.get("method")
args = cmd.get("args", [])
if method and hasattr(canvas, method):
try:
getattr(canvas, method)(*args)
except Exception:
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"):
"""将 Picture 回放到指定 Canvas。"""
if self._picture is not None:
self._picture.draw(canvas)
# ═══════════════════════════════════════════════════════════════
# SurfaceTexture
# ═══════════════════════════════════════════════════════════════
class SurfaceTexture(Stub):
"""SurfaceTexture — 表面纹理"""
def __init__(self, texName: int = 0):
super().__init__()
self._tex_name = texName
self._timestamp = 0
self._buffer_width = 0
self._buffer_height = 0
self._attached = texName != 0
self._released = False
def getTimestamp(self) -> int:
return self._timestamp
def updateTexImage(self):
self._timestamp = int(time.time() * 1e9)
def release(self):
"""释放 SurfaceTexture 资源。"""
self._released = True
self._attached = False
def setDefaultBufferSize(self, width: int, height: int):
"""设置默认缓冲区大小。"""
self._buffer_width = width
self._buffer_height = height
def attachToGLContext(self, texName: int):
"""将 SurfaceTexture 附加到 GL 纹理。"""
self._tex_name = texName
self._attached = True
self._released = False
def detachFromGLContext(self):
"""从 GL 纹理分离 SurfaceTexture。"""
self._attached = False
# ═══════════════════════════════════════════════════════════════
# 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:
"""对区域执行布尔运算。支持 op(rect, op) / op(region, op) / op(left,top,right,bottom,op)。"""
if len(args) == 2:
operand, op_code = args
if isinstance(operand, Rect):
other = Rect(operand.left, operand.top, operand.right, operand.bottom)
elif isinstance(operand, Region):
other = Rect(operand._rect.left, operand._rect.top,
operand._rect.right, operand._rect.bottom)
else:
return False
elif len(args) == 5:
left, top, right, bottom, op_code = args
other = Rect(left, top, right, bottom)
else:
return False
self_r = self._rect
if op_code == Region.OP_INTERSECT:
nL = max(self_r.left, other.left)
nT = max(self_r.top, other.top)
nR = min(self_r.right, other.right)
nB = min(self_r.bottom, other.bottom)
if nL < nR and nT < nB:
self_r.set(nL, nT, nR, nB)
return True
self_r.setEmpty()
return False
if op_code == Region.OP_UNION:
if self_r.isEmpty():
self_r.set(other.left, other.top, other.right, other.bottom)
else:
self_r.left = min(self_r.left, other.left)
self_r.top = min(self_r.top, other.top)
self_r.right = max(self_r.right, other.right)
self_r.bottom = max(self_r.bottom, other.bottom)
return True
if op_code == Region.OP_REPLACE:
self_r.set(other.left, other.top, other.right, other.bottom)
return True
if op_code == Region.OP_DIFFERENCE:
# 差集:若不相交则不变;若完全包含则清空;否则近似为不变
if not Rect.intersects(self_r, other):
return True
if other.contains(self_r.left, self_r.top) and other.contains(self_r.right - 1, self_r.bottom - 1):
self_r.setEmpty()
return False
return True
if op_code == Region.OP_REVERSE_DIFFERENCE:
self_r.set(other.left, other.top, other.right, other.bottom)
return True
if op_code == Region.OP_XOR:
if not Rect.intersects(self_r, other):
self_r.union(other.left, other.top, other.right, other.bottom)
return True
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
self._font_cache = None
def getTextSize(self) -> float:
return self._text_size
def _get_font(self):
"""获取 PIL ImageFont 对象(带缓存)。"""
if not HAS_PIL:
return None
cache_key = (self._text_size, id(self._typeface) if self._typeface else None)
if getattr(self, "_font_cache_key", None) == cache_key and getattr(self, "_font_cache", None):
return self._font_cache
try:
from PIL import ImageFont
font_path = None
if self._typeface and hasattr(self._typeface, "_path") and self._typeface._path:
font_path = self._typeface._path
if font_path and os.path.exists(font_path):
self._font_cache = ImageFont.truetype(font_path, int(self._text_size))
else:
self._font_cache = ImageFont.load_default()
self._font_cache_key = cache_key
return self._font_cache
except Exception:
return None
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:
"""测量文本宽度。优先使用 PIL ImageFont 获取精确宽度。"""
font = self._get_font()
if font is not None:
try:
if hasattr(font, "getlength"):
return float(font.getlength(text))
bbox = font.getbbox(text)
return float(bbox[2] - bbox[0])
except Exception:
pass
return len(text) * self._text_size * 0.5
def breakText(self, text: str, maxWidth: float, measuredWidth: List[float]) -> int:
"""在最大宽度内截断文本,返回可容纳的字符数。"""
font = self._get_font()
width = 0.0
count = 0
for i, ch in enumerate(text):
if font is not None:
try:
cw = font.getlength(ch) if hasattr(font, "getlength") else (font.getbbox(ch)[2])
except Exception:
cw = self._text_size * 0.5
else:
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:
"""获取每个字符的宽度。"""
font = self._get_font()
for i, ch in enumerate(text):
if font is not None:
try:
widths[i] = float(font.getlength(ch)) if hasattr(font, "getlength") else float(font.getbbox(ch)[2])
except Exception:
widths[i] = self._text_size * 0.5
else:
widths[i] = self._text_size * 0.5
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]:
"""返回字体度量信息。优先使用 PIL ImageFont 获取真实度量。"""
size = self._text_size
font = self._get_font()
if font is not None:
try:
if hasattr(font, "getmetrics"):
ascent, descent = font.getmetrics()
return {
"ascent": -float(ascent),
"descent": float(descent),
"top": -float(ascent) - 2.0,
"bottom": float(descent) + 2.0,
"leading": 0.0,
}
except Exception:
pass
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
def setStrokeCap(self, cap):
self._stroke_cap = cap
def getStrokeCap(self):
return getattr(self, '_stroke_cap', 0)
def setStrokeJoin(self, join):
self._stroke_join = join
def getStrokeJoin(self):
return getattr(self, '_stroke_join', 0)
def setStrokeMiter(self, miter):
self._stroke_miter = miter
def getStrokeMiter(self):
return getattr(self, '_stroke_miter', 4.0)
def getTextBounds(self, text, start, end, bounds):
"""返回文本边界矩形。优先使用 PIL ImageFont 获取真实边界。"""
font = self._get_font()
if font is not None:
try:
sub = text[start:end]
bbox = font.getbbox(sub)
bounds.left = int(bbox[0])
bounds.top = int(bbox[1])
bounds.right = int(bbox[2])
bounds.bottom = int(bbox[3])
return
except Exception:
pass
if hasattr(self, '_text_size'):
w = len(text[start:end]) * self._text_size * 0.5
h = self._text_size
else:
w = len(text[start:end]) * 12
h = 24
bounds.left = 0
bounds.top = -int(h * 0.8)
bounds.right = int(w)
bounds.bottom = int(h * 0.2)
def setBlendMode(self, blendmode):
self._blend_mode = blendmode
def getBlendMode(self):
return getattr(self, '_blend_mode', None)
def setLetterSpacing(self, letterSpacing):
self._letter_spacing = letterSpacing
def getLetterSpacing(self):
return getattr(self, '_letter_spacing', 0.0)
def setFontFeatureSettings(self, settings):
self._font_feature_settings = settings
def getFontFeatureSettings(self):
return getattr(self, '_font_feature_settings', None)
def setHinting(self, mode):
self._hinting = mode
def getHinting(self):
return getattr(self, '_hinting', 2) # HINTING_ON
def setFakeBoldText(self, fakeBoldText):
self._fake_bold_text = fakeBoldText
def isFakeBoldText(self):
return getattr(self, '_fake_bold_text', False)
def hasGlyph(self, string):
return len(string) > 0
# ═══════════════════════════════════════════════════════════════
# 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:
"""比较两个 Bitmap 的像素是否完全相同。"""
if not isinstance(other, Bitmap):
return False
if self._width != other._width or self._height != other._height:
return False
if self._config != other._config:
return False
if self._recycled or other._recycled:
return False
# 使用 PIL 比较像素
if HAS_PIL and hasattr(self, "_image") and hasattr(other, "_image"):
try:
from PIL import ImageChops
diff = ImageChops.difference(self._image, other._image)
return diff.getbbox() is None
except Exception:
return list(self._image.getdata()) == list(other._image.getdata())
# 回退到字节比较
if hasattr(self, "_pixels") and hasattr(other, "_pixels"):
return bytes(self._pixels) == bytes(other._pixels)
return True
def prepareToDraw(self):
"""提示将位图数据上传到 GPU 缓存。桌面端为空操作。"""
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]:
"""从资源解码位图。尝试通过 Resources 打开原始资源流。"""
# 尝试通过 Resources 获取资源路径或流
if res is not None:
try:
# 尝试 openRawResource
stream = res.openRawResource(resId)
if stream is not None:
return BitmapFactory.decodeStream(stream, None, opts)
except Exception:
pass
try:
# 尝试通过资源名查找文件
name = res.getResourceEntryName(resId) if hasattr(res, "getResourceEntryName") else None
if name and name != "unknown":
for ext in (".png", ".jpg", ".jpeg", ".webp", ".bmp"):
candidate = resolve_android_path("res/drawable/" + name + ext)
if os.path.exists(candidate):
return BitmapFactory.decodeFile(candidate, opts)
candidate = resolve_android_path("res/drawable-xxhdpi/" + name + ext)
if os.path.exists(candidate):
return BitmapFactory.decodeFile(candidate, opts)
except Exception:
pass
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:
"""判断给定矩形是否完全在裁剪区域之外。"""
clip = self._clip_stack[-1]
if len(args) == 1:
r = args[0]
if isinstance(r, Rect):
left, top, right, bottom = r.left, r.top, r.right, r.bottom
elif isinstance(r, RectF):
left, top, right, bottom = r.left, r.top, r.right, r.bottom
else:
return False
elif len(args) == 4:
left, top, right, bottom = args
else:
return False
return (right <= clip.left or left >= clip.right or
bottom <= clip.top or top >= clip.bottom)
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):
"""沿路径绘制文本。使用 PathMeasure 逐字符放置。"""
if not text or not HAS_PIL or self._draw is None:
return
pm = PathMeasure(path, False)
total = pm.getLength()
if total <= 0:
return
c = paint.getColor()
fill_color = (Color.red(c), Color.green(c), Color.blue(c), Color.alpha(c))
dist = hOffset
pos = [0.0, 0.0]
tan = [0.0, 0.0]
for ch in text:
cw = paint.measureText(ch)
if dist > total:
break
if pm.getPosTan(dist, pos, tan):
angle = math.degrees(math.atan2(tan[1], tan[0]))
# 使用 PIL 旋转绘制每个字符
try:
from PIL import Image as _PILImage
char_img = _PILImage.new("RGBA", (int(cw) + 4, int(paint._text_size) + 4), (0, 0, 0, 0))
from PIL import ImageDraw as _PILDraw
cd = _PILDraw.Draw(char_img)
cd.text((0, 0), ch, fill=fill_color)
rotated = char_img.rotate(-angle, expand=True, resample=_PILImage.BICUBIC)
self._image.paste(rotated, (int(pos[0] + vOffset * tan[1]), int(pos[1] - vOffset * tan[0])),
rotated)
except Exception:
self._draw.text((pos[0], pos[1]), ch, fill=fill_color)
dist += cw
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):
"""回放 Picture 中录制的绘制命令。"""
if picture is None:
return
recorded = getattr(picture, "_recorded", [])
if not recorded:
return
for cmd in recorded:
method = cmd.get("method")
args = cmd.get("args", [])
if method and hasattr(self, method):
try:
getattr(self, method)(*args)
except Exception:
pass
def drawPatch(self, patch: NinePatch, dst: Rect, paint: Paint = None):
"""绘制 NinePatch(拉伸位图)到目标矩形。"""
if patch is None or patch._bitmap is None:
return
bitmap = patch._bitmap
if dst is None:
self.drawBitmap(bitmap, 0, 0, paint)
return
# 简单缩放绘制
if isinstance(dst, (Rect, RectF)):
dw = dst.right - dst.left
dh = dst.bottom - dst.top
else:
dw, dh = dst[2] - dst[0], dst[3] - dst[1]
scaled = Bitmap.createScaledBitmap(bitmap, int(dw), int(dh), True)
self.drawBitmap(scaled, dst.left if hasattr(dst, "left") else dst[0],
dst.top if hasattr(dst, "top") else dst[1], paint)
def drawDoubleRoundRect(self, outer: RectF, outerRx: float, outerRy: float,
inner: RectF, innerRx: float, innerRy: float, paint: Paint):
"""绘制双层圆角矩形(外层和内层)。"""
self.drawRoundRect(outer, outerRx, outerRy, paint)
self.drawRoundRect(inner, innerRx, innerRy, paint)
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):
"""设置画布密度(DPI)。"""
self._density = density
def getDensity(self) -> int:
"""返回画布密度(DPI),默认 0 表示未知。"""
return getattr(self, "_density", 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",
]