29

Ok so I have a class that extends SurfaceView and overrides

surfaceChanged - just calls startPreview
surfaceCreated - opens camera, edits params *, sets surfaceHolder
surfaceDestroyed - calls stopPreview, release camera

this all work great because when the orientation is Portrait:

from surfaceCreated *

m_camera = Camera.open();
Camera.Parameters p = m_camera.getParameters();

if (getResources().getConfiguration().orientation != 
    Configuration.ORIENTATION_LANDSCAPE)
{
    p.set("orientation", "portrait");

    // CameraApi is a wrapper to check for backwards compatibility  
    if (CameraApi.isSetRotationSupported())
    {
         CameraApi.setRotation(p, 90);
    }
}

However, everytime the orientation changes it calls Camera.open()... which as you may know is quite an expensive operation, causing the transitions to be not so smooth.

When i force the orientation to landscape, the preview is great. Create only gets called once which works because the preview is in landscape the camera is always what the user sees. However, I need a way to set the orientation of the actual picture taken when in portrait. When I force landscape though, the surface never gets recreated and the parameters are never set when the camera is held in portrait.

So how can I do one of the following (exclusively)?

  1. Hold onto m_camera between onDestroy and onCreate when orientation changes so that the transition is smooth

  2. Force landscape and detect orientation changes another way... rotating the final snaped picture if held in portrait.

Also, if I am off base can someone point me in a better direction? Thank you.

Tom Fobear
  • 6,459
  • 5
  • 39
  • 72
  • 3
    +1 I'm also interested in this. Default google camera app performs this beautifully: it doesn't recreated activity, but buttons and last image preview are nicely rotated to match landscape/portrait orientation. Btw, _p.set("orientation", "portrait")_ is, in my understanding, a hidden API usage and is not officially supported, isn't it? – Audrius Mar 01 '11 at 18:27
  • 1
    I don't think it acctually does anything, lol. My preffered method would be to force landscape. The problem is I would need to somehow detect orientation another way because then the cameraActivity would not get recreated. – Tom Fobear Mar 01 '11 at 19:31
  • 1
    Ah I see what you have in mind. So you would force camera activity to a landscape and then depending on the *real* orientation, just rotate a picture, right? [This](http://android-er.blogspot.com/2010/08/orientationeventlistener-detect.html) could help you. It is not a bad idea, I might go and implement it myself (-. – Audrius Mar 02 '11 at 13:33

3 Answers3

59

The way I implemented it:

private Camera mCamera;
private OrientationEventListener mOrientationEventListener;
private int mOrientation =  -1;

private static final int ORIENTATION_PORTRAIT_NORMAL =  1;
private static final int ORIENTATION_PORTRAIT_INVERTED =  2;
private static final int ORIENTATION_LANDSCAPE_NORMAL =  3;
private static final int ORIENTATION_LANDSCAPE_INVERTED =  4;

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // force Landscape layout
    setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR | ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
   /*
   Your other initialization code here
   */
}

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

    if (mOrientationEventListener == null) {            
        mOrientationEventListener = new OrientationEventListener(this, SensorManager.SENSOR_DELAY_NORMAL) {

            @Override
            public void onOrientationChanged(int orientation) {

                // determine our orientation based on sensor response
                int lastOrientation = mOrientation;

                if (orientation >= 315 || orientation < 45) {
                    if (mOrientation != ORIENTATION_PORTRAIT_NORMAL) {                          
                        mOrientation = ORIENTATION_PORTRAIT_NORMAL;
                    }
                }
                else if (orientation < 315 && orientation >= 225) {
                    if (mOrientation != ORIENTATION_LANDSCAPE_NORMAL) {
                        mOrientation = ORIENTATION_LANDSCAPE_NORMAL;
                    }                       
                }
                else if (orientation < 225 && orientation >= 135) {
                    if (mOrientation != ORIENTATION_PORTRAIT_INVERTED) {
                        mOrientation = ORIENTATION_PORTRAIT_INVERTED;
                    }                       
                }
                else { // orientation <135 && orientation > 45
                    if (mOrientation != ORIENTATION_LANDSCAPE_INVERTED) {
                        mOrientation = ORIENTATION_LANDSCAPE_INVERTED;
                    }                       
                }   

                if (lastOrientation != mOrientation) {
                    changeRotation(mOrientation, lastOrientation);
                }
            }
        };
    }
    if (mOrientationEventListener.canDetectOrientation()) {
        mOrientationEventListener.enable();
    }
}

@Override protected void onPause() {
    super.onPause();
    mOrientationEventListener.disable();
}

/**
 * Performs required action to accommodate new orientation
 * @param orientation
 * @param lastOrientation
 */
