Open GL ES->EGL渲染环境、数据、引擎、线程的创建

EGL环境

抽象工厂模式定义EGL环境接口

kotlin 复制代码
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 复制代码
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, mEGLContext, mEGLSurface)
    }

    // EGL环境类
    inner class EGLEnvironment(
        val egl: EGL10,
        val display: EGLDisplay,
        val context: EGLContext,
        val surface: EGLSurface
    ) {

        fun swapBuffers() {
            if (!egl.eglSwapBuffers(display, surface)) {
                throw RuntimeException("eglSwapBuffers failed")
            }
        }

        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 {

    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

    /**
     * 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}"
        )
        mTextureID[0] = textureId[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 // 远平面
        )

        // 最终变换矩阵,第一次变换,模型矩阵 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资源
    }
}


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

渲染引擎接口实现

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) {
            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?.shutdown()
            mRenderThread = null
        }
    }

    override fun requestRender(mode: Int?) {
        mRenderThread?.requestRender(mode)
    }
}

渲染线程

kotlin 复制代码
class RenderThread(private val 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 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 updateSize(width: Int, height: Int) {
        synchronized(lock) {
            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
            lock.notifyAll()
            Log.d(TAG, "requestRender shutdown")
        }
        join() // 等待线程完成

        renderData.onSurfaceDestroyed()
        mEGLEnvironment?.release()
    }

    override fun run() {
        mEGLEnvironment = EGLEnvironmentBuilder().build(surface)
        renderData.onSurfaceCreated()
        renderLoop()
    }

    private fun renderLoop() {
        while (running) {
            synchronized(lock) {
                // 等待渲染条件
                while (running && !requestRender && !sizeChanged && renderMode != RENDERMODE_CONTINUOUSLY) {
                    lock.wait()
                }

                if (!running) return

                // 处理尺寸变化
                if (sizeChanged) {
                    renderData.onSurfaceChanged(mCacheWidth, mCacheHeight)
                    sizeChanged = false
                }

                // 处理渲染
                if (requestRender || renderMode == RENDERMODE_CONTINUOUSLY) {
                    renderData.onDrawFrame()
                    mEGLEnvironment?.swapBuffers()
                    requestRender = false
                }
            }
        }
    }
}

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

效果图

相关推荐
sheji34161 小时前
【开题答辩全过程】以 基于springboot游泳馆管理系统为例,包含答辩的问题和答案
java·spring boot·后端
高级盘丝洞1 小时前
如何通过Powerlink协议读取PLC数据
开发语言·数据库·php
unicrom_深圳市由你创科技1 小时前
使用 Vue3 + Nest.js 构建前后端分离项目的完整指南
开发语言·javascript·状态模式
Savvy..1 小时前
包装类详解
java·包装类
我叫张小白。1 小时前
Vue3 v-model:组件通信的语法糖
开发语言·前端·javascript·vue.js·elementui·前端框架·vue
q***16081 小时前
解决 IntelliJ IDEA 中 Tomcat 日志乱码问题的详细指南
java·tomcat·intellij-idea
天天摸鱼的java工程师2 小时前
MySQL 的锁机制和数据隔离:一个 Java 老兵的实战总结
java·后端
城东米粉儿2 小时前
为ViewGroup 对象的布局更改添加动画效果 笔记
android
AI_56782 小时前
从“插件装一堆”到“效率翻一倍”——IntelliJ IDEA的插件化开发革命
java·ide·intellij-idea