Open GL ES -> 应用前后台、Recent切换,SurfaceView纹理贴图闪烁问题分析解决

Android SurfaceView + OpenGL 纹理贴图切换前后台闪烁问题解决方案

一、问题现象

在使用 SurfaceView + 自定义 OpenGL 渲染线程实现纹理贴图时,应用切换到 Recent 后台再回到前台,图片会出现明显的闪烁(黑屏一帧或白屏一帧)。

二、问题分析

2.1 Surface 生命周期回顾

当应用切到后台再回前台时,SurfaceHolder.Callback 的回调顺序如下:

复制代码
切到后台: surfaceDestroyed()
回到前台: surfaceCreated() → surfaceChanged()

2.2 错误的实现方式

很多开发者(包括我最初)会这样写:

kotlin 复制代码
override fun surfaceCreated(holder: SurfaceHolder) {
    // 每次都新建渲染线程
    mRenderThread = RenderThread(holder.surface, renderData).apply {
        start()
    }
}

override fun surfaceDestroyed(holder: SurfaceHolder) {
    // 销毁线程和所有 GL 资源
    mRenderThread?.shutdown()
    mRenderThread = null
}

2.3 根本原因

问题的核心在于:每次回前台都会销毁并重建整个 EGL 环境和 GL 资源!

操作 耗时 影响
销毁 EGLContext GL 资源(纹理、VAO、VBO)全部失效
重建 EGLContext 需要重新初始化
重新加载纹理 Bitmap 解码 + 上传 GPU

纹理重新加载需要时间,而此时 Surface 已经可见但内容还未准备好,系统合成器会显示空白/黑色缓冲区,导致用户看到闪烁。

三、解决方案

3.1 核心思路

保持渲染线程和 EGLContext 存活,只重建 EGLSurface

GL 资源(纹理、VAO、VBO 等)是绑定在 EGLContext 上的,只要 Context 不销毁,这些资源就不会丢失。

复制代码
切到后台: 暂停渲染(保留线程和 Context)
回到前台: 重建 EGLSurface,绑定到已有 Context,立即恢复渲染

3.2 关键代码改动

修改 SurfaceHolder.Callback
kotlin 复制代码
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()
    }
}
RenderThread 新增 pauseupdateSurface 方法
kotlin 复制代码
class RenderThread(
    @Volatile private var surface: Surface,
    private val renderData: OpenGLData
) : Thread() {
    
    @Volatile
    private var paused = false
    
    @Volatile
    private var surfaceUpdated = false

    fun pause() {
        synchronized(lock) {
            paused = true
        }
    }

    fun updateSurface(newSurface: Surface) {
        synchronized(lock) {
            surface = newSurface
            surfaceUpdated = true
            paused = false
            lock.notifyAll()
        }
    }
    
    private fun renderLoop() {
        while (running) {
            synchronized(lock) {
                // 等待条件
                while (running && (paused || ...)) {
                    lock.wait()
                }
                
                // 处理 Surface 更新
                if (surfaceUpdated) {
                    mEGLEnvironment?.updateSurface(surface)
                    surfaceUpdated = false
                    requestRender = true
                }
                
                // ... 渲染逻辑
            }
        }
    }
}
EGLEnvironment 新增 updateSurface 方法
kotlin 复制代码
class EGLEnvironment(
    private val egl: EGL10,
    private val display: EGLDisplay,
    private val config: EGLConfig,  // 需要保存 config
    private val context: EGLContext,
    private var surface: EGLSurface
) {
    /**
     * 更新 Surface(保留 EGLContext,只重建 EGLSurface)
     * 这是解决闪烁的关键:GL 资源绑定在 Context 上,不会丢失
     */
    fun updateSurface(newSurface: Surface) {
        // 1. 解绑当前 surface
        egl.eglMakeCurrent(display, EGL10.EGL_NO_SURFACE, 
                           EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT)
        // 2. 销毁旧 surface
        egl.eglDestroySurface(display, surface)
        // 3. 创建新 surface
        surface = egl.eglCreateWindowSurface(display, config, newSurface, null)
        // 4. 重新绑定到已有的 context
        egl.eglMakeCurrent(display, surface, surface, context)
    }
}
View 销毁时释放资源
kotlin 复制代码
open class BaseSurfaceView : SurfaceView {
    
    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        // View 真正销毁时才释放所有资源
        renderEngine.release()
    }
}

四、方案对比

方案 是否有效 原因
❌ 设置 alpha = 0f 隐藏 部分机型无效 SurfaceView 是独立合成层,alpha 不一定生效
❌ 设置 visibility = INVISIBLE 有延迟 仍然会有时序问题
❌ 使用 Callback2.surfaceRedrawNeededAsync 治标不治本 首帧仍需重新加载纹理
保留 EGLContext,只重建 EGLSurface 有效 GL 资源不丢失,无需重新加载

五、总结

SurfaceView 切换前后台闪烁的根本原因是:

每次 Surface 销毁重建时,都销毁了整个 EGL 上下文,导致 GL 资源(尤其是纹理)需要重新加载,而加载期间 Surface 已可见,造成闪烁。

