rokevin
移动
前端
语言
  • 基础

    • Linux
    • 实施
    • 版本构建
  • 应用

    • WEB服务器
    • 数据库
  • 资讯

    • 工具
    • 部署
开放平台
产品设计
  • 人工智能
  • 云计算
计算机
其它
GitHub
移动
前端
语言
  • 基础

    • Linux
    • 实施
    • 版本构建
  • 应用

    • WEB服务器
    • 数据库
  • 资讯

    • 工具
    • 部署
开放平台
产品设计
  • 人工智能
  • 云计算
计算机
其它
GitHub
  • SurfaceView 与 TextureView

  • 一、 SurfaceView 详解
    • 1. 核心定义
    • 2. 核心原理
    • 3. 典型应用场景
    • 4. 优缺点
  • 二、 TextureView 详解
    • 1. 核心定义
    • 2. 核心原理
    • 3. 典型应用场景
    • 4. 优缺点
  • 三、 SurfaceView 与 TextureView 核心差异对比表
  • 四、 选型建议
  • 五、SurfaceView 与 TextureView 基础使用代码模板(Android)
    • 1. SurfaceView 基础使用模板
      • a. 布局文件(activity_surface_view.xml)
      • b. Java 代码(SurfaceViewActivity.java)
      • c. 关键注意点
    • 2. TextureView 基础使用模板
      • a. 布局文件(activity_texture_view.xml)
      • b. Java 代码(TextureViewActivity.java)
      • c. 关键注意点
    • 3. 通用配置与调试说明
    • 4. 扩展说明
  • 六、Android 相机预览 + 视频播放 组件绑定完整示例(含代码模板)
    • 一、 前置准备:添加依赖(模块 build.gradle)
    • 二、 示例 1:相机预览(CameraX + SurfaceView/TextureView)
      • 1. 布局文件(layout/activity_camera_preview.xml)
      • 2. 核心代码(CameraPreviewActivity.kt)
      • 3. 关键说明(Surface 绑定核心逻辑)
    • 三、 示例 2:视频播放(Media3 ExoPlayer + SurfaceView/TextureView)
      • 1. 布局文件(layout/activity_video_play.xml)
      • 2. 核心代码(VideoPlayActivity.kt)
      • 3. 关键说明(Surface 绑定核心逻辑)
    • 四、 权限配置与运行说明
    • 五、 总结
  • 七、相机预览 + 视频播放 功能增强版代码(全屏切换 + 拍照保存)
    • 一、 前置准备:补充权限与依赖
      • 1. 新增权限(AndroidManifest.xml)
      • 2. 新增文件路径配置(res/xml/file_paths.xml)
      • 3. 补充依赖(若需压缩图片,可选)
    • 二、 增强版 1:相机预览 + 拍照保存功能
      • 1. 增强版布局文件(activity_camera_preview.xml)
      • 2. 增强版核心代码(CameraPreviewActivity.kt)
      • 3. 关键说明
    • 三、 增强版 2:视频播放 + 全屏切换功能
      • 1. 增强版布局文件(activity_video_play.xml)
      • 2. 增强版核心代码(VideoPlayActivity.kt)
      • 3. 关键说明
    • 四、 集成到组件化项目说明

SurfaceView 与 TextureView

SurfaceView 和 TextureView 是 Android 中两个支持独立渲染的核心组件,均用于解决普通 View 渲染(依赖 UI 线程 + RenderThread)的性能瓶颈,适用于视频播放、游戏、相机预览等高帧率 / 低延迟场景,但二者的设计原理、使用场景和特性差异显著。

一、 SurfaceView 详解

1. 核心定义

SurfaceView 是 View 的子类,拥有独立的 Surface 和渲染线程,其渲染操作完全脱离 Activity 的主 Surface,由 SurfaceFlinger 直接管理和合成。

2. 核心原理

  • 独立渲染载体:SurfaceView 会创建一个独立于主窗口的 Surface,这个 Surface 有自己的 BufferQueue,渲染数据直接写入专属缓冲区,不占用主 Surface 的资源。
  • 双缓冲机制:内部默认采用双缓冲(前台缓冲区用于显示,后台缓冲区用于渲染),避免渲染过程中出现画面撕裂。
  • 层级合成特性:SurfaceView 的 Surface 在 SurfaceFlinger 中的层级独立于 Activity 视图层级,可以通过 setZOrderOnTop()/setZOrderMediaOverlay() 控制其与主窗口的层级关系。

3. 典型应用场景

  • 相机预览:Android 相机 API 的预览画面必须通过 SurfaceView 承载,低延迟且不阻塞 UI 线程。
  • 高性能游戏:早期 2D/3D 游戏(如基于 OpenGL ES 开发的游戏),利用独立渲染线程提升帧率。
  • 全屏视频播放:原生视频播放器的底层渲染载体,支持硬件解码数据直接写入 Surface。

4. 优缺点

优点缺点
渲染性能极高,完全脱离 UI 线程,适合高帧率场景不支持 View 相关的变换(如 setAlpha() 透明度、setRotation() 旋转),变换需通过 OpenGL ES 实现
支持硬件加速合成,SurfaceFlinger 优先使用 HWC 合成层级固定,无法嵌入到 ScrollView、RecyclerView 等滚动容器中(滚动时会出现画面卡顿 / 错位)
内存占用相对较低,缓冲区独立管理可见性控制较复杂,visibility 切换时会触发 Surface 的创建 / 销毁,存在短暂延迟

二、 TextureView 详解

1. 核心定义

TextureView 是 Android 4.0(API 14)引入的 View 子类,通过 SurfaceTexture 间接关联 Surface,将渲染数据转换为 OpenGL 纹理,再作为普通 View 绘制到主 Surface 上。

2. 核心原理

  • SurfaceTexture 桥梁作用:TextureView 内部持有一个 SurfaceTexture 对象,SurfaceTexture 会创建一个后台 Surface,渲染数据写入该 Surface 后,会被自动转换为 OpenGL 纹理。
  • 依赖主渲染管线:TextureView 的最终绘制依赖 Activity 的 RenderThread,它会将 OpenGL 纹理作为普通 View 的内容,参与主视图层级的测量、布局和绘制。
  • 完全兼容 View 特性:因为本质是 View,所以支持所有 View 的属性和变换操作。

3. 典型应用场景

  • 带 UI 叠加的视频播放:如视频画面上叠加弹幕、控制按钮,支持透明度调整和旋转。
  • 嵌入滚动容器的渲染内容:如在 RecyclerView 中播放短视频,支持随列表滚动而平滑移动。
  • 需要变换的相机预览:如相机预览画面需要裁剪、旋转、添加滤镜效果。

