[Android] [SnapdragonCamera] 单摄(横屏)阶段总结

在研高通平台的单摄项目中遇到了很多适配问题,做一下初步的总结,为今后遇到相似的问题,提供参考方案。

1. 横屏设置相机预览显示不正常

1.1问题现象

1.2分析与解决

骁龙相机默认的预览方向是"portrait"。在横屏设备上显示的时候就会出现上面效果。实际操作中这样的预览效果很不友好,所以针对这种情况,需要作出如下修改: 我们以XXX-A项目为例,XXX-A的代码是共用VYYYC的代码,其适用的范围包含单摄像头和双摄像头的机器。所以不能直接修改AndroidManifest.xml的"screenOrientation"参数。需要做一个判断,所以其具体修改下:

a.先移除AndroidManifest.xml中的固定设置:

b. 再在主Activity: CameraActivity.java中添加相关的判断及设置:

java 复制代码
   @Override
    public void onCreate(Bundle state) {
        super.onCreate(state);
        if (Build.isLandScapeDevice()) {
            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
        } else {
            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
        }
        if (PersistUtil.isTraceEnable())
            Trace.beginSection("CameraActivity onCreate");
        try {
            //Print version info here
            String versionName = getPackageManager().getPackageInfo(getPackageName(), 0).versionName;
            Log.d(TAG, "snapdragoncamera_version: " + versionName);
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        // Check if this is in the secure camera mode.
        ....
    }

如果是屏幕横置的机器就将预览的Orientation设置成"LANDSCAPE"。如果是BBB的单一项目的代码可以将AndroidManifest.xml中的参数直接修改成"landscape"即可。

修改后的效果:

相机的菜单和拍摄按钮都调整到屏幕的下方。

2. 预览画面和菜单显示不匹配

问题现象:

相机打开的预览画面是"Photo",但是在下方的模式菜单的选项中,选中的却是"ProMode". 造成了预览图像和菜单设置的不匹配的现象。

分析与解决:

造成这个问题的原因是:由于双摄像头项目在预览画面会有四个选项模式:"Video/HFR/Photo/ProMode"。而单摄项目在前置摄像头的预览时只有三个选项模式:"Video/Photo/ProMode". 追踪Code,定位到 Camera2ModeAdapter.java中是设置这个模式的位置:

java 复制代码
public class Camera2ModeAdapter extends RecyclerView.Adapter<Camera2ModeAdapter.ViewHolder> {
    private List<String> mModeList;
    private int mSelectedPos = 2;  //设置默认的模式  
    private OnItemClickListener mOnItemClickListener;

    public Camera2ModeAdapter(List<String> list) {
        this.mModeList = list;
    }

    public void setOnItemClickListener(OnItemClickListener listener) {
        this.mOnItemClickListener = listener;
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.camera2_mode_item,
                parent, false);
        ViewHolder viewHolder = new ViewHolder(view);
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        holder.mCameraModeText.setText(mModeList.get(position));
        holder.mCameraModeText.setSelected(mSelectedPos == position); //选中预览模式
        holder.mCameraModeText.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mOnItemClickListener.onItemClick(position) >= 0) {
                    mSelectedPos = position;
                    notifyDataSetChanged();
                }
            }
        });
    }

    @Override
    public int getItemCount() {
        return mModeList.size();
    }

    public interface OnItemClickListener {
        int onItemClick(int mode);
    }

    public void setSelectedPosition(int position) {
        mSelectedPos = position;
        notifyDataSetChanged(); 
    }

    class ViewHolder extends RecyclerView.ViewHolder{

        protected TextView mCameraModeText;

        public ViewHolder(View itemView) {
            super(itemView);
            mCameraModeText = (TextView) itemView.findViewById(R.id.mode_text);
        }
    }
}

mSelectedPos = 2 在四个模式下对应的是"Photo ", 但是在只有三个模式的情况下对应的mode name则是"ProMode".

继续追Code,发现是CaptureUI.java中使用到这个adapter:

java 复制代码
 mCameraModeAdapter = new Camera2ModeAdapter(mModule.getCameraModeList());
 mCameraModeAdapter.setOnItemClickListener(mModule.getModeItemClickListener());
 mModeSelectLayout.setAdapter(mCameraModeAdapter);

决定"mModeSelectLayout"中显示的预览模式是"mModule.getCameraModeList()" 其对应的Code则是在CaptureModule.java

java 复制代码
public List<String> getCameraModeList() {
        ArrayList<String> cameraModes = new ArrayList<>();
        for (SceneModule sceneModule : mSceneCameraIds) {
            cameraModes.add(mSelectableModes[sceneModule.mode.ordinal()]);
        }
        return cameraModes;
}