正确做法是:

  1. surfaceDestroyed只暂停渲染 ,不销毁线程和 EGLContext
  2. surfaceCreated复用已有线程 ,只重建 EGLSurface 并绑定到已有 Context
  3. onDetachedFromWindow 时才真正释放所有资源

这个方案类似于 GLSurfaceView.setPreserveEGLContextOnPause(true) 的效果,但适用于自定义渲染线程的场景。


参考资料

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 {

    // ⭐ 缩放配置
    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 var mWidth = 0
    private var mHeight = 0

    private var animator: ValueAnimator? = 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 - 变换接口 ====================

    /**
     * 设置平移
     */
    fun setTranslation(x: Float, y: Float, z: Float = 0f) {
        translateX = x
        translateY = y
        translateZ = z
        computeMVPMatrix()
    }

    /**
     * 获取平移
     */
    fun getTranslation(): Triple<Float, Float, Float> {
        return Triple(translateX, translateY, translateZ)
    }

    /**
     * 设置缩放
     */
    fun setScale(sx: Float, sy: Float = sx, sz: Float = 1f) {
        scaleX = sx
        scaleY = sy
        scaleZ = sz
        computeMVPMatrix()
    }

    /**
     * 获取缩放(返回平均缩放值)
     */
    fun getScale(): Triple<Float, Float, Float> {
        return Triple(scaleX, scaleY, scaleZ)
    }

    /**
     * 设置旋转
     */
    fun setRotation(angleX: Float = 0f, angleY: Float = 0f, angleZ: Float = 0f) {
        rotationX = angleX
        rotationY = angleY
        rotationZ = angleZ
        computeMVPMatrix()
    }

    /**
     * 获取旋转
     */
    fun getRotation(): Triple<Float, Float, Float> {
        return Triple(rotationX, rotationY, rotationZ)
    }

    /**
     * 重置所有变换
     */
    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() {
        initTexture()
        initShaderProgram()
        initVertexBuffer()
        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.picture, 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)
    }

    /**
     * 计算最终变换矩阵
     */
    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),    // 上边界
            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)
        // 最终变换矩阵,第一次变换,模型矩阵 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 ====================

    /**
     * 以指定点为中心进行缩放(立即生效,无动画)
     * @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(屏幕坐标)
     * @param centerY 缩放中心Y(屏幕坐标)
     * @param duration 动画时长
     */
    fun animateScaleWithCenter(
        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 = (centerX / mWidth) * 2f - 1f
        val normalizedY = 1f - (centerY / mHeight) * 2f

        // 计算缩放前该点在模型空间的位置
        val pointBeforeX = (normalizedX - startTranslateX) / startScaleX
        val pointBeforeY = (normalizedY - startTranslateY) / startScaleY

        // 限制目标缩放范围
        val clampedTargetScale = targetScale.coerceIn(MIN_SCALE, MAX_SCALE)

        // 计算目标平移量
        val pointAfterX = pointBeforeX * clampedTargetScale
        val pointAfterY = pointBeforeY * clampedTargetScale
        val targetTranslateX = normalizedX - pointAfterX
        val targetTranslateY = normalizedY - pointAfterY

        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">

    <!-- 预览区Fragment容器 -->
    <FrameLayout
        android:id="@id/preview_fragment_container"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:background="#000000">
        <com.example.render.opengl.BaseSurfaceView
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    </FrameLayout>

    <!-- 操作区Fragment容器 -->
    <FrameLayout
        android:id="@+id/operate_fragment_container"
        android:layout_width="match_parent"
        android:layout_height="230dp"
        android:background="#F5F5F5" />
</LinearLayout>

Activity的代码

kotlin 复制代码
class MainActivity : AppCompatActivity() {\
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

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.getScale().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.getScale().first < 1.0f) {
                    data.animateReset {
                        requestRender()
                    }
                }
            }
        }
    }

    init {
        holder.addCallback(renderEngine.callback)
        touchManager.setOnGestureListener(touchListener)
        requestRender(RENDERMODE_WHEN_DIRTY)
    }

    protected open fun createRenderEngine(): OpenGLEngine = BaseOpenGLEngine(renderData)
    protected open fun createRenderData(): OpenGLData = BaseOpenGLData(context)

    fun requestRender(mode: Int? = null) {
        renderEngine.requestRender(mode)
    }

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

效果图

相关推荐
m0_561359672 小时前
嵌入式C++调试技术
开发语言·c++·算法
Howrun7772 小时前
UE C++ 开发全生命周期 + 全场景的知识点清单
开发语言·c++
2301_763472462 小时前
C++中的享元模式高级应用
开发语言·c++·算法
不会代码的小测试2 小时前
UI自动化-针对验证码登录的系统,通过首次手动登录存储cookie的方式后续访问免登录方法
开发语言·python·selenium
weixin_458923202 小时前
分布式日志系统实现
开发语言·c++·算法
开发者小天2 小时前
python中calss的用法
开发语言·python
沉默-_-2 小时前
MyBatis 学习笔记
java·开发语言·tomcat
会游泳的石头2 小时前
构建企业级知识库智能问答系统:基于 Java 与 Spring Boot 的轻量实现
java·开发语言·spring boot·ai
m0_748229992 小时前
Laravel4.x核心更新全解析
开发语言·php