4. 优缺点

优点缺点
完全兼容 View 的所有特性(透明度、旋转、缩放、动画)渲染性能略低于 SurfaceView,因为多了 “纹理转换 + 主管线绘制” 两步操作
支持嵌入任意布局容器,滚动时无卡顿必须在开启硬件加速的窗口中使用(默认开启,关闭后会崩溃)
可见性切换平滑,不会触发 Surface 销毁重建内存占用略高,需维护 OpenGL 纹理和缓冲区

三、 SurfaceView 与 TextureView 核心差异对比表

对比维度SurfaceViewTextureView
核心载体独立的 Surface,由 SurfaceFlinger 直接管理基于 SurfaceTexture 关联后台 Surface,渲染数据转为 OpenGL 纹理
渲染线程拥有独立的渲染线程,与 UI 线程完全分离依赖 Activity 的 RenderThread,属于主渲染管线的一部分
View 特性兼容不支持 alpha、rotation 等 View 变换,不支持动画完全支持所有 View 特性和属性动画
层级管理Surface 层级独立于主视图,由 ZOrder 控制属于主视图层级,与其他 View 遵循相同的层级规则
布局兼容性无法嵌入滚动容器(如 ScrollView),滚动时画面错位可嵌入任意布局容器,支持平滑滚动
硬件加速依赖无强制依赖,关闭硬件加速仍可工作强制依赖硬件加速,关闭后会抛出异常
性能表现高,适合 60/120Hz 高帧率场景中,适合对交互性要求高于极致性能的场景
适用场景相机预览、高性能游戏、全屏无叠加视频带 UI 叠加的视频、滚动列表中的短视频、需要变换的渲染内容
创建 / 销毁成本高,visibility 切换会触发 Surface 重建低,visibility 切换仅控制纹理绘制,无 Surface 重建

四、 选型建议

  1. 优先选 SurfaceView:当你需要极致渲染性能,且不需要对画面做复杂变换时(如相机预览、原生游戏、全屏视频)。
  2. 优先选 TextureView:当你需要兼容 View 特性,或需要将渲染内容嵌入布局 / 滚动容器时(如带弹幕的视频、列表中的短视频、带滤镜的相机预览)。

五、SurfaceView 与 TextureView 基础使用代码模板(Android)

以下模板包含组件初始化、渲染数据绑定、生命周期管理核心内容,基于 Android 原生 API 编写,可直接复制到项目中调试,适配 API 14+(TextureView 最低要求 API 14,SurfaceView 最低要求 API 1)。

1. SurfaceView 基础使用模板

a. 布局文件(activity_surface_view.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">

    <!-- SurfaceView 核心布局,设置固定大小或 match_parent -->
    <SurfaceView
        android:id="@+id/surfaceView"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:background="@android:color/black" />

    <!-- 辅助控制按钮(可选) -->
    <Button
        android:id="@+id/btnStartRender"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="开始渲染" />

</LinearLayout>

b. Java 代码(SurfaceViewActivity.java)

import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Bundle;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.widget.Button;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SurfaceViewActivity extends Activity implements SurfaceHolder.Callback {
    // 核心组件
    private SurfaceView mSurfaceView;
    private SurfaceHolder mSurfaceHolder;
    // 渲染线程(独立于 UI 线程,避免阻塞)
    private ExecutorService mRenderExecutor;
    private boolean isRendering = false;
    // 绘制工具
    private Paint mPaint;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_surface_view);

        // 初始化组件
        initView();
        // 初始化绘制工具
        initPaint();
        // 初始化渲染线程池
        mRenderExecutor = Executors.newSingleThreadExecutor();
    }

    /**
     * 初始化 View 组件
     */
    private void initView() {
        mSurfaceView = findViewById(R.id.surfaceView);
        Button btnStartRender = findViewById(R.id.btnStartRender);

        // 获取 SurfaceHolder 并注册回调(监听 Surface 创建/销毁/改变)
        mSurfaceHolder = mSurfaceView.getHolder();
        mSurfaceHolder.addCallback(this);

        // 控制按钮点击事件
        btnStartRender.setOnClickListener(v -> {
            if (!isRendering) {
                startRender();
            } else {
                stopRender();
            }
        });
    }

    /**
     * 初始化绘制画笔
     */
    private void initPaint() {
        mPaint = new Paint();
        mPaint.setColor(Color.RED);
        mPaint.setStrokeWidth(5);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setAntiAlias(true);
    }

    /**
     * 开始渲染(启动独立线程执行绘制任务)
     */
    private void startRender() {
        isRendering = true;
        mRenderExecutor.execute(this::doRender);
    }

    /**
     * 停止渲染
     */
    private void stopRender() {
        isRendering = false;
    }

    /**
     * 核心渲染逻辑(在独立线程中执行)
     */
    private void doRender() {
        // 循环渲染,直到 isRendering 为 false
        while (isRendering) {
            // 关键:锁定 Surface 画布,获取可绘制的 Canvas
            // LOCK_SCREEN_BEHIND:确保绘制内容在屏幕后方,避免闪烁
            Canvas canvas = mSurfaceHolder.lockCanvas();
            if (canvas == null) {
                // 画布获取失败,跳过本次渲染(Surface 可能已销毁)
                continue;
            }

            try {
                // 1. 清空画布(避免上一帧内容残留)
                canvas.drawColor(Color.BLACK);

                // 2. 执行自定义绘制逻辑(示例:绘制移动的圆形)
                long currentTime = System.currentTimeMillis();
                float x = (float) (Math.sin(currentTime / 1000.0) * canvas.getWidth() / 2) + canvas.getWidth() / 2;
                float y = (float) (Math.cos(currentTime / 1000.0) * canvas.getHeight() / 2) + canvas.getHeight() / 2;
                canvas.drawCircle(x, y, 50, mPaint);

                // 3. 短暂休眠,模拟 60Hz 帧率(约 16ms 一帧)
                Thread.sleep(16);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                // 关键:解锁画布并提交绘制内容(必须在 finally 中执行,避免画布泄露)
                mSurfaceHolder.unlockCanvasAndPost(canvas);
            }
        }
    }

    // ==================== SurfaceHolder.Callback 回调方法 ====================
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        // Surface 创建完成(Activity 可见时触发),可准备渲染资源
        // 此时 Surface 已具备绘制条件
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        // Surface 大小/格式改变(如屏幕旋转、窗口大小调整)
        // 可在此更新渲染尺寸、重新初始化绘制参数
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        // Surface 销毁(Activity 不可见/销毁时触发),停止渲染并释放资源
        stopRender();
    }

    // ==================== 生命周期管理 ====================
    @Override
    protected void onPause() {
        super.onPause();
        // 页面暂停时,停止渲染
        stopRender();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 页面销毁时,释放线程池和资源
        if (mRenderExecutor != null && !mRenderExecutor.isShutdown()) {
            mRenderExecutor.shutdownNow();
        }
        mPaint = null;
    }
}