private void changeRotation(int orientation, int lastOrientation) {
    switch (orientation) {
        case ORIENTATION_PORTRAIT_NORMAL:
            mSnapButton.setImageDrawable(getRotatedImage(android.R.drawable.ic_menu_camera, 270));
            mBackButton.setImageDrawable(getRotatedImage(android.R.drawable.ic_menu_revert, 270));
            Log.v("CameraActivity", "Orientation = 90");
            break;
        case ORIENTATION_LANDSCAPE_NORMAL:
            mSnapButton.setImageResource(android.R.drawable.ic_menu_camera);
            mBackButton.setImageResource(android.R.drawable.ic_menu_revert);
            Log.v("CameraActivity", "Orientation = 0");
            break;
        case ORIENTATION_PORTRAIT_INVERTED:
            mSnapButton.setImageDrawable(getRotatedImage(android.R.drawable.ic_menu_camera, 90));
            mBackButton.setImageDrawable(getRotatedImage(android.R.drawable.ic_menu_revert, 90));
            Log.v("CameraActivity", "Orientation = 270");
            break;
        case ORIENTATION_LANDSCAPE_INVERTED:
            mSnapButton.setImageDrawable(getRotatedImage(android.R.drawable.ic_menu_camera, 180));
            mBackButton.setImageDrawable(getRotatedImage(android.R.drawable.ic_menu_revert, 180));      
            Log.v("CameraActivity", "Orientation = 180");
            break;
    }
}

    /**
 * Rotates given Drawable
 * @param drawableId    Drawable Id to rotate
 * @param degrees       Rotate drawable by Degrees
 * @return              Rotated Drawable
 */
private Drawable getRotatedImage(int drawableId, int degrees) {
    Bitmap original = BitmapFactory.decodeResource(getResources(), drawableId);
    Matrix matrix = new Matrix();
    matrix.postRotate(degrees);

    Bitmap rotated = Bitmap.createBitmap(original, 0, 0, original.getWidth(), original.getHeight(), matrix, true);
    return new BitmapDrawable(rotated);
}

And then in your PictureCallback set metadata to indicate rotation level:

    private Camera.PictureCallback mJpegCallback = new Camera.PictureCallback() {

    @Override
    public void onPictureTaken(byte[] data, Camera camera) {
        try {
            // Populate image metadata

            ContentValues image = new ContentValues();
            // additional picture metadata
            image.put(Media.DISPLAY_NAME, [picture name]);
            image.put(Media.MIME_TYPE, "image/jpg");
            image.put(Media.TITLE, [picture title]);
            image.put(Media.DESCRIPTION, [picture description]);
            image.put(Media.DATE_ADDED, [some time]);
            image.put(Media.DATE_TAKEN, [some time]);
            image.put(Media.DATE_MODIFIED, [some time]);

            // do not rotate image, just put rotation info in
            switch (mOrientation) {
                case ORIENTATION_PORTRAIT_NORMAL:
                    image.put(Media.ORIENTATION, 90);
                    break;
                case ORIENTATION_LANDSCAPE_NORMAL:
                    image.put(Media.ORIENTATION, 0);
                    break;
                case ORIENTATION_PORTRAIT_INVERTED:
                    image.put(Media.ORIENTATION, 270);
                    break;
                case ORIENTATION_LANDSCAPE_INVERTED:
                    image.put(Media.ORIENTATION, 180);
                    break;
            }

            // store the picture
            Uri uri = getContentResolver().insert(
                    Media.EXTERNAL_CONTENT_URI, image);

            try {
                Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0,
                        data.length);
                OutputStream out = getContentResolver().openOutputStream(
                        uri);
                boolean success = bitmap.compress(
                        Bitmap.CompressFormat.JPEG, 75, out);
                out.close();
                if (!success) {
                    finish(); // image output failed without any error,
                                // silently finish
                }

            } catch (Exception e) {
                e.printStackTrace();
                // handle exceptions
            }

            mResultIntent = new Intent();
            mResultIntent.setData(uri);
        } catch (Exception e) {
            e.printStackTrace();
        }

        finish();
    }
};

I hope it helps.

UPDATE Now when landscape based devices are appearing an additional check for it is required in OrientationEventListener.

Display display = ((WindowManager)getSystemService(WINDOW_SERVICE)).getDefaultDisplay();                                        
if (display.getOrientation() == Surface.ROTATION_0) { 
    // landscape oriented devices
} else { 
    // portrait oriented device
}

Full code (a bit wasteful by LC, but easily demonstrates the approach)

