OpenGL ES ->图片纹理叠自定义View固定裁剪框,图片单指滑动回弹,双指缩放,裁剪框不带任何黑边
一、功能概述
在基于 OpenGL ES 3.0 的图片渲染预览界面上,叠加一个可交互的自由裁剪功能:
带遮罩和三分网格的裁剪框,初始贴合图片边界,
单指平移拖动图片,图片跟手移动,
双指缩放,以手指中点为焦点缩放图片
智能回弹,松手后若裁剪框内出现黑边,自动动画回弹至合法位置
二、架构总览
整个裁剪功能仅 3 个文件,叠加在原有 OpenGL 渲染层之上:
kotlin
render/opengl/src/main/java/com/example/render/
├── crop/ ← 裁剪功能包(3个文件)
│ ├── TextureTransformer.kt ← OpenGL 层变换接口
│ ├── CropTransformBridge.kt ← 隔离桥接层
│ └── CropView.kt ← 裁剪视图(绘制+手势+逻辑+回弹)
│
├── opengl/ ← 原有 OpenGL 包
│ ├── BaseOpenGLData.kt ← [改动] 实现 TextureTransformer
│ ├── BaseSurfaceView.kt ← [改动] 暴露 getTextureTransformer()
│ └── ... ← 其余文件未改动
隔离架构
kotlin
CropView 不直接引用 TextureTransformer,TextureTransformer 不知道 CropView,
二者通过 CropTransformBridge 进行通信:
┌─────────────────┐ ┌─────────────────────────┐
│ OpenGL 渲染层 │ │ 裁剪视图层 │
│ │ │ │
│ BaseOpenGLData │ ┌──────────────────┐ │ CropView │
│ implements │◄───│CropTransformBridge│──▶│ uses │
│ TextureTransf. │ │ (隔离桥接) │ │ TransformDelegate │
│ │ └──────────────────┘ │ (内部接口) │
└─────────────────┘ └─────────────────────────┘
▲ ▲ ▲
│ │ │
不知道 CropView 同时知道两侧 不知道 TextureTransformer
依赖方向:
CropView→ 仅依赖自己定义的CropView.TransformDelegateCropTransformBridge→ 同时知道TextureTransformer和CropView.TransformDelegateTextureTransformer / BaseOpenGLData→ 不知道CropView的存在
对原有代码的改动:BaseOpenGLData--- 实现TextureTransformer接口 + 图片宽高比校正BaseSurfaceView--- 暴露getTextureTransformer()方法- 其余所有裁剪逻辑完全在
CropView一个类中
三、核心类设计
3.1 TextureTransformer --- OpenGL 层变换接口
kotlin
interface TextureTransformer {
fun setTranslation(x: Float, y: Float)
fun getTranslation(): Pair<Float, Float>
fun setScale(sx: Float, sy: Float)
fun getScale(): Pair<Float, Float>
fun setRotation(angle: Float)
fun getRotation(): Float
fun getViewportSize(): Pair<Int, Int>
fun getImageSize(): Pair<Int, Int>
fun requestRender()
}
由 BaseOpenGLData 实现。CropView 不引用此接口。
3.2 CropView.TransformDelegate --- 裁剪视图层变换接口
kotlin
// 定义在 CropView 内部
interface TransformDelegate {
fun setTranslation(x: Float, y: Float)
fun getTranslation(): Pair<Float, Float>
fun setScale(sx: Float, sy: Float)
fun getScale(): Pair<Float, Float>
fun setRotation(angle: Float)
fun getRotation(): Float
fun getViewportSize(): Pair<Int, Int>
fun getImageSize(): Pair<Int, Int>
fun requestRender()
}
CropView 仅通过此接口与外部交互。不 import、不引用 TextureTransformer。
3.3 CropTransformBridge --- 隔离桥接层
kotlin
class CropTransformBridge(
private val transformer: TextureTransformer
) : CropView.TransformDelegate {
// 将 TransformDelegate 的每个调用委托给 TextureTransformer
override fun setTranslation(x, y) = transformer.setTranslation(x, y)
override fun getTranslation() = transformer.getTranslation()
// ... 其余方法同理
}
唯一同时知道两侧的类,将 TextureTransformer 适配为 TransformDelegate。
3.4 CropView --- 裁剪视图
kotlin
一个 View 同时承担四个职责:
┌─────────────────────────────────────────────┐
│ CropView (View) │
├─────────────────────────────────────────────┤
│ │
│ 内部接口: TransformDelegate │
│ 外部依赖: delegate: TransformDelegate? │
│ │
│ ① 绘制层 (onDraw) │
│ 遮罩 / 网格 / 边框 / 四角装饰 │
│ │
│ ② 手势层 (onTouchEvent) │
│ 单指平移 / 双指缩放 / 手势结束回弹 │
│ │
│ ③ 坐标转换 │
│ modelToScreen(mx,my) 模型→屏幕 │
│ screenToModel(sx,sy) 屏幕→模型 │
│ │
│ ④ 回弹逻辑 (bounceBackToValidPosition) │
│ 确定缩放 → 边界约束 → 动画执行 │
│ │
│ 公开 API: │
│ bindDelegate / unbindDelegate │
│ startCrop / endCrop │
│ getCropRect / resetTransform │
└─────────────────────────────────────────────┘
四、核心流程
4.1 进入裁剪模式
kotlin
用户点击 [裁剪]
│
▼
MainActivity.enterCropMode()
│
├── surfaceView.getTextureTransformer()
│ └── 返回 BaseOpenGLData (as TextureTransformer)
│
├── CropTransformBridge(transformer) ← 创建桥接
│
├── cropView.bindDelegate(bridge) ← CropView 只见 TransformDelegate
│ └── calculateBaseImageSize()
│
└── cropView.startCrop()
├── visibility = VISIBLE
└── initCropRect()
├── calculateImageBounds() → 图片屏幕边界
└── cropRect.set(bounds) → 裁剪框贴合图片
4.2 单指平移
kotlin
ACTION_MOVE (单指)
│
├── deltaX = currentX - lastX
├── deltaY = currentY - lastY
│
└── doPan(dx, dy)
│
├── halfMin = min(W,H) / 2
├── deltaTx = dx / halfMin ← 屏幕px → 模型空间
├── deltaTy = -dy / halfMin ← Y轴反转
│
├── transformer.setTranslation(tx + deltaTx, ty + deltaTy)
└── transformer.requestRender() → OpenGL 重绘
4.3 双指缩放
kotlin
ACTION_MOVE (双指)
│
├── factor = 当前双指间距 / 上次双指间距
├── (fx, fy) = 两指中点
│
└── doScale(factor, fx, fy)
│
├── newScale = currentScale * factor (限制 1.0~5.0)
│
├── (fmx, fmy) = toModel(fx, fy) ← 焦点转模型空间
│
│ 以焦点为不动点:
├── newTx = fmx - (fmx - tx) * actualFactor
├── newTy = fmy - (fmy - ty) * actualFactor
│
├── transformer.setScale(newScale, newScale)
└── transformer.setTranslation(newTx, newTy)
4.4 回弹算法(核心)
kotlin
手势松手时触发,确保裁剪框内无黑边:
bounceBack()
│
▼
Step 1: 目标缩放
│ tgtScale = max(currentScale, 覆盖裁剪框所需的最小缩放)
│ minScale = max(cropW/baseImgW, cropH/baseImgH)
│
▼
Step 2: 图片中心(屏幕坐标)
│ (cx, cy) = toScreen(tx, ty)
│ if 需要放大:
│ 以裁剪框中心为焦点,等比放大
│
▼
Step 3: 边界约束(屏幕坐标系,直觉清晰)
│ halfW = baseImgW * tgtScale / 2
│ halfH = baseImgH * tgtScale / 2
│
│ X方向:
│ 图片左边 > 裁剪框左边 → 左对齐
│ 图片右边 < 裁剪框右边 → 右对齐
│
│ Y方向:
│ 图片上边 > 裁剪框上边 → 上对齐
│ 图片下边 < 裁剪框下边 → 下对齐
│
▼
Step 4: 转回模型空间 + 动画
│ (tTx, tTy) = toModel(tcx, tcy)
│ ValueAnimator 0→1 插值执行
└── 每帧: setTranslation + setScale + requestRender
回弹示意:
回弹前(黑边): 回弹后(贴合):
┌──────────┐ 裁剪框 ┌──────────┐ 裁剪框
│ ■■■■■■■■ │ │ ▓▓▓▓▓▓▓▓ │
│ ■ 黑 ┌──┼──┐ │ ▓▓ 图片 ▓│
│ ■ 边 │图│片│ →回弹→ │ ▓▓▓▓▓▓▓▓ │
│ ■■■■■ │ │ │ │ ▓▓▓▓▓▓▓▓ │
└───────┤──┘ │ └──────────┘
└─────┘ 图片角 = 裁剪框角
五、坐标系统
5.1 关键公式
kotlin
透视投影使得模型空间 1 单位 = min(W,H)/2 像素(X 和 Y 方向相同):
halfMin = min(viewWidth, viewHeight) / 2
模型 → 屏幕:
screenX = W/2 + modelX * halfMin
screenY = H/2 - modelY * halfMin
屏幕 → 模型:
modelX = (screenX - W/2) / halfMin
modelY = (H/2 - screenY) / halfMin
为什么两方向都用 halfMin?
竖屏 frustum 设置:X 范围 [-d, d],Y 范围 [-vr*d, vr*d]。
经推导:screenY = H/2 - ty * W/2,系数同样是 W/2 = halfMin。
5.2 图片基础尺寸
kotlin
scale=1 时图片在屏幕上的像素尺寸:
minDim = min(W, H)
宽图 (aspect > 1, 如 4000×3000):
baseImgW = minDim → 1080 px
baseImgH = minDim / aspect → 810 px
窄图 (aspect ≤ 1, 如 3000×4000):
baseImgW = minDim * aspect → 810 px
baseImgH = minDim → 1080 px
任意缩放下:
displayW = baseImgW * scale
displayH = baseImgH * scale
六、数据流
kotlin
手指触摸
│
▼
CropView.onTouchEvent()
│
├── doPan / doScale (坐标转换 + 更新变换)
│ │
│ ▼
│ TextureTransformer
│ ├── setTranslation / setScale
│ ├── requestRender()
│ │ │
│ │ ▼
│ │ BaseOpenGLData.computeMVPMatrix()
│ │ │
│ │ ▼
│ │ GLThread → onDrawFrame() → 屏幕更新
│ │
│ └── (BaseOpenGLData 实现)
│
└── ACTION_UP → bounceBack()
│
├── 边界约束计算
└── animateTo() → ValueAnimator
└── 每帧: setTranslation + setScale + requestRender
七、OpenGL 渲染管线
kotlin
MVP 矩阵构建
ModelMatrix(右乘,先写后执行):
Identity
× Translate(tx, ty, tz) ← 平移
× Rotate(rx, ry, rz) ← 旋转
× Scale(sx, sy, sz) ← 用户缩放
× Scale(1, 1/imageAspect, 1) ← 宽高比校正
最终:MVP = Projection × View × Model × Scale(1,-1,1)
↑ Y翻转(Bitmap纹理修正)
源代码
EGL渲染环境、数据、引擎、线程的创建
抽象工厂模式定义EGL环境接口
kotlin
// 抽象工厂模式:负责创建EGL相关组件族
interface EGLComponentFactory {
fun createEGL(): EGL10
fun createEGLDisplay(egl: EGL10): EGLDisplay
fun createEGLConfig(egl: EGL10, display: EGLDisplay): EGLConfig
fun createEGLContext(egl: EGL10, display: EGLDisplay, config: EGLConfig): EGLContext
fun createEGLSurface(egl: EGL10, display: EGLDisplay, config: EGLConfig, surface: Surface): EGLSurface
}
EGL环境接口的具体实现
kotlin
// 具体工厂实现
class DefaultEGLFactory : EGLComponentFactory {
override fun createEGL(): EGL10 = EGLContext.getEGL() as EGL10
override fun createEGLDisplay(egl: EGL10): EGLDisplay {
val eglDisplay = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY)
if (eglDisplay == EGL10.EGL_NO_DISPLAY) {
throw RuntimeException("eglGetDisplay failed")
}
val version = IntArray(2)
if (!egl.eglInitialize(eglDisplay, version)) {
throw RuntimeException("eglInitialize failed")
}
return eglDisplay
}
override fun createEGLConfig(egl: EGL10, display: EGLDisplay): EGLConfig {
val attributes = intArrayOf(
EGL_RED_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_BLUE_SIZE, 8,
EGL_ALPHA_SIZE, 8,
EGL_DEPTH_SIZE, 8,
EGL_STENCIL_SIZE, 8,
EGL_NONE
)
val numConfigs = IntArray(1)
egl.eglChooseConfig(display, attributes, null, 0, numConfigs)
if (numConfigs[0] <= 0) {
throw RuntimeException("No matching EGL configs")
}
val configs = arrayOfNulls<EGLConfig>(numConfigs[0])
egl.eglChooseConfig(display, attributes, configs, numConfigs.size, numConfigs)
return configs[0] ?: throw RuntimeException("No suitable EGL config found")
}
override fun createEGLContext(egl: EGL10, display: EGLDisplay, config: EGLConfig): EGLContext {
val contextAttrs = intArrayOf(
EGL_CONTEXT_CLIENT_VERSION, 3,
EGL_NONE
)
val eglContext = egl.eglCreateContext(display, config, EGL10.EGL_NO_CONTEXT, contextAttrs)
if (eglContext == EGL10.EGL_NO_CONTEXT) {
throw RuntimeException("eglCreateContext failed")
}
return eglContext
}
override fun createEGLSurface(
egl: EGL10,
display: EGLDisplay,
config: EGLConfig,
surface: Surface
): EGLSurface {
val eglSurface = egl.eglCreateWindowSurface(display, config, surface, null)
if (eglSurface == EGL10.EGL_NO_SURFACE) {
throw RuntimeException("eglCreateWindowSurface failed")
}
return eglSurface
}
}
构造者实现EGL环境
kotlin
// 构建者模式:配置和构建EGL环境
class EGLEnvironmentBuilder(private val factory: EGLComponentFactory = DefaultEGLFactory()) {
private lateinit var mEGL: EGL10
private lateinit var mEGLDisplay: EGLDisplay
private lateinit var mEGLConfig: EGLConfig
private lateinit var mEGLContext: EGLContext
private lateinit var mEGLSurface: EGLSurface
fun build(surface: Surface): EGLEnvironment {
mEGL = factory.createEGL()
mEGLDisplay = factory.createEGLDisplay(mEGL)
mEGLConfig = factory.createEGLConfig(mEGL, mEGLDisplay)
mEGLContext = factory.createEGLContext(mEGL, mEGLDisplay, mEGLConfig)
mEGLSurface = factory.createEGLSurface(mEGL, mEGLDisplay, mEGLConfig, surface)
if (!mEGL.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext)) {
throw RuntimeException("eglMakeCurrent failed")
}
return EGLEnvironment(mEGL, mEGLDisplay, mEGLConfig, mEGLContext, mEGLSurface)
}
// EGL环境类
class EGLEnvironment(
private val egl: EGL10,
private val display: EGLDisplay,
private val config: EGLConfig,
private val context: EGLContext,
private var surface: EGLSurface
) {
/**
* 更新 Surface(保留 EGLContext,只重建 EGLSurface)
* 这是解决闪烁的关键:GL 资源绑定在 Context 上,不会丢失
*/
fun updateSurface(newSurface: Surface) {
// 解绑当前 surface
egl.eglMakeCurrent(display, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT)
// 销毁旧 surface
egl.eglDestroySurface(display, surface)
// 创建新 surface
surface = egl.eglCreateWindowSurface(display, config, newSurface, null)
if (surface == EGL10.EGL_NO_SURFACE) {
throw RuntimeException("eglCreateWindowSurface failed on updateSurface")
}
// 重新绑定
if (!egl.eglMakeCurrent(display, surface, surface, context)) {
throw RuntimeException("eglMakeCurrent failed on updateSurface")
}
}
fun swapBuffers() {
if (!egl.eglSwapBuffers(display, surface)) {
// 忽略 swap 失败(可能 surface 已失效)
}
}
fun release() {
egl.eglMakeCurrent(
display,
EGL10.EGL_NO_SURFACE,
EGL10.EGL_NO_SURFACE,
EGL10.EGL_NO_CONTEXT
)
egl.eglDestroySurface(display, surface)
egl.eglDestroyContext(display, context)
egl.eglTerminate(display)
}
}
}
渲染数据
渲染数据接口定义
kotlin
/**
* OpenGL渲染数据接口
*/
interface OpenGLData {
/**
* Surface创建时调用,用于初始化OpenGL资源
*/
fun onSurfaceCreated()
/**
* Surface尺寸变化时调用,用于更新视口
*/
fun onSurfaceChanged(width: Int, height: Int)
/**
* 每帧渲染时调用,执行实际的绘制操作
*/
fun onDrawFrame()
/**
* Surface销毁时调用,可以标记资源需要重新初始化
*/
fun onSurfaceDestroyed()
}
渲染数据接口实现
kotlin
/**
* OpenGL渲染数据默认实现类
* 提供OpenGL渲染所需的基本数据结构和操作方法
*/
class BaseOpenGLData(private val context: Context) : OpenGLData, TextureTransformer {
// ⭐ 缩放配置
companion object {
const val MIN_SCALE = 0.5f // 最小缩放
const val MAX_SCALE = 5f // 最大缩放
const val BOUNCE_THRESHOLD = 0.8f // 回弹阈值
const val ANIMATION_DURATION = 300L // 动画时长
}
private val NO_OFFSET = 0
private val VERTEX_POS_DATA_SIZE = 3
private val TEXTURE_POS_DATA_SIZE = 2
private val STRIDE = (VERTEX_POS_DATA_SIZE + TEXTURE_POS_DATA_SIZE) * 4 // 每个顶点的总字节数
// 着色器程序ID
private var mProgram: Int = -1
// 顶点和纹理坐标合并在一个数组中
// 格式:x, y, z, u, v (顶点坐标后跟纹理坐标)
val vertexData = floatArrayOf(
// 顶点坐标 // 纹理坐标
-1.0f, 1.0f, 0.0f, 0.0f, 1.0f, // 左上
-1.0f, -1.0f, 0.0f, 0.0f, 0.0f, // 左下
1.0f, 1.0f, 0.0f, 1.0f, 1.0f, // 右上
1.0f, -1.0f, 0.0f, 1.0f, 0.0f // 右下
)
val vertexDataBuffer = ByteBuffer.allocateDirect(vertexData.size * 4)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(vertexData)
.position(NO_OFFSET)
val index = shortArrayOf(
0, 1, 2, // 第一个三角形
1, 3, 2 // 第二个三角形
)
val indexBuffer = ByteBuffer.allocateDirect(index.size * 2)
.order(ByteOrder.nativeOrder())
.asShortBuffer()
.put(index)
.position(NO_OFFSET)
// VAO(Vertex Array Object), 顶点数组对象, 用于存储VBO
private var mVAO = IntArray(1)
// VBO(Vertex Buffer Object), 顶点缓冲对象,用于存储顶点数据和纹理数据
private var mVBO = IntArray(1) // 只需要一个VBO
// IBO(Index Buffer Object), 索引缓冲对象,用于存储顶点索引数据
private var mIBO = IntArray(1)
// 纹理ID
private var mTextureID = IntArray(1)
// 变换矩阵
private var mMVPMatrix = FloatArray(16) // 最终变换矩阵
private val mProjectionMatrix = FloatArray(16) // 投影矩阵
private val mViewMatrix = FloatArray(16) // 视图矩阵
private val mModelMatrix = FloatArray(16) // 模型矩阵
private val mMVPNoWithFlipMatrix = FloatArray(16) // 不翻转的变换矩阵
// 视口尺寸
private var mWidth = 0
private var mHeight = 0
private var animator: ValueAnimator? = null
private var matrixInitialized = false
// 渲染请求回调(供 TextureTransformer 使用)
var onRequestRender: (() -> Unit)? = null
// ⭐ 变换参数(完整的变换属性)
var translateX = 0f
var translateY = 0f
private var translateZ = 0f
private var scaleX = 1f
private var scaleY = 1f
private var scaleZ = 1f
private var rotationX = 0f
private var rotationY = 0f
private var rotationZ = 0f
// ==================== 公开 API - 变换接口 ====================
/**
* 设置3D平移
*/
fun setTranslation3D(x: Float, y: Float, z: Float = 0f) {
translateX = x
translateY = y
translateZ = z
computeMVPMatrix()
}
/**
* 获取3D平移
*/
fun getTranslation3D(): Triple<Float, Float, Float> {
return Triple(translateX, translateY, translateZ)
}
/**
* 设置3D缩放
*/
fun setScale3D(sx: Float, sy: Float = sx, sz: Float = 1f) {
scaleX = sx
scaleY = sy
scaleZ = sz
computeMVPMatrix()
}
/**
* 获取3D缩放
*/
fun getScale3D(): Triple<Float, Float, Float> {
return Triple(scaleX, scaleY, scaleZ)
}
/**
* 设置3D旋转
*/
fun setRotation3D(angleX: Float = 0f, angleY: Float = 0f, angleZ: Float = 0f) {
rotationX = angleX
rotationY = angleY
this.rotationZ = angleZ
computeMVPMatrix()
}
/**
* 获取3D旋转
*/
fun getRotation3D(): Triple<Float, Float, Float> {
return Triple(rotationX, rotationY, rotationZ)
}
// ==================== TextureTransformer 接口实现 ====================
override fun setTranslation(x: Float, y: Float) {
translateX = x
translateY = y
computeMVPMatrix()
}
override fun getTranslation(): Pair<Float, Float> {
return Pair(translateX, translateY)
}
override fun setScale(sx: Float, sy: Float) {
scaleX = sx
scaleY = sy
computeMVPMatrix()
}
override fun getScale(): Pair<Float, Float> {
return Pair(scaleX, scaleY)
}
override fun setRotation(angle: Float) {
rotationZ = angle
computeMVPMatrix()
}
override fun getRotation(): Float {
return rotationZ
}
override fun getViewportSize(): Pair<Int, Int> {
return Pair(mWidth, mHeight)
}
override fun getImageSize(): Pair<Int, Int> {
return Pair(mImageWidth, mImageHeight)
}
override fun requestRender() {
onRequestRender?.invoke()
}
/**
* 重置所有变换
*/
fun reset() {
animator?.cancel()
translateX = 0f
translateY = 0f
translateZ = 0f
scaleX = 1f
scaleY = 1f
scaleZ = 1f
rotationX = 0f
rotationY = 0f
rotationZ = 0f
computeMVPMatrix()
}
/**
* 重置平移
*/
fun resetTranslation() {
translateX = 0f
translateY = 0f
translateZ = 0f
computeMVPMatrix()
}
/**
* 重置缩放
*/
fun resetScale() {
scaleX = 1f
scaleY = 1f
scaleZ = 1f
computeMVPMatrix()
}
/**
* 重置旋转
*/
fun resetRotation() {
rotationX = 0f
rotationY = 0f
rotationZ = 0f
computeMVPMatrix()
}
// ==================== OpenGL 生命周期 ====================
/**
* Surface创建时调用,用于初始化OpenGL资源
*/
override fun onSurfaceCreated() {
if (!areGLResourcesValid()) {
release()
initTexture()
initShaderProgram()
initVertexBuffer()
}
if (!matrixInitialized) {
resetMatrix()
}
}
/**
* Surface尺寸变化时调用,用于更新视口
*/
override fun onSurfaceChanged(width: Int, height: Int) {
GLES30.glViewport(0, 0, width, height)
mWidth = width
mHeight = height
computeMVPMatrix()
}
/**
* 每帧渲染时调用,执行实际的绘制操作
*/
override fun onDrawFrame() {
clearBuffers()
draw()
}
/**
* Surface销毁时调用,可以标记资源需要重新初始化
*/
override fun onSurfaceDestroyed() {
release()
}
/**
* 初始化着色器程序
*/
private fun initShaderProgram() {
val vertexShaderCode = """#version 300 es
uniform mat4 uMVPMatrix; // 变换矩阵
in vec4 aPosition; // 顶点坐标
in vec2 aTexCoord; // 纹理坐标
out vec2 vTexCoord;
void main() {
// 输出顶点坐标和纹理坐标到片段着色器
gl_Position = uMVPMatrix * aPosition;
vTexCoord = aTexCoord;
}""".trimIndent()
val fragmentShaderCode = """#version 300 es
precision mediump float;
uniform sampler2D uTexture_0;
in vec2 vTexCoord;
out vec4 fragColor;
void main() {
fragColor = texture(uTexture_0, vTexCoord);
}""".trimIndent()
// 加载顶点着色器和片段着色器, 并创建着色器程序
val vertexShader = OpenGLUtils.loadShader(GLES30.GL_VERTEX_SHADER, vertexShaderCode)
val fragmentShader = OpenGLUtils.loadShader(GLES30.GL_FRAGMENT_SHADER, fragmentShaderCode)
mProgram = GLES30.glCreateProgram()
GLES30.glAttachShader(mProgram, vertexShader)
GLES30.glAttachShader(mProgram, fragmentShader)
GLES30.glLinkProgram(mProgram)
// 删除着色器对象
GLES30.glDeleteShader(vertexShader)
GLES30.glDeleteShader(fragmentShader)
}
/**
* 初始化顶点缓冲区
*/
private fun initVertexBuffer() {
// 绑定VAO
GLES30.glGenVertexArrays(mVAO.size, mVAO, NO_OFFSET)
GLES30.glBindVertexArray(mVAO[0])
// 绑定VBO - 只需要一个VBO存储所有数据
GLES30.glGenBuffers(mVBO.size, mVBO, NO_OFFSET)
GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, mVBO[0])
GLES30.glBufferData(
GLES30.GL_ARRAY_BUFFER,
vertexData.size * 4,
vertexDataBuffer,
GLES30.GL_STATIC_DRAW
)
// 设置顶点属性指针 - 顶点坐标
val positionHandle = GLES30.glGetAttribLocation(mProgram, "aPosition")
GLES30.glEnableVertexAttribArray(positionHandle)
GLES30.glVertexAttribPointer(
positionHandle,
VERTEX_POS_DATA_SIZE,
GLES30.GL_FLOAT,
false,
STRIDE, // 步长,每个顶点5个float (x,y,z,u,v)
NO_OFFSET // 偏移量,位置数据在前
)
// 设置顶点属性指针 - 纹理坐标
val textureHandle = GLES30.glGetAttribLocation(mProgram, "aTexCoord")
GLES30.glEnableVertexAttribArray(textureHandle)
GLES30.glVertexAttribPointer(
textureHandle,
TEXTURE_POS_DATA_SIZE,
GLES30.GL_FLOAT,
false,
STRIDE, // 步长,每个顶点5个float (x,y,z,u,v)
VERTEX_POS_DATA_SIZE * 4 // 偏移量,纹理数据在位置数据之后
)
// 绑定IBO
GLES30.glGenBuffers(mIBO.size, mIBO, NO_OFFSET)
// 绑定索引缓冲区数据到IBO[0]
GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER, mIBO[0])
GLES30.glBufferData(
GLES30.GL_ELEMENT_ARRAY_BUFFER,
index.size * 2,
indexBuffer,
GLES30.GL_STATIC_DRAW
)
// 解绑VAO
GLES30.glBindVertexArray(0)
// 解绑VBO和IBO
GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, 0)
GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER, 0)
}
/**
* 初始化纹理
*/
private fun initTexture() {
val textureId = IntArray(1)
// 生成纹理
GLES30.glGenTextures(1, textureId, 0)
// 绑定纹理
GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureId[0])
// 设置纹理参数
GLES30.glTexParameteri(
GLES30.GL_TEXTURE_2D,
GLES30.GL_TEXTURE_MIN_FILTER,
GLES30.GL_LINEAR
) // 纹理缩小时使用线性插值
GLES30.glTexParameteri(
GLES30.GL_TEXTURE_2D,
GLES30.GL_TEXTURE_MAG_FILTER,
GLES30.GL_LINEAR
) // 纹理放大时使用线性插值
GLES30.glTexParameteri(
GLES30.GL_TEXTURE_2D,
GLES30.GL_TEXTURE_WRAP_S,
GLES30.GL_CLAMP_TO_EDGE
) // 纹理坐标超出范围时,超出部分使用最边缘像素进行填充
GLES30.glTexParameteri(
GLES30.GL_TEXTURE_2D,
GLES30.GL_TEXTURE_WRAP_T,
GLES30.GL_CLAMP_TO_EDGE
) // 纹理坐标超出范围时,超出部分使用最边缘像素进行填充
// 加载图片
val options = BitmapFactory.Options().apply {
inScaled = false // 不进行缩放
}
val bitmap = BitmapFactory.decodeResource(context.resources, R.drawable.crop, options)
// 将图片数据加载到纹理中
GLUtils.texImage2D(GLES30.GL_TEXTURE_2D, 0, bitmap, 0)
// 释放资源
bitmap.recycle()
// 解绑纹理
GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, 0)
Log.e(
"yang",
"loadTexture: 纹理加载成功 bitmap.width:${bitmap.width} bitmap.height:${bitmap.height}"
)
mImageWidth = bitmap.width
mImageHeight = bitmap.height
mTextureID[0] = textureId[0]
}
var mImageHeight = 0
var mImageWidth = 0
/**
* 重置矩阵
*/
private fun resetMatrix() {
Matrix.setIdentityM(mProjectionMatrix, NO_OFFSET)
Matrix.setIdentityM(mViewMatrix, NO_OFFSET)
Matrix.setIdentityM(mModelMatrix, NO_OFFSET)
Matrix.setIdentityM(mMVPMatrix, NO_OFFSET)
matrixInitialized = true
}
private fun areGLResourcesValid(): Boolean {
if (mProgram == -1 || !GLES30.glIsProgram(mProgram)) return false
if (mTextureID[0] == 0 || !GLES30.glIsTexture(mTextureID[0])) return false
if (mVAO[0] == 0 || !GLES30.glIsVertexArray(mVAO[0])) return false
if (mVBO[0] == 0 || !GLES30.glIsBuffer(mVBO[0])) return false
if (mIBO[0] == 0 || !GLES30.glIsBuffer(mIBO[0])) return false
return true
}
/**
* 计算最终变换矩阵
*/
private fun computeMVPMatrix() {
val isLandscape = mWidth > mHeight
val viewPortRatio =
if (isLandscape) mWidth.toFloat() / mHeight else mHeight.toFloat() / mWidth
// 计算包围图片的球半径
val radius = sqrt(1f + viewPortRatio * viewPortRatio)
val near = 0.1f
val far = near + 2 * radius
val distance = near / (near + radius)
// 视图矩阵View Matrix
Matrix.setLookAtM(
mViewMatrix, NO_OFFSET,
0f, 0f, near + radius, // 相机位置
0f, 0f, 0f, // 看向原点
0f, 1f, 0f // 上方向
)
// 投影矩阵Projection Matrix
Matrix.frustumM(
mProjectionMatrix, NO_OFFSET,
if (isLandscape) (-viewPortRatio * distance) else (-1f * distance), // 左边界
if (isLandscape) (viewPortRatio * distance) else (1f * distance), // 右边界
if (isLandscape) (-1f * distance) else (-viewPortRatio * distance), // 下边界
if (isLandscape) (1f * distance) else (viewPortRatio * distance), // 上边界
// -distance,
// distance,
// -distance,
// distance,
near, // 近平面
far // 远平面
)
// ⭐ 模型矩阵(完整的变换)
Matrix.setIdentityM(mModelMatrix, NO_OFFSET)
// 1. 平移
Matrix.translateM(mModelMatrix, NO_OFFSET, translateX, translateY, translateZ)
// 2. 旋转(顺序:X → Y → Z)
if (rotationX != 0f) {
Matrix.rotateM(mModelMatrix, NO_OFFSET, rotationX, 1f, 0f, 0f)
}
if (rotationY != 0f) {
Matrix.rotateM(mModelMatrix, NO_OFFSET, rotationY, 0f, 1f, 0f)
}
if (rotationZ != 0f) {
Matrix.rotateM(mModelMatrix, NO_OFFSET, rotationZ, 0f, 0f, 1f)
}
// 3. 缩放(包含用户缩放)
Matrix.scaleM(mModelMatrix, NO_OFFSET, scaleX, scaleY, scaleZ)
// 4. 图片宽高比校正 - 保持图片原始比例不变形
// 顶点是 [-1,1] 的正方形,需要根据图片宽高比调整成正确的矩形
if (mImageWidth > 0 && mImageHeight > 0) {
val imageAspect = mImageWidth.toFloat() / mImageHeight.toFloat()
if (imageAspect > 1f) {
// 图片宽 > 高(如 4000x3000),y 方向缩小
// 例如:4000x3000 -> y 缩放为 3000/4000 = 0.75
Matrix.scaleM(mModelMatrix, NO_OFFSET, 1f, 1f / imageAspect, 1f)
} else {
// 图片高 > 宽,x 方向缩小
Matrix.scaleM(mModelMatrix, NO_OFFSET, imageAspect, 1f, 1f)
}
}
// 最终变换矩阵,第一次变换,模型矩阵 x 视图矩阵 = Model x View, 但是OpenGL ES矩阵乘法是右乘,所以是View x Model
Matrix.multiplyMM(
mMVPMatrix,
NO_OFFSET,
mViewMatrix,
NO_OFFSET,
mModelMatrix,
NO_OFFSET
)
// 最终变换矩阵,第二次变换,模型矩阵 x 视图矩阵 x 投影矩阵 = Model x View x Projection, 但是OpenGL ES矩阵乘法是右乘,所以是Projection x View x Model
Matrix.multiplyMM(
mMVPMatrix,
NO_OFFSET,
mProjectionMatrix,
NO_OFFSET,
mMVPMatrix,
NO_OFFSET
)
// 纹理坐标系为(0, 0), (1, 0), (1, 1), (0, 1)的正方形逆时针坐标系,从Bitmap生成纹理,即像素拷贝到纹理坐标系
// 变换矩阵需要加上一个y方向的翻转, x方向和z方向不改变
Matrix.scaleM(
mMVPMatrix,
NO_OFFSET,
1f,
-1f,
1f,
)
}
/**
* 清除缓冲区
*/
private fun clearBuffers() {
// 清除颜色缓冲区
GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT)
}
/**
* 绘制图形
*/
private fun draw() {
val state = saveGLState()
try {
GLES30.glUseProgram(mProgram)
enableTexture0(mProgram, mTextureID[0])
// 解析变换矩阵
val matrixHandle = GLES30.glGetUniformLocation(mProgram, "uMVPMatrix")
GLES30.glUniformMatrix4fv(matrixHandle, 1, false, mMVPMatrix, NO_OFFSET)
// 绑定VAO
GLES30.glBindVertexArray(mVAO[0])
// 绘制图形
GLES30.glDrawElements(
GLES30.GL_TRIANGLES,
index.size,
GLES30.GL_UNSIGNED_SHORT,
NO_OFFSET
)
// 解绑VAO
GLES30.glBindVertexArray(0)
disableTexture0()
} finally {
restoreGLState(state)
}
}
/**
* 释放资源
*/
private fun release() {
// 删除着色器程序、VAO、VBO和纹理等OpenGL资源
animator?.cancel()
animator = null
if (mVAO[0] != 0) {
GLES30.glDeleteVertexArrays(1, mVAO, 0)
}
if (mVBO[0] != 0) {
GLES30.glDeleteBuffers(1, mVBO, 0)
}
if (mIBO[0] != 0) {
GLES30.glDeleteBuffers(1, mIBO, 0)
}
if (mTextureID[0] != 0) {
GLES30.glDeleteTextures(1, mTextureID, 0)
}
if (mProgram != -1) {
GLES30.glDeleteProgram(mProgram)
}
}
// ==================== 新增:带中心点的缩放 API ====================
private fun transformViewToModel(viewX : Float, viewY : Float) : PointF {
// 1. 变换View坐标系到归一化坐标系
val openGLX = transformViewToNormalized(viewX, viewY).x
val openGLY = transformViewToNormalized(viewX, viewY).y
val openGLZ = 0.0f
val homogeneousW = 1.0f
// 2. 计算MVP变换矩阵的逆矩阵
val invertMatrix = FloatArray(16)
val success = Matrix.invertM(invertMatrix, NO_OFFSET, mMVPMatrix, NO_OFFSET)
if (!success) {
Log.d("yang", "transformViewToOpenGL: 矩阵不可逆,无法转换坐标系")
}
// 3. 将OpenGL坐标系计算到变换之前的模型坐标系
val openGLVector = floatArrayOf(openGLX, openGLY, openGLZ, homogeneousW)
val modelVector = FloatArray(4)
Matrix.multiplyMV(modelVector, NO_OFFSET, invertMatrix, NO_OFFSET, openGLVector, NO_OFFSET)
// 4. 透视除法
val modelX = modelVector[0] / modelVector[3]
val modelY = modelVector[1] / modelVector[3]
return PointF(modelX, modelY)
}
private fun transformViewToNormalized(viewX : Float, viewY : Float) : PointF {
// 变换View坐标系到归一化坐标系
val openGLX = (viewX / mWidth) * 2.0f - 1.0f
val openGLY = 1.0f - (viewY / mHeight) * 2.0f
return PointF(openGLX, openGLY)
}
// ==================== 新增:带中心点的缩放 API ====================
/**
* 以指定点为中心进行缩放(立即生效,无动画)
* @param scaleFactor 缩放因子(相对于当前缩放)
* @param centerX 缩放中心X(屏幕坐标)
* @param centerY 缩放中心Y(屏幕坐标)
*/
fun scaleWithCenter(
scaleFactor: Float,
centerX: Float,
centerY: Float,
requestCallback: () -> Unit
) {
if (mWidth == 0 || mHeight == 0) {
Log.w("BaseOpenGLData", "View size not set, cannot scale with center")
return
}
// 1. 屏幕坐标 → OpenGL 归一化坐标
val normalizedX = (centerX / mWidth) * 2f - 1f
val normalizedY = 1f - (centerY / mHeight) * 2f
// 2. 计算缩放前该点在模型空间的位置
val pointBeforeX = (normalizedX - translateX) / scaleX
val pointBeforeY = (normalizedY - translateY) / scaleY
// 3. 应用新的缩放
val newScaleX = scaleX * scaleFactor
val newScaleY = scaleY * scaleFactor
// 限制缩放范围
scaleX = newScaleX.coerceIn(MIN_SCALE, MAX_SCALE)
scaleY = newScaleY.coerceIn(MIN_SCALE, MAX_SCALE)
// 4. 计算缩放后该点在模型空间的位置
val pointAfterX = pointBeforeX * scaleX
val pointAfterY = pointBeforeY * scaleY
// 5. 调整平移量,使该点保持在原位置
translateX = normalizedX - pointAfterX
translateY = normalizedY - pointAfterY
// 6. 更新矩阵
computeMVPMatrix()
requestCallback()
}
/**
* 对锚点进行缩放,锚点下的纹理不动,周边的纹理跟着缩放
* @param targetScale 目标缩放值(绝对值)
* @param centerX 锚点中心X(View坐标系)
* @param centerY 锚点中心Y(View坐标系)
* @param duration 动画时长
*/
fun zoomWithAnchor(
targetScale: Float,
centerX: Float,
centerY: Float,
duration: Long = ANIMATION_DURATION,
requestCallback: () -> Unit
) {
cancelAnimation()
val startScaleX = scaleX
val startScaleY = scaleY
val startTranslateX = translateX
val startTranslateY = translateY
// 屏幕坐标 → OpenGL 归一化坐标
val normalizedX = transformViewToNormalized(centerX, centerY).x
val normalizedY = transformViewToNormalized(centerX, centerY).y
// 计算缩放前该点在模型空间的位置
val modelX = (normalizedX - startTranslateX) / startScaleX
val modelY = (normalizedY - startTranslateY) / startScaleY
// 限制目标缩放范围
val clampedTargetScale = targetScale.coerceIn(MIN_SCALE, MAX_SCALE)
// 计算目标平移量
val targetTranslateX = normalizedX - modelX * clampedTargetScale
val targetTranslateY = normalizedY - modelY * clampedTargetScale
animator = ValueAnimator.ofFloat(0f, 1f).apply {
this.duration = duration
interpolator = DecelerateInterpolator()
addUpdateListener { animator ->
val fraction = animator.animatedValue as Float
scaleX = startScaleX + (clampedTargetScale - startScaleX) * fraction
scaleY = startScaleY + (clampedTargetScale - startScaleY) * fraction
translateX = startTranslateX + (targetTranslateX - startTranslateX) * fraction
translateY = startTranslateY + (targetTranslateY - startTranslateY) * fraction
computeMVPMatrix()
requestCallback()
}
start()
}
}
/**
* 重置到初始状态(带动画)
*/
fun animateReset(
targetScale: Float = 1f,
duration: Long = ANIMATION_DURATION,
requestCallback: () -> Unit
) {
cancelAnimation()
val startScaleX = scaleX
val startScaleY = scaleY
val startTranslateX = translateX
val startTranslateY = translateY
animator = ValueAnimator.ofFloat(0f, 1f).apply {
this.duration = duration
interpolator = DecelerateInterpolator()
addUpdateListener { animator ->
val fraction = animator.animatedValue as Float
scaleX = startScaleX + (targetScale - startScaleX) * fraction
scaleY = startScaleY + (targetScale - startScaleY) * fraction
translateX = startTranslateX + (0f - startTranslateX) * fraction
translateY = startTranslateY + (0f - startTranslateY) * fraction
computeMVPMatrix()
requestCallback()
}
start()
}
}
fun cancelAnimation(){
animator?.cancel()
animator = null
}
}
object OpenGLUtils {
// OpenGL状态数据类
data class GLState(
val viewport: IntArray,
val program: Int,
val framebuffer: Int
)
// 保存OpenGL状态
fun saveGLState(): GLState {
val viewport = IntArray(4)
val program = IntArray(1)
val framebuffer = IntArray(1)
GLES30.glGetIntegerv(GLES30.GL_VIEWPORT, viewport, 0)
GLES30.glGetIntegerv(GLES30.GL_CURRENT_PROGRAM, program, 0)
GLES30.glGetIntegerv(GLES30.GL_FRAMEBUFFER_BINDING, framebuffer, 0)
return GLState(viewport, program[0], framebuffer[0])
}
// 恢复OpenGL状态
fun restoreGLState(state: GLState) {
GLES30.glViewport(
state.viewport[0],
state.viewport[1],
state.viewport[2],
state.viewport[3]
)
GLES30.glUseProgram(state.program)
GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, state.framebuffer)
}
fun enableTexture0(program: Int, id: Int) {
GLES30.glActiveTexture(GLES30.GL_TEXTURE0)
GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, id)
val textureSampleHandle = GLES30.glGetUniformLocation(program, "uTexture_0")
if (textureSampleHandle != -1) {
GLES30.glUniform1i(textureSampleHandle, 0)
}
}
fun disableTexture0() {
GLES30.glActiveTexture(GLES30.GL_TEXTURE0)
GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, 0)
}
// 创建着色器对象
fun loadShader(type: Int, source: String): Int {
val shader = GLES30.glCreateShader(type)
GLES30.glShaderSource(shader, source)
GLES30.glCompileShader(shader)
return shader
}
}
渲染引擎
渲染引擎接口
kotlin
/**
* OpenGL渲染引擎接口
*/
interface OpenGLEngine {
/**
* SurfaceHolder回调,用于绑定到SurfaceView
*/
val callback: SurfaceHolder.Callback
/**
* 请求执行一次渲染
*/
fun requestRender(mode : Int? = null)
/**
* 释放资源(在 View detach 时调用)
*/
fun release()
}
渲染引擎接口实现
kotlin
open class BaseOpenGLEngine(private val renderData: OpenGLData) : OpenGLEngine {
private var mRenderThread: RenderThread? = null
override val callback: SurfaceHolder.Callback = object : SurfaceHolder.Callback {
override fun surfaceCreated(holder: SurfaceHolder) {
val thread = mRenderThread
if (thread != null && thread.isAlive) {
// 复用已有线程,只更新 Surface
thread.updateSurface(holder.surface)
} else {
mRenderThread = RenderThread(holder.surface, renderData).apply {
start()
}
}
}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
mRenderThread?.updateSize(width, height)
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
// 不销毁线程,只暂停渲染
mRenderThread?.pause()
}
}
override fun requestRender(mode: Int?) {
mRenderThread?.requestRender(mode)
}
override fun release() {
mRenderThread?.shutdown()
mRenderThread = null
}
}
渲染线程
kotlin
class RenderThread(
@Volatile private var surface: Surface,
private val renderData: OpenGLData
) : Thread() {
private val TAG = "RenderThread"
private var mEGLEnvironment: EGLEnvironmentBuilder.EGLEnvironment? = null
@Volatile
private var running = true
@Volatile
private var paused = false
@Volatile
private var surfaceUpdated = false
@Volatile
private var sizeChanged = false
@Volatile
private var renderMode = RENDERMODE_WHEN_DIRTY
@Volatile
private var requestRender = true
private var mCacheWidth = 0
private var mCacheHeight = 0
private val lock = Object()
fun updateSurface(newSurface: Surface) {
synchronized(lock) {
surface = newSurface
surfaceUpdated = true
paused = false
lock.notifyAll()
Log.d(TAG, "updateSurface")
}
}
fun pause() {
synchronized(lock) {
paused = true
Log.d(TAG, "pause")
}
}
fun updateSize(width: Int, height: Int) {
synchronized(lock) {
if (width <= 0 || height <= 0) return
mCacheWidth = width
mCacheHeight = height
sizeChanged = true
requestRender = true
lock.notifyAll()
Log.d(TAG, "updateSize width = $width, height = $height")
}
}
fun requestRender(mode: Int? = null) {
synchronized(lock) {
mode?.let { renderMode = it }
requestRender = true
lock.notifyAll()
Log.d(TAG, "requestRender${mode?.let { " mode = $it" } ?: ""}")
}
}
fun shutdown() {
synchronized(lock) {
running = false
paused = false
lock.notifyAll()
Log.d(TAG, "shutdown")
}
join()
renderData.onSurfaceDestroyed()
mEGLEnvironment?.release()
}
override fun run() {
mEGLEnvironment = EGLEnvironmentBuilder().build(surface)
renderData.onSurfaceCreated()
renderLoop()
}
private fun renderLoop() {
while (running) {
var shouldRender = false
synchronized(lock) {
// 等待条件:未暂停且有渲染请求
while (running && (paused || (!requestRender && !sizeChanged && !surfaceUpdated && renderMode != RENDERMODE_CONTINUOUSLY))) {
lock.wait()
}
if (!running) return
// 处理 Surface 更新(重建 EGLSurface)
if (surfaceUpdated) {
mEGLEnvironment?.updateSurface(surface)
surfaceUpdated = false
requestRender = true
}
if (!paused) {
// 处理尺寸变化
if (sizeChanged) {
renderData.onSurfaceChanged(mCacheWidth, mCacheHeight)
sizeChanged = false
}
// 处理渲染
if (requestRender || renderMode == RENDERMODE_CONTINUOUSLY) {
shouldRender = true
requestRender = false
}
}
}
// 渲染放在锁外执行,避免长时间持锁
if (shouldRender) {
renderData.onDrawFrame()
mEGLEnvironment?.swapBuffers()
}
}
}
}
activity_main的XML文件
xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#000000">
<!-- 预览区容器 -->
<FrameLayout
android:id="@+id/preview_container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="#000000">
<com.example.render.opengl.BaseSurfaceView
android:id="@+id/surfaceView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<com.example.render.crop.CropView
android:id="@+id/cropView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>
<!-- 底部按钮栏 -->
<LinearLayout
android:id="@+id/buttonBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#1A1A1A"
android:gravity="center"
android:orientation="horizontal"
android:padding="16dp">
<!-- 裁剪按钮 -->
<TextView
android:id="@+id/btnCrop"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_marginEnd="8dp"
android:layout_weight="1"
android:background="@drawable/btn_crop_bg"
android:gravity="center"
android:text="裁剪"
android:textColor="#FFFFFF"
android:textSize="16sp" />
<!-- 确认按钮 -->
<TextView
android:id="@+id/btnConfirm"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_marginEnd="8dp"
android:layout_weight="1"
android:background="@drawable/btn_crop_bg"
android:gravity="center"
android:text="确认"
android:textColor="#FFFFFF"
android:textSize="16sp"
android:visibility="gone" />
<!-- 取消按钮 -->
<TextView
android:id="@+id/btnCancel"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_marginEnd="8dp"
android:layout_weight="1"
android:background="@drawable/btn_crop_bg"
android:gravity="center"
android:text="取消"
android:textColor="#FFFFFF"
android:textSize="16sp"
android:visibility="gone" />
<!-- 重置按钮 -->
<TextView
android:id="@+id/btnReset"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="1"
android:background="@drawable/btn_crop_bg"
android:gravity="center"
android:text="重置"
android:textColor="#FFFFFF"
android:textSize="16sp"
android:visibility="gone" />
</LinearLayout>
</LinearLayout>
Activity的代码
kotlin
class MainActivity : AppCompatActivity() {
private lateinit var surfaceView: BaseSurfaceView
private lateinit var cropView: CropView
private lateinit var btnCrop: TextView
private lateinit var btnConfirm: TextView
private lateinit var btnCancel: TextView
private lateinit var btnReset: TextView
private var isCropping = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
surfaceView = findViewById(R.id.surfaceView)
cropView = findViewById(R.id.cropView)
btnCrop = findViewById(R.id.btnCrop)
btnConfirm = findViewById(R.id.btnConfirm)
btnCancel = findViewById(R.id.btnCancel)
btnReset = findViewById(R.id.btnReset)
btnCrop.setOnClickListener { enterCropMode() }
btnConfirm.setOnClickListener { /* TODO: 确认裁剪 */ }
btnCancel.setOnClickListener { exitCropMode() }
btnReset.setOnClickListener { cropView.resetTransform() }
}
private fun enterCropMode() {
if (isCropping) return
isCropping = true
surfaceView.getTextureTransformer()?.let { transformer ->
cropView.bindDelegate(CropTransformBridge(transformer))
cropView.startCrop()
}
btnCrop.visibility = View.GONE
btnConfirm.visibility = View.VISIBLE
btnCancel.visibility = View.VISIBLE
btnReset.visibility = View.VISIBLE
}
private fun exitCropMode() {
if (!isCropping) return
isCropping = false
cropView.endCrop()
cropView.unbindDelegate()
btnCrop.visibility = View.VISIBLE
btnConfirm.visibility = View.GONE
btnCancel.visibility = View.GONE
btnReset.visibility = View.GONE
}
override fun onDestroy() {
super.onDestroy()
if (isCropping) {
cropView.endCrop()
cropView.unbindDelegate()
}
}
}
SurfaceView的代码
kotlin
open class BaseSurfaceView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : SurfaceView(context, attrs) {
protected val renderData: OpenGLData by lazy { createRenderData() }
protected val renderEngine: OpenGLEngine by lazy { createRenderEngine() }
private val touchManager = BaseSurfaceTouchManager()
private val touchListener = object : BaseSurfaceTouchManager.OnGestureListener {
override fun onDoubleTap(centerX: Float, centerY: Float) {
(renderData as? BaseOpenGLData)?.let { data ->
if (data.getScale3D().first >= 1.5f) {
data.animateReset(duration = 200) {
requestRender()
}
} else {
data.zoomWithAnchor(
targetScale = 2f,
centerX = centerX,
centerY = centerY,
duration = 200
) {
requestRender()
}
}
}
}
override fun onDoubleScale(
scale: Float,
centerX: Float,
centerY: Float
) {
(renderData as? BaseOpenGLData)?.let { data ->
data.scaleWithCenter(scale, centerX, centerY) {
requestRender()
}
}
}
override fun onScaleEnd() {
(renderData as? BaseOpenGLData)?.let { data ->
if (data.getScale3D().first < 1.0f) {
data.animateReset {
requestRender()
}
}
}
}
}
init {
holder.addCallback(renderEngine.callback)
touchManager.setOnGestureListener(touchListener)
requestRender(RENDERMODE_WHEN_DIRTY)
// 设置渲染请求回调
(renderData as? BaseOpenGLData)?.onRequestRender = { requestRender() }
}
protected open fun createRenderEngine(): OpenGLEngine = BaseOpenGLEngine(renderData)
protected open fun createRenderData(): OpenGLData = BaseOpenGLData(context)
fun requestRender(mode: Int? = null) {
renderEngine.requestRender(mode)
}
/**
* 获取纹理变换器用于裁剪模块
*/
fun getTextureTransformer(): TextureTransformer? {
return renderData as? TextureTransformer
}
override fun onTouchEvent(event: MotionEvent): Boolean {
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
(renderData as? BaseOpenGLData)?.cancelAnimation()
}
val handled = touchManager.onTouchEvent(event)
return handled || super.onTouchEvent(event)
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
renderEngine.release()
}
}
TextureTransformer 纹理变换接口
kotlin
package com.example.render.crop
/**
* 纹理变换接口
*
* 桥接裁剪模块与 OpenGL 实现,提供纹理变换的抽象接口。
*/
interface TextureTransformer {
fun setTranslation(x: Float, y: Float)
fun getTranslation(): Pair<Float, Float>
fun setScale(sx: Float, sy: Float)
fun getScale(): Pair<Float, Float>
fun setRotation(angle: Float)
fun getRotation(): Float
fun getViewportSize(): Pair<Int, Int>
fun getImageSize(): Pair<Int, Int>
fun requestRender()
}
CropView裁剪框代码
kotlin
package com.example.render.crop
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Path
import android.graphics.RectF
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import android.view.View
import android.view.animation.DecelerateInterpolator
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
import kotlin.math.sqrt
/**
* 自由裁剪视图(单文件实现)
*
* 集成:裁剪框绘制 + 手势处理 + 坐标变换 + 回弹逻辑
*
* 本类仅依赖内部定义的 [TransformDelegate] 接口,
* 不直接引用 [TextureTransformer] 或任何 OpenGL 类。
*
* 坐标映射:透视投影使模型空间 [-1,1] 在视口短边铺满,
* 因此 模型↔屏幕 统一使用 halfMinDimension = min(W,H)/2 做转换:
* screenX = viewWidth/2 + modelX * halfMinDimension
* screenY = viewHeight/2 - modelY * halfMinDimension·
*/
class CropView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
/**
* CropView 所需的图片变换能力抽象。
*
* CropView 仅通过此接口与外部交互,不感知底层实现(OpenGL/Canvas/Vulkan 等)。
* 调用方通过 [CropTransformBridge] 将具体实现适配为此接口。
*/
interface TransformDelegate {
fun setTranslation(x: Float, y: Float)
fun getTranslation(): Pair<Float, Float>
fun setScale(sx: Float, sy: Float)
fun getScale(): Pair<Float, Float>
fun setRotation(angle: Float)
fun getRotation(): Float
fun getViewportSize(): Pair<Int, Int>
fun getImageSize(): Pair<Int, Int>
fun requestRender()
}
companion object {
private const val MIN_SCALE = 1.0f
private const val MAX_SCALE = 5.0f
private const val ANIMATION_DURATION = 200L
private const val BORDER_WIDTH = 2f
private const val CORNER_LENGTH = 30f
private const val CORNER_WIDTH = 4f
private const val GRID_LINE_WIDTH = 1f
}
// ── 外部依赖(仅通过 TransformDelegate 交互) ──
private var delegate: TransformDelegate? = null
// ── 裁剪状态 ──
private var isCropping = false
private val cropRect = RectF()
private var baseImageWidth = 0f
private var baseImageHeight = 0f
// ── 手势状态 ──
private var lastTouchX = 0f
private var lastTouchY = 0f
private var lastPointerSpan = 0f
private var activePointerId = -1
private var isScaling = false
// ── 动画 ──
private var bounceAnimator: ValueAnimator? = null
// ── 画笔 ──
private val borderPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.STROKE
color = Color.WHITE
strokeWidth = BORDER_WIDTH
}
private val cornerPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.STROKE
color = Color.WHITE
strokeWidth = CORNER_WIDTH
strokeCap = Paint.Cap.ROUND
}
private val maskPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.FILL
color = Color.argb(160, 0, 0, 0)
}
private val gridPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.STROKE
color = Color.argb(80, 255, 255, 255)
strokeWidth = GRID_LINE_WIDTH
}
private val maskPath = Path()
init {
visibility = GONE
}
// ======================== 公开 API ========================
/** 绑定变换代理(由 [CropTransformBridge] 提供) */
fun bindDelegate(transformDelegate: TransformDelegate) {
delegate = transformDelegate
calculateBaseImageSize()
}
/** 解除绑定 */
fun unbindDelegate() {
bounceAnimator?.cancel()
delegate = null
baseImageWidth = 0f
baseImageHeight = 0f
}
fun startCrop() {
check(delegate != null) { "Must call bindDelegate() first" }
isCropping = true
visibility = VISIBLE
post { initCropRect() }
}
fun endCrop() {
isCropping = false
visibility = GONE
}
fun getCropRect(): RectF? = if (isCropping) RectF(cropRect) else null
fun resetTransform() {
val currentDelegate = delegate ?: return
bounceAnimator?.cancel()
currentDelegate.setTranslation(0f, 0f)
currentDelegate.setScale(1f, 1f)
currentDelegate.setRotation(0f)
currentDelegate.requestRender()
post { initCropRect() }
}
// ======================== 绘制 ========================
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (cropRect.isEmpty) return
drawMask(canvas)
drawGrid(canvas)
drawBorder(canvas)
drawCorners(canvas)
}
private fun drawMask(canvas: Canvas) {
maskPath.reset()
maskPath.addRect(0f, 0f, width.toFloat(), height.toFloat(), Path.Direction.CW)
maskPath.addRect(cropRect, Path.Direction.CCW)
canvas.drawPath(maskPath, maskPaint)
}
private fun drawGrid(canvas: Canvas) {
val thirdWidth = cropRect.width() / 3f
val thirdHeight = cropRect.height() / 3f
for (i in 1..2) {
canvas.drawLine(
cropRect.left + thirdWidth * i, cropRect.top,
cropRect.left + thirdWidth * i, cropRect.bottom,
gridPaint
)
canvas.drawLine(
cropRect.left, cropRect.top + thirdHeight * i,
cropRect.right, cropRect.top + thirdHeight * i,
gridPaint
)
}
}
private fun drawBorder(canvas: Canvas) {
canvas.drawRect(cropRect, borderPaint)
}
private fun drawCorners(canvas: Canvas) {
val left = cropRect.left
val top = cropRect.top
val right = cropRect.right
val bottom = cropRect.bottom
val halfCornerWidth = CORNER_WIDTH / 2f
canvas.drawLine(left - halfCornerWidth, top, left + CORNER_LENGTH, top, cornerPaint)
canvas.drawLine(left, top - halfCornerWidth, left, top + CORNER_LENGTH, cornerPaint)
canvas.drawLine(right - CORNER_LENGTH, top, right + halfCornerWidth, top, cornerPaint)
canvas.drawLine(right, top - halfCornerWidth, right, top + CORNER_LENGTH, cornerPaint)
canvas.drawLine(left - halfCornerWidth, bottom, left + CORNER_LENGTH, bottom, cornerPaint)
canvas.drawLine(left, bottom - CORNER_LENGTH, left, bottom + halfCornerWidth, cornerPaint)
canvas.drawLine(right - CORNER_LENGTH, bottom, right + halfCornerWidth, bottom, cornerPaint)
canvas.drawLine(right, bottom - CORNER_LENGTH, right, bottom + halfCornerWidth, cornerPaint)
}
// ======================== 手势处理 ========================
override fun onTouchEvent(event: MotionEvent): Boolean {
if (!isCropping) return false
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
activePointerId = event.getPointerId(0)
lastTouchX = event.x
lastTouchY = event.y
isScaling = false
return true
}
MotionEvent.ACTION_POINTER_DOWN -> {
if (event.pointerCount == 2) {
isScaling = true
lastPointerSpan = calculatePointerSpan(event)
}
return true
}
MotionEvent.ACTION_MOVE -> {
if (isScaling && event.pointerCount >= 2) {
val currentSpan = calculatePointerSpan(event)
if (lastPointerSpan > 10f && currentSpan > 10f) {
val focusX = (event.getX(0) + event.getX(1)) / 2f
val focusY = (event.getY(0) + event.getY(1)) / 2f
handleScale(currentSpan / lastPointerSpan, focusX, focusY)
}
lastPointerSpan = currentSpan
} else if (!isScaling && event.pointerCount == 1) {
val pointerIndex = event.findPointerIndex(activePointerId)
if (pointerIndex >= 0) {
val currentX = event.getX(pointerIndex)
val currentY = event.getY(pointerIndex)
handlePan(currentX - lastTouchX, currentY - lastTouchY)
lastTouchX = currentX
lastTouchY = currentY
}
}
return true
}
MotionEvent.ACTION_POINTER_UP -> {
if (event.pointerCount == 2) {
isScaling = false
val remainingIndex = if (event.actionIndex == 0) 1 else 0
activePointerId = event.getPointerId(remainingIndex)
lastTouchX = event.getX(remainingIndex)
lastTouchY = event.getY(remainingIndex)
}
return true
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
bounceBackToValidPosition()
activePointerId = -1
isScaling = false
return true
}
}
return false
}
private fun calculatePointerSpan(event: MotionEvent): Float {
if (event.pointerCount < 2) return 0f
val deltaX = event.getX(0) - event.getX(1)
val deltaY = event.getY(0) - event.getY(1)
return sqrt(deltaX * deltaX + deltaY * deltaY)
}
// ======================== 坐标转换 ========================
private fun getHalfMinDimension(): Float {
val (viewWidth, viewHeight) = delegate?.getViewportSize() ?: return 0f
return min(viewWidth, viewHeight) / 2f
}
private fun modelToScreen(modelX: Float, modelY: Float): Pair<Float, Float> {
val (viewWidth, viewHeight) = delegate?.getViewportSize() ?: return Pair(0f, 0f)
val halfMinDimension = min(viewWidth, viewHeight) / 2f
return Pair(
viewWidth / 2f + modelX * halfMinDimension,
viewHeight / 2f - modelY * halfMinDimension
)
}
private fun screenToModel(screenX: Float, screenY: Float): Pair<Float, Float> {
val (viewWidth, viewHeight) = delegate?.getViewportSize() ?: return Pair(0f, 0f)
val halfMinDimension = min(viewWidth, viewHeight) / 2f
return Pair(
(screenX - viewWidth / 2f) / halfMinDimension,
(viewHeight / 2f - screenY) / halfMinDimension
)
}
// ======================== 变换逻辑 ========================
private fun handlePan(deltaScreenX: Float, deltaScreenY: Float) {
val currentDelegate = delegate ?: return
val halfMinDimension = getHalfMinDimension()
if (halfMinDimension == 0f) return
val (translateX, translateY) = currentDelegate.getTranslation()
val deltaModelX = deltaScreenX / halfMinDimension
val deltaModelY = -deltaScreenY / halfMinDimension
currentDelegate.setTranslation(translateX + deltaModelX, translateY + deltaModelY)
currentDelegate.requestRender()
}
/*
* 缩放之后,锚点不变,模型坐标生成的归一化坐标也是不变的
* 模型空间中的锚点:P_model,缩放前参数:S0, T0,缩放后参数:S1, T1
* P_model × S0 + T0 = P_model × S1 + T1 = P_ndc
* T1 = P_ndc - (P_model × S1),代入 P_model = (P_ndc - T0) / S0
* T1 = P_ndc - (P_ndc - T0) / S0 × S1)
*/
private fun handleScale(scaleFactor: Float, focusScreenX: Float, focusScreenY: Float) {
val currentDelegate = delegate ?: return
val (currentScale, _) = currentDelegate.getScale()
val newScale = (currentScale * scaleFactor).coerceIn(MIN_SCALE, MAX_SCALE)
val actualFactor = newScale / currentScale
if (abs(actualFactor - 1f) < 0.001f) return
val (focusModelX, focusModelY) = screenToModel(focusScreenX, focusScreenY)
val (translateX, translateY) = currentDelegate.getTranslation()
val newTranslateX = focusModelX - (focusModelX - translateX) * actualFactor
val newTranslateY = focusModelY - (focusModelY - translateY) * actualFactor
currentDelegate.setScale(newScale, newScale)
currentDelegate.setTranslation(newTranslateX, newTranslateY)
currentDelegate.requestRender()
}
// ======================== 回弹 ========================
private fun bounceBackToValidPosition() {
val currentDelegate = delegate ?: return
if (cropRect.isEmpty || baseImageWidth == 0f || baseImageHeight == 0f) return
val (viewWidth, viewHeight) = currentDelegate.getViewportSize()
if (viewWidth == 0 || viewHeight == 0) return
val (currentTranslateX, currentTranslateY) = currentDelegate.getTranslation()
val currentScale = currentDelegate.getScale().first
val minRequiredScale = max(
cropRect.width() / baseImageWidth,
cropRect.height() / baseImageHeight
).coerceAtLeast(MIN_SCALE)
Log.d("yang", "minRequiredScale: $minRequiredScale, " +
"cropRect: $cropRect, baseImageWidth: $baseImageWidth, baseImageHeight: $baseImageHeight")
val targetScale = max(currentScale, minRequiredScale)
val (imageCenterX, imageCenterY) = modelToScreen(currentTranslateX, currentTranslateY)
var targetCenterX: Float
var targetCenterY: Float
if (targetScale > currentScale) {
val scaleRatio = targetScale / currentScale
targetCenterX = cropRect.centerX() + (imageCenterX - cropRect.centerX()) * scaleRatio
targetCenterY = cropRect.centerY() + (imageCenterY - cropRect.centerY()) * scaleRatio
} else {
targetCenterX = imageCenterX
targetCenterY = imageCenterY
Log.d("yang", "else")
}
val imageHalfWidth = baseImageWidth * targetScale / 2f
val imageHalfHeight = baseImageHeight * targetScale / 2f
if (targetCenterX - imageHalfWidth > cropRect.left) {
targetCenterX = cropRect.left + imageHalfWidth
} else if (targetCenterX + imageHalfWidth < cropRect.right) {
targetCenterX = cropRect.right - imageHalfWidth
}
if (targetCenterY - imageHalfHeight > cropRect.top) {
targetCenterY = cropRect.top + imageHalfHeight
} else if (targetCenterY + imageHalfHeight < cropRect.bottom) {
targetCenterY = cropRect.bottom - imageHalfHeight
}
val (targetTranslateX, targetTranslateY) = screenToModel(targetCenterX, targetCenterY)
val needsAnimation = abs(targetTranslateX - currentTranslateX) > 0.001f ||
abs(targetTranslateY - currentTranslateY) > 0.001f ||
abs(targetScale - currentScale) > 0.001f
if (needsAnimation) {
animateToTarget(targetTranslateX, targetTranslateY, targetScale)
}
}
private fun animateToTarget(targetTranslateX: Float, targetTranslateY: Float, targetScale: Float) {
val currentDelegate = delegate ?: return
bounceAnimator?.cancel()
val (startTranslateX, startTranslateY) = currentDelegate.getTranslation()
val startScale = currentDelegate.getScale().first
Log.d("yang", "animateToTarget startScale = $startScale, targetScale = $targetScale")
bounceAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
duration = ANIMATION_DURATION
interpolator = DecelerateInterpolator()
addUpdateListener { animation ->
val fraction = animation.animatedValue as Float
currentDelegate.setTranslation(
startTranslateX + (targetTranslateX - startTranslateX) * fraction,
startTranslateY + (targetTranslateY - startTranslateY) * fraction
)
currentDelegate.setScale(
startScale + (targetScale - startScale) * fraction,
startScale + (targetScale - startScale) * fraction
)
currentDelegate.requestRender()
}
start()
}
}
// ======================== 初始化 ========================
private fun calculateBaseImageSize() {
val currentDelegate = delegate ?: return
val (viewWidth, viewHeight) = currentDelegate.getViewportSize()
val (imageWidth, imageHeight) = currentDelegate.getImageSize()
if (viewWidth == 0 || viewHeight == 0 || imageWidth == 0 || imageHeight == 0) return
val imageAspectRatio = imageWidth.toFloat() / imageHeight.toFloat()
val minDimension = min(viewWidth, viewHeight).toFloat()
if (imageAspectRatio > 1f) {
baseImageWidth = minDimension
baseImageHeight = minDimension / imageAspectRatio
} else {
baseImageHeight = minDimension
baseImageWidth = minDimension * imageAspectRatio
}
Log.d("yang", "calculateBaseImageSize: $baseImageWidth, $baseImageHeight, " +
"viewHeight: $viewWidth, viewHeight: $viewHeight" +
", imageWidth: $imageWidth, imageHeight: $imageHeight ")
}
private fun initCropRect() {
val currentDelegate = delegate ?: return
val (viewWidth, viewHeight) = currentDelegate.getViewportSize()
if (viewWidth == 0 || viewHeight == 0) {
postDelayed({ initCropRect() }, 50)
return
}
calculateBaseImageSize()
val imageBounds = calculateImageBounds() ?: return
cropRect.set(imageBounds)
invalidate()
}
private fun calculateImageBounds(): RectF? {
val currentDelegate = delegate ?: return null
val (viewWidth, viewHeight) = currentDelegate.getViewportSize()
if (viewWidth == 0 || viewHeight == 0 || baseImageWidth == 0f || baseImageHeight == 0f) return null
val (translateX, translateY) = currentDelegate.getTranslation()
val scale = currentDelegate.getScale().first
val (centerX, centerY) = modelToScreen(translateX, translateY)
val halfWidth = baseImageWidth * scale / 2f
val halfHeight = baseImageHeight * scale / 2f
return RectF(centerX - halfWidth, centerY - halfHeight, centerX + halfWidth, centerY + halfHeight)
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
bounceAnimator?.cancel()
unbindDelegate()
}
}
CropTransformBridge裁剪变换桥接器代码
kotlin
package com.example.render.crop
/**
* 隔离层:桥接 [TextureTransformer] 与 [CropView.TransformDelegate]
*
* CropView 不直接引用 TextureTransformer,TextureTransformer 不知道 CropView,
* 二者通过此桥接类进行通信。
*
* 依赖关系:
* TextureTransformer ←── CropTransformBridge ──→ CropView.TransformDelegate
* (OpenGL 层) (隔离桥接) (裁剪视图层)
*/
class CropTransformBridge(
private val transformer: TextureTransformer
) : CropView.TransformDelegate {
override fun setTranslation(x: Float, y: Float) = transformer.setTranslation(x, y)
override fun getTranslation(): Pair<Float, Float> = transformer.getTranslation()
override fun setScale(sx: Float, sy: Float) = transformer.setScale(sx, sy)
override fun getScale(): Pair<Float, Float> = transformer.getScale()
override fun setRotation(angle: Float) = transformer.setRotation(angle)
override fun getRotation(): Float = transformer.getRotation()
override fun getViewportSize(): Pair<Int, Int> = transformer.getViewportSize()
override fun getImageSize(): Pair<Int, Int> = transformer.getImageSize()
override fun requestRender() = transformer.requestRender()
}
效果图