c. 关键注意点

  1. SurfaceHolder 是操作 SurfaceView 的核心,必须通过 getHolder() 获取并注册回调。
  2. 绘制前必须调用 lockCanvas() 锁定画布,绘制完成后必须调用 unlockCanvasAndPost() 提交,二者成对出现。
  3. 渲染逻辑必须放在独立线程中,避免阻塞 UI 线程(示例使用单线程线程池)。
  4. surfaceDestroyed() 中必须停止渲染,否则会出现空指针异常(Surface 已销毁但仍尝试绘制)。

2. TextureView 基础使用模板

a. 布局文件(activity_texture_view.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">

    <!-- TextureView 核心布局,支持 View 所有属性(如 alpha、rotation) -->
    <TextureView
        android:id="@+id/textureView"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:alpha="0.9"
        android:rotation="0" />

    <!-- 辅助控制按钮(可选) -->
    <Button
        android:id="@+id/btnStartRender"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="开始渲染" />

</LinearLayout>

b. Java 代码(TextureViewActivity.java)

import android.app.Activity;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.SurfaceTexture;
import android.os.Bundle;
import android.view.TextureView;
import android.view.View;
import android.widget.Button;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TextureViewActivity extends Activity implements TextureView.SurfaceTextureListener {
    // 核心组件
    private TextureView mTextureView;
    private SurfaceTexture mSurfaceTexture;
    // 渲染线程
    private ExecutorService mRenderExecutor;
    private boolean isRendering = false;
    // 绘制工具
    private Paint mPaint;
    // TextureView 宽高
    private int mViewWidth, mViewHeight;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_texture_view);

        // 初始化组件
        initView();
        // 初始化绘制工具
        initPaint();
        // 初始化渲染线程池
        mRenderExecutor = Executors.newSingleThreadExecutor();
    }

    /**
     * 初始化 View 组件
     */
    private void initView() {
        mTextureView = findViewById(R.id.textureView);
        Button btnStartRender = findViewById(R.id.btnStartRender);

        // 注册 SurfaceTexture 回调(监听 TextureView 初始化/销毁/改变)
        mTextureView.setSurfaceTextureListener(this);

        // 支持 View 变换(示例:设置旋转动画,体现 TextureView 的灵活性)
        mTextureView.animate().rotation(360).setDuration(5000).repeatCount(-1);

        // 控制按钮点击事件
        btnStartRender.setOnClickListener(v -> {
            if (!isRendering) {
                startRender();
            } else {
                stopRender();
            }
        });
    }

    /**
     * 初始化绘制画笔
     */
    private void initPaint() {
        mPaint = new Paint();
        mPaint.setColor(Color.BLUE);
        mPaint.setStrokeWidth(5);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setAntiAlias(true);
    }

    /**
     * 开始渲染
     */
    private void startRender() {
        if (mSurfaceTexture == null) {
            return; // SurfaceTexture 未初始化,无法渲染
        }
        isRendering = true;
        mRenderExecutor.execute(this::doRender);
    }

    /**
     * 停止渲染
     */
    private void stopRender() {
        isRendering = false;
    }

    /**
     * 核心渲染逻辑(在独立线程中执行)
     */
    private void doRender() {
        while (isRendering) {
            // 关键:获取 TextureView 的可绘制画布(API 24+ 支持,低于 24 需通过 SurfaceTexture 绑定 OpenGL)
            Canvas canvas = mTextureView.lockCanvas();
            if (canvas == null) {
                continue;
            }

            try {
                // 1. 清空画布
                canvas.drawColor(Color.BLACK);

                // 2. 执行自定义绘制逻辑(示例:绘制移动的矩形)
                long currentTime = System.currentTimeMillis();
                float x = (float) (Math.sin(currentTime / 1500.0) * mViewWidth / 2) + mViewWidth / 2;
                float y = (float) (Math.cos(currentTime / 1500.0) * mViewHeight / 2) + mViewHeight / 2;
                canvas.drawRect(x - 50, y - 50, x + 50, y + 50, mPaint);

                // 3. 模拟 60Hz 帧率
                Thread.sleep(16);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                // 3. 解锁画布并提交
                mTextureView.unlockCanvasAndPost(canvas);
            }
        }
    }

    // ==================== TextureView.SurfaceTextureListener 回调方法 ====================
    @Override
    public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
        // SurfaceTexture 可用(TextureView 初始化完成)
        mSurfaceTexture = surface;
        mViewWidth = width;
        mViewHeight = height;
    }

    @Override
    public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
        // TextureView 大小改变
        mViewWidth = width;
        mViewHeight = height;
    }

    @Override
    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
        // SurfaceTexture 销毁,停止渲染并释放资源
        stopRender();
        mSurfaceTexture = null;
        // 返回 true 表示由系统释放 SurfaceTexture,返回 false 表示自行管理
        return true;
    }

    @Override
    public void onSurfaceTextureUpdated(SurfaceTexture surface) {
        // TextureView 内容更新(每帧绘制完成后触发,可用于监控渲染状态)
    }

    // ==================== 生命周期管理 ====================
    @Override
    protected void onPause() {
        super.onPause();
        stopRender();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mRenderExecutor != null && !mRenderExecutor.isShutdown()) {
            mRenderExecutor.shutdownNow();
        }
        mPaint = null;
    }
}

c. 关键注意点

  1. TextureView 强制依赖硬件加速(默认开启,若手动关闭会抛出 IllegalStateException)。
  2. 低版本 Android(< API 24)不支持直接 lockCanvas(),需通过 SurfaceTexture 绑定 OpenGL ES 进行渲染。
  3. onSurfaceTextureDestroyed() 返回 true 表示交由系统释放 SurfaceTexture,避免内存泄露。
  4. 支持 View 所有属性变换(如 alpha、rotation、translation),无需额外处理。

