同一片段中的两个Android相机预览

时间:2015-06-17 07:47:21

标签: android android-camera surfaceview

我正在尝试在相同的片段布局中创建两个相机预览,但我遇到了一些奇怪的问题,例如:

  • 预览会冻结,相机预览不会更改
  • 有时相机根本无法启动。

这是我为相机设计的自定义SurfaceView:

@SuppressWarnings("deprecation")
public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback {

    private static final String TAG = "CameraPreview";

    private SurfaceHolder mHolder;
    private Camera mCamera;
    private List<Camera.Size> mSupportedPreviewSizes;
    private Camera.Size mPreviewSize;
    private int currentVolume;
    private boolean isVolumeChanged = false;

    public CameraPreview(Context context, Camera mCamera) {
        super(context);
        setCamera(mCamera);
        init();
    }

    public CameraPreview(Context context) {
        super(context);
    }

    public CameraPreview(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public CameraPreview(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }


    public CameraPreview(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    /**
     * Initialize the SurfaceView which will be used to display the camera preview.
     * <p/>
     * Only an general setup for the SurfaceView should be done in this method.
     */
    private void init() {
        setDrawingCacheEnabled(true);
    }

    /**
     * Set the camera to the SurfaceView.
     *
     * @param camera
     *         The camera object which will be set to the SurfaceView.
     */
    public void setCamera(Camera camera) {
        mCamera = camera;
        // supported preview sizes
        mSupportedPreviewSizes = mCamera.getParameters().getSupportedPreviewSizes();
        // Install a SurfaceHolder.Callback so we get notified when the
        // underlying surface is created and destroyed.
        mHolder = getHolder();
        mHolder.addCallback(this);
        // deprecated setting, but required on Android versions prior to 3.0
        mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
    }

    public void surfaceCreated(SurfaceHolder holder) {
        // empty. surfaceChanged will take care of stuff
        // The Surface has been created, now tell the camera where to draw the preview.
        try {
            mCamera.setPreviewDisplay(holder);
            mCamera.startPreview();
        } catch (IOException e) {
            LogUtils.d(TAG, "Error setting camera preview: " + e.getMessage());
        }
    }

    public void surfaceDestroyed(SurfaceHolder holder) {
        // empty. Take care of releasing the Camera preview in your activity.
    }

    public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
        // If your preview can change or rotate, take care of those events here.
        // Make sure to stop the preview before resizing or reformatting it.
        if (mHolder.getSurface() == null) {
            // preview surface does not exist
            return;
        }

        // stop preview before making changes
        try {
            mCamera.stopPreview();
        } catch (Exception e) {
            // ignore: tried to stop a non-existent preview
        }

        // set preview size and make any resize, rotate or reformatting changes here
        try {
            Camera.Parameters parameters = mCamera.getParameters();
            parameters.setPreviewSize(mPreviewSize.width, mPreviewSize.height);
            // set the orientation of the pictures taken
            //TODO: match this orientation value with the one used for display
            parameters.setRotation(90);
            // set the focus mode
            parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
            mCamera.setParameters(parameters);
        } catch (RuntimeException e) {
            // This could happen when concurrency of the camera is happening (especially in dual shot mode)
            //TODO: optimize the camera flow to avoid this issue
            LogUtils.d(TAG, "Error setting camera parameters: " + e.getMessage());
        }
        // Force the orientation in Portrait mode
        // TODO: get the orientation from the device
        mCamera.setDisplayOrientation(90);

        // start preview with new settings
        try {
            mCamera.setPreviewDisplay(mHolder);
            mCamera.startPreview();
        } catch (IOException e) {
            LogUtils.e(TAG, "Error starting camera preview: " + e.getMessage());
        }
    }

    /**
     * Disable the default shutter sound when taking a picture.
     */
    private void disableShutterSound() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            boolean shutterState = mCamera.enableShutterSound(false);
            LogUtils.i(TAG, "Shutter sound was" + (!shutterState ? "not " : " ") + "disabled");
        } else {
            LogUtils.i(TAG, "Trying to disable shutter sound by altering the system audio manager.");
            // Backward compatibility method for disabling the shutter sound
            AudioManager audio = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
            currentVolume = audio.getStreamVolume(AudioManager.STREAM_SYSTEM);
            audio.setStreamVolume(AudioManager.STREAM_SYSTEM, 0, AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE);
            MediaPlayer media = MediaPlayer.create(getContext(), R.raw.camera_shutter_click);
            media.setAudioStreamType(AudioManager.STREAM_NOTIFICATION);
            isVolumeChanged = true;
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mCamera != null) {
            final int width = resolveSize(getSuggestedMinimumWidth(), widthMeasureSpec);
            final int height = resolveSize(getSuggestedMinimumHeight(), heightMeasureSpec);

            if (mSupportedPreviewSizes != null) {
                mPreviewSize = getOptimalPreviewSize(mSupportedPreviewSizes, width, height);
            }

            float ratio;
            if (mPreviewSize.height >= mPreviewSize.width) {
                ratio = (float) mPreviewSize.height / (float) mPreviewSize.width;
            } else {
                ratio = (float) mPreviewSize.width / (float) mPreviewSize.height;
            }

            // One of these methods should be used, second method squishes preview slightly
            //        setMeasuredDimension(width, width);
            setMeasuredDimension(width, (int) (width * ratio));
            //        setMeasuredDimension((int) (width * ratio), height);
        } else {
            setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
        }
    }

    private Camera.Size getOptimalPreviewSize(List<Camera.Size> sizes, int w, int h) {
        final double ASPECT_TOLERANCE = 0.1;
        double targetRatio = (double) h / w;

        if (sizes == null) {
            return null;
        }

        Camera.Size optimalSize = null;
        double minDiff = Double.MAX_VALUE;

        int targetHeight = h;

        for (Camera.Size size : sizes) {
            double ratio = (double) size.height / size.width;
            if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE) {
                continue;
            }

            if (Math.abs(size.height - targetHeight) < minDiff) {
                optimalSize = size;
                minDiff = Math.abs(size.height - targetHeight);
            }
        }

        if (optimalSize == null) {
            minDiff = Double.MAX_VALUE;
            for (Camera.Size size : sizes) {
                if (Math.abs(size.height - targetHeight) < minDiff) {
                    optimalSize = size;
                    minDiff = Math.abs(size.height - targetHeight);
                }
            }
        }

        return optimalSize;
    }

    public void releaseCamera() {
        if (mCamera != null) {
            mCamera.stopPreview();
            mCamera.setPreviewCallback(null);
            mCamera.release();
            mCamera = null;
        }
    }

    /**
     * Check if the Camera object was passed and set to the SurfaceView.
     *
     * @return {@code true} if the Camera object is valid,  {@code false} otherwise
     */
    public boolean isCameraSet() {
        return null != mCamera;
    }

    public void takePicture() {
        if (null != mCamera) {
            // Disable the shutter sound when taking an picture
            disableShutterSound();
            mCamera.takePicture(shutterCallback, rawCallback, postViewCallback, jpegCallback);
        }
    }

    private void resetCam() {
        if (null != mCamera) {
            mCamera.startPreview();
            setCamera(mCamera);
        }
    }

    /**
     * The shutter callback occurs after the image is captured
     */
    private Camera.ShutterCallback shutterCallback = new Camera.ShutterCallback() {
        public void onShutter() {
            MediaPlayer.create(getContext(), R.raw.camera_shutter_click).start();
            LogUtils.d(TAG, "onShutter");
        }
    };

    /**
     * The raw callback occurs when the raw image data is available.<br/>(<b>NOTE:</b> the data will be null if there is
     * no raw image callback buffer available or the raw image callback buffer is not large enough to hold the raw
     * image).
     */
    private Camera.PictureCallback rawCallback = new Camera.PictureCallback() {
        public void onPictureTaken(byte[] data, Camera camera) {
            LogUtils.d(TAG, "onPictureTaken - raw");
        }
    };

    /**
     * The jpeg callback occurs when the compressed image is available.
     */
    private Camera.PictureCallback jpegCallback = new Camera.PictureCallback() {
        public void onPictureTaken(byte[] data, Camera camera) {
            if (isVolumeChanged) {
                // Reset the audio settings which where set before trying to take an picture
                AudioManager audio = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
                audio.setStreamVolume(AudioManager.STREAM_SYSTEM, currentVolume,
                                      AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE);
                // reset flag for volume control
                isVolumeChanged = false;
            }
            new CameraSavePicture().execute(data);
            resetCam();
            LogUtils.d(TAG, "onPictureTaken - jpeg");
        }
    };

    /**
     * The postview callback occurs when a scaled, fully processed postview image is available. <br/> (<b>NOTE:</b> not
     * all hardware supports this)
     */
    private Camera.PictureCallback postViewCallback = new Camera.PictureCallback() {
        public void onPictureTaken(byte[] data, Camera camera) {
            LogUtils.d(TAG, "onPictureTaken - postview");
        }
    };
}

