Android Camera2Video预览问题:自定义FrameLayout中偏移与拉伸优化需求
Hey there! Let’s work through this camera preview issue you’re facing with Camera2VideoFragment, AutoFitTextureView, and your custom FrameLayout. I’ve wrestled with similar scaling/alignment problems before, so here’s a practical, step-by-step fix to get that centered, letterboxed preview (no more stretching or top-left offsets):
The problem happens because:
- The camera’s native preview aspect ratio doesn’t match your custom FrameLayout’s ratio.
- The default
AutoFitTextureViewonly adjusts its own size to fit the parent, but doesn’t handle centering or letterboxing (hence the top-left offset when you force it to fill the frame, leading to distortion).
The core fix lies in modifying AutoFitTextureView’s onMeasure method to calculate the correct scaled size (maintaining the camera’s aspect ratio) and then centering itself within the FrameLayout using margins. Replace your existing onMeasure with this:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int parentWidth = MeasureSpec.getSize(widthMeasureSpec); int parentHeight = MeasureSpec.getSize(heightMeasureSpec); if (mRatioWidth == 0 || mRatioHeight == 0) { // Fallback if ratio isn't set yet setMeasuredDimension(parentWidth, parentHeight); return; } // Calculate the correct size to preserve the camera's aspect ratio float cameraRatio = (float) mRatioWidth / mRatioHeight; float parentRatio = (float) parentWidth / parentHeight; int scaledWidth; int scaledHeight; if (cameraRatio > parentRatio) { // Camera is wider than parent; scale based on parent width scaledWidth = parentWidth; scaledHeight = Math.round(parentWidth / cameraRatio); } else { // Camera is taller than parent; scale based on parent height scaledHeight = parentHeight; scaledWidth = Math.round(parentHeight * cameraRatio); } // Set the TextureView's measured size to the scaled dimensions setMeasuredDimension(scaledWidth, scaledHeight); // Center the TextureView within the parent FrameLayout using margins ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) getLayoutParams(); params.leftMargin = (parentWidth - scaledWidth) / 2; params.topMargin = (parentHeight - scaledHeight) / 2; setLayoutParams(params); }
What this does:
- It calculates the largest possible size for the preview that fits your FrameLayout without stretching the camera’s aspect ratio.
- It adds left/top margins to center the preview, creating natural black bars (letterboxing) where needed.
- Eliminates the top-left offset by ensuring the preview is anchored in the center.
In Camera2VideoFragment, the chooseOptimalSize method needs to prioritize preview sizes that match the camera’s native aspect ratio. Double-check your implementation to make sure it selects sizes that align with the camera’s output ratio (not just arbitrary sizes). Here’s a refined version:
private static Size chooseOptimalSize(Size[] choices, int textureViewWidth, int textureViewHeight, int maxWidth, int maxHeight, Size aspectRatio) { List<Size> validSizes = new ArrayList<>(); int targetWidth = aspectRatio.getWidth(); int targetHeight = aspectRatio.getHeight(); for (Size option : choices) { // Only consider sizes that match the camera's aspect ratio if (option.getWidth() * targetHeight == option.getHeight() * targetWidth) { // Filter sizes that fit within the parent bounds and are large enough for the preview if (option.getWidth() <= maxWidth && option.getHeight() <= maxHeight && option.getWidth() >= textureViewWidth && option.getHeight() >= textureViewHeight) { validSizes.add(option); } } } // Pick the smallest valid size (balances performance and quality) if (!validSizes.isEmpty()) { return Collections.min(validSizes, new CompareSizesByArea()); } // Fallback if no valid sizes found Log.e(TAG, "No optimal preview size found; using first available"); return choices[0]; }
Key note:
Make sure you’re passing the correct aspect ratio (from the camera’s characteristics) to this method. You can get it like this when setting up the camera:
CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId); StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); assert map != null; Size[] outputSizes = map.getOutputSizes(SurfaceTexture.class); // Use the camera's native aspect ratio Size aspectRatio = outputSizes[0]; // Or calculate from the largest size for better accuracy
Ensure your custom FrameLayout and AutoFitTextureView are configured correctly in XML:
<com.yourpackage.YourCustomFrameLayout android:layout_width="match_parent" android:layout_height="match_parent"> <com.yourpackage.AutoFitTextureView android:id="@+id/texture" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </com.yourpackage.YourCustomFrameLayout>
- Setting
wrap_contentfor the TextureView lets ouronMeasurelogic take full control of its size. - The custom FrameLayout should fill the space you want the preview to occupy (use
match_parentor fixed dimensions as needed).
When initializing the camera preview surface, make sure you set the default buffer size to the optimal preview size you selected:
SurfaceTexture texture = mTextureView.getSurfaceTexture(); assert texture != null; // Match the buffer size to our chosen preview size texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight()); Surface previewSurface = new Surface(texture);
This ensures the camera sends frames in the correct resolution, preventing any unexpected scaling or offsets.
After implementing these changes, your preview should now center perfectly within the custom FrameLayout, with black bars on the sides/top/bottom (no stretching, no top-left offset). Test it across different devices to make sure it works with various screen ratios!
内容的提问来源于stack exchange,提问作者Richard