3. 通用配置与调试说明

  1. 权限配置:无需额外权限,直接运行即可。

  2. 最低 API 要求:

    • SurfaceView:API 1+
    • TextureView:API 14+,lockCanvas() 直接绘制要求 API 24+
  3. 调试技巧:

    • 若出现画面闪烁,可在 lockCanvas() 时指定绘制区域(lockCanvas(new Rect()))。
    • 若渲染线程阻塞,可通过 Android Studio 的 Profiler 监控线程状态。
  4. 生命周期规范:在 onPause()/onDestroy() 中必须停止渲染并释放线程资源,避免内存泄露。

4. 扩展说明

  1. 上述模板是纯 Canvas 绘制示例,实际场景中(如视频播放、相机预览),无需手动绘制,只需将渲染数据源(如 MediaCodec、Camera)绑定到 SurfaceView/TextureView 即可。
  2. 对于高性能需求(如 3D 游戏),需通过 OpenGL ES 绑定 Surface/SurfaceTexture 进行渲染,模板中的 Canvas 绘制仅用于演示基础用法。

六、Android 相机预览 + 视频播放 组件绑定完整示例(含代码模板)

下面将分别实现「相机预览(基于 CameraX + SurfaceView/TextureView)」和「视频播放(基于 Media3 ExoPlayer + SurfaceView/TextureView)」,清晰展示 Surface、BufferQueue 与核心渲染组件的绑定逻辑,兼容 Android 10(API 29)及以上,贴合之前的组件化项目架构。

一、 前置准备:添加依赖(模块 build.gradle)

在 module_image/module_video 或独立功能模块中添加以下依赖,支持 CameraX 相机预览和 Media3 视频播放:

dependencies {
    // 相机预览:CameraX(Google 官方,简化相机操作)
    implementation "androidx.camera:camera-core:1.2.3"
    implementation "androidx.camera:camera-camera2:1.2.3"
    implementation "androidx.camera:camera-lifecycle:1.2.3"
    implementation "androidx.camera:camera-view:1.2.3"

    // 视频播放:Media3(ExoPlayer 升级版,兼容 Android 10)
    implementation "androidx.media3:media3-exoplayer:1.2.0"
    implementation "androidx.media3:media3-ui:1.2.0"
    implementation "androidx.media3:media3-exoplayer-core:1.2.0"

    // 基础依赖
    implementation "androidx.core:core-ktx:1.7.0"
    implementation "androidx.appcompat:appcompat:1.6.1"
    implementation "com.google.android.material:material:1.9.0"
}

二、 示例 1:相机预览(CameraX + SurfaceView/TextureView)

CameraX 已封装底层 Surface 绑定逻辑,我们只需将相机输出与 SurfaceView/TextureView 绑定,即可实现预览,核心是将预览帧数据写入 Surface 的 BufferQueue,再由 SurfaceFlinger + HWC 合成显示。

1. 布局文件(layout/activity_camera_preview.xml)

提供 SurfaceView 和 TextureView 两种预览载体,可切换测试:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!-- 相机预览载体 1:SurfaceView(独立 Surface,性能更优) -->
    <SurfaceView
        android:id="@+id/surfaceViewCamera"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="visible" />

    <!-- 相机预览载体 2:TextureView(集成到 View 树,支持变换,默认隐藏) -->
    <TextureView
        android:id="@+id/textureViewCamera"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="gone"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

    <!-- 切换预览载体按钮 -->
    <com.google.android.material.button.MaterialButton
        android:id="@+id/btnSwitchPreview"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="切换预览载体"
        android:layout_margin="20dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

2. 核心代码(CameraPreviewActivity.kt)

package com.example.module_image.camera

import android.Manifest
import android.content.pm.PackageManager
import android.os.Bundle
import android.view.SurfaceView
import android.view.TextureView
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.CameraSelector
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.example.module_image.R
import com.google.common.util.concurrent.ListenableFuture
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors

/**
 * 相机预览示例(CameraX + SurfaceView/TextureView)
 * 核心:将相机预览帧绑定到 Surface 载体,实现高效预览
 */
class CameraPreviewActivity : AppCompatActivity() {
    // 相机相关变量
    private lateinit var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>
    private lateinit var cameraExecutor: ExecutorService
    private var isUsingSurfaceView = true // 当前是否使用 SurfaceView 预览

    // 视图控件
    private lateinit var surfaceViewCamera: SurfaceView
    private lateinit var textureViewCamera: TextureView

    // 所需权限(相机权限,Android 10 及以上无需 WRITE_EXTERNAL_STORAGE 用于预览)
    private val requiredPermissions = arrayOf(Manifest.permission.CAMERA)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_camera_preview)

        // 初始化视图
        initView()

        // 初始化相机执行器
        cameraExecutor = Executors.newSingleThreadExecutor()

        // 检查权限并启动预览
        if (allPermissionsGranted()) {
            startCameraPreview()
        } else {
            ActivityCompat.requestPermissions(this, requiredPermissions, REQUEST_CODE_PERMISSIONS)
        }
    }

    /**
     * 初始化视图
     */
    private fun initView() {
        surfaceViewCamera = findViewById(R.id.surfaceViewCamera)
        textureViewCamera = findViewById(R.id.textureViewCamera)

        // 切换预览载体按钮点击事件
        findViewById<View>(R.id.btnSwitchPreview).setOnClickListener {
            isUsingSurfaceView = !isUsingSurfaceView
            // 切换视图可见性
            surfaceViewCamera.visibility = if (isUsingSurfaceView) View.VISIBLE else View.GONE
            textureViewCamera.visibility = if (isUsingSurfaceView) View.GONE else View.VISIBLE
            // 重新启动相机预览(绑定新的 Surface 载体)
            startCameraPreview()
        }
    }

    /**
     * 启动相机预览(核心:绑定 Surface 载体)
     */
    private fun startCameraPreview() {
        cameraProviderFuture = ProcessCameraProvider.getInstance(this)

        // 监听 CameraProvider 就绪
        cameraProviderFuture.addListener({
            // 获取 CameraProvider(用于绑定相机生命周期)
            val cameraProvider = cameraProviderFuture.get()

            // 1. 配置预览用例
            val preview = Preview.Builder()
                .build()
                .also { previewUseCase ->
                    // 2. 绑定预览载体(SurfaceView/TextureView)
                    val previewSurfaceProvider = if (isUsingSurfaceView) {
                        // 绑定 SurfaceView:获取其 Surface 作为预览输出
                        Preview.SurfaceProvider { request ->
                            val surface = surfaceViewCamera.holder.surface
                            val surfaceRequest = request.provideSurface(surface, cameraExecutor) {
                                // 表面释放回调,无需额外处理
                            }
                        }
                    } else {
                        // 绑定 TextureView:获取其 SurfaceTexture 对应的 Surface 作为预览输出
                        Preview.SurfaceProvider { request ->
                            val surfaceTexture = textureViewCamera.surfaceTexture ?: return@SurfaceProvider
                            // 配置 SurfaceTexture 尺寸(与预览尺寸匹配)
                            surfaceTexture.setDefaultBufferSize(
                                request.resolution.width,
                                request.resolution.height
                            )
                            val surface = android.view.Surface(surfaceTexture)
                            val surfaceRequest = request.provideSurface(surface, cameraExecutor) {
                                // 表面释放时销毁 Surface
                                surface.release()
                            }
                        }
                    }

                    // 3. 将 SurfaceProvider 绑定到预览用例
                    previewUseCase.setSurfaceProvider(previewSurfaceProvider)
                }

            // 4. 选择相机(后置相机优先)
            val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

            try {
                // 先解绑所有已绑定的用例
                cameraProvider.unbindAll()

                // 5. 绑定相机用例与 Activity 生命周期(核心:保证相机与页面生命周期同步)
                cameraProvider.bindToLifecycle(
                    this,
                    cameraSelector,
                    preview
                )

            } catch (e: Exception) {
                e.printStackTrace()
            }

        }, ContextCompat.getMainExecutor(this))
    }

    /**
     * 检查所有权限是否已授予
     */
    private fun allPermissionsGranted(): Boolean {
        return requiredPermissions.all {
            ContextCompat.checkSelfPermission(
                this, it
            ) == PackageManager.PERMISSION_GRANTED
        }
    }

    /**
     * 权限请求回调
     */
    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (requestCode == REQUEST_CODE_PERMISSIONS) {
            if (allPermissionsGranted()) {
                startCameraPreview()
            } else {
                // 权限未授予,提示用户
                finish()
            }
        }
    }

    /**
     * 销毁时释放相机执行器
     */
    override fun onDestroy() {
        super.onDestroy()
        cameraExecutor.shutdown()
    }

    companion object {
        private const val REQUEST_CODE_PERMISSIONS = 1001
    }
}