这是片段布局的相关部分:

<RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/black">

    <FrameLayout
            android:id="@+id/full_camera_preview"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            />

    <FrameLayout
            android:id="@+id/small_camera_preview"
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:layout_alignParentTop="true"
            android:layout_alignParentStart="true"
            android:layout_alignParentLeft="true"
            />

</RelativeLayout>

这是Camera片段:

public class CameraFragment extends Fragment implements View.OnClickListener, EventBusInterface {

    private static final String TAG = "Camera Fragment";

    private ImageButton flipCameraButton;
    private Button takePicture;

    private RelativeLayout content;
    private RelativeLayout controlPanel;

    private CameraPreview fullCameraPreview;
    private FrameLayout fullCameraLayout;
    private CameraPreview smallCameraPreview;
    private FrameLayout smallCameraLayout;

    /**
     * The mode of the camera which shoul initially be displayed.
     */
    private CameraMode currentCameraMode = CameraMode.BACK;

    /**
     * Do not use to instantiate a fragment. Use newInstance method instead.
     */
    public CameraFragment() {
    }

    /**
     * Use this method to get a new instance of the fragment, and give eventually add parameters if you need to pass
     * data to it and retrieve it in onCreate using getArguments.
     *
     * @return a new instance of the current fragment.
     */
    public static CameraFragment newInstance() {
        final CameraFragment cameraFragment = new CameraFragment();

        Bundle arguments = new Bundle();

        cameraFragment.setArguments(arguments);
        return cameraFragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Get the argument passed on the custom initialization of the fragment
        Bundle arguments = getArguments();

        if (null != savedInstanceState) {
            // Get the last camera mode set by the user
            CameraMode savedCameraMode = (CameraMode) savedInstanceState.getSerializable("CAMERA_MODE");
            if (null != savedCameraMode) {
                currentCameraMode = savedCameraMode;
            }
        }

        //        setRetainInstance(true); //Will ignore onDestroy Method (Nested Fragments no need this if parent
        // have it)
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        outState.putSerializable("CAMERA_MODE", currentCameraMode);
        super.onSaveInstanceState(outState);
    }