预设的模式:

java 复制代码
private String[] mSelectableModes = {"Video", "HFR", "Photo", "Bokeh", "SAT", "ProMode"};
...
public enum CameraMode {
        VIDEO,
        HFR,
        DEFAULT,
        RTB,
        SAT,
        PRO_MODE
    }

在初始化的时候,将六种默认的模式都加载到 mSceneCameraIds 中

java 复制代码
for (int i = 0; i < mSelectableModes.length; i++) {
            module = new SceneModule();
            module.mode = CameraMode.values()[i];
            mSceneCameraIds.add(module);
 }

DEFAULT对应的模式则是:"Photo"。接下来程序会根据不同的情况进行筛选:

在initCameraIds()的时候,会默认将所有模式的删除属性设置成"true":

java 复制代码
private void initCameraIds() {
        CameraManager manager = (CameraManager) mActivity.getSystemService(Context.CAMERA_SERVICE);
        boolean isFirstDefault = true;
        boolean[] removeList = new boolean[mSelectableModes.length];
        for (int i = 0; i < mSelectableModes.length; i++) {
            removeList[i] = true;
        }
 ....
        for (int i = 0; i < cameraIdList.length; i++) {
            String cameraId = cameraIdList[i];
            mCameraId[i] = cameraId;
            CameraCharacteristics characteristics;
            try {
                characteristics = manager.getCameraCharacteristics(cameraId);
            } catch (CameraAccessException e) {
                e.printStackTrace();
                continue;
            }
            isFirstDefault = setUpLocalMode(i, characteristics, removeList,
                    isFirstDefault, cameraId);
        }
 }       

setUpLocalMode方法中会根据预设的情况进行初步的筛选:

java 复制代码
private boolean setUpLocalMode(int cameraId, CameraCharacteristics characteristics,
                                boolean[] removeList, boolean isFirstDefault, String physicalId) {
        Byte type = 0;
        try {
            type = characteristics.get(logical_camera_type);
        } catch (IllegalArgumentException e) {
            Log.e(TAG, "setUpLocalMode no vendorTag logical_camera_type:" + logical_camera_type);
        }
        Log.d(TAG,"init cameraId " + cameraId + " | logical_camera_type = " + type +
                " | physical id = " + physicalId);
        int facing = characteristics.get(CameraCharacteristics.LENS_FACING);
        switch (type) {
            case TYPE_DEFAULT:// default
                removeList[CameraMode.DEFAULT.ordinal()] = false;
                removeList[CameraMode.VIDEO.ordinal()] = false;
                removeList[CameraMode.PRO_MODE.ordinal()] = false;
                if (facing == CameraCharacteristics.LENS_FACING_FRONT) {
                    CaptureModule.FRONT_ID = cameraId;
                    mSceneCameraIds.get(CameraMode.DEFAULT.ordinal()).frontCameraId = cameraId;
                    mSceneCameraIds.get(CameraMode.VIDEO.ordinal()).frontCameraId = cameraId;
                    mSceneCameraIds.get(CameraMode.HFR.ordinal()).frontCameraId = cameraId;
                    mSceneCameraIds.get(CameraMode.PRO_MODE.ordinal()).frontCameraId = cameraId;
                } else {
                ...
                
          }
          ... 
      return isFirstDefault;
  }

上面部分代码将"Photo/Video/ProMode"三个模式的删除属性设置成"false"

最终确定通过下面的筛选方式将需要显示的Mode存放在mSceneCameraIds 列表中

java 复制代码
for (int i = 0; i<mSceneCameraIds.size(); i++){
       Log.i("WKS","START----->mSceneCameraIds[" + i + "]: " + mSceneCameraIds.get(i).mode);
  }  
 
 for (int i = 0; i < removeList.length; i++) {
            if (!removeList[i]) {
                continue; //"Photo/Video/ProMode"在上面已经将改属性设置成false
            }
            for (SceneModule sceneModule : mSceneCameraIds) {
                if (sceneModule.mode.ordinal() == i) {
                    mSceneCameraIds.remove(sceneModule); //不需要的Mode在此从mSceneCameraIds中移除
                    break;
                }
            }
        }
  }   
  
  for (int i = 0; i<mSceneCameraIds.size(); i++){
       Log.i("WKS","END----->mSceneCameraIds[" + i + "]: " + mSceneCameraIds.get(i).mode);
  }  

通过上面的流程操作,可以看到筛选前mSceneCameraIds里存放的对象是:"VIDEO/HFR/DEFAULT/RTB/SAT/PRO_MODE"

