综合实战:仿微信相机(拍照+录像+滤镜+切换摄像头+手势缩放+对焦)
终于到了综合实战环节。说实话,前面学那么多零散的知识点,就是为了这一刻——把它们串起来,做一个真正能用的相机应用。
微信的相机大家天天用,拍个照、录个视频、加个滤镜、双指缩放、点一下对焦……看起来简单,但背后涉及的技术点其实不少。我当年第一次尝试仿写时,光是对焦和缩放的冲突就折腾了两天。嗯,今天咱们就把这些坑都填上。
本章核心目标:实现一个功能完整的相机应用,支持拍照、录像、实时滤镜、前后摄像头切换、双指手势缩放、点击对焦。所有功能基于 CameraX 实现。
整体架构设计
先别急着写代码。我个人习惯是先画一张图,把模块之间的关系理清楚。你看,整个相机应用可以拆成这么几块:
说白了,CameraX 是底座,上面挂了四个功能模块。状态管理贯穿始终——你得知道当前是在拍照模式还是录像模式,滤镜有没有生效,这些状态直接影响 UI 的显示和操作逻辑。
第一步:项目配置与基础搭建
老规矩,先加依赖。在 build.gradle 里引入 CameraX 和相关库:
dependencies {
// CameraX 核心
def camerax_version = "1.3.1"
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
implementation "androidx.camera:camera-view:${camerax_version}"
// 滤镜相关(OpenGL)
implementation "androidx.camera:camera-extensions:${camerax_version}"
// 权限
implementation 'com.karumi:dexter:6.2.3'
}
权限方面,Android 6.0 以上需要动态申请 CAMERA 和 RECORD_AUDIO。录音权限很多人会忘,我刚开始做录像功能时就漏了它,结果录出来的视频全是静音……
小提示:如果只拍照,可以只申请 CAMERA 权限。但一旦涉及录像,RECORD_AUDIO 必须加上,否则 MediaRecorder 会直接报错。
第二步:实现拍照与录像
拍照用 ImageCapture,录像用 VideoCapture。这两个是 CameraX 提供的 UseCase,直接绑定到生命周期即可。
先看拍照的核心代码:
// 创建 ImageCapture 实例
val imageCapture = ImageCapture.Builder()
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
.build()
// 拍照
imageCapture.takePicture(
ContextCompat.getMainExecutor(this),
object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
// 保存成功,更新相册
Toast.makeText(this@MainActivity, "拍照成功", Toast.LENGTH_SHORT).show()
}
override fun onError(exception: ImageCaptureException) {
// 处理错误
Log.e("Camera", "拍照失败: ${exception.message}")
}
}
)
录像稍微复杂一点,因为涉及 MediaRecorder 的配置。CameraX 的 VideoCapture 内部封装了 MediaRecorder,但你需要自己管理录制状态:
// 创建 VideoCapture
val videoCapture = VideoCapture.Builder()
.setVideoFrameRate(30)
.setBitRate(10_000_000) // 10 Mbps
.build()
// 开始录制
fun startRecording() {
val outputFile = File(getExternalFilesDir(null), "video_${System.currentTimeMillis()}.mp4")
val outputOptions = VideoCapture.OutputFileOptions.Builder(outputFile).build()
videoCapture.startRecording(
outputOptions,
ContextCompat.getMainExecutor(this),
object : VideoCapture.OnVideoSavedCallback {
override fun onVideoSaved(outputFileResults: VideoCapture.OutputFileResults) {
// 录制完成
}
override fun onError(videoCaptureError: Int, message: String, cause: Throwable?) {
// 录制出错
}
}
)
}
// 停止录制
fun stopRecording() {
videoCapture.stopRecording()
}
注意:录像时不能同时拍照,反之亦然。CameraX 虽然支持同时绑定多个 UseCase,但 ImageCapture 和 VideoCapture 不能同时处于活跃状态。我见过有人想边录边拍,结果两个 UseCase 互相抢占资源,直接崩溃。
第三步:实时滤镜实现
滤镜这块,CameraX 官方提供了 CameraExtensions 库,但说实话,它支持的滤镜种类有限,而且不同厂商兼容性不一样。我更推荐自己用 OpenGL 做。
核心思路是:拿到 CameraX 的 PreviewView,然后通过 SurfaceTexture 把每一帧画面传给 OpenGL 做处理,最后渲染到屏幕上。
这里我给出一个简化版的滤镜切换逻辑:
// 假设你有一个 OpenGL 渲染器
class FilterRenderer : GLSurfaceView.Renderer {
private var currentFilter: Int = 0 // 0=原图, 1=黑白, 2=怀旧, 3=冷色调
fun switchFilter(filterIndex: Int) {
currentFilter = filterIndex
// 重新编译着色器
updateShaderProgram(filterIndex)
}
private fun updateShaderProgram(filterIndex: Int) {
// 根据 filterIndex 加载不同的 fragment shader
val fragmentShader = when (filterIndex) {
1 -> getBlackWhiteShader()
2 -> getSepiaShader()
3 -> getCoolToneShader()
else -> getNormalShader()
}
// 重新创建着色器程序
}
}
你想想看,滤镜的本质其实就是修改 fragment shader 里的颜色矩阵。黑白滤镜就是把 RGB 转成灰度值,怀旧滤镜加一点棕色调,冷色调就提高蓝色通道的权重。原理并不复杂。
我的经验:滤镜切换时,最好做一个平滑过渡动画,而不是瞬间切换。用户视觉体验会好很多。可以用 ValueAnimator 在 0.3 秒内渐变调整颜色矩阵。
第四步:手势缩放与对焦
这两个功能是用户最常操作的。微信相机里,双指捏合缩放,点击屏幕对焦,非常流畅。
缩放用 CameraControl 的 setZoomRatio() 方法。你需要监听 ScaleGestureDetector:
// 缩放监听
class ZoomGestureListener(private val cameraControl: CameraControl) : ScaleGestureDetector.SimpleOnScaleGestureListener() {
private var currentZoom = 1.0f
override fun onScale(detector: ScaleGestureDetector): Boolean {
currentZoom *= detector.scaleFactor
currentZoom = currentZoom.coerceIn(1.0f, 5.0f) // 限制最大5倍变焦
cameraControl.setZoomRatio(currentZoom)
return true
}
}
对焦稍微麻烦一点。CameraX 的 CameraControl 提供了 startFocusAndMetering() 方法,但你需要把屏幕点击坐标转换成传感器坐标:
// 点击对焦
previewView.setOnTouchListener { _, event ->
if (event.action == MotionEvent.ACTION_UP) {
val x = event.x
val y = event.y
// 将屏幕坐标转换为归一化坐标 (0~1)
val normalizedX = x / previewView.width
val normalizedY = y / previewView.height
// 创建 MeteringPoint
val meteringPoint = previewView.meteringPointFactory.createPoint(normalizedX, normalizedY)
val action = FocusMeteringAction.Builder(meteringPoint).build()
camera.cameraControl.startFocusAndMetering(action)
}
true
}
避坑指南:我曾经遇到过一个问题——缩放和对焦手势冲突。双指缩放时,手指抬起可能会触发点击事件,导致对焦。解决办法是在 ScaleGestureDetector 检测到缩放时,设置一个标志位,屏蔽点击对焦事件,等缩放结束后再恢复。
第五步:切换摄像头
前后摄像头切换,本质上是重新绑定 CameraSelector。CameraX 让这件事变得很简单:
private var lensFacing = CameraSelector.LENS_FACING_BACK
fun switchCamera() {
lensFacing = if (lensFacing == CameraSelector.LENS_FACING_BACK) {
CameraSelector.LENS_FACING_FRONT
} else {
CameraSelector.LENS_FACING_BACK
}
// 重新绑定 UseCase
val cameraSelector = CameraSelector.Builder()
.requireLensFacing(lensFacing)
.build()
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(this, cameraSelector, imageCapture, videoCapture, preview)
}
注意,切换摄像头时,之前设置的缩放比例会重置。我建议在切换后把 zoomRatio 恢复为 1.0f,或者记录下当前缩放值,切换后重新设置。
状态管理:录制中 vs 空闲
这个很容易被忽略。用户正在录像时,拍照按钮应该禁用,滤镜切换也应该禁用。反过来,拍照模式下录像按钮不可用。
我习惯用一个枚举来管理状态:
enum class CameraState {
IDLE, // 空闲
CAPTURING, // 拍照中(短暂状态)
RECORDING // 录像中
}
private var currentState = CameraState.IDLE
fun updateUIForState(state: CameraState) {
when (state) {
CameraState.IDLE -> {
btnCapture.isEnabled = true
btnRecord.isEnabled = true
btnFilter.isEnabled = true
btnSwitch.isEnabled = true
}
CameraState.CAPTURING -> {
btnCapture.isEnabled = false
btnRecord.isEnabled = false
btnFilter.isEnabled = false
btnSwitch.isEnabled = false
}
CameraState.RECORDING -> {
btnCapture.isEnabled = false
btnRecord.text = "停止"
btnFilter.isEnabled = false
btnSwitch.isEnabled = false
}
}
}
说白了,状态管理就是一把锁。锁住了,用户就不能乱操作。等当前任务完成,再解锁。
完整流程梳理
最后,我把整个流程串起来,方便你理解:
- 初始化:申请权限 → 创建 PreviewView → 绑定 CameraX UseCase(ImageCapture + VideoCapture + Preview)
- 拍照:点击拍照按钮 → ImageCapture.takePicture() → 保存到文件 → 更新相册
- 录像:点击录像按钮 → VideoCapture.startRecording() → 再次点击停止 → 保存 MP4
- 滤镜:点击滤镜按钮 → 切换 OpenGL 着色器 → 实时渲染
- 缩放:双指手势 → ScaleGestureDetector → CameraControl.setZoomRatio()
- 对焦:点击屏幕 → 坐标转换 → startFocusAndMetering()
- 切换摄像头:点击切换按钮 → 重新绑定 CameraSelector
每一步都不难,但合在一起就需要细心了。尤其是状态管理和手势冲突,这两个是 bug 的高发区。
最后一个小建议:开发过程中,多打 Log,把每个状态切换、每个手势事件都打印出来。调试时你会感谢这些日志的。
公众号:蓝海资料掘金营,微信deep3321