OpenGL ES ->图片纹理叠自定义View固定裁剪框,图片单指滑动回弹,双指缩放,裁剪框不带任何黑边

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.TransformDelegate
  • CropTransformBridge → 同时知道 TextureTransformerCropView.TransformDelegate
  • TextureTransformer / 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_mainXML文件

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()
}

效果图

相关推荐
寻寻觅觅☆6 小时前
东华OJ-基础题-106-大整数相加(C++)
开发语言·c++·算法
l1t7 小时前
在wsl的python 3.14.3容器中使用databend包
开发语言·数据库·python·databend
青云计划7 小时前
知光项目知文发布模块
java·后端·spring·mybatis
赶路人儿7 小时前
Jsoniter(java版本)使用介绍
java·开发语言
冬奇Lab8 小时前
Android系统启动流程深度解析:从Bootloader到Zygote的完整旅程
android·源码阅读
ceclar1238 小时前
C++使用format
开发语言·c++·算法
探路者继续奋斗8 小时前
IDD意图驱动开发之意图规格说明书
java·规格说明书·开发规范·意图驱动开发·idd
码说AI8 小时前
python快速绘制走势图对比曲线
开发语言·python
Gofarlic_OMS8 小时前
科学计算领域MATLAB许可证管理工具对比推荐
运维·开发语言·算法·matlab·自动化
星空下的月光影子9 小时前
易语言开发从入门到精通:补充篇·网络爬虫与自动化采集分析系统深度实战·HTTP/HTTPS请求·HTML/JSON解析·反爬策略·电商价格监控·新闻资讯采集
开发语言