筛选后则是:

"VIDEO/DEFAULT/PRO_MODE"

3. 强制断电后图片无法保存

问题分析:

在突然断电的情况下,Android设备可能会丢失已拍摄的图片。这是因为设备的电源管理策略和文件系统缓存机制可能导致图片数据未能及时写入存储设备。

问题解决:

在每次拍照完成后进行一次数据同步。以确保拍摄的图片和视频有同步到储存空间里:

java 复制代码
try {
      Runtime.getRuntime().exec("sync data/misc/apexdata/com.android.media/");
 }  catch (Exception e) {
      Log.e(TAG, "Run error:" + e);
 }

在Android系统中,sync data/misc/apexdata/com.android.media/这个命令的含义是同步com.android.media模块的数据。com.android.media模块通常包含与媒体处理相关的库和资源,例如音频和视频编解码器、媒体播放服务等。

这个问题和单摄/横屏没有直接关系。在其它设备上也能复制出问题,但是由于目前在研的横屏设备没有电池,是通过AC直接供电,操作的时候如果直接移除电源就会比较容易复制出此类的现象。

4. 滑动屏幕不能切换相机的模式

问题分析:

复制现象的时候发现,点击模式按钮进行切换的时候是正常操作的。结合问题描述的状况看,应该是滑动的时候,程序处理出现了问题。那首先我们需要定位到程序里滑动功能的位置,看其是如何定义和操作的。然后再根据其提供的线索去追踪到模式功能切换的实现位置。

解决方案:

确定实现滑动的代码在PreviewGestures.java: 在其onScroll的方法,定义了向左isLeftSwipe,向右isRightSwipe以及上下滑动isUpSwipe的行为。

java 复制代码
@Override
        public boolean onScroll (MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            if (e1 == null) {
                // e1 can be null if for some cases.
                return false;
            }
            if (mZoomOnly || mMode == MODE_ZOOM) return false;

            int deltaX = (int) (e1.getX() - e2.getX());
            int deltaY = (int) (e1.getY() - e2.getY());
            if((Math.abs(deltaX) > 40 || Math.abs(deltaY) > 40) && Math.abs(e1.getY()) < 1800) {
                int orientation = 0;
                if (mCaptureUI != null)
                    orientation = mCaptureUI.getOrientation();

                if (isLeftSwipe(orientation, deltaX, deltaY)) {
                    waitUntilNextDown = true;
                    if (mCaptureUI != null)
                        mCaptureUI.swipeCameraMode(-1);
                    if (mMultiCameraUI != null)
                        mMultiCameraUI.swipeCameraMode(-1);
                    return true;
                }
                if (isRightSwipe(orientation, deltaX, deltaY)) {
                    waitUntilNextDown = true;
                    if (mCaptureUI != null)
                        mCaptureUI.swipeCameraMode(1);
                    if (mMultiCameraUI != null)
                        mMultiCameraUI.swipeCameraMode(1);
                    return true;
                }
                if (isUpSwipe(orientation, deltaX, deltaY) ||
                        isDownSwipe(orientation, deltaX, deltaY)) {
                    if (Camera.getNumberOfCameras() == 1 || mModelName.equals(Build.MODEL)) {
                        return false;
                    } else {
                        if (e1.getY() < 200) {
                            return false;
                        }
                        waitUntilNextDown = true;
                        if (mCaptureUI != null)
                            mCaptureUI.switchFrontBackCamera();
                        return true;
                    }
                }
            }
            return false;
        }

继而追踪到更新UI的位置是在CaptureUI.java的 swipeCameraMode()方法里:

java 复制代码
public void swipeCameraMode(int move) {
        Log.i("TD","---->mModule.getCameraModeSwitcherAllowed(): " + mModule.getCameraModeSwitcherAllowed());  
        if (mIsVideoUI || !mModule.getCameraModeSwitcherAllowed() ||
                mModule.getCurrentIntentMode() != CaptureModule.INTENT_MODE_NORMAL) { 
            return;
        }
        Log.i("TD","---->mModule.getCurrentModeIndex(): " + mModule.getCurrentModeIndex());
        int index = mModule.getCurrentModeIndex() + move;
        int modeListSize = mModule.getCameraModeList().size();
        if (index >= modeListSize || index == -1) {
            return;
        }
        int mode = index % modeListSize;
        mModule.setCameraModeSwitcherAllowed(false);
        mCameraModeAdapter.setSelectedPosition(mode);
        mModeSelectLayout.smoothScrollToPosition(mode);
        mModule.selectCameraMode(mode);
}       