@Override
public void onOrientationChanged(int orientation) {

    // determine our orientation based on sensor response
    int lastOrientation = mOrientation;

    Display display = ((WindowManager)getSystemService(WINDOW_SERVICE)).getDefaultDisplay();                                        

    if (display.getOrientation() == Surface.ROTATION_0) {   // landscape oriented devices
        if (orientation >= 315 || orientation < 45) {
            if (mOrientation != ORIENTATION_LANDSCAPE_NORMAL) {                         
                mOrientation = ORIENTATION_LANDSCAPE_NORMAL;
            }
        } else if (orientation < 315 && orientation >= 225) {
            if (mOrientation != ORIENTATION_PORTRAIT_INVERTED) {
                mOrientation = ORIENTATION_PORTRAIT_INVERTED;
            }                       
        } else if (orientation < 225 && orientation >= 135) {
            if (mOrientation != ORIENTATION_LANDSCAPE_INVERTED) {
                mOrientation = ORIENTATION_LANDSCAPE_INVERTED;
            }                       
        } else if (orientation <135 && orientation > 45) { 
            if (mOrientation != ORIENTATION_PORTRAIT_NORMAL) {
                mOrientation = ORIENTATION_PORTRAIT_NORMAL;
            }                       
        }                       
    } else {  // portrait oriented devices
        if (orientation >= 315 || orientation < 45) {
            if (mOrientation != ORIENTATION_PORTRAIT_NORMAL) {                          
                mOrientation = ORIENTATION_PORTRAIT_NORMAL;
            }
        } else if (orientation < 315 && orientation >= 225) {
            if (mOrientation != ORIENTATION_LANDSCAPE_NORMAL) {
                mOrientation = ORIENTATION_LANDSCAPE_NORMAL;
            }                       
        } else if (orientation < 225 && orientation >= 135) {
            if (mOrientation != ORIENTATION_PORTRAIT_INVERTED) {
                mOrientation = ORIENTATION_PORTRAIT_INVERTED;
            }                       
        } else if (orientation <135 && orientation > 45) { 
            if (mOrientation != ORIENTATION_LANDSCAPE_INVERTED) {
                mOrientation = ORIENTATION_LANDSCAPE_INVERTED;
            }                       
        }
    }

    if (lastOrientation != mOrientation) {
        changeRotation(mOrientation, lastOrientation);
    }
}
Audrius
  • 2,796
  • 1
  • 23
  • 35
  • 2
    I ended up using the default camera activity, however this looks good I may come back to this at another time. Thank you. – Tom Fobear Mar 03 '11 at 19:38
  • 3
    You welcome. To be honest, I implemented my own camera activity because of all the inconsistencies with the default camera activity on different android handsets and I wasn't feeling right doing all kind of hacks to go around them. – Audrius Mar 04 '11 at 09:52
  • 1
    @Audrius i created the simple custom camera with buttons but i need to rotate camera in 4 side of device... how to do that? – Gorgeous_DroidVirus Feb 27 '14 at 05:58
  • 1
    DON'T ever use `mOrientationEventListene` which updated more frequently than your app orientation changes... it's better to use `DisplayManager.DisplayListener` – user924 May 02 '18 at 14:52
18

Have you considered using the standard method thats provided in the API doc, which you can call on surfaceChanged? You could store the degrees in a global variable to later use when saving the picture. Also could do a simple null checker on your camera variable, so you don't create it again in surfaceCreated.

public void setCameraDisplayOrientation() 
{        
     if (mCamera == null)
     {
         Log.d(TAG,"setCameraDisplayOrientation - camera null");
         return;             
     }

     Camera.CameraInfo info = new Camera.CameraInfo();
     Camera.getCameraInfo(CAM_ID, info);

     WindowManager winManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
     int rotation = winManager.getDefaultDisplay().getRotation();

     int degrees = 0;

     switch (rotation) 
     {
         case Surface.ROTATION_0: degrees = 0; break;
         case Surface.ROTATION_90: degrees = 90; break;
         case Surface.ROTATION_180: degrees = 180; break;
         case Surface.ROTATION_270: degrees = 270; break;
     }

     int result;
     if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) 
     {
         result = (info.orientation + degrees) % 360;
         result = (360 - result) % 360;  // compensate the mirror
     } else {  // back-facing
         result = (info.orientation - degrees + 360) % 360;
     }
     mCamera.setDisplayOrientation(result);
}
ilanco
  • 8,757
  • 4
  • 29
  • 37
OriginalCliche
  • 381
  • 4
  • 15
3

As you've seen from the other answers, this code gets very complicated. You may want to investigate using a library to help you provide this feature, for example, CWAC-Camera supports OS 2.3 and up (hopefully you can drop OS 2.1 and OS 2.2 support now):
https://github.com/commonsguy/cwac-camera

CWAC-Camera supports locking the camera preview to landscape, and will auto-rotate images into the correction orientation for you. Browse the project issues if you want a taste of all the device specific problems that need to be solved, which IMO are more reasons for trying to use a library instead of maintaining all this code and testing yourself.

Dan J
  • 24,430
  • 17
  • 95
  • 168