Android16 Camera2录像中拍照后预览模糊问题分析与解决总结
文章目录
- [Android16 Camera2录像中拍照后预览模糊问题分析与解决总结](#Android16 Camera2录像中拍照后预览模糊问题分析与解决总结)
-
- 一、前言
- 二、分析解决
-
- 1、结论先行
- 2、三级渲染档位实测对照表
- 3、日志证据链
- [4、未修复的第二个缺陷:录像中拍照只有 1920×1080](#4、未修复的第二个缺陷:录像中拍照只有 1920×1080)
- [5、修改方案(已实施:缺陷① 渲染阶段)](#5、修改方案(已实施:缺陷① 渲染阶段))
- 6、修复效果(量化)
- 三、其他
一、前言
Android16 AML Y3 方案的原生相机Camera2 录像过程中按快门拍照,停止录像后立即 滑入胶片流查看预览刚拍的照片,照片很模糊 ;
继续滑动查看之前的 照片则正常。
用户假设:"是不是有滑动阶段和预览阶段,滑动阶段就会模糊?"
结论:假设成立,而且比猜的更严重 ------ 确实存在分级渲染,但预览阶段(全分辨率档)在滑动浏览时从头到尾不会被进入 ,
只有双击缩放 / 双指捏合才会触发。所以看到的一直是"滑动档"。
这里把问题现象、根因分析和修改方案做个总结,方便大家快速上手。
Android16 其他的方案好像不没有,可能是不同的系统版本差异代码差异会比较大。
二、分析解决
模糊效果热如下:

左边是视频预览正常的,右边是录像过程的图片相机中预览效果是模糊的,从相册应用查看是清晰的。
1、结论先行
| 问题 | 结论 |
|---|---|
| 有没有分阶段 | 有 。RenderSize 共三档:TINY → THUMBNAIL(屏幕档)→ FULL_RES |
| 滑动浏览会进到哪一档 | 只到 THUMBNAIL 。全程 renderFullRes 调用 0 次(日志实测) |
renderFullRes 什么时候才会被调 |
整个 FilmstripView 里只有 2 处 调用,都在缩放手势里:onDoubleTap 和 onScale 捏合。正常滑动永远到不了 |
| 那段写着"确保全分辨率"的代码呢 | onScrollEnd() 打印 Ensuring that items are at full resolution.,实际调的却是 renderThumbnail() ;而 renderThumbnail() 在已经是 THUMBNAIL 时直接 return ------ 彻底的空操作(双重 bug:调错方法 + 守卫短路) |
| 屏幕档到底多大 | loadScreen 被 MAXIMUM_SMOOTH_PIXELS = 1,000,000(1MP )钳位 → 在 4K 面板上解码成 1333×750 ,再放大 2.88× 铺满 3840×2160 → 这就是"模糊" |
| 为什么"之前的照片正常" | 旧照片源分辨率更高,同样 1333×750 里承载的真实细节更多;加上新项插入时还会被降档抖动 |
| 还有第二个独立缺陷吗 | 有 。录像中拍照的 JPEG 只有 1920×1080 (HAL 侧硬证据),而录像是 3840×2160。本次按要求未修改 |
简单的说相机预览界面显示有滑动档和全分辨率查看档,如果在滑动档查看就会画面模糊。
2、三级渲染档位实测对照表
代码位置:widget/FilmstripView.java → data/PhotoItem.java → data/GlideFilmstripManager.java
按本机 3840×2160 面板计算:
| 档位 | 加载器 | 像素上限常量 | 实际解码尺寸 | 铺满 4K 的放大倍数 |
|---|---|---|---|---|
TINY |
loadTinyThumb |
256×256 | 256×256 | 15× |
THUMBNAIL(屏幕档) |
loadScreen |
1,000,000 | 1333×750 | 2.88× ← 滑动浏览只到这里 |
FULL_RES |
loadFull |
4,500,000 | 1920×1080(本例源) | 2.00× |
钳位算法:ratio = min(sqrt(maxArea / (w*h)), 1.0)
→ sqrt(1,000,000 / 8,294,400) = 0.347 → 3840×0.347 = 1333,2160×0.347 = 750。
关键:MAXIMUM_SMOOTH_PIXELS 这个 1MP 常量是按 2014 年的 1080p 手机 定的,
放到 4K 面板上就是 1/8 的屏幕像素。
3、日志证据链
renderFullRes 全程 0 次(决定性):
$ grep -c "renderFullRes" → 0
$ grep -c "renderThumbnail" → 33
$ grep -c "full resolution" → 16 ← 只打印了日志,没真正执行
renderFullRes() 里有 Log.i(TAG, "[ViewItem:N] mData.renderFullRes()"),
只要被调到就一定有日志。0 次 = 从未进入全分辨率档。
停止录像后的关键时序:
21:29:30.623 CAM_VideoModule: stopVideoRecording ← 停止录像
21:29:30.959 FS: [ViewItem:0] mData.renderThumbnail() ← 新项只渲染到屏幕档(1333x750)
21:29:33.907 FS: [fling] mController.isScrolling() - true ← 用户滑入胶片流
21:29:34.721 FS: [fling] onScrollEnd() - onScrollEnd
21:29:40.570 FS: [fling] onScrollEnd() - Ensuring that items are at full resolution. ← 命中
21:29:40.570 FS: [ViewItem:4] mData.renderThumbnail() ← 但调的还是 renderThumbnail!
两个要点:
- 从照片出现到 9.6 秒后,一次"全分辨率"都没触发。 原因是
onScrollEnd()先调scrollCurrentItemToCenter(),只有居中才进入那段代码;用户连续滑动时居中动画一直被取消,分支进不去。 - 即便命中了,调的也是
renderThumbnail(),对已经是THUMBNAIL的项是空操作 → 画质毫无变化。
源码侧交叉验证:
$ grep -n "renderFullRes" widget/FilmstripView.java
224: public void renderFullRes() { ← 定义
2665: renderFullRes(BUFFER_CENTER); ← onDoubleTap → zoomAt
3011: renderFullRes(BUFFER_CENTER); ← onScale 捏合 → onEnterZoomView
只有缩放手势两处。 日志(0 次)与源码(无其他调用点)互相印证。
4、未修复的第二个缺陷:录像中拍照只有 1920×1080
本次按评审决定只修复渲染阶段(缺陷①),此项仅记录,建议另开单。
HAL 侧硬证据:
21:29:28.876 EmulatedCamera_FakeCamera3: processCaptureRequest::
mainsize_width=1920,mainsize_height=1080 ← 主图只要了 1920x1080
而同一时刻录像链路是 4K:
21:29:25.205 USBSensorHWDec: setOutputFormat - set mjpeg 3840x2160
根因(应用侧):
VideoModule.initializeRecorder() 传的是预览尺寸 1920x1080:
java
Size optimalSize = CameraUtil.getOptimalVideoSnapshotPictureSize(supported,
mDesiredPreviewWidth, mDesiredPreviewHeight); // ← 传的是【预览】尺寸 1920x1080
CameraUtil.getOptimalVideoSnapshotPictureSize() 第一条规则就是 "WYSIWYG Override"------精确匹配预览尺寸就直接返回:
java
for (Size size : sizes) {
if (size.height() == targetHeight && size.width() == targetWidth) {
return size; // ← 预览是 1920x1080,这里立刻返回 1920x1080
}
}
HAL 其实支持 4K 静拍(kUsbAvailablePictureSize[] 第一项就是 {3840, 2160})。
5、修改方案(已实施:缺陷① 渲染阶段)
修改 1(核心):onScrollEnd() 真正进入全分辨率档
widget/FilmstripView.java --- 把名不副实的 renderThumbnail(BUFFER_CENTER) 换成 renderFullRes:
java
if (isCurrentItemCentered()) {
Log.d(TAG, "[fling] onScrollEnd() - Ensuring that items are at full resolution.");
renderFullRes(BUFFER_CENTER); // ← 原来是 renderThumbnail(BUFFER_CENTER),空操作
renderThumbnail(BUFFER_CENTER + 1); // 邻项保持屏幕档,保证再次滑动流畅
renderThumbnail(BUFFER_CENTER - 1);
renderThumbnail(BUFFER_CENTER + 2);
}
修改 2:新项插入即居中时直接给全分辨率
widget/FilmstripView.java → updateViewItem():
java
if (!mController.isScrolling() || !mIsUserScrolling) {
if (bufferIndex == BUFFER_CENTER) {
renderFullRes(bufferIndex);
} else {
renderThumbnail(bufferIndex);
}
}
修改 3:reload() 后补一次中心项全分辨率
java
renderAllThumbnails();
renderFullRes(BUFFER_CENTER); // 重新进入胶片流时无需滑动/缩放即清晰
FilmstripView.java 需要修改三处代码。
修改 4(关键陷阱):VideoItem.renderFullRes() 原来是空实现
data/VideoItem.java 原本是 public void renderFullRes(View view) { },
停止录像后最新一项恰好是刚录完的视频,若不处理会把问题改坏。视频没有比"屏幕尺寸帧"更大的表示,因此委托给 renderThumbnail:
java
@Override
public void renderFullRes(@Nonnull View view) {
renderThumbnail(view);
}
修改 5:像素预算适配 4K 面板
data/GlideFilmstripManager.java:
| 常量 | 改前 | 改后 | 说明 |
|---|---|---|---|
MAXIMUM_SMOOTH_PIXELS |
1.0MP | 2.0MP | 滑动档。作用于全部 5 项,只适度上调 |
MAXIMUM_FULL_RES_PIXELS |
4.5MP | 8.3MP | 全分辨率档。只作用于居中 1 项,放开到可铺满 4K |
6、修复效果(量化)
| 场景 | 改前 | 改后 |
|---|---|---|
| 录像中拍的照片(源 1920×1080) | 1333×750,放大 2.88× | 1920×1080,放大 2.00×(源分辨率已用尽) |
| 照片模式拍的照片(源 3840×2160) | 1333×750,放大 2.88× | 3840×2160,1:1 像素级 |
| 滑动过程中(未落位) | 1333×750 | 1886×1061 |
| 刚录完的视频项 | 屏幕档 | 屏幕档(行为不变,但不再有空渲染风险) |
诚实说明 :录像中拍的照片源只有 1920×1080 ,所以改完仍是 2.00× 放大,
相比改前的 2.88× 有明显改善(纵向细节 +44%),但做不到像素级清晰 。
要彻底解决必须同时修第二个缺陷。
完整的git修改:
diff --git a/apps/Camera2/src/com/android/camera/app/CameraController.java b/apps/Camera2/src/com/android/camera/app/CameraController.java
old mode 100644
new mode 100755
index 32002eb38..95f996e51
--- a/apps/Camera2/src/com/android/camera/app/CameraController.java
+++ b/apps/Camera2/src/com/android/camera/app/CameraController.java
@@ -174,6 +174,23 @@ public class CameraController implements CameraAgent.CameraOpenCallback, CameraP
}
mCameraProxy = camera;
mRequestingCameraId = EMPTY_REQUEST;
+ // modify by lwz start, 20261015
+ // The device info snapshot is taken while the camera is being opened.
+ // Right after boot that can happen before the camera service has
+ // finished enumerating the USB/UVC camera, leaving us with a snapshot
+ // that reports no devices. A successful open proves the device is
+ // there, so re-read the info before any listener queries it.
+ if (mInfo == null || mInfo.getCharacteristics(camera.getCameraId()) == null) {
+ Log.w(TAG, "Stale camera device info, refreshing for camera "
+ + camera.getCameraId());
+ CameraAgent agent = (mUsingNewApi && mCameraAgentNg != null)
+ ? mCameraAgentNg : mCameraAgent;
+ CameraDeviceInfo refreshed = agent.getCameraDeviceInfo();
+ if (refreshed != null) {
+ mInfo = refreshed;
+ }
+ }
+ // modify by lwz end
if (mCallbackReceiver != null) {
mCallbackReceiver.onCameraOpened(camera);
}
diff --git a/apps/Camera2/src/com/android/camera/data/GlideFilmstripManager.java b/apps/Camera2/src/com/android/camera/data/GlideFilmstripManager.java
old mode 100644
new mode 100755
index 83363f0ed..72590b54f
--- a/apps/Camera2/src/com/android/camera/data/GlideFilmstripManager.java
+++ b/apps/Camera2/src/com/android/camera/data/GlideFilmstripManager.java
@@ -85,13 +85,29 @@ public final class GlideFilmstripManager {
// Give a 20% margin for error and real conditions.
private static final int EST_PIXELS_PER_MILLI = 100000;
+ // modify by lwz start, 20261015
+ // These budgets were tuned for 1080p phones. On a 4K (3840x2160 = 8.3MP)
+ // IFPD panel the original 10ms/1MP "smooth" budget means a settled
+ // full-screen preview is only ever decoded at ~1334x750 and then upscaled
+ // 2.9x, which reads as "blurry". The scrolling budget is raised only
+ // moderately (it applies to all BUFFER_SIZE=5 buffered items), while the
+ // full-res budget - which in practice only applies to the single centered
+ // item - is allowed to fill the panel.
+ //
+ // Memory (ARGB_8888, 4 bytes/pixel):
+ // scrolling : 5 items * 2MP * 4B = ~40MB
+ // full res : 1 item * 8.3MP * 4B = ~33MB (also capped by
+ // MAX_GL_TEXTURE_SIZE 4096 in getMaxImageDisplaySize())
+ // The app runs with android:largeHeap="true", so this is affordable.
+
// Estimated number of bytes that can be used to usually display a thumbnail
// in under a frame at 60fps (16ms).
- public static final int MAXIMUM_SMOOTH_PIXELS = EST_PIXELS_PER_MILLI * 10 /* millis */;
+ public static final int MAXIMUM_SMOOTH_PIXELS = EST_PIXELS_PER_MILLI * 20 /* millis */;
- // Estimated number of bytes that can be used to generate a large thumbnail in under
- // (about) 3 frames at 60fps (16ms).
- public static final int MAXIMUM_FULL_RES_PIXELS = EST_PIXELS_PER_MILLI * 45 /* millis */;
+ // Budget for the image that is actually being looked at. Sized to fill a
+ // 4K panel rather than the original 45ms/4.5MP estimate.
+ public static final int MAXIMUM_FULL_RES_PIXELS = EST_PIXELS_PER_MILLI * 83 /* millis */;
+ // modify by skg end
public static final int JPEG_COMPRESS_QUALITY = 90;
private final GenericRequestBuilder<Uri, ?, ?, GlideDrawable> mTinyImageBuilder;
diff --git a/apps/Camera2/src/com/android/camera/data/VideoItem.java b/apps/Camera2/src/com/android/camera/data/VideoItem.java
old mode 100644
new mode 100755
index 8c263a328..ec152207c
--- a/apps/Camera2/src/com/android/camera/data/VideoItem.java
+++ b/apps/Camera2/src/com/android/camera/data/VideoItem.java
@@ -211,7 +211,16 @@ public class VideoItem extends FilmstripItemBase<VideoItemData> {
}
@Override
- public void renderFullRes(@Nonnull View view) { }
+ public void renderFullRes(@Nonnull View view) {
+ // modify by skg start, 20261015
+ // This used to be an empty body. Now that the filmstrip promotes the
+ // centered item to full resolution (see FilmstripView.onScrollEnd and
+ // updateViewItem), a no-op here would leave a just-recorded video
+ // showing only its small MediaStore thumbnail. A video has no
+ // representation larger than its screen-sized frame, so render that.
+ renderThumbnail(view);
+ // modify by skg end
+ }
@Override
public void recycle(@Nonnull View view) {
diff --git a/apps/Camera2/src/com/android/camera/widget/FilmstripView.java b/apps/Camera2/src/com/android/camera/widget/FilmstripView.java
old mode 100644
new mode 100755
index fa5476f2f..b681bc604
--- a/apps/Camera2/src/com/android/camera/widget/FilmstripView.java
+++ b/apps/Camera2/src/com/android/camera/widget/FilmstripView.java
@@ -1716,7 +1716,18 @@ public class FilmstripView extends ViewGroup {
// is unreliable. Load the full resolution if either value
// reports that the item is not scrolling.
if (!mController.isScrolling() || !mIsUserScrolling) {
- renderThumbnail(bufferIndex);
+ // modify by skg start, 20261015
+ // The comment above says "full resolution" but this only rendered a
+ // thumbnail. A photo taken during video recording is inserted here,
+ // so if the user is already sitting on it (nothing scrolling) it was
+ // left at the ~1MP "smooth" size and looked blurry until they
+ // scrolled away and back. Promote the centered item properly.
+ if (bufferIndex == BUFFER_CENTER) {
+ renderFullRes(bufferIndex);
+ } else {
+ renderThumbnail(bufferIndex);
+ }
+ // modify by skg end
}
adjustChildZOrder();
@@ -1845,6 +1856,13 @@ public class FilmstripView extends ViewGroup {
Log.d(TAG, "reload() - Ensure all items are loaded at max size.");
renderAllThumbnails();
+ // modify by lwz start, 20261015
+ // renderAllThumbnails() only reaches RenderSize.THUMBNAIL, so after a
+ // reload the item on screen was left at the ~1MP "smooth" size. Promote
+ // the centered item to full resolution so re-entering the filmstrip
+ // shows a sharp image without requiring a scroll or a zoom gesture.
+ renderFullRes(BUFFER_CENTER);
+ // modify by lwz end
invalidate();
if (mListener != null) {
@@ -1961,12 +1979,22 @@ public class FilmstripView extends ViewGroup {
// we want to ensure the item that is "most in view" is
// the first one rendered at max size.
+ // modify by lwz start, 20261015
+ // This used to call renderThumbnail() for the centered
+ // item, which is a no-op once the item is already at
+ // RenderSize.THUMBNAIL - so renderFullRes() was never
+ // reached by swiping and the settled preview stayed at
+ // the ~1MP "smooth" size (visibly soft on a 4K panel).
+ // The item being looked at is now promoted to full
+ // resolution; its neighbours stay at thumbnail size so
+ // that scrolling away again remains smooth.
Log.d(TAG, "[fling] onScrollEnd() - Ensuring that items are at"
+ " full resolution.");
- renderThumbnail(BUFFER_CENTER);
+ renderFullRes(BUFFER_CENTER);
renderThumbnail(BUFFER_CENTER + 1);
renderThumbnail(BUFFER_CENTER - 1);
renderThumbnail(BUFFER_CENTER + 2);
+ // modify by lwz end
}
}
};
三、其他
1、小结
- 胶片流有三级渲染档位 :
TINY(256×256)→THUMBNAIL(屏幕档,1MP 钳位)→FULL_RES(全分辨率)。滑动浏览只到屏幕档,全分辨率档只有双击/捏合缩放才触发; - "确保全分辨率"的日志是假的 :
onScrollEnd()打印Ensuring that items are at full resolution.,实际调的却是renderThumbnail(),而该方法在已是 THUMBNAIL 时直接 return ------ 双重 bug(调错方法 + 守卫短路); - 屏幕档在 4K 面板上只有 1333×750 ,放大 2.88× 铺满 3840×2160,这就是模糊的根因。
MAXIMUM_SMOOTH_PIXELS(1MP)是按 2014 年 1080p 手机定的,4K 面板需要复核; - 修复核心 :
onScrollEnd()居中时改调renderFullRes;新项插入即居中时直接给全分辨率;reload()后补一次中心项全分辨率; - 关键陷阱 :
VideoItem.renderFullRes()原本是空实现,启用全分辨率路径前必须检查所有子类,否则会把视频项改坏; - 第二个独立缺陷 :录像中拍照只有 1920×1080(HAL 侧硬证据),根因是
VideoModule传了预览尺寸给getOptimalVideoSnapshotPictureSize(),WYSIWYG 规则精确匹配预览就返回了。本次未修,建议另开单; - 经验:日志文案和代码行为可能完全相反,别拿日志当需求文档;grep 计数对比(期望日志 vs 实际日志)是很高效的取证手段。
2、改动文件清单
全部位于 packages 仓(release/packages,独立 git 仓库):
| 文件 | 改动 |
|---|---|
apps/Camera2/src/com/android/camera/widget/FilmstripView.java |
onScrollEnd() 改调 renderFullRes;updateViewItem() 居中项升档;reload() 补中心项全分辨率 |
apps/Camera2/src/com/android/camera/data/VideoItem.java |
renderFullRes() 由空实现改为委托 renderThumbnail() |
apps/Camera2/src/com/android/camera/data/GlideFilmstripManager.java |
MAXIMUM_SMOOTH_PIXELS 1MP→2MP;MAXIMUM_FULL_RES_PIXELS 4.5MP→8.3MP |
3、验证步骤
主用例(原问题):
- 进入相机 → 切到录像模式 → 开始录像(4K);
- 录像过程中按快门拍 1~2 张;
- 停止录像,立即滑入胶片流查看刚拍的照片;
- 预期:落位后立刻清晰(1920×1080 原生,不再是 1333×750 放大),不需要来回滑动或双击缩放。
日志验证:
bash
# 改前:0 次;改后落位时应当出现
adb logcat -s CAM_FilmstripView | grep "renderFullRes"
# 应当与 "Ensuring that items are at full resolution." 成对出现
adb logcat -s CAM_FilmstripView | grep -E "full resolution|renderFullRes"
回归用例(重点关注内存与流畅度):
| 用例 | 预期 |
|---|---|
| 照片模式拍照后立即预览 | 清晰(源 4K → 1:1 像素级) |
| 胶片流快速连续滑动 20+ 张 | 不卡顿、不白屏;滑动中走 1886×1061 档 |
| 停止录像后查看最新的"视频"项 | 缩略图正常显示(修改 4 的回归点,务必验证) |
| 视频项双击 / 捏合缩放 | 画面正常(原先是空渲染) |
| 大相册(100+ 张)反复进出胶片流 | 无 OOM、无明显 GC 抖动 |
以上就是录像中拍照后预览模糊问题的分析与解决总结,整体下来:
问题根因是胶片流的全分辨率渲染路径在正常滑动时永远走不到,加上 4K 面板用了 1080p 时代的像素预算常量。
修复后落位即清晰,但录像中拍照的源分辨率偏低是第二个独立缺陷,需要另开单处理。
有问题欢迎评论区交流。