在swipeCameraModed方法的关键位置加上log. 发现 mModule.getCurrentModeIndex()的初始值是"2".

此时如果向右滑动,move = 1,index的值就会是"3",方法直接return. 此时如果向左滑动,move = -1,index的值为"1"。而默认的模式对应的index也是为"1"。这就造成了预览画面没有发生变化,那就无法走到下面代码的方法里去设置mCameraModeSwitcherAllowed值为"true"。而swipeCameraMoed 方法里mCameraModeSwitcherAllowed的值已经设置成"false".后续继续滑动的话,直接就return出来了。这就造成了滑动无作用的现象。

CaptureModule.java:

java 复制代码
private void createSession(final int id) {
        Log.d(TAG, "createSession,id: " + id + ",mPaused:" + mPaused + ",mCameraOpened:" + !mCameraOpened[id] + ",mCameraDevice:"+ (mCameraDevice[id] == null));
        if (mPaused || !mCameraOpened[id] || (mCameraDevice[id] == null)) return;
        List<Surface> list = new LinkedList<Surface>();
        mState[id] = STATE_PREVIEW;
        mControlAFMode = CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE;
        try {
            // We set up a CaptureRequest.Builder with the output Surface.
            mPreviewRequestBuilder[id] = getRequestBuilder(id);
            mPreviewRequestBuilder[id].setTag(id);

            CameraCaptureSession.StateCallback captureSessionCallback =
                    new CameraCaptureSession.StateCallback() {

                        @Override
                        public void onConfigured(CameraCaptureSession cameraCaptureSession) {
                            if (mPaused || null == mCameraDevice[id] ||
                                    cameraCaptureSession == null) {
                                return;
                            }
                            Log.i(TAG, "cameracapturesession - onConfigured "+ id);
                            setCameraModeSwitcherAllowed(true);
                            // When the session is ready, we start displaying the preview.
                            ...
}                            

定位到造成问题的原因是初始状态下,由于是单个摄像头,mCurrentModeIndex 的值不能直接等于CameraMode 数组里 Photo **对应的坐标。**所以需要在下面的代码里作出如下处理:如果是单个摄像头的设备,需要将默认的mCurrentModeIndex 值设置成"1"。

java 复制代码
if (mCurrentSceneMode == null) {
   int index = mIntentMode == INTENT_MODE_VIDEO ?
       CameraMode.VIDEO.ordinal() : CameraMode.DEFAULT.ordinal(); // CameraMode.DEFAULT.ordinal()的值是2
   if ((cameraIdList.length == 1 || mModelName.equals(Build.MODEL)) && mIntentMode != INTENT_MODE_VIDEO) {
       mCurrentModeIndex = mNextModeIndex = 1;
   } else {
       mCurrentModeIndex = mNextModeIndex = index;
   }
   mCurrentSceneMode = mSceneCameraIds.get(index);
}

public int getCurrentModeIndex() {
    return mCurrentModeIndex;
}

在后续的操作中,会通过下面的方法将index 的值传递给mCurrentModeIndex

java 复制代码
public void setNextSceneMode(int index) {
        mNextModeIndex = index;
}

private void reinitSceneMode() {
        mCurrentSceneMode = mSceneCameraIds.get(mNextModeIndex);
        mCurrentModeIndex = mNextModeIndex;
        CURRENT_MODE = mCurrentSceneMode.mode;
        CURRENT_ID = mCurrentSceneMode.getNextCameraId(CURRENT_MODE);
        Log.d(TAG, "reinitSceneMode: CURRENT_ID :" + CURRENT_ID);
}

5. 相机Promode下UI显示异常

问题现象:

在横屏机器上,ProMode的选项发生了偏移

分析与解决:

根据Promode中的按钮控件找到对应的布局文件pro_mode_layout.xml, 再通过布局文件的引用找到对应的java文件:++src/com/android/camera/ui/OneUICameraControls.java++

在其初始化的时候有对promode的控件做了一些位置的调整:

java 复制代码
private void initializeProMode(boolean promode) {
        if (!promode) {
            mProMode.setMode(ProMode.NO_MODE);
            mProModeLayout.setVisibility(INVISIBLE);
            return;
        }
        mProModeLayout.setVisibility(VISIBLE);
        mProModeLayout.setY(mHeight - mBottom - mProModeLayout.getHeight() - 48);
}

通过上述代码可以知晓其是对mProModeLayout的竖直方向上做了一些调整。而对于横屏的机器遇到的问题是水平方向上发生了偏移。根据机器在水平方向上的偏移量(这边需要根据不同设备的情况作出相应的数据调整)作出相应的修改:

java 复制代码
private void initializeProMode(boolean promode) {
        if (!promode) {
            mProMode.setMode(ProMode.NO_MODE);
            mProModeLayout.setVisibility(INVISIBLE);
            return;
        }
        mProModeLayout.setVisibility(VISIBLE);
        if (Camera.getNumberOfCameras() == 1 || mModelName.equals(Build.MODEL)) {
            if (density >= 280){
                mProModeLayout.setY(mHeight - mBottom - mProModeLayout.getHeight() + 120);
            } else {
                mProModeLayout.setY(mHeight - mBottom - mProModeLayout.getHeight());
            }
            mProModeLayout.setX(242);
        } else {
            mProModeLayout.setY(mHeight - mBottom - mProModeLayout.getHeight() - 48);
        }
 }

上面部分的代码是由于另外一个需求:

Promode的选项和功能部分会发生重叠,影响实际的功能操作。所以需要对竖直方向上坐标位置进行相应的调整。这个还需要考虑到Display Size 的调整会对其控件大小和布局的影响,所以要根据不同的density进行调整规划。 修改后的效果如下:

6.相机拍照/摄像时没有声音

问题分析:

抓取拍照/录像时的即时log:

bash 复制代码
8-02 19:43:09.941 V/SnapCam_CaptureUI( 3687): surfaceChanged: width =1280, height = 720
08-02 19:43:09.943 E/SoundPool( 3687): error loading /product/media/audio/ui/VideoRecord.ogg
08-02 19:43:09.943 E/SoundPool( 3687): error loading /system/media/audio/ui/VideoRecord.ogg
08-02 19:43:09.943 E/MediaActionSound( 3687): load() error loading sound: 2
08-02 19:43:09.944 E/SoundPool( 3687): error loading /product/media/audio/ui/VideoStop.ogg
08-02 19:43:09.944 E/SoundPool( 3687): error loading /system/media/audio/ui/VideoStop.ogg
08-02 19:43:09.944 E/MediaActionSound( 3687): load() error loading sound: 3
08-02 19:43:09.944 E/SoundPool( 3687): error loading /product/media/audio/ui/camera_focus.ogg
08-02 19:43:09.945 E/SoundPool( 3687): error loading /system/media/audio/ui/camera_focus.ogg
08-02 19:43:09.945 E/MediaActionSound( 3687): load() error loading sound: 1
08-02 19:43:09.945 E/SoundPool( 3687): error loading /product/media/audio/ui/camera_click.ogg
08-02 19:43:09.945 E/SoundPool( 3687): error loading /system/media/audio/ui/camera_click.ogg
08-02 19:43:09.945 E/MediaActionSound( 3687): load() error loading sound: 0
08-02 19:43:09.946 D/SnapCam_CaptureModule( 3687): Chosen postproc filter id : 0

通过上面log可以看出拍照/摄像时无声的根本原因是加载音频文件的时候出现了异常。进入系统内部检查是否存在这些音频文件:

可以见当前设备的目标位置并未存放相关的资源。 正常可播放声音的机器在当前目录下会存放相关资源:

解决方案:

找到音频文件的存放位置,将相关资源Copy到系统中:

bash 复制代码
$(LOCAL_PATH)/effects/ogg/camera_click.ogg:$(TARGET_COPY_OUT_PRODUCT)/media/audio/ui/camera_click.ogg \

另外有些机器考虑到本身的配置比较低,加上这些资源之后可能会影响到性能。会主动移除这些功能,本案最后结合自身情况就选择不加入相关的音频资源。

相关推荐
Kapaseker1 小时前
2026年,我们还该不该学编程?
android·kotlin
雨白17 小时前
Android 快捷方式实战指南:静态、动态与固定快捷方式详解
android
hqk17 小时前
鸿蒙项目实战:手把手带你实现 WanAndroid 布局与交互
android·前端·harmonyos
LING18 小时前
RN容器启动优化实践
android·react native
恋猫de小郭20 小时前
Flutter 发布官方 Skills ,Flutter 在 AI 领域再添一助力
android·前端·flutter
Kapaseker1 天前
一杯美式搞懂 Any、Unit、Nothing
android·kotlin
黄林晴1 天前
你的 Android App 还没接 AI?Gemini API 接入全攻略
android
恋猫de小郭1 天前
2026 Flutter VS React Native ,同时在 AI 时代 VS Native 开发,你没见过的版本
android·前端·flutter
冬奇Lab2 天前
PowerManagerService(上):电源状态与WakeLock管理
android·源码阅读
BoomHe2 天前
Now in Android 架构模式全面分析
android·android jetpack