3. 关键说明(Surface 绑定核心逻辑)

  1. SurfaceView 绑定:通过 SurfaceView.holder.surface 获取现成 Surface,直接传递给 CameraX 预览用例,Surface 由 SurfaceView 管理生命周期,无需手动释放;
  2. TextureView 绑定:先获取 TextureView.surfaceTexture,再通过 android.view.Surface(surfaceTexture) 包装为 Surface,需手动配置缓冲区尺寸并释放 Surface;
  3. 数据流转:相机采集的帧数据 → 写入 Surface 对应的 BufferQueue → SurfaceFlinger 读取数据并与其他窗口合成 → HWC 硬件加速输出到屏幕;
  4. 生命周期绑定:通过 ProcessCameraProvider.bindToLifecycle() 绑定相机与 Activity,避免内存泄漏和相机占用异常。

三、 示例 2:视频播放(Media3 ExoPlayer + SurfaceView/TextureView)

ExoPlayer 支持直接绑定 SurfaceView/TextureView,底层将视频解码帧写入 Surface 的 BufferQueue,实现流畅播放,兼容本地视频和网络视频。

1. 布局文件(layout/activity_video_play.xml)

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!-- 视频播放载体 1:SurfaceView(ExoPlayer 推荐,性能更优) -->
    <SurfaceView
        android:id="@+id/surfaceViewVideo"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="visible" />

    <!-- 视频播放载体 2:TextureView(支持缩放/旋转,默认隐藏) -->
    <TextureView
        android:id="@+id/textureViewVideo"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="gone"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

    <!-- 控制按钮 -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_margin="20dp"
        app:layout_constraintBottom_toBottomOf="parent">

        <com.google.android.material.button.MaterialButton
            android:id="@+id/btnPlay"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="播放" />

        <com.google.android.material.button.MaterialButton
            android:id="@+id/btnPause"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="暂停" />

        <com.google.android.material.button.MaterialButton
            android:id="@+id/btnSwitchPlayer"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="2"
            android:text="切换播放载体" />

    </LinearLayout>

</androidx.constraintlayout.widget.ConstraintLayout>

2. 核心代码(VideoPlayActivity.kt)

package com.example.module_video.play

import android.os.Bundle
import android.view.Surface
import android.view.SurfaceView
import android.view.TextureView
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import com.example.module_video.R
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.exoplayer.ExoPlayer

/**
 * 视频播放示例(Media3 ExoPlayer + SurfaceView/TextureView)
 * 核心:将视频解码帧绑定到 Surface 载体,实现高效播放
 */
class VideoPlayActivity : AppCompatActivity() {
    // 视频播放器变量
    private lateinit var exoPlayer: ExoPlayer
    private var isUsingSurfaceView = true // 当前是否使用 SurfaceView 播放
    private var videoUrl = "https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/720/Big_Buck_Bunny_720_10s_1MB.mp4" // 测试视频地址

