16. 缩放功能:数字缩放原理、设置缩放区域、平滑缩放实现
说到摄像头预览的缩放,很多人第一反应就是「调个API不就行了?」。嗯,确实有现成的接口,但如果你做过几个实际项目就会发现——事情没那么简单。
我记得第一次做视频通话应用时,产品经理说「加个两指缩放,很简单吧?」。我当时天真地点了点头,结果踩了一堆坑。今天我就把这些经验掰开揉碎了讲给你听。
16.1 数字缩放 vs 光学缩放
先搞清楚一个基本概念。摄像头缩放分两种:
- 光学缩放:靠镜头物理移动改变焦距。画质无损,但需要硬件支持。
- 数字缩放:对采集到的图像做裁剪放大。说白了就是「裁图+插值」,画质会下降。
咱们Android开发中,绝大多数场景用的都是数字缩放。为什么?因为大部分手机只有一个定焦镜头,根本没有光学变焦能力。你想想看,连iPhone都只有少数几款有真正的光学变焦。
核心要点:数字缩放的本质 = 从原图中裁剪一个矩形区域,然后拉伸到目标分辨率。
16.2 数字缩放的数学原理
数字缩放其实就两个参数:缩放倍数和缩放中心点。
假设原图宽为 srcW,高为 srcH。缩放倍数为 zoomFactor(≥1.0),那么:
- 裁剪区域的宽 =
srcW / zoomFactor - 裁剪区域的高 =
srcH / zoomFactor
举个例子:原图是1920×1080,缩放2倍,那裁剪区域就是960×540。说白了就是把中间那一块抠出来,再拉伸回1920×1080显示。
那中心点怎么算?如果用户手指点在 (focusX, focusY),裁剪区域的左上角坐标就是:
cropLeft = focusX - (srcW / zoomFactor) / 2
cropTop = focusY - (srcH / zoomFactor) / 2
当然,边界要 clamp 一下,不能超出原图范围。我在项目中就吃过这个亏——没做边界检查,结果画面边缘出现黑边,被测试小姐姐提了bug。
16.3 设置缩放区域:两种实现方式
实际开发中,设置缩放区域主要有两条路:
方式一:Camera2 的 CropRegion
Camera2 API 提供了 CONTROL_ZOOM_RATIO 和 SCALER_CROP_REGION。前者是Android 11引入的,直接传一个float倍数就行:
CaptureRequest.Builder builder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
builder.set(CaptureRequest.CONTROL_ZOOM_RATIO, 2.0f); // 2倍缩放
cameraCaptureSession.setRepeatingRequest(builder.build(), null, null);
简单吧?但有个坑——这个API不是所有设备都支持。我遇到过一台低端平板,CONTROL_ZOOM_RATIO 设了没反应,最后还是老老实实用 SCALER_CROP_REGION。
SCALER_CROP_REGION 需要你手动计算裁剪矩形:
Rect zoomRect = new Rect(cropLeft, cropTop, cropLeft + cropWidth, cropTop + cropHeight);
builder.set(CaptureRequest.SCALER_CROP_REGION, zoomRect);
我的建议:优先用 CONTROL_ZOOM_RATIO,因为它更简单。如果返回null或者不支持,再回退到 SCALER_CROP_REGION。这叫「优雅降级」。
方式二:SurfaceView/TextureView 的 Matrix 变换
有时候你不想动Camera的配置,或者需要更灵活的动画效果。这时候可以在View层做文章。
用 Matrix 对预览画面做缩放:
Matrix matrix = new Matrix();
matrix.setScale(zoomFactor, zoomFactor, pivotX, pivotY);
textureView.setTransform(matrix);
这种方式的好处是:不涉及Camera重新配置,切换非常快。坏处是:画质会损失,因为是在GPU层面做的放大。
注意:View层缩放和Camera层缩放不要同时用!我曾经见过一个同事两个都做了,结果画面被放大了4倍,用户一脸懵逼。二选一就行。
16.4 平滑缩放实现
直接跳变缩放倍数,用户体验很差。你想想看,画面「咔」一下变大,谁受得了?
平滑缩放的核心思路:用动画插值。
我一般用 ValueAnimator 来做:
ValueAnimator animator = ValueAnimator.ofFloat(currentZoom, targetZoom);
animator.setDuration(300); // 300毫秒完成缩放
animator.addUpdateListener(animation -> {
float value = (float) animation.getAnimatedValue();
applyZoom(value); // 设置缩放值
});
animator.start();
这里有个细节:插值器选择。默认的加速减速插值器其实不太适合缩放,因为开始和结束时会有点「顿挫感」。我个人习惯用 DecelerateInterpolator,让缩放先快后慢,视觉上更自然。
另外,如果是两指手势缩放,需要实时计算缩放倍数:
float newZoom = currentZoom * (currentSpan / previousSpan);
每次手指移动都重新计算,然后立即应用。注意要限制最大最小缩放倍数,我一般设1.0到4.0之间。
避坑指南:我曾经在某个项目里,两指缩放时忘记重置 previousSpan,结果缩放倍数越滚越大,画面直接飞出去了。每次手势开始时,记得记录初始跨度。
16.5 完整流程梳理
说了这么多,咱们用一张图把整个流程串起来:
16.6 性能与兼容性考量
最后聊几个实战中容易踩的坑:
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 缩放后画面模糊 | 数字缩放本质是插值放大 | 限制最大缩放倍数(建议≤4x) |
| 缩放时卡顿 | 频繁设置CropRegion触发Camera重配 | 用View层Matrix做实时缩放,松手后再应用Camera层 |
| 某些设备不支持缩放 | 硬件限制或驱动bug | 查询 SCALER_STREAM_CONFIGURATION_MAP 的缩放范围 |
| 画面边缘出现黑边 | 裁剪区域超出原图边界 | 做 clamp 边界检查 |
我的经验:如果你做的是直播或视频通话类应用,建议用Camera层缩放。因为View层缩放会增加GPU负载,发热和耗电都会上升。如果是拍照预览,View层缩放就够用了,实现起来也快。
嗯,数字缩放这块内容差不多就这些。核心就是理解「裁剪+拉伸」的原理,然后根据场景选择合适的实现方式。别小看这个功能,做得好用户觉得理所当然,做不好就是「这App怎么一缩放就卡」的差评。