Android Camera2 API数码变焦实现:从SCALER_CROP_REGION到平滑缩放

Android Camera2 API数码变焦实现:从SCALER_CROP_REGION到平滑缩放 1. 从Camera1到Camera2为什么数码变焦的实现方式变了如果你是从Camera1 API时代过来的Android开发者第一次接触Camera2 API时可能会觉得有点懵。在Camera1里控制相机预览缩放我们通常直接操作Camera.Parameters调用setZoom(int)方法传入一个索引值就完事了。简单粗暴但问题也不少缩放过程是阶梯式的不够平滑不同设备的缩放能力差异巨大你很难知道最大能缩放到多少倍而且整个缩放过程是黑盒你无法精细控制。Camera2 API的设计哲学完全不同。它把相机抽象成一个管道Pipeline应用通过发送请求CaptureRequest来驱动这个管道相机硬件则通过结果CaptureResult和图像数据Image来响应。这种模型更接近相机的物理工作流程也赋予了开发者前所未有的控制力。对于数码变焦Digital Zoom这个功能Camera2不再提供一个简单的“缩放”开关而是要求我们通过修改两个核心参数来实现SCALER_CROP_REGION。简单来说数码变焦的本质就是在全尺寸的图像传感器Sensor上划出一个更小的矩形区域作为取景框。这个矩形区域就是SCALER_CROP_REGION。传感器总像素是固定的当你只读取其中一部分像素时这部分像素再被拉伸到同样大小的预览界面上画面自然就被“放大”了。反之如果你读取的区域就是整个传感器那就是1倍无缩放。所以实现数码变焦核心就变成了两件事计算根据用户想要的缩放比例比如2倍计算出对应的裁剪区域Crop Region。设置与生效将这个裁剪区域设置到每一个发送给相机的CaptureRequest中并确保预览和拍照都能使用这个设置。听起来不复杂对吧但魔鬼藏在细节里。SCALER_CROP_REGION的坐标系、最大缩放倍数的计算、与预览界面的联动、以及如何实现平滑的缩放动画这里面每一步都有坑。我花了相当长的时间才把这些坑一个个填平。接下来我们就从最基础的相机会话搭建开始一步步拆解如何实现一个稳定、流畅的数码变焦功能。2. 搭建支持变焦的Camera2会话基础框架在开始折腾变焦之前我们必须先有一个能正常跑起来的Camera2预览会话。这个基础框架的健壮性直接决定了后续变焦功能是否稳定。很多变焦相关的问题其实根源都在于会话配置不当。2.1 关键组件初始化与相机选择首先我们需要几个核心对象CameraManager: 系统的相机服务入口用于枚举和打开相机设备。CameraDevice: 代表一个打开的相机设备比如后置主摄。CameraCaptureSession: 相机捕获会话负责管理预览、拍照等请求的流水线。CaptureRequest.Builder: 用于构建发送给相机的请求比如预览请求(TEMPLATE_PREVIEW)或拍照请求(TEMPLATE_STILL_CAPTURE)。Surface: 用于承载图像数据的平面比如TextureView或SurfaceView的Surface或者用于拍照的ImageReader的Surface。在初始化时有一个关键步骤常被忽略检查相机特性。我们需要通过CameraCharacteristics来获取相机的固有能力其中就包括与变焦相关的信息。private void openCamera(int width, int height) { CameraManager manager (CameraManager) context.getSystemService(Context.CAMERA_SERVICE); try { // 1. 获取相机ID列表通常我们选择后置摄像头 String[] cameraIdList manager.getCameraIdList(); String cameraId cameraIdList[0]; // 简单起见选第一个。实际应遍历选择后置。 // 2. 获取相机特性这是变焦信息的来源 CameraCharacteristics characteristics manager.getCameraCharacteristics(cameraId); // 3. 检查是否支持手动控制裁剪区域即数码变焦 Integer maxRegions characteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE); // 但更关键的是获取传感器的有效阵列大小用于计算最大缩放 Rect activeArraySize characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); if (activeArraySize null) { throw new RuntimeException(此相机不支持获取有效传感器区域无法实现变焦。); } mActiveArrayRect activeArraySize; // 保存下来后面计算缩放要用 // 4. 打开相机 manager.openCamera(cameraId, mStateCallback, mBackgroundHandler); } catch (CameraAccessException e) { e.printStackTrace(); } }这里我们拿到了SENSOR_INFO_ACTIVE_ARRAY_SIZE它是一个Rect对象定义了图像传感器上可用于成像的像素区域以像素为单位。这个矩形就是我们的“全画幅”是计算所有裁剪区域的基准。把它保存为成员变量mActiveArrayRect。2.2 配置输出Surface与创建会话创建会话时我们需要至少一个用于预览的Surface。为了支持平滑变焦我强烈建议使用TextureView而不是SurfaceView。因为TextureView本身支持矩阵变换我们可以很方便地在其上应用缩放、平移等效果作为变焦动画的“UI层”而让相机硬件处理实际的图像裁剪。这是一种性能和体验的折中方案。private void createCameraPreviewSession() { try { // 1. 从TextureView获取Surface SurfaceTexture texture mTextureView.getSurfaceTexture(); // 设置默认大小最好选择与预览分辨率匹配的尺寸 texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight()); Surface previewSurface new Surface(texture); // 2. 创建预览请求的构建器 mPreviewRequestBuilder mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); mPreviewRequestBuilder.addTarget(previewSurface); // 将预览Surface作为输出目标 // 3. 初始状态下裁剪区域就是整个有效阵列1倍缩放 mCurrentCropRegion mActiveArrayRect; mPreviewRequestBuilder.set(CaptureRequest.SCALER_CROP_REGION, mCurrentCropRegion); // 4. 创建捕获会话传入输出Surface列表这里只有预览Surface mCameraDevice.createCaptureSession( Arrays.asList(previewSurface), new CameraCaptureSession.StateCallback() { Override public void onConfigured(NonNull CameraCaptureSession session) { mCaptureSession session; // 会话创建成功后开始重复发送预览请求 updatePreview(); } Override public void onConfigureFailed(NonNull CameraCaptureSession session) { Toast.makeText(context, 创建相机会话失败, Toast.LENGTH_SHORT).show(); } }, mBackgroundHandler); } catch (CameraAccessException e) { e.printStackTrace(); } }注意第3步我们在构建第一个预览请求时就设置了SCALER_CROP_REGION并将其初始化为整个mActiveArrayRect这意味着初始缩放倍数为1倍。updatePreview()方法很简单就是将会话设置为重复请求模式持续输出预览画面。private void updatePreview() { if (mCaptureSession null) { return; } try { // 设置重复请求相机将持续输出预览帧到我们的Surface mCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, mBackgroundHandler); } catch (CameraAccessException e) { e.printStackTrace(); } }至此一个基础的、支持设置裁剪区域的Camera2预览框架就搭建好了。接下来我们进入核心部分如何根据缩放比例计算出正确的SCALER_CROP_REGION。3. 核心算法如何计算任意缩放倍数下的Crop Region这是实现数码变焦最核心也最容易出错的一步。我们的目标是给定一个缩放比例zoomRatio例如2.0表示放大2倍计算出一个以传感器有效区域中心为锚点的、大小合适的矩形Rect。3.1 理解坐标系与计算原理SENSOR_INFO_ACTIVE_ARRAY_SIZE的坐标系原点(0,0)在传感器的左上角。假设我们得到的mActiveArrayRect是(0, 0, 4000, 3000)这意味着传感器有效区域宽4000像素高3000像素。当我们想要放大2倍时意味着我们希望取景框的尺寸是原来的1/2。因为取景框越小被拉伸到同样大小的屏幕上就显得越大。所以裁剪区域的宽和高应该分别是cropWidth activeArrayWidth / zoomRatiocropHeight activeArrayHeight / zoomRatio接下来我们需要让这个裁剪区域居中。那么它的左上角坐标(left, top)应该是left (activeArrayWidth - cropWidth) / 2top (activeArrayHeight - cropHeight) / 2最后裁剪区域就是Rect(left, top, left cropWidth, top cropHeight)。3.2 代码实现与边界处理根据上面的原理我们可以写出一个计算方法/** * 根据缩放比例计算裁剪区域 * param zoomRatio 缩放比例必须 1.0 * return 计算出的裁剪区域Rect */ private Rect calculateCropRegion(float zoomRatio) { if (mActiveArrayRect null) { return null; } // 确保缩放比例不小于1 zoomRatio Math.max(1.0f, zoomRatio); int activeWidth mActiveArrayRect.width(); int activeHeight mActiveArrayRect.height(); // 计算裁剪区域的宽高 int cropWidth (int) (activeWidth / zoomRatio); int cropHeight (int) (activeHeight / zoomRatio); // 计算居中后的左上角坐标 int left (activeWidth - cropWidth) / 2; int top (activeHeight - cropHeight) / 2; // 创建并返回Rect对象 return new Rect(left, top, left cropWidth, top cropHeight); }看起来很简单对吗但这里有几个至关重要的细节和坑坑1整数精度丢失。cropWidth和cropHeight是整数除法。当activeWidth / zoomRatio不能整除时会丢失精度。这可能导致两个问题一是计算出的缩放比例有微小偏差二是在多次计算后裁剪区域可能因为累积误差而轻微漂移。对于大多数场景这个误差可以接受。如果追求极致可以考虑用浮点数计算最后四舍五入到整数。坑2最大缩放倍数的限制。理论上裁剪区域最小可以是1x1像素那缩放倍数可以非常大。但现实中相机硬件或驱动可能有限制。通常我们可以通过CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM来获取系统支持的最大数码变焦倍数。这是一个float值。强烈建议在计算前对zoomRatio进行限制private float getMaxDigitalZoom() { Float maxZoom mCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM); return (maxZoom ! null maxZoom 1.0f) ? maxZoom : 1.0f; // 如果不支持或数据异常默认为1倍无缩放 } // 在设置缩放比例时 float maxZoom getMaxDigitalZoom(); float targetZoom Math.min(desiredZoom, maxZoom); Rect newCropRegion calculateCropRegion(targetZoom);坑3宽高比与预览视图的匹配。我们的裁剪区域是基于传感器有效区域activeArrayRect计算的它的宽高比Aspect Ratio是固定的比如4:3。但我们的预览TextureView可能是16:9或其他比例。如果我们简单地将裁剪区域的图像直接显示到不同比例的View上画面会被拉伸变形。正确的做法是在配置预览分辨率时就选择与TextureView宽高比匹配的分辨率。同时在显示时需要配置TextureView的setTransform(matrix)或者让Camera2API的SCALER_CROP_REGION与输出Surface的尺寸协同工作。更常见的简化方案是让预览Surface的宽高比与传感器有效区域的宽高比一致这样画面就不会变形。我们在选择mPreviewSize时就需要考虑这一点。计算出了裁剪区域下一步就是让它生效并让用户能通过手势如双指捏合来交互控制。4. 实现交互将手势缩放比例映射到Crop Region用户通过双指捏合来缩放我们得到的是一个基于屏幕触摸点的缩放因子scaleFactor。我们需要将这个因子映射到相机的数码变焦比例zoomRatio上并实时更新Crop Region。4.1 手势检测与缩放因子我们可以使用ScaleGestureDetector来检测双指捏合手势。public class CameraActivity extends AppCompatActivity { private ScaleGestureDetector mScaleGestureDetector; private float mCurrentZoomRatio 1.0f; // 当前缩放比例 private float mMaxZoomRatio; // 最大缩放比例 Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // ... 其他初始化 mScaleGestureDetector new ScaleGestureDetector(this, new ScaleListener()); mMaxZoomRatio getMaxDigitalZoom(); // 获取硬件支持的最大变焦 } Override public boolean onTouchEvent(MotionEvent event) { // 将触摸事件传递给缩放手势检测器 return mScaleGestureDetector.onTouchEvent(event); } private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener { Override public boolean onScale(ScaleGestureDetector detector) { // detector.getScaleFactor() 是相对于上次事件的缩放因子 // 例如两指张开因子可能为1.2捏合因子可能为0.8 float scaleFactor detector.getScaleFactor(); // 根据手势因子更新当前的缩放比例 mCurrentZoomRatio * scaleFactor; // 将缩放比例限制在[1.0, mMaxZoomRatio]之间 mCurrentZoomRatio Math.max(1.0f, Math.min(mCurrentZoomRatio, mMaxZoomRatio)); // 根据新的缩放比例更新相机预览 updateZoom(mCurrentZoomRatio); return true; } } }4.2 更新预览请求并实现平滑动画在updateZoom方法中我们需要做三件事根据新的mCurrentZoomRatio计算新的Crop Region。将新的Crop Region设置到预览请求构建器中。更新正在进行的重复预览请求。直接更新请求并设置画面会“跳变”体验生硬。为了实现平滑的缩放动画我们可以使用ValueAnimator在短时间内连续更新多个中间值。private void updateZoom(float targetZoomRatio) { if (mCaptureSession null || mPreviewRequestBuilder null) { return; } // 如果正在执行动画先取消之前的 if (mZoomAnimator ! null mZoomAnimator.isRunning()) { mZoomAnimator.cancel(); } final float startZoom mCurrentZoomRatio; final float endZoom targetZoomRatio; // 创建属性动画在200毫秒内从当前值变化到目标值 mZoomAnimator ValueAnimator.ofFloat(startZoom, endZoom); mZoomAnimator.setDuration(200); mZoomAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { Override public void onAnimationUpdate(ValueAnimator animation) { float animatedValue (float) animation.getAnimatedValue(); // 计算当前动画帧对应的裁剪区域 Rect cropRegion calculateCropRegion(animatedValue); if (cropRegion ! null) { try { // 更新预览请求的裁剪区域 mPreviewRequestBuilder.set(CaptureRequest.SCALER_CROP_REGION, cropRegion); // 关键步骤使用setRepeatingRequest的“部分更新”模式 // 这里我们重新构建整个请求对于连续动画也可以考虑使用session.capture()单次请求 mCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, mBackgroundHandler); } catch (CameraAccessException e) { e.printStackTrace(); } } } }); mZoomAnimator.start(); }注意在动画的每一帧都调用setRepeatingRequest可能会有点重。对于高性能要求的场景可以考虑使用CaptureSession#capture单次请求模式来更新或者控制动画的更新频率如每帧计算但每2-3帧更新一次请求。不过对于大多数手机的数码变焦预览上述方法的性能已经足够。至此一个基本的、带平滑动画的数码变焦预览功能就实现了。用户双指捏合可以看到预览画面平滑地放大和缩小。但是这离一个健壮的功能还有距离我们还需要考虑拍照、对焦、以及各种边界情况。5. 功能联调变焦与拍照、对焦的协同工作数码变焦不仅影响预览也必须影响拍照结果。同时缩放区域改变后相机的自动对焦AF区域也应该相应调整否则可能对焦到错误的物体上。5.1 让拍照结果同步变焦效果在Camera2中拍照是发起一个单次捕获请求TEMPLATE_STILL_CAPTURE。为了让拍出的照片和当前预览的缩放效果一致我们必须在构建拍照请求时使用和当前预览请求相同的SCALER_CROP_REGION。private void takePicture() { if (mCameraDevice null || mCaptureSession null) return; try { // 1. 创建拍照请求构建器 final CaptureRequest.Builder captureBuilder mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); // 2. 添加拍照用的ImageReader的Surface作为输出目标 captureBuilder.addTarget(mImageReader.getSurface()); // 3. 关键步骤设置与当前预览一致的裁剪区域 captureBuilder.set(CaptureRequest.SCALER_CROP_REGION, mCurrentCropRegion); // 4. 可以设置其他拍照参数如JPEG质量、方向等 captureBuilder.set(CaptureRequest.JPEG_QUALITY, (byte) 95); // ... 其他设置 // 5. 创建拍照回调 CameraCaptureSession.CaptureCallback captureCallback new CameraCaptureSession.CaptureCallback() { Override public void onCaptureCompleted(NonNull CameraCaptureSession session, NonNull CaptureRequest request, NonNull TotalCaptureResult result) { // 拍照完成处理 unlockFocus(); // 例如拍照后重置对焦状态 } }; // 6. 停止预览执行拍照 mCaptureSession.stopRepeating(); // 停止持续预览 mCaptureSession.capture(captureBuilder.build(), captureCallback, mBackgroundHandler); } catch (CameraAccessException e) { e.printStackTrace(); } }这里的关键是第3步mCurrentCropRegion应该是一个成员变量在updateZoom方法中随着预览更新而同步更新。这样无论何时触发拍照使用的都是最新的裁剪区域。5.2 变焦过程中的对焦区域AF/AE映射当画面放大后用户点击屏幕某处进行对焦这个触摸点的坐标是基于预览视图TextureView的。但是相机需要的对焦区域CONTROL_AF_REGIONS和测光区域CONTROL_AE_REGIONS的坐标是基于当前的裁剪区域CROP_REGION的。这意味着我们需要进行一次坐标转换触摸点 (在TextureView上) → 在裁剪区域内的归一化坐标 (0~1)步骤1将触摸点坐标转换为在预览画面上的比例坐标。假设TextureView的尺寸是viewWidth x viewHeight触摸点是(touchX, touchY)。注意预览画面可能没有填满整个TextureView为了保持宽高比可能有黑边。我们需要先找到预览画面实际渲染的矩形区域previewRect。// 假设我们已经计算好了预览画面在TextureView中的实际显示区域 mPreviewRect float normalizedX (touchX - mPreviewRect.left) / mPreviewRect.width(); float normalizedY (touchY - mPreviewRect.top) / mPreviewRect.height(); // 将坐标限制在[0, 1]范围内 normalizedX Math.max(0, Math.min(1, normalizedX)); normalizedY Math.max(0, Math.min(1, normalizedY));步骤2将比例坐标映射到当前裁剪区域。CONTROL_AF_REGIONS要求的是一个MeteringRectangle数组其坐标是相对于SCALER_CROP_REGION的。所以在裁剪区域内的X坐标 mCurrentCropRegion.left normalizedX * mCurrentCropRegion.width()在裁剪区域内的Y坐标 mCurrentCropRegion.top normalizedY * mCurrentCropRegion.height()private MeteringRectangle convertTouchPointToMeteringRectangle(float normalizedX, float normalizedY) { if (mCurrentCropRegion null) return null; int cropX (int) (mCurrentCropRegion.left normalizedX * mCurrentCropRegion.width()); int cropY (int) (mCurrentCropRegion.top normalizedY * mCurrentCropRegion.height()); // 对焦区域的大小这里设为裁剪区域宽高的1/8可以根据需要调整 int areaSize (int) (Math.min(mCurrentCropRegion.width(), mCurrentCropRegion.height()) / 8); // 确保区域大小至少为1 areaSize Math.max(1, areaSize); // 创建以触摸点为中心的对焦区域 Rect meteringRect new Rect( cropX - areaSize / 2, cropY - areaSize / 2, cropX areaSize / 2, cropY areaSize / 2 ); // MeteringRectangle的最后一个参数是权重通常设为MeteringRectangle.METERING_WEIGHT_MAX return new MeteringRectangle(meteringRect, MeteringRectangle.METERING_WEIGHT_MAX); }步骤3将对焦区域设置到请求中并触发对焦。private void handleFocus(MotionEvent event) { // ... 计算 normalizedX, normalizedY ... MeteringRectangle focusArea convertTouchPointToMeteringRectangle(normalizedX, normalizedY); if (focusArea ! null) { try { // 锁定并重置AF和AE mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO); mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); mCaptureSession.capture(mPreviewRequestBuilder.build(), null, mBackgroundHandler); // 设置新的对焦和测光区域 mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_REGIONS, new MeteringRectangle[]{focusArea}); mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_REGIONS, new MeteringRectangle[]{focusArea}); mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO); mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_START); // 更新预览请求 mCaptureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, mBackgroundHandler); } catch (CameraAccessException e) { e.printStackTrace(); } } }经过这样的坐标转换无论画面放大到多少倍用户的点击对焦都能准确地映射到传感器上正确的区域。6. 避坑指南与性能优化实战在实际项目中仅仅实现功能是不够的稳定性和流畅性同样重要。下面是我在多个项目中总结的几个关键坑点和优化建议。6.1 坑Crop Region 设置不生效或导致预览停滞现象调用setRepeatingRequest更新了SCALER_CROP_REGION后预览画面没有变化或者直接卡住、变黑。排查与解决检查会话状态确保CameraCaptureSession已经处于onConfigured状态并且正在正常进行重复预览setRepeatingRequest。在会话创建完成前设置是无效的。验证Rect值打印出计算出的Rect值确保其left 0,top 0,right activeArrayWidth,bottom activeArrayHeight。越界的Rect会导致请求被拒绝。使用正确的Handler确保更新请求的操作是在用于相机操作的后台线程的Handler中执行的。如果在主线程执行可能会因为相机设备调用超时而失败。捕获异常setRepeatingRequest可能会抛出CameraAccessException务必用try-catch包裹并检查异常信息常见原因是ERROR_CAMERA_DEVICE设备断开或ERROR_CAMERA_SERVICE服务异常。单次请求测试如果重复请求有问题可以尝试先用session.capture(builder.build(), callback, handler)发送一个单次请求看看裁剪区域是否能生效。这有助于排除是否是连续请求的冲突问题。6.2 优化减少频繁请求带来的性能开销在实现平滑缩放动画时我们每一帧都可能更新请求。虽然Camera2 API设计上支持动态更新参数但过于频繁的请求仍可能给相机管线带来压力导致功耗上升或帧率下降。优化策略节流Throttle在ValueAnimator的onAnimationUpdate中不要每帧都更新请求。可以记录上一次更新的时间戳或者使用固定的时间间隔如每50毫秒更新一次。对于缩放动画人眼对流畅度的要求低于平移适当的节流是可以接受的。private long mLastZoomUpdateTime 0; private static final long ZOOM_UPDATE_INTERVAL_MS 50; // 每50毫秒更新一次 public void onAnimationUpdate(ValueAnimator animation) { long currentTime System.currentTimeMillis(); if (currentTime - mLastZoomUpdateTime ZOOM_UPDATE_INTERVAL_MS) { // ... 计算并更新请求 ... mLastZoomUpdateTime currentTime; } }使用setRepeatingBurst不对于参数变化的连续请求setRepeatingBurst并不适用它用于连续发送几个固定的请求。最终同步在缩放动画结束时onAnimationEnd确保发送一次最终的正确请求保证状态完全同步。6.3 坑宽高比引发的预览拉伸或黑边问题这是最令人头疼的视觉问题之一。根本原因在于传感器有效区域ACTIVE_ARRAY、你选择的预览分辨率、以及TextureView的显示区域三者宽高比不一致。解决方案链选择匹配的预览尺寸在CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP中获取支持的输出尺寸时选择与你的TextureView或你期望的显示比例宽高比最接近的尺寸。例如如果TextureView是16:9就优先选择16:9的预览分辨率。正确配置TextureView的变换矩阵即使预览分辨率比例匹配了由于手机屏幕和传感器比例千差万别仍然可能需要调整。在TextureView.SurfaceTextureListener的onSurfaceTextureSizeChanged回调中根据预览尺寸和TextureView尺寸计算一个变换矩阵Matrix将其设置给TextureView。private void configureTransform(int viewWidth, int viewHeight) { if (mPreviewSize null || mTextureView null) { return; } Matrix matrix new Matrix(); float scaleX (float) viewWidth / mPreviewSize.getWidth(); float scaleY (float) viewHeight / mPreviewSize.getHeight(); // 选择能填满View的缩放比例FIT_CENTER的相反效果CENTER_CROP float scale Math.max(scaleX, scaleY); matrix.postScale(scale, scale); // 居中 matrix.postTranslate( (viewWidth - mPreviewSize.getWidth() * scale) / 2, (viewHeight - mPreviewSize.getHeight() * scale) / 2 ); mTextureView.setTransform(matrix); }这段代码实现了类似ImageView的CENTER_CROP效果保证预览画面填满TextureView且不变形多余部分被裁剪。你也可以根据需要实现FIT_CENTER保持比例可能有黑边。记录实际预览区域如上一步所述经过矩阵变换后预览画面在TextureView上的实际显示区域mPreviewRect对于坐标转换如点击对焦至关重要必须精确计算。6.4 进阶与光学变焦Optical Zoom或混合变焦的配合现代多摄手机通常具备光学变焦镜头如2x长焦和超广角镜头。纯数码变焦体验远不如混合变焦。我们可以通过CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES检查设备是否支持LOGICAL_MULTI_CAMERA。如果支持我们可以通过CameraManager.getCameraIdList()获取所有物理相机ID并根据其LENS_FACING和LENS_INFO_AVAILABLE_FOCAL_LENGTHS等特性选择不同焦距的摄像头来模拟光学变焦。当用户缩放时算法可以这样设计在1.0倍附近使用主摄广角。当缩放比例达到长焦镜头的等效焦距倍数时例如2.0倍切换到长焦镜头并以该镜头为基准进行数码变焦例如从2.0倍到4.0倍。切换镜头时会有明显的画面跳变需要设计平滑的过渡动画如淡入淡出或者利用OUTPUT_STREAM_CONFIG等高级特性。这部分涉及Multi-CameraAPI复杂度更高但能提供质的体验提升。实现一个稳定、流畅的Camera2数码变焦功能是一个对细节要求极高的过程。从基础会话搭建、核心算法实现到与拍照对焦的联动再到各种边界情况的处理每一步都需要仔细考量。我最深的体会是一定要善用CameraCharacteristics来查询设备能力不要做任何假设同时坐标系的转换是万恶之源务必在关键节点打印出所有矩形和坐标的值确保逻辑正确。希望这篇详细的拆解能帮你绕过我踩过的那些坑。