2

I'm going crazy with the use of SurfaceView.

I've developed an app which takes pictures at fixed time intervals. It works well with my device with Androd 2.3. Here is a sample code:

public class MainActivity extends Activity 
{
    private Camera mCamera;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button captureButton = (Button) findViewById(R.id.button_capture);
        captureButton.setOnClickListener(
            new View.OnClickListener() {
              @Override
              public void onClick(View v) {
                 takeAPicture();
              }
            }
        );
    }


    private PictureCallback mPicture = new PictureCallback() {
        @Override
        public void onPictureTaken(byte[] data, Camera camera) 
        {
            // ... save image ...

            camera.release();
            camera = null;
        }
    };

    private void takeAPicture()
    {
        mCamera = getCameraInstance();

        mCamera.takePicture(null, null, mPicture);        
    }
}

The same code does not work with Android 4.1. Googling a bit I've found that "most of the modern Android devices now check that a SurfaceView exists and has a non-zero width and height". So I've modified the above code as follows:

public class MainActivity extends Activity 
{
    private Camera mCamera;
    private CameraPreview mPreview;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button captureButton = (Button) findViewById(R.id.button_capture);
        captureButton.setOnClickListener(
            new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                takeAPicture();
                }
            }
        );
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }

    private PictureCallback mPicture = new PictureCallback() {

        @Override
        public void onPictureTaken(byte[] data, Camera camera) 
        {
            // ... save image ...

            camera.release();
            camera = null;
        }
    };

    private void takeAPicture()
    {
        mCamera = getCameraInstance();

        mPreview = new CameraPreview(this, mCamera);
        FrameLayout preview = (FrameLayout) findViewById(R.id.camera_preview);
        preview.addView(mPreview);

        mCamera.takePicture(null, null, mPicture);        
    } 
}

where CameraPreview is:

public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback {
    private SurfaceHolder mHolder;
    private Camera mCamera;

    public CameraPreview(Context context, Camera camera) 
    {
        super(context);

        mCamera = camera;

        // 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) 
    {
        // The Surface has been created, now tell the camera where to draw the preview.
        try {
            mCamera.setPreviewDisplay(holder);
            mCamera.startPreview();
        } catch (IOException e) {
            Log.d(MainActivity.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

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

But the PictureCallback is not invoked and the user interface of the app disappear. The user interface is simply a FrameLayout and a button as follows:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">

  <FrameLayout
    android:id="@+id/camera_preview"
    android:layout_width="100dp"
    android:layout_height="100dp" />

  <Button
    android:id="@+id/button_capture"
    android:text="Capture"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />
</LinearLayout>

What's wrong?

Please note that if I do:

public class MainActivity extends Activity 
{
private Camera mCamera;
private CameraPreview mPreview;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    mCamera = getCameraInstance();

    mPreview = new CameraPreview(this, mCamera);
    FrameLayout preview = (FrameLayout) findViewById(R.id.camera_preview);
    preview.addView(mPreview);

    Button captureButton = (Button) findViewById(R.id.button_capture);
    captureButton.setOnClickListener(
        new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                 mCamera.takePicture(null, null, mPicture);
            }
        }
    );
}

of course removing the mCamera.release() from the PictureCallback, it works perfectly on a device with Android 4.1. Any comment?

user2923045
  • 369
  • 2
  • 6
  • 16

1 Answers1

0

On click, you create an instance of CameraPreview - which is fine, but takes time and some callbacks. You cannot call mCamera.takePicture() until the SurfaceView is ready - this won't take long, but still you need a delay here. The easiest change would be to call mCamera.takePicture() from your CameraPreview.surfaceCreated().

Alex Cohn
  • 56,089
  • 9
  • 113
  • 307
  • Many thanks Alex. You right, In fact, if I call mCamera.takePicture() a couple of seconds later the creation of the SurfaceView it works. – user2923045 Dec 29 '13 at 13:06
  • It should not be a couple of seconds; for many modern devices, the delay would be less than 400 ms. BTW, I would strongly recommend to open the camera (calling your `getCameraInstance()`) in a background (event) thread: this call is quite slow on some devices, so [the official doc](http://developer.android.com/training/camera/cameradirect.html) doesn't recommend to put it into `Activity.onCreate()`; even more so, not into an `OnClickListener.onClick()`. – Alex Cohn Dec 29 '13 at 13:35
  • Sure Alex, it was just an example to show you the issue. Anyway, coming back to your first comment, I don't want to call `mCamera.takePicture()` from `CameraPreview.surfaceCreated()`. Instead, I've passed the `PictureCallback` defined in MainActivity to the CameraPreview constructor: `CameraPreview mPreview = new CameraPreview(this, mCamera, mPicture);`. So, as you suggested, I've called `mCamera.takePicture()` as last instruction of `CameraPreview.surfaceCreated()` as follows: `mCamera.takePicture(null, null, mPicture);`. Unfortunately I've got the same result described in my original post. – user2923045 Dec 29 '13 at 14:05
  • I see you restart preview in `CameraPreview.surfaceChanged()`. This means that if this callback is activated, `takePicture()` is bound to fail. – Alex Cohn Dec 29 '13 at 14:10
  • I've simply copied&pasted the CameraPreview class from developer.android... sorry! Actually, I have a lot of confusion with this class. If you have a couple of minutes, could you please provide a minimal version of this class? My final aim is to have a 1dip x 1dip SurfaceView as I do not want to show the preview. So I think that I can omit to redefine `surfaceChanded`. Is it correct? – user2923045 Dec 29 '13 at 14:38
  • Oops, I must be running right now. A couple of hints: don't ever use 1x1 SurfaceView: it may cause lots of troubles on some devices which fail to handle resize to this size. The best option is to hide the SurfaceView under some button. **B)** No, you don't need to restart preview on `surfaceChanged()`. – Alex Cohn Dec 29 '13 at 16:00
  • Thanks again Alex. Not it works. Last question. I have noticed that surfaceDestroyed in called when the activity goes in background and it is invoked several times (the number of picture taken). I expected that surfaceDestroyed when invoked when the camera is released and the preview is stopped. In fact, the last two instructions in my pictureCallback are `camera.stopPreview()` and `camera.release()` but it seems that surfaceDestroyed is not invoked. Is it a problem in your opinion? – user2923045 Dec 29 '13 at 20:08
  • I've tried to hide the SurfaceView under a button but doing so the app crashes instantaneously. It works only if I declare the FrameLayout as last element in the .xml file. That is, if I put the FrameLayout above the button it works but when the FrameLayout is under the button the app crashes as soon as it starts! – user2923045 Dec 29 '13 at 20:28
  • _surfaceDestroyed in called when the activity goes in background and it is invoked several times (the number of picture taken)_ Oops, you should create a new surface only once for the first picture. – Alex Cohn Dec 30 '13 at 11:01
  • _tried to hide the SurfaceView under a button but doing so the app crashes_ Is there some **logcat** output about that? It will be even easier to setup the surface during Activity start - why not have the camera ready to shoot? – Alex Cohn Dec 30 '13 at 11:05
  • _why not have the camera ready to shoot?_ Because my app runs in background. That is, there is a periodic alarm that, when starts, takes a picture. Now, the app works perfect in my device (Android 2.3) but it has some problem when it runs on an Android 4.1 device. Precisely, pictures are taken when the app is in foreground, but when it goes in background the takePicture method fails. Is it possible that it happens because the SurfacePreview is not visible? – user2923045 Dec 30 '13 at 13:54
  • _Is it possible that it happens because the SurfacePreview is not visible?_ Yes. You are supposed to bring your app to foreground, or at least to open a view. – Alex Cohn Dec 30 '13 at 18:59
  • I've found this [Launch popup window from service](http://stackoverflow.com/questions/7678356/launch-popup-window-from-service) Maybe I can display custom "alert window" with no content but a surfaceview (invisible or somewhere off-screen), using this one as preview display. What do you think? – user2923045 Dec 30 '13 at 20:32
  • Yes, that's one possibility. – Alex Cohn Dec 30 '13 at 22:15