    // 视图控件
    private lateinit var surfaceViewVideo: SurfaceView
    private lateinit var textureViewVideo: TextureView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_video_play)

        // 初始化视图
        initView()

        // 初始化 ExoPlayer
        initExoPlayer()

        // 准备并播放视频
        prepareAndPlayVideo()
    }

    /**
     * 初始化视图
     */
    private fun initView() {
        surfaceViewVideo = findViewById(R.id.surfaceViewVideo)
        textureViewVideo = findViewById(R.id.textureViewVideo)

        // 播放按钮
        findViewById<View>(R.id.btnPlay).setOnClickListener {
            if (!exoPlayer.isPlaying) {
                exoPlayer.play()
            }
        }

        // 暂停按钮
        findViewById<View>(R.id.btnPause).setOnClickListener {
            if (exoPlayer.isPlaying) {
                exoPlayer.pause()
            }
        }

        // 切换播放载体按钮
        findViewById<View>(R.id.btnSwitchPlayer).setOnClickListener {
            isUsingSurfaceView = !isUsingSurfaceView
            // 切换视图可见性
            surfaceViewVideo.visibility = if (isUsingSurfaceView) View.VISIBLE else View.GONE
            textureViewVideo.visibility = if (isUsingSurfaceView) View.GONE else View.VISIBLE
            // 重新绑定播放载体
            bindPlayerToSurface()
        }
    }

    /**
     * 初始化 ExoPlayer(核心:创建播放器实例,配置参数)
     */
    private fun initExoPlayer() {
        exoPlayer = ExoPlayer.Builder(this)
            .build()
            .also { player ->
                // 设置播放完成回调(循环播放)
                player.addListener(object : Player.Listener {
                    override fun onPlaybackStateChanged(playbackState: Int) {
                        super.onPlaybackStateChanged(playbackState)
                        if (playbackState == Player.STATE_ENDED) {
                            player.seekTo(0)
                            player.play()
                        }
                    }
                })
            }

        // 初始绑定播放载体
        bindPlayerToSurface()
    }

    /**
     * 绑定播放器到 Surface 载体(SurfaceView/TextureView)
     */
    private fun bindPlayerToSurface() {
        val playerSurface = if (isUsingSurfaceView) {
            // 绑定 SurfaceView:获取其 Surface
            Surface(surfaceViewVideo.holder.surface)
        } else {
            // 绑定 TextureView:获取 SurfaceTexture 并包装为 Surface
            val surfaceTexture = textureViewVideo.surfaceTexture ?: return
            // 配置缓冲区尺寸(与视频尺寸匹配,可选)
            surfaceTexture.setDefaultBufferSize(1280, 720)
            Surface(surfaceTexture)
        }

        // 将 Surface 绑定到 ExoPlayer(核心:视频解码帧写入该 Surface 的 BufferQueue)
        exoPlayer.setVideoSurface(playerSurface)
    }

    /**
     * 准备并播放视频
     */
    private fun prepareAndPlayVideo() {
        // 构建媒体项(支持本地视频:MediaItem.fromUri(Uri.fromFile(file)))
        val mediaItem = MediaItem.fromUri(videoUrl)

        // 设置媒体项并准备播放
        exoPlayer.setMediaItem(mediaItem)
        exoPlayer.prepare()
        exoPlayer.play()
    }

    /**
     * 页面暂停时,暂停视频播放
     */
    override fun onPause() {
        super.onPause()
        if (exoPlayer.isPlaying) {
            exoPlayer.pause()
        }
    }

    /**
     * 页面销毁时,释放播放器资源
     */
    override fun onDestroy() {
        super.onDestroy()
        exoPlayer.release() // 必须释放,避免内存泄漏和资源占用
    }
}

3. 关键说明(Surface 绑定核心逻辑)

  1. ExoPlayer 与 Surface 绑定:通过 exoPlayer.setVideoSurface() 将 Surface 传递给播放器,底层 MediaCodec 解码视频帧后,直接写入该 Surface 对应的 BufferQueue;

  2. 两种载体差异:

    • SurfaceView:拥有独立 Surface,不占用 View 树绘制资源,播放高帧率视频更流畅,无过度绘制;
    • TextureView:将 SurfaceTexture 作为 View 绘制内容,支持缩放、旋转、透明度等 View 变换,性能略低于 SurfaceView;
  3. 数据流转:网络 / 本地视频 → ExoPlayer 解码 → 帧数据写入 Surface 的 BufferQueue → SurfaceFlinger 合成 → HWC 硬件加速显示 → 屏幕呈现视频画面;

  4. 资源释放:ExoPlayer 必须在 onDestroy() 中调用 release(),否则会导致内存泄漏和音频继续播放。

四、 权限配置与运行说明

  1. 权限添加(AndroidManifest.xml)
<!-- 相机权限 -->
<uses-permission android:name="android.permission.CAMERA" />
<!-- 网络权限(用于播放网络视频) -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- 声明相机功能(可选) -->
<uses-feature android:name="android.hardware.camera" android:required="true" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
  1. 运行要求:
    • 相机预览需在真实设备上测试(模拟器无相机硬件);
    • 网络视频播放需保证设备联网,且 Android 9.0 及以上需配置 cleartextTrafficPermitted="true"(允许明文 HTTP 访问):
<application
    ...
    android:usesCleartextTraffic="true">
</application>
  1. 组件化集成:可将这两个 Activity 注册到 ARouter 路由,在首页添加跳转入口,融入之前的组件化项目。

五、 总结

  1. 相机预览和视频播放的核心是将「数据源(相机采集 / 视频解码)」与「Surface 载体(SurfaceView/TextureView)」绑定,帧数据最终写入 Surface 的 BufferQueue 完成渲染;
  2. SurfaceView 适合高性能场景(直播、高帧率视频),TextureView 适合需要 View 变换的场景(视频裁剪、旋转);
  3. 代码模板已兼容 Android 10,可直接拷贝到之前的组件化项目中,无需额外修改核心逻辑。

七、相机预览 + 视频播放 功能增强版代码(全屏切换 + 拍照保存)

以下是基于之前代码模板的功能增强版,新增 视频全屏切换 和 相机拍照保存 功能,兼容 Android 10(API 29),可直接集成到组件化项目中。

一、 前置准备:补充权限与依赖

1. 新增权限(AndroidManifest.xml)

拍照保存需要文件读写权限,视频全屏需要屏幕方向权限:

<!-- 拍照保存所需权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="29"/>
<!-- 允许屏幕旋转(全屏切换需要) -->
<uses-permission android:name="android.permission.CHANGE_CONFIGURATION"/>
<uses-permission android:name="android.permission.CHANGE_ORIENTATION"/>

<!-- 声明文件提供者(Android 7.0+ 拍照必须) -->
<application>
    <provider
        android:name="androidx.core.content.FileProvider"
        android:authorities="${applicationId}.fileprovider"
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/file_paths"/>
    </provider>
</application>

2. 新增文件路径配置(res/xml/file_paths.xml)

用于指定拍照文件的存储路径,兼容 Android 7.0+ 文件访问权限:

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- 拍照保存到应用私有目录 -->
    <external-files-path name="camera_photos" path="Pictures/"/>
</paths>

3. 补充依赖(若需压缩图片,可选)

// 图片压缩(可选,用于优化拍照后的图片大小)
implementation "id.zelory:compressor:3.0.1"

二、 增强版 1:相机预览 + 拍照保存功能

在原有 CameraPreviewActivity 基础上,新增 拍照按钮 和 图片保存逻辑,使用 CameraX 的 ImageCapture 用例实现拍照。

1. 增强版布局文件(activity_camera_preview.xml)

新增拍照按钮和预览载体切换按钮:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <SurfaceView
        android:id="@+id/surfaceViewCamera"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="visible"/>

    <TextureView
        android:id="@+id/textureViewCamera"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="gone"
        app:layout_constraintTop_toTopOf="parent"/>

    <!-- 拍照按钮 -->
    <com.google.android.material.button.MaterialButton
        android:id="@+id/btnTakePhoto"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:background="@drawable/ic_camera_shutter"
        android:elevation="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        android:layout_marginBottom="30dp"/>

    <!-- 切换载体按钮 -->
    <com.google.android.material.button.MaterialButton
        android:id="@+id/btnSwitchPreview"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="切换预览载体"
        android:elevation="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        android:layout_marginBottom="30dp"
        android:layout_marginEnd="20dp"/>