    @Override
    public void onViewStateRestored(Bundle savedInstanceState) {
        super.onViewStateRestored(savedInstanceState);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_timeline_camera, container, false);

        // BUG FIX: To avoid the window background overlapping this fragment we set it's background to transparent so
        // that when the fragment is moved(scrolled) it will still be visible
        this.getActivity().getWindow().setBackgroundDrawable(new ColorDrawable(android.graphics.Color.TRANSPARENT));

        initViews(view);
        setListeners();

        // Setup the camera mode which will initially be displayed.
        setUpCameraMode(currentCameraMode);

        return view;
    }

    @Override
    public void onResume() {
        super.onResume();


    }

    @SuppressWarnings("deprecation")
    private void setUpCameraMode(final CameraMode cameraMode) {
        // The front camera SurfaceView will need to be changed, release it first
        releaseSmallCamera();
        // The full SurfaceView has to be changed because the camera mode will be changed from Back to Front or
        // Front to Back
        releaseFullCamera();

        resizeFullCamera(cameraMode == CameraMode.DUAL);

        switch (cameraMode) {
            case BACK:
                setupFullCamera(CameraInfo.CAMERA_FACING_BACK);
                break;
            case FRONT:
                setupFullCamera(CameraInfo.CAMERA_FACING_FRONT);
                break;
            case DUAL:
                setUpDualCamera();
                break;
        }
    }

    @Override
    public void onStart() {
        super.onStart();
        if (!EventBus.getDefault().isRegistered(this)) {
            EventBus.getDefault().register(this);
        }

        // This is required because we need to resume the camera when we get back in the fragment.
        if (null != getView()) {
            // Setup the camera mode which will initially be displayed.
            setUpCameraMode(currentCameraMode);
        }
    }

    @Override
    public void onStop() {
        EventBus.getDefault().unregister(this);
        super.onStop();
        // The front camera SurfaceView will need to be changed, release it first
        releaseSmallCamera();
        // The full SurfaceView has to be changed because the camera mode will be changed from Back to Front or
        // Front to Back
        releaseFullCamera();
    }

    /**
     * Initialize the view for the current fragment.
     *
     * @param view
     *         The view which was inflated for this fragment.
     */
    private void initViews(View view) {
        controlPanel = (RelativeLayout) view.findViewById(R.id.control_panel);
        fullCameraLayout = (FrameLayout) view.findViewById(R.id.full_camera_preview);
        smallCameraLayout = (FrameLayout) view.findViewById(R.id.small_camera_preview);

        flipCameraButton = (ImageButton) view.findViewById(R.id.flip_camera_button);
        takePicture = (Button) view.findViewById(R.id.camera_take_picture);
    }

    /**
     * Setup the listeners required for this fragment.
     */
    private void setListeners() {
        flipCameraButton.setOnClickListener(this);
        takePicture.setOnClickListener(this);
    }

    /**
     * Sets up the SurfaceView where the Full preview will be display. <br/> The full SurfaceView can show the camera
     * that faces the same direction as the screen and the camera tjat faces the opposite direction as the screen.
     *
     * @param facingOfTheCamera
     *         The facing of the camera that has to be shown in the Full SurfaceView. <br/>Usually {@link
     *         CameraInfo#CAMERA_FACING_BACK} or {@link CameraInfo#CAMERA_FACING_FRONT}.<br/> If an invalid value is
     *         set, the preview for the back camera will be setup.
     */
    @SuppressWarnings("deprecation")
    private void setupFullCamera(int facingOfTheCamera) {
        // Full camera can show the back & front cameras
        CameraMode cameraMode;
        if (facingOfTheCamera == CameraInfo.CAMERA_FACING_BACK) {
            cameraMode = CameraMode.BACK;
        } else if (facingOfTheCamera == CameraInfo.CAMERA_FACING_FRONT) {
            cameraMode = CameraMode.FRONT;
        } else {
            // Default to the back camera
            cameraMode = CameraMode.BACK;
        }

        if (null == fullCameraPreview) {
            fullCameraPreview = new CameraPreview(getActivity(), CameraUtils.getCamera(cameraMode));
        }

        if (fullCameraLayout.getVisibility() != View.VISIBLE) {
            fullCameraLayout.setVisibility(View.VISIBLE);
        }

        if (fullCameraLayout.getChildCount() == 0) {
            // The View for displaying the Camera preview was not added in the layout, we need to add it now.
            fullCameraLayout.addView(fullCameraPreview);
        }
    }

    /**
     * Sets up the SurfaceView where the Small preview will be display. <br/> The small SurfaceView will always show the
     * camera that faces the same direction as the screen.
     */
    private void setupSmallCamera() {
        // Small camera always shows the camera that faces the same as the screen
        if (null == smallCameraPreview) {
            smallCameraPreview = new CameraPreview(getActivity(), CameraUtils.getCamera(CameraMode.FRONT));
        }

        smallCameraPreview.setZOrderOnTop(true);
        //            smallCameraPreview.setZOrderMediaOverlay(true);

        if (smallCameraLayout.getVisibility() != View.VISIBLE) {
            smallCameraLayout.setVisibility(View.VISIBLE);
        }

        if (smallCameraLayout.getChildCount() == 0) {
            // The View for displaying the Camera preview was not added in the layout, we need to add it now.
            smallCameraLayout.addView(smallCameraPreview);
        }
    }

    /**
     * Setup Full SurfaceView preview (display Back camera) and Small SurfaceView preview (display Front camera).
     */
    @SuppressWarnings("deprecation")
    private void setUpDualCamera() {
        // In dual mode camera preview display, the full preview is showing the back camera & the small preview
        // displays the front camera
        setupFullCamera(CameraInfo.CAMERA_FACING_BACK);
        setupSmallCamera();
    }

    private void resizeFullCamera(boolean shrink) {
        // The dual camera doesn't work if two SurfaceViews overlap - use this to resize the full camera so that it
        // is not overlapped by the small camera
        RelativeLayout.LayoutParams p = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                                                                        ViewGroup.LayoutParams.MATCH_PARENT);
        if (shrink) {
            p.addRule(RelativeLayout.BELOW, R.id.small_camera_preview);
        } else {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
                p.removeRule(RelativeLayout.BELOW);
            } else {
                p.addRule(RelativeLayout.BELOW, 0);
            }
        }
        fullCameraLayout.setLayoutParams(p);
    }

    /**
     * Release the Camera Object used for the full camera mode. <br/> Remove the SurfaceView from the layout and
     * invalidate it.
     */
    private void releaseFullCamera() {
        if (null != fullCameraPreview) {
            fullCameraPreview.releaseCamera();
        }

        if (null != fullCameraLayout) {
            // Remove the SurfaceView from the layout
            fullCameraLayout.removeAllViews();
            fullCameraLayout.setVisibility(View.INVISIBLE);
        }

        // Invalidate the SurfaceView
        fullCameraPreview = null;
    }

    /**
     * Release the Camera Object used for the small camera mode. <br/> Remove the SurfaceView from the layout and
     * invalidate it.
     */
    private void releaseSmallCamera() {
        if (null != smallCameraPreview) {
            smallCameraPreview.releaseCamera();
        }

        if (null != smallCameraLayout) {
            // Remove the SurfaceView from the layout
            smallCameraLayout.removeAllViews();
            smallCameraLayout.setVisibility(View.INVISIBLE);
        }

        // Invalidate the SurfaceView
        smallCameraPreview = null;
    }

    /**
     * Flip between the camera modes supported. <br/> The flip is like an infinite carousel.
     */
    private void flipCamera() {
        // Set up the next mode for the camera preview/s display
        setUpCameraMode(currentCameraMode.getNext());

        // Update the current mode of the camera preview/s display
        currentCameraMode = currentCameraMode.getNext();
    }


    @Override
    public void onClick(View v) {
        final int id = v.getId();
        switch (id) {
            case R.id.flip_camera_button:
                // Flip camera to the next mode available.
                flipCamera();
                break;
            case R.id.camera_take_picture:
                takePicture();
                break;
        }
    }

    private void takePicture() {
        switch (currentCameraMode) {
            case BACK:
            case FRONT:
                if (null != fullCameraPreview) {
                    fullCameraPreview.takePicture();
                } else {
                    ToastModel toastModel = new ToastModel(ToastType.GENERAL, "Please wait for camera warming up!");
                    new CustomToast(getActivity()).displayToast(toastModel);
                }
                break;
            case DUAL:
                ToastModel toastModel = new ToastModel(ToastType.GENERAL, "Dual picture not supported yet!");
                new CustomToast(getActivity()).displayToast(toastModel);
                break;
        }
    }

    @Override
    public void onEvent(Object event) {

    }

    @Override
    public void onEventMainThread(Object event) {
        if (event instanceof String) {
            ToastModel toastModel = new ToastModel(ToastType.GENERAL, (String) event);
            new CustomToast(getActivity()).displayToast(toastModel);
        }
    }

    @Override
    public void onEventBackgroundThread(Object event) {

    }

    @Override
    public void onEventAsync(Object event) {

    }
}

