核心类方法补全: - Activity: +20方法 (requestWindowFeature, setTitle/getTitle, onKeyDown/onKeyUp, enterPictureInPictureMode, getReferrer, isTaskRoot, navigateUpTo等) - Fragment: +15方法 (getChildFragmentManager, getParentFragment, requireContext/requireActivity, setMenuVisibility等) - Intent: +20方法 (getParcelableExtra, setComponent/getComponent, getDataString, getScheme, putExtras等) - View: +20方法 (setLabelFor, setRotationX/Y, setForeground, setBackgroundTintList等) - KeyEvent: +7方法 (getCharacters, isSystem, isPrintingKey, isModifierKey, startTracking等) - Paint: +12方法 (setStrokeCap/Join, getTextBounds, setBlendMode, setLetterSpacing等) - Notification.Builder: +10方法 (setProgress, setShowWhen, setOnlyAlertOnce, setSound等) 存根转真实实现: - TrafficStats: 读取 /proc/net/dev 返回真实网络流量 - RecyclerView.Adapter: 观察者模式触发通知 - BaseAdapter: 观察者模式触发通知 - ExifInterface: 使用PIL实现图片翻转/旋转 - Process: 使用os.kill实现进程信号 - FileUtils: 真实文件I/O - SharedPreferences: 监听器模式 - TelephonyManager.listen: 维护监听器列表 - ScrollView.smoothScrollTo: 更新滚动位置 Exception体系扩展: - 新增15个Exception/Error类 (ClassNotFoundException, IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchElementException, AssertionError, OutOfMemoryError, StackOverflowError, NoClassDefFoundError, UnsatisfiedLinkError, LinkageError, VerifyError, AbstractMethodError, IncompatibleClassChangeError, ExceptionInInitializerError) Math/System/Thread/StringBuilder/Integer增强: - Math: +20方法 (subtractExact, multiplyExact, toIntExact, floorDiv/floorMod, fma等) - System: getProperties/setProperties/mapLibraryName/getLogger - Thread: +10方法 (getUncaughtExceptionHandler, getAllStackTraces, getStackTrace, holdLock等) - StringBuilder: +7方法 (ensureCapacity, trimToSize, setLength, subSequence等) - Integer: +8静态方法 (compare, compareUnsigned, divideUnsigned, sum, max, min等) 修复: - app_fragment.py: 使用java.lang.IllegalStateException替代本地定义 - content_intent.py: Intent.setData/getScheme正确从Uri提取scheme - view_ext.py: KeyEvent继承view.KeyEvent获取所有KEYCODE常量 - __init__.py: 补全Fragment/KeyEvent/MotionEvent/TrafficStats/RecyclerView等导出 统计: 221模块, 3660类, 30826方法, 180217行
2897 lines
100 KiB
Python
2897 lines
100 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
|
|
|
|
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):
|
|
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:
|
|
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",
|
|
] |