</androidx.constraintlayout.widget.ConstraintLayout>

2. 增强版核心代码(CameraPreviewActivity.kt)

新增 ImageCapture 用例和拍照逻辑:

package com.example.module_image.camera

import android.Manifest
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.view.Surface
import android.view.SurfaceView
import android.view.TextureView
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import com.example.module_image.R
import com.google.common.util.concurrent.ListenableFuture
import java.io.File
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors

class CameraPreviewActivity : AppCompatActivity() {
    private lateinit var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>
    private lateinit var cameraExecutor: ExecutorService
    private var isUsingSurfaceView = true

    // 新增:拍照用例
    private var imageCapture: ImageCapture? = null
    // 拍照文件存储路径
    private lateinit var photoFile: File

    private lateinit var surfaceViewCamera: SurfaceView
    private lateinit var textureViewCamera: TextureView
    private lateinit var btnTakePhoto: View

    // 新增:权限包含存储权限
    private val requiredPermissions = arrayOf(
        Manifest.permission.CAMERA,
        Manifest.permission.WRITE_EXTERNAL_STORAGE,
        Manifest.permission.READ_EXTERNAL_STORAGE
    )

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_camera_preview)
        initView()
        cameraExecutor = Executors.newSingleThreadExecutor()

        if (allPermissionsGranted()) {
            startCameraPreview()
        } else {
            ActivityCompat.requestPermissions(this, requiredPermissions, REQUEST_CODE_PERMISSIONS)
        }
    }

    private fun initView() {
        surfaceViewCamera = findViewById(R.id.surfaceViewCamera)
        textureViewCamera = findViewById(R.id.textureViewCamera)
        btnTakePhoto = findViewById(R.id.btnTakePhoto)
        findViewById<View>(R.id.btnSwitchPreview).setOnClickListener {
            isUsingSurfaceView = !isUsingSurfaceView
            surfaceViewCamera.visibility = if (isUsingSurfaceView) View.VISIBLE else View.GONE
            textureViewCamera.visibility = if (isUsingSurfaceView) View.GONE else View.VISIBLE
            startCameraPreview()
        }

        // 新增:拍照按钮点击事件
        btnTakePhoto.setOnClickListener {
            takePhoto()
        }
    }

    private fun startCameraPreview() {
        cameraProviderFuture = ProcessCameraProvider.getInstance(this)
        cameraProviderFuture.addListener({
            val cameraProvider = cameraProviderFuture.get()

            // 1. 配置预览用例(原有逻辑不变)
            val preview = Preview.Builder()
                .build()
                .also {
                    val previewSurfaceProvider = if (isUsingSurfaceView) {
                        Preview.SurfaceProvider { request ->
                            val surface = surfaceViewCamera.holder.surface
                            request.provideSurface(surface, cameraExecutor) {}
                        }
                    } else {
                        Preview.SurfaceProvider { request ->
                            val surfaceTexture = textureViewCamera.surfaceTexture ?: return@SurfaceProvider
                            surfaceTexture.setDefaultBufferSize(request.resolution.width, request.resolution.height)
                            val surface = Surface(surfaceTexture)
                            request.provideSurface(surface, cameraExecutor) { surface.release() }
                        }
                    }
                    it.setSurfaceProvider(previewSurfaceProvider)
                }

            // 2. 新增:配置拍照用例
            imageCapture = ImageCapture.Builder()
                .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) // 低延迟拍照
                .setTargetRotation(windowManager.defaultDisplay.rotation) // 适配屏幕旋转
                .build()

            // 3. 选择后置相机
            val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

            try {
                cameraProvider.unbindAll()
                // 绑定预览 + 拍照用例
                cameraProvider.bindToLifecycle(
                    this, cameraSelector, preview, imageCapture
                )
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }, ContextCompat.getMainExecutor(this))
    }

    // 新增:拍照核心逻辑
    private fun takePhoto() {
        val imageCapture = imageCapture ?: return

        // 1. 创建拍照文件
        val photoDir = File(externalFilesDir("Pictures"), "CameraXPhotos")
        if (!photoDir.exists()) photoDir.mkdirs()
        photoFile = File(photoDir, "IMG_${System.currentTimeMillis()}.jpg")

        // 2. 配置拍照输出选项
        val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile)
            // Android 7.0+ 必须使用 FileProvider
            .setContentUri(
                FileProvider.getUriForFile(
                    this,
                    "${packageName}.fileprovider",
                    photoFile
                )
            )
            .build()

        // 3. 执行拍照
        imageCapture.takePicture(
            outputOptions,
            ContextCompat.getMainExecutor(this),
            object : ImageCapture.OnImageSavedCallback {
                override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
                    // 拍照成功回调
                    val savedUri = outputFileResults.savedUri ?: Uri.fromFile(photoFile)
                    Toast.makeText(this@CameraPreviewActivity, "照片已保存:${savedUri.path}", Toast.LENGTH_LONG).show()
                }

                override fun onError(exception: ImageCaptureException) {
                    // 拍照失败回调
                    Toast.makeText(this@CameraPreviewActivity, "拍照失败:${exception.message}", Toast.LENGTH_SHORT).show()
                    exception.printStackTrace()
                }
            }
        )
    }

    private fun allPermissionsGranted() = requiredPermissions.all {
        ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (requestCode == REQUEST_CODE_PERMISSIONS) {
            if (allPermissionsGranted()) {
                startCameraPreview()
            } else {
                Toast.makeText(this, "需要相机和存储权限才能使用", Toast.LENGTH_SHORT).show()
                finish()
            }
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        cameraExecutor.shutdown()
    }

    companion object {
        private const val REQUEST_CODE_PERMISSIONS = 1001
    }
}

3. 关键说明

  1. 拍照用例绑定:通过 ImageCapture 用例实现拍照,需与 Preview 用例一起绑定到 CameraProvider;
  2. 文件权限处理:Android 7.0+ 必须使用 FileProvider 生成文件 URI,避免 FileUriExposedException;
  3. 存储路径:照片保存到应用私有目录 externalFilesDir/Pictures,无需申请 MANAGE_EXTERNAL_STORAGE 权限(兼容 Android 10)。

三、 增强版 2:视频播放 + 全屏切换功能