这种奇怪的行为可能是什么问题? 目前我并不是想同时展示两个预览,而是逐个展示。

LE 我更新了相机模式之间的翻转逻辑。 当只有前置或后置摄像头显示在完整的SurfaceView中时,一切正常。当我同时使用它们时不会出现问题(不一定同时使用两个Camera对象,但两个SurfaceViews同时可见并且只是从一个变为另一个)。 我注意到日志中存在以下错误:

D/Camera﹕ [seungmin]_open start
D/Camera﹕ [China_security]_before
D/Camera﹕ [China_security]_after
D/Camera﹕ [seungmin]_open End
D/CameraPreview﹕ Error setting camera parameters: getParameters failed (empty parameters)
W/CameraBase﹕ mediaserver's remote binder Camera object died
W/CameraBase﹕ Camera service died!
W/CameraBase﹕ mediaserver's remote binder Camera object died
E/Camera﹕ Error 100
E/Camera﹕ Error 100

LE2: 我更新了我的代码&amp;优化流程。 我还想知道为什么相机会死 - 这是因为第二台相机的SurfaceView放在第一台相机的顶部。一旦SurfaceViews不再重叠,一切都开始正常工作。 我现在的问题是如何解决这个问题?为什么会这样?

PS:LG G2设备上直接支持双击的大部分测试。

1 个答案:

答案 0 :(得分:0)

您可能偶然发现了有关重叠SurfaceViews的Android限制。您可以详细了解此限制here 不要将SurfaceViews视为经典视图,它们更像是一个占位符,用于获取要将控件交给渲染组件的屏幕区域(位置和大小)。渲染实际上是在一个单独的窗口上完成的。

作为一种解决方案,您可以尝试在最适合的SurfaceView上调用setZOrderOnTop(true)。默认情况下,这是false,因此2 SurfaceViews不会启用同样的z - 让他们工作;一个缺点是顶部的那个也将位于您拥有的任何视图的顶部 如果他们最终工作,这并不能保证在所有设备上都能发生,因此我建议对您认为重要的设备进行大量测试。

您可以研究的另一件事是查看是否可以使用Camera Feed并查看是否可以合并2个流并将结果提供给1 SurfaceView