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_RATIOSCALER_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 完整流程梳理

说了这么多,咱们用一张图把整个流程串起来:

数字缩放实现流程 用户手势输入 计算缩放倍数 & 裁剪区域 Camera层:CropRegion View层:Matrix变换 ValueAnimator 平滑过渡 Matrix动画插值 画质较好,兼容性需注意 灵活方便,画质略损失

16.6 性能与兼容性考量

最后聊几个实战中容易踩的坑:

问题 原因 解决方案
缩放后画面模糊 数字缩放本质是插值放大 限制最大缩放倍数(建议≤4x)
缩放时卡顿 频繁设置CropRegion触发Camera重配 用View层Matrix做实时缩放,松手后再应用Camera层
某些设备不支持缩放 硬件限制或驱动bug 查询 SCALER_STREAM_CONFIGURATION_MAP 的缩放范围
画面边缘出现黑边 裁剪区域超出原图边界 做 clamp 边界检查

我的经验:如果你做的是直播或视频通话类应用,建议用Camera层缩放。因为View层缩放会增加GPU负载,发热和耗电都会上升。如果是拍照预览,View层缩放就够用了,实现起来也快。

嗯,数字缩放这块内容差不多就这些。核心就是理解「裁剪+拉伸」的原理,然后根据场景选择合适的实现方式。别小看这个功能,做得好用户觉得理所当然,做不好就是「这App怎么一缩放就卡」的差评。

公众号:蓝海资料掘金营,微信deep3321