在原有 VideoPlayActivity 基础上,新增 全屏按钮 和 屏幕方向切换逻辑,适配横屏 / 竖屏播放。

1. 增强版布局文件(activity_video_play.xml)

新增全屏按钮和进度条:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <SurfaceView
        android:id="@+id/surfaceViewVideo"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="visible"/>

    <TextureView
        android:id="@+id/textureViewVideo"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="gone"/>

    <!-- 控制栏 -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:padding="10dp"
        android:background="#80000000"
        app:layout_constraintBottom_toBottomOf="parent">

        <com.google.android.material.button.MaterialButton
            android:id="@+id/btnPlay"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="播放"/>

        <com.google.android.material.button.MaterialButton
            android:id="@+id/btnPause"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="暂停"/>

        <com.google.android.material.button.MaterialButton
            android:id="@+id/btnSwitchPlayer"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="2"
            android:text="切换载体"/>

        <!-- 新增:全屏按钮 -->
        <com.google.android.material.button.MaterialButton
            android:id="@+id/btnFullScreen"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="全屏"/>
    </LinearLayout>

</androidx.constraintlayout.widget.ConstraintLayout>

2. 增强版核心代码(VideoPlayActivity.kt)

新增全屏切换逻辑,处理屏幕方向和控件适配:

package com.example.module_video.play

import android.content.pm.ActivityInfo
import android.os.Bundle
import android.view.Surface
import android.view.SurfaceView
import android.view.TextureView
import android.view.View
import android.view.WindowManager
import androidx.appcompat.app.AppCompatActivity
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.exoplayer.ExoPlayer
import com.example.module_video.R

class VideoPlayActivity : AppCompatActivity() {
    private lateinit var exoPlayer: ExoPlayer
    private var isUsingSurfaceView = true
    private var videoUrl = "https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/720/Big_Buck_Bunny_720_10s_1MB.mp4"
    
    // 新增:全屏状态标记
    private var isFullScreen = false

    private lateinit var surfaceViewVideo: SurfaceView
    private lateinit var textureViewVideo: TextureView
    private lateinit var btnFullScreen: View

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_video_play)
        initView()
        initExoPlayer()
        prepareAndPlayVideo()
    }

    private fun initView() {
        surfaceViewVideo = findViewById(R.id.surfaceViewVideo)
        textureViewVideo = findViewById(R.id.textureViewVideo)
        btnFullScreen = findViewById(R.id.btnFullScreen)

        findViewById<View>(R.id.btnPlay).setOnClickListener {
            if (!exoPlayer.isPlaying) exoPlayer.play()
        }
        findViewById<View>(R.id.btnPause).setOnClickListener {
            if (exoPlayer.isPlaying) exoPlayer.pause()
        }
        findViewById<View>(R.id.btnSwitchPlayer).setOnClickListener {
            isUsingSurfaceView = !isUsingSurfaceView
            surfaceViewVideo.visibility = if (isUsingSurfaceView) View.VISIBLE else View.GONE
            textureViewVideo.visibility = if (isUsingSurfaceView) View.GONE else View.VISIBLE
            bindPlayerToSurface()
        }

        // 新增:全屏按钮点击事件
        btnFullScreen.setOnClickListener {
            toggleFullScreen()
        }
    }

    private fun initExoPlayer() {
        exoPlayer = ExoPlayer.Builder(this)
            .build()
            .also {
                it.addListener(object : Player.Listener {
                    override fun onPlaybackStateChanged(playbackState: Int) {
                        super.onPlaybackStateChanged(playbackState)
                        if (playbackState == Player.STATE_ENDED) {
                            it.seekTo(0)
                            it.play()
                        }
                    }
                })
            }
        bindPlayerToSurface()
    }

    private fun bindPlayerToSurface() {
        val playerSurface = if (isUsingSurfaceView) {
            Surface(surfaceViewVideo.holder.surface)
        } else {
            val surfaceTexture = textureViewVideo.surfaceTexture ?: return
            surfaceTexture.setDefaultBufferSize(1280, 720)
            Surface(surfaceTexture)
        }
        exoPlayer.setVideoSurface(playerSurface)
    }

    // 新增:全屏切换核心逻辑
    private fun toggleFullScreen() {
        isFullScreen = !isFullScreen
        if (isFullScreen) {
            // 进入全屏:隐藏状态栏、导航栏,设置横屏
            window.setFlags(
                WindowManager.LayoutParams.FLAG_FULLSCREEN,
                WindowManager.LayoutParams.FLAG_FULLSCREEN
            )
            supportActionBar?.hide()
            requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
            btnFullScreen.text = "退出全屏"
        } else {
            // 退出全屏:恢复状态栏、导航栏,设置竖屏
            window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
            supportActionBar?.show()
            requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
            btnFullScreen.text = "全屏"
        }
    }

    private fun prepareAndPlayVideo() {
        val mediaItem = MediaItem.fromUri(videoUrl)
        exoPlayer.setMediaItem(mediaItem)
        exoPlayer.prepare()
        exoPlayer.play()
    }

    override fun onPause() {
        super.onPause()
        if (exoPlayer.isPlaying) exoPlayer.pause()
    }

    override fun onDestroy() {
        super.onDestroy()
        exoPlayer.release()
    }

    // 新增:返回键处理全屏状态
    override fun onBackPressed() {
        if (isFullScreen) {
            toggleFullScreen()
        } else {
            super.onBackPressed()
        }
    }
}

3. 关键说明

  1. 全屏切换逻辑:通过 WindowManager 设置全屏标志,隐藏状态栏和导航栏,切换屏幕方向为横屏;
  2. 返回键适配:重写 onBackPressed(),全屏状态下先退出全屏再执行返回操作;
  3. 方向适配:使用 requestedOrientation 设置屏幕方向,保证视频横屏全屏显示。

四、 集成到组件化项目说明

  1. 注册 Activity:在对应模块的 AndroidManifest.xml中注册两个 Activity:

    <activity android:name=".camera.CameraPreviewActivity"
        android:screenOrientation="portrait"/>
    <activity android:name=".play.VideoPlayActivity"
        android:configChanges="orientation|screenSize"/>
    
  2. ARouter 路由配置(可选):

    @Route(path = "/camera/preview")
    class CameraPreviewActivity : AppCompatActivity() {}
    
    @Route(path = "/video/play")
    class VideoPlayActivity : AppCompatActivity() {}
    
  3. 权限适配:Android 11+ 存储权限变更,若需将照片共享到系统相册,需使用 MediaStore API。

最近更新:: 2026/1/15 03:00
Contributors: luokaiwen