ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 안드로이드 카메라 예제 ( 프리뷰 및 사진찍기 )
    Android/카메라 2019.10.07 20:28



    안드로이드에서 카메라에 접근하여 영상을 가져와 화면에 보여주고 버튼을 클릭하면 이미지를 캡쳐하는 예제입니다.

    포스트에는 android.hardware.camera2와 android.hardware.Camera를 위한 코드 2가지가 포함되어 있습니다. 


    최초작성 2015. 11. 19

    최종작성 2019. 10. 07




    android.hardware.camera2를 사용한 코드를 포스트에  추가하였습니다.

    카메라 프리뷰를 전체 화면으로 하면 가로세로비가 안맞기 때문에 약간 왜곡된 프리뷰가 보입니다. 

    그래서 https://stackoverflow.com/a/43516672 에서 소개하는 방법을 적용했습니다. 


    실행해보면 프리뷰 화면 아래로 꽤 많은 공간이 남아있습니다. 다른 앱도 같은 식으로 하는 듯합니다. 

    안드로이드폰에  디폴트로 설치된 카메라앱을 확인하니 프리뷰가 보이지 안는 공간이 비슷합니다. 


    android.hardware.camera2




    AndroidManifest.xml


    카메라와 저장 공간을 사용하기 위해 필요한 권한과 카메라 사용시 필요한 기능을 추가합니다. 

    두 개의 액티비티를 등록합니다. LaunchActivity와 MainActivity 순서에 주의하세요.

    LaunchActivity에서 카메라와 저장공간 권한이 허용되면 MainActivity가 시작하여 카메라로부터 영상을 가져오게 됩니다. 


    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.tistory.webnautes.androidcameraexample">

        <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
        <uses-permission android:name="android.permission.CAMERA" />

        <uses-feature android:name="android.hardware.camera" />
        <uses-feature android:name="android.hardware.camera.autofocus" />
        <uses-feature android:name="android.hardware.location.gps" />


        <application
            android:allowBackup="true"
            android:icon="@mipmap/ic_launcher"
            android:label="@string/app_name"
            android:roundIcon="@mipmap/ic_launcher_round"
            android:supportsRtl="true"
            android:theme="@style/AppTheme">
            <activity android:name=".LaunchActivity">
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />

                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
            </activity>
            <activity android:name=".MainActivity"></activity>
       </application>

    </manifest>



    styles.xml

    타이틀 바를 안보이도록 합니다. 


    <resources>

        <!-- Base application theme. -->
        <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
            <!-- Customize your theme here. -->
            <item name="colorPrimary">@color/colorPrimary</item>
            <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
            <item name="colorAccent">@color/colorAccent</item>
            <!-- 타이틀 바를 안보이도록 합니다. -->
            <item name="windowActionBar">false</item>
            <item name="windowNoTitle">true</item>
       </style>

    </resources>




    LaunchActivity.java


    카메라와 저장 공간 접근을 위한 퍼미션 처리만 다룹니다. 

    피미션이 허용되면 MainActivity를 실행합니다. 


    package com.tistory.webnautes.androidcameraexample;


    import android.Manifest;
    import android.annotation.TargetApi;
    import android.content.DialogInterface;
    import android.content.Intent;
    import android.content.pm.PackageManager;
    import android.os.Build;

    import androidx.annotation.NonNull;
    import androidx.appcompat.app.AlertDialog;
    import androidx.appcompat.app.AppCompatActivity;
    import androidx.core.content.ContextCompat;

    import android.os.Bundle;


    public class LaunchActivity extends AppCompatActivity {


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

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                //퍼미션 상태 확인
                if (!hasPermissions(PERMISSIONS)) {

                    //퍼미션 허가 안되어있다면 사용자에게 요청
                    requestPermissions(PERMISSIONS, PERMISSIONS_REQUEST_CODE);
                }else{
                    Intent mainIntent = new Intent(LaunchActivity.this, MainActivity.class);
                    startActivity(mainIntent);
                    finish();
                }
            }

        }

        // 여기서부터는 퍼미션 관련 코드입니다.
        static final int PERMISSIONS_REQUEST_CODE = 1000;
        String[] PERMISSIONS  = {Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE};

        private boolean hasPermissions(String[] permissions) {
            int result;

            //스트링 배열에 있는 퍼미션들의 허가 상태 여부 확인
            for (String perms : permissions){

                result = ContextCompat.checkSelfPermission(this, perms);

                if (result == PackageManager.PERMISSION_DENIED){
                    //허가 안된 퍼미션 발견
                    return false;
                }
            }

            //모든 퍼미션이 허가되었음
            return true;
        }



        @Override
        public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
                                              @NonNull int[] grantResults) {
            super.onRequestPermissionsResult(requestCode, permissions, grantResults);

            switch(requestCode){

                case PERMISSIONS_REQUEST_CODE:

                    if (grantResults.length > 0) {
                        boolean cameraPermissionAccepted = grantResults[0]
                                == PackageManager.PERMISSION_GRANTED;
                        boolean diskPermissionAccepted = grantResults[1]
                                == PackageManager.PERMISSION_GRANTED;

                        if (!cameraPermissionAccepted || !diskPermissionAccepted)
                            showDialogForPermission("앱을 실행하려면 퍼미션을 허가하셔야합니다.");
                        else
                        {
                            Intent mainIntent = new Intent(LaunchActivity.this, MainActivity.class);
                            startActivity(mainIntent);
                            finish();
                        }
                    }
                    break;
            }
        }


        @TargetApi(Build.VERSION_CODES.M)
        private void showDialogForPermission(String msg) {

            AlertDialog.Builder builder = new AlertDialog.Builder( LaunchActivity.this);
            builder.setTitle("알림");
            builder.setMessage(msg);
            builder.setCancelable(false);
            builder.setPositiveButton("예", new DialogInterface.OnClickListener() {
                public void onClick(DialogInterface dialog, int id){
                    requestPermissions(PERMISSIONS, PERMISSIONS_REQUEST_CODE);
                }
            });
            builder.setNegativeButton("아니오", new DialogInterface.OnClickListener() {
                public void onClick(DialogInterface arg0, int arg1) {
                    finish();
                }
            });
            builder.create().show();
        }

    }




    activity_launch.xml

    LaunchActivity에서 퍼미션 허용을 처리하는 중에 화면에 보여지는 레이아웃입니다. 

    현재는 흰화면으로 보여지게 됩니다. 


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

    </LinearLayout>




    MainActivity.java

    카메라 영상을 다루는 액티비티입니다. 


    /*
    원본 코드
    https://github.com/SkyeBeFreeman/SkyeCamera
    https://github.com/googlearchive/android-Camera2Basic/blob/master/Application/src/main/java/com/example/android/camera2basic/Camera2BasicFragment.java

    수정
    webnautes
    */

    package com.tistory.webnautes.androidcameraexample;


    import android.annotation.TargetApi;
    import android.content.ContentResolver;
    import android.content.ContentValues;
    import android.content.Context;
    import android.content.pm.PackageManager;
    import android.graphics.Bitmap;
    import android.graphics.BitmapFactory;
    import android.graphics.ImageFormat;
    import android.graphics.Matrix;
    import android.hardware.Sensor;
    import android.hardware.SensorManager;
    import android.hardware.camera2.CameraAccessException;
    import android.hardware.camera2.CameraCaptureSession;
    import android.hardware.camera2.CameraCharacteristics;
    import android.hardware.camera2.CameraDevice;
    import android.hardware.camera2.CameraManager;
    import android.hardware.camera2.CameraMetadata;
    import android.hardware.camera2.CaptureFailure;
    import android.hardware.camera2.CaptureRequest;
    import android.hardware.camera2.CaptureResult;
    import android.hardware.camera2.TotalCaptureResult;
    import android.hardware.camera2.params.StreamConfigurationMap;
    import android.media.ExifInterface;
    import android.media.Image;
    import android.media.ImageReader;
    import android.net.Uri;
    import android.os.AsyncTask;
    import android.os.Bundle;
    import android.os.Handler;
    import android.os.HandlerThread;
    import androidx.annotation.NonNull;
    import androidx.core.app.ActivityCompat;
    import androidx.appcompat.app.AppCompatActivity;

    import android.provider.MediaStore;
    import android.util.DisplayMetrics;
    import android.util.Log;
    import android.util.Size;
    import android.util.SparseIntArray;
    import android.view.SurfaceHolder;
    import android.view.SurfaceView;
    import android.view.View;
    import android.view.WindowManager;
    import android.widget.FrameLayout;
    import android.widget.ImageButton;
    import android.widget.Toast;


    import java.io.OutputStream;
    import java.nio.ByteBuffer;
    import java.util.Arrays;


    public class MainActivity extends AppCompatActivity{

        private SurfaceView mSurfaceView;
        private SurfaceHolder mSurfaceViewHolder;
        private Handler mHandler;
        private ImageReader mImageReader;
        private CameraDevice mCameraDevice;
        private CaptureRequest.Builder mPreviewBuilder;
        private CameraCaptureSession mSession;
        private int mDeviceRotation;
        private Sensor mAccelerometer;
        private Sensor mMagnetometer;
        private SensorManager mSensorManager;
        private DeviceOrientation deviceOrientation;
        int mDSI_height, mDSI_width;

        private static final SparseIntArray ORIENTATIONS = new SparseIntArray();
        static {
            ORIENTATIONS.append(ExifInterface.ORIENTATION_NORMAL, 0);
            ORIENTATIONS.append(ExifInterface.ORIENTATION_ROTATE_90, 90);
            ORIENTATIONS.append(ExifInterface.ORIENTATION_ROTATE_180, 180);
            ORIENTATIONS.append(ExifInterface.ORIENTATION_ROTATE_270, 270);
        }




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

            // 상태바를 안보이도록 합니다.
            getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
                    WindowManager.LayoutParams.FLAG_FULLSCREEN);

            // 화면 켜진 상태를 유지합니다.
            getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,
                    WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

            setContentView(R.layout.activity_main);


            ImageButton button = findViewById(R.id.take_photo);
            button.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    takePicture();
                }
            });

            mSurfaceView = findViewById(R.id.surfaceView);
            mSensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
            mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
            mMagnetometer = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
            deviceOrientation = new DeviceOrientation();

            initSurfaceView();



        }

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

            mSensorManager.registerListener(deviceOrientation.getEventListener(), mAccelerometer, SensorManager.SENSOR_DELAY_UI);
            mSensorManager.registerListener(deviceOrientation.getEventListener(), mMagnetometer, SensorManager.SENSOR_DELAY_UI);
        }

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

            mSensorManager.unregisterListener(deviceOrientation.getEventListener());
        }

        public void initSurfaceView() {

            DisplayMetrics displayMetrics = new DisplayMetrics();
            getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
            mDSI_height = displayMetrics.heightPixels;
            mDSI_width = displayMetrics.widthPixels;


            mSurfaceViewHolder = mSurfaceView.getHolder();
            mSurfaceViewHolder.addCallback(new SurfaceHolder.Callback() {

                @Override
                public void surfaceCreated(SurfaceHolder holder) {
                    initCameraAndPreview();
                }

                @Override
                public void surfaceDestroyed(SurfaceHolder holder) {

                    if (mCameraDevice != null) {
                        mCameraDevice.close();
                        mCameraDevice = null;
                    }
                }

                @Override
                public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

                }


            });
        }


        @TargetApi(19)
        public void initCameraAndPreview() {
            HandlerThread handlerThread = new HandlerThread("CAMERA2");
            handlerThread.start();
            mHandler = new Handler(handlerThread.getLooper());
            Handler mainHandler = new Handler(getMainLooper());
            try {
                String mCameraId = "" + CameraCharacteristics.LENS_FACING_FRONT; // 후면 카메라 사용

                CameraManager mCameraManager = (CameraManager) this.getSystemService(Context.CAMERA_SERVICE);
                CameraCharacteristics characteristics = mCameraManager.getCameraCharacteristics(mCameraId);
                StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);

                Size largestPreviewSize = map.getOutputSizes(ImageFormat.JPEG)[0];
                Log.i("LargestSize", largestPreviewSize.getWidth() + " " + largestPreviewSize.getHeight());

                setAspectRatioTextureView(largestPreviewSize.getHeight(),largestPreviewSize.getWidth());

                mImageReader = ImageReader.newInstance(largestPreviewSize.getWidth(), largestPreviewSize.getHeight(), ImageFormat.JPEG,/*maxImages*/7);
                mImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mainHandler);
                if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
                    return;
                }
                mCameraManager.openCamera(mCameraId, deviceStateCallback, mHandler);
            } catch (CameraAccessException e) {
                Toast.makeText(this, "카메라를 열지 못했습니다.", Toast.LENGTH_SHORT).show();
            }
        }


        private ImageReader.OnImageAvailableListener mOnImageAvailableListener = new ImageReader.OnImageAvailableListener() {
            @Override
            public void onImageAvailable(ImageReader reader) {

                Image image = reader.acquireNextImage();
                ByteBuffer buffer = image.getPlanes()[0].getBuffer();
                byte[] bytes = new byte[buffer.remaining()];
                buffer.get(bytes);
                final Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
                new SaveImageTask().execute(bitmap);
            }
        };


        private CameraDevice.StateCallback deviceStateCallback = new CameraDevice.StateCallback() {
            @Override
            public void onOpened(CameraDevice camera) {
                mCameraDevice = camera;
                try {
                    takePreview();
                } catch (CameraAccessException e) {
                    e.printStackTrace();
                }
            }

            @Override
            public void onDisconnected(@NonNull CameraDevice camera) {
                if (mCameraDevice != null) {
                    mCameraDevice.close();
                    mCameraDevice = null;
                }
            }

            @Override
            public void onError(CameraDevice camera, int error) {
                Toast.makeText(MainActivity.this, "카메라를 열지 못했습니다.", Toast.LENGTH_SHORT).show();
            }
        };


        public void takePreview() throws CameraAccessException {
            mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
            mPreviewBuilder.addTarget(mSurfaceViewHolder.getSurface());
            mCameraDevice.createCaptureSession(Arrays.asList(mSurfaceViewHolder.getSurface(), mImageReader.getSurface()), mSessionPreviewStateCallback, mHandler);
        }

        private CameraCaptureSession.StateCallback mSessionPreviewStateCallback = new CameraCaptureSession.StateCallback() {
            @Override
            public void onConfigured(@NonNull CameraCaptureSession session) {
                mSession = session;

                try {

                    mPreviewBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
                    mPreviewBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
                    mSession.setRepeatingRequest(mPreviewBuilder.build(), null, mHandler);
                } catch (CameraAccessException e) {
                    e.printStackTrace();
                }
            }

            @Override
            public void onConfigureFailed(@NonNull CameraCaptureSession session) {
                Toast.makeText(MainActivity.this, "카메라 구성 실패", Toast.LENGTH_SHORT).show();
            }
        };

        private CameraCaptureSession.CaptureCallback mSessionCaptureCallback = new CameraCaptureSession.CaptureCallback() {
            @Override
            public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) {
                mSession = session;
                unlockFocus();
            }

            @Override
            public void onCaptureProgressed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureResult partialResult) {
                mSession = session;
            }

            @Override
            public void onCaptureFailed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureFailure failure) {
                super.onCaptureFailed(session, request, failure);
            }
        };



        public void takePicture() {

            try {
                CaptureRequest.Builder captureRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);//用来设置拍照请求的request
                captureRequestBuilder.addTarget(mImageReader.getSurface());
                captureRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
                captureRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);


                // 화면 회전 안되게 고정시켜 놓은 상태에서는 아래 로직으로 방향을 얻을 수 없어서
                // 센서를 사용하는 것으로 변경
                //deviceRotation = getResources().getConfiguration().orientation;
                mDeviceRotation = ORIENTATIONS.get(deviceOrientation.getOrientation());
                Log.d("@@@", mDeviceRotation+"");

                captureRequestBuilder.set(CaptureRequest.JPEG_ORIENTATION, mDeviceRotation);
                CaptureRequest mCaptureRequest = captureRequestBuilder.build();
                mSession.capture(mCaptureRequest, mSessionCaptureCallback, mHandler);
            } catch (CameraAccessException e) {
                e.printStackTrace();
            }
        }

        public Bitmap getRotatedBitmap(Bitmap bitmap, int degrees) throws Exception {
            if(bitmap == null) return null;
            if (degrees == 0) return bitmap;

            Matrix m = new Matrix();
            m.setRotate(degrees, (float) bitmap.getWidth() / 2, (float) bitmap.getHeight() / 2);

            return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), m, true);
        }



        /**
        * Unlock the focus. This method should be called when still image capture sequence is
        * finished.
        */
        private void unlockFocus() {
            try {
                // Reset the auto-focus trigger
                mPreviewBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER,
                        CameraMetadata.CONTROL_AF_TRIGGER_CANCEL);
                mPreviewBuilder.set(CaptureRequest.CONTROL_AE_MODE,
                        CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
                mSession.capture(mPreviewBuilder.build(), mSessionCaptureCallback,
                        mHandler);
                // After this, the camera will go back to the normal state of preview.
                mSession.setRepeatingRequest(mPreviewBuilder.build(), mSessionCaptureCallback,
                        mHandler);
            } catch (CameraAccessException e) {
                e.printStackTrace();
            }
        }


        //출처 - https://codeday.me/ko/qa/20190310/39556.html
        /**
        * A copy of the Android internals  insertImage method, this method populates the
        * meta data with DATE_ADDED and DATE_TAKEN. This fixes a common problem where media
        * that is inserted manually gets saved at the end of the gallery (because date is not populated).
        * @see android.provider.MediaStore.Images.Media#insertImage(ContentResolver, Bitmap, String, String)
        */
        public static final String insertImage(ContentResolver cr,
                                              Bitmap source,
                                              String title,
                                              String description) {

            ContentValues values = new ContentValues();
            values.put(MediaStore.Images.Media.TITLE, title);
            values.put(MediaStore.Images.Media.DISPLAY_NAME, title);
            values.put(MediaStore.Images.Media.DESCRIPTION, description);
            values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
            // Add the date meta data to ensure the image is added at the front of the gallery
            values.put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis());
            values.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis());

            Uri url = null;
            String stringUrl = null;    /* value to be returned */

            try {
                url = cr.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);

                if (source != null) {
                    OutputStream imageOut = cr.openOutputStream(url);
                    try {
                        source.compress(Bitmap.CompressFormat.JPEG, 50, imageOut);
                    } finally {
                        imageOut.close();
                    }

                } else {
                    cr.delete(url, null, null);
                    url = null;
                }
            } catch (Exception e) {
                if (url != null) {
                    cr.delete(url, null, null);
                    url = null;
                }
            }

            if (url != null) {
                stringUrl = url.toString();
            }

            return stringUrl;
        }


        private class SaveImageTask extends AsyncTask<Bitmap, Void, Void> {

            @Override
            protected void onPostExecute(Void aVoid) {
                super.onPostExecute(aVoid);

                Toast.makeText(MainActivity.this, "사진을 저장하였습니다.", Toast.LENGTH_SHORT).show();
            }

            @Override
            protected Void doInBackground(Bitmap... data) {

                Bitmap bitmap = null;
                try {
                    bitmap = getRotatedBitmap(data[0], mDeviceRotation);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                insertImage(getContentResolver(), bitmap, ""+System.currentTimeMillis(), "");

                return null;
            }

        }


        // 출처 https://stackoverflow.com/a/43516672
        private void setAspectRatioTextureView(int ResolutionWidth , int ResolutionHeight )
        {
            if(ResolutionWidth > ResolutionHeight){
                int newWidth = mDSI_width;
                int newHeight = ((mDSI_width * ResolutionWidth)/ResolutionHeight);
                updateTextureViewSize(newWidth,newHeight);

            }else {
                int newWidth = mDSI_width;
                int newHeight = ((mDSI_width * ResolutionHeight)/ResolutionWidth);
                updateTextureViewSize(newWidth,newHeight);
            }

        }

        private void updateTextureViewSize(int viewWidth, int viewHeight) {
            Log.d("@@@", "TextureView Width : " + viewWidth + " TextureView Height : " + viewHeight);
            mSurfaceView.setLayoutParams(new FrameLayout.LayoutParams(viewWidth, viewHeight));
        }


    }




    activity_main.xml

    카메라 프리뷰와 사진찍는 버튼을 보여주는 레이아웃입니다. 


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

        <SurfaceView
            android:id="@+id/surfaceView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            />

        <ImageButton
            android:id="@+id/take_photo"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal|bottom"
            android:src="@android:drawable/ic_menu_camera"
            />

    </FrameLayout>




    DeviceOrientation.java

    package com.tistory.webnautes.androidcameraexample;


    import android.hardware.Sensor;
    import android.hardware.SensorEvent;
    import android.hardware.SensorEventListener;
    import android.hardware.SensorManager;
    import android.media.ExifInterface;

    /**
    *  https://gist.github.com/abdelhady/501f6e48c1f3e32b253a#file-deviceorientation
    * Created by abdelhady on 9/23/14.
    *
    * to use this class do the following 3 steps in your activity:
    *
    * define 3 sensors as member variables
    Sensor accelerometer;
    Sensor magnetometer;
    Sensor vectorSensor;
    DeviceOrientation deviceOrientation;
    *
    * add this to the activity's onCreate
    mSensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
    accelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
    magnetometer = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
    deviceOrientation = new DeviceOrientation();
    *
    * add this to onResume
    mSensorManager.registerListener(deviceOrientation.getEventListener(), accelerometer, SensorManager.SENSOR_DELAY_UI);
    mSensorManager.registerListener(deviceOrientation.getEventListener(), magnetometer, SensorManager.SENSOR_DELAY_UI);
    *
    * add this to onPause
    mSensorManager.unregisterListener(deviceOrientation.getEventListener());
    *
    *
    * then, you can simply call * deviceOrientation.getOrientation() * wherever you want
    *
    *
    * another alternative to this class's approach:
    * http://stackoverflow.com/questions/11175599/how-to-measure-the-tilt-of-the-phone-in-xy-plane-using-accelerometer-in-android/15149421#15149421
    *
    */
    public class DeviceOrientation {
        private final int ORIENTATION_PORTRAIT = ExifInterface.ORIENTATION_ROTATE_90; // 6
        private final int ORIENTATION_LANDSCAPE_REVERSE = ExifInterface.ORIENTATION_ROTATE_180; // 3
        private final int ORIENTATION_LANDSCAPE = ExifInterface.ORIENTATION_NORMAL; // 1
        private final int ORIENTATION_PORTRAIT_REVERSE = ExifInterface.ORIENTATION_ROTATE_270; // 8

        int smoothness = 1;
        private float averagePitch = 0;
        private float averageRoll = 0;
        private int orientation = ORIENTATION_PORTRAIT;

        private float[] pitches;
        private float[] rolls;

        public DeviceOrientation() {
            pitches = new float[smoothness];
            rolls = new float[smoothness];
        }

        public SensorEventListener getEventListener() {
            return sensorEventListener;
        }

        public int getOrientation() {
            return orientation;
        }



        SensorEventListener sensorEventListener = new SensorEventListener() {
            float[] mGravity;
            float[] mGeomagnetic;

            @Override
            public void onSensorChanged(SensorEvent event) {
                if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER)
                    mGravity = event.values;
                if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD)
                    mGeomagnetic = event.values;
                if (mGravity != null && mGeomagnetic != null) {
                    float R[] = new float[9];
                    float I[] = new float[9];
                    boolean success = SensorManager.getRotationMatrix(R, I, mGravity, mGeomagnetic);
                    if (success) {
                        float orientationData[] = new float[3];
                        SensorManager.getOrientation(R, orientationData);
                        averagePitch = addValue(orientationData[1], pitches);
                        averageRoll = addValue(orientationData[2], rolls);
                        orientation = calculateOrientation();
                    }
                }
            }

            @Override
            public void onAccuracyChanged(Sensor sensor, int accuracy) {
                // TODO Auto-generated method stub

            }
        };

        private float addValue(float value, float[] values) {
            value = (float) Math.round((Math.toDegrees(value)));
            float average = 0;
            for (int i = 1; i < smoothness; i++) {
                values[i - 1] = values[i];
                average += values[i];
            }
            values[smoothness - 1] = value;
            average = (average + value) / smoothness;
            return average;
        }

        private int calculateOrientation() {
            // finding local orientation dip
            if (((orientation == ORIENTATION_PORTRAIT || orientation == ORIENTATION_PORTRAIT_REVERSE)
                    && (averageRoll > -30 && averageRoll < 30))) {
                if (averagePitch > 0)
                    return ORIENTATION_PORTRAIT_REVERSE;
                else
                    return ORIENTATION_PORTRAIT;
            } else {
                // divides between all orientations
                if (Math.abs(averagePitch) >= 30) {
                    if (averagePitch > 0)
                        return ORIENTATION_PORTRAIT_REVERSE;
                    else
                        return ORIENTATION_PORTRAIT;
                } else {
                    if (averageRoll > 0) {
                        return ORIENTATION_LANDSCAPE_REVERSE;
                    } else {
                        return ORIENTATION_LANDSCAPE;
                    }
                }
            }
        }
    }




    android.hardware.Camera


    아래 코드를 수정했으며 카메라 프리뷰와 카메라 캡처 후 저장시킨 후 갤러리에서 해당 이미지를 찾을 수 있도록 refresh해주는 기능이 구현되어있습니다.

    https://github.com/josnidhin/Android-Camera-Example 




    실행결과입니다. Android 6.0(API 23) 이상 에서 실행시키면 런타임에 퍼미션을 허용할지 물어보는 메시지 박스가 보입니다. 허용을 해주어야 앱이 제대로 동작합니다. 



     



    카메라 영상이 들어오기 시작하면 상단에 있는 캡처 버튼을 눌러 저장을 합니다. 

    안드로이드 디바이스를  회전시킬 경우, 그 방향에 맞게 이미지가 저장되도록 구현되어 있는데.. 회전하는 순간에 약간의 딜레이가 발생하는 상태입니다.


     




    전체 소스코드입니다. 

    buidle.gradle

    테스트에 사용한 compileSdkVersion과 TargetSdkVersion은 27이고, minSdkVersion은 15입니다.

    Snackbar를 사용하기 위해 com.android.support:design를 추가해야 합니다. 


    apply plugin: 'com.android.application'

    android {
        compileSdkVersion 27
        buildToolsVersion '27.0.3'
        defaultConfig {
            applicationId "com.tistory.webnautes.androidcameraexample"
            minSdkVersion 15
            targetSdkVersion 27
            versionCode 1
            versionName "1.0"
            testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        }
        buildTypes {
            release {
                minifyEnabled false
                proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            }
        }
    }


    dependencies {
        implementation fileTree(dir: 'libs', include: ['*.jar'])
        implementation 'com.android.support:appcompat-v7:27.1.1'
        implementation 'com.android.support.constraint:constraint-layout:1.1.2'
        testImplementation 'junit:junit:4.12'
        androidTestImplementation 'com.android.support.test:runner:1.0.2'
        androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
        implementation 'com.android.support:design:27.1.1'
    }




    AndroidManifest.xml


    카메라 관련 퍼미션과 이미지 저장을 위한 퍼미션을 추가해줍니다. 

    SDK API 22 이하에서는 아래처럼 추가만 해주면 되지만 API 23이상에서는 자바 코드에도 런타임 퍼미션 관련 코드가 추가로 필요합니다. 


    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
              package="com.tistory.webnautes.androidcameraexample">


        <!-- 앱에서 필요한 모든 퍼미션은 매니페스트 파일에 선언해야 합니다.
        안드로이드 6.0 이상에서는 실행 중에 퍼미션을 요청합니다.           -->
        <uses-permission android:name="android.permission.CAMERA" />
        <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />


        <application
            android:allowBackup="true"
            android:icon="@mipmap/ic_launcher"
            android:label="@string/app_name"
            android:supportsRtl="true"
            android:theme="@style/AppTheme">
            <activity android:name=".MainActivity">
                <intent-filter>
                    <action android:name="android.intent.action.MAIN"/>

                    <category android:name="android.intent.category.LAUNCHER"/>
                </intent-filter>
            </activity>
        </application>

    </manifest>




    styles.xml


    AppCompat에서 타이틀 바을 없애기 위해서는 res\values\styles.xml파일에 다음 노란색 코드를 추가해야 합니다. ( 참고 http://commin.tistory.com/63  )


    <resources>

        <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
            <!-- Customize your theme here. -->
            <item name="colorPrimary">@color/colorPrimary</item>
            <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
            <item name="colorAccent">@color/colorAccent</item>
            <!-- 타이틀 바를 안보이도록 합니다. -->
            <item name="windowActionBar">false</item>
            <item name="windowNoTitle">true</item>

        </style>

    </resources>



    그리고 상단에 있는 상태바를 없애기 위해 MainActivity의 onCreate() 메소드에도 코드가 필요한데 포스팅에 있는 코드에 이미 포함되어 있습니다.


            getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
                    WindowManager.LayoutParams.FLAG_FULLSCREEN);
           setContentView(R.layout.activity_main);




    activity_main.xml

    SufaceView에 카메라 영상이 출력되게 됩니다.

    FrameLayout을 사용했기 때문에 카메라 영상 위에 캡처 버튼이 올라갑니다. 


    <?xml version="1.0" encoding="utf-8"?>
    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:id="@+id/layout_main"
                android:layout_width="match_parent"
                android:layout_height="match_parent" >

        <SurfaceView
            android:id="@+id/camera_preview_main"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>

        <Button
            android:id="@+id/button_main_capture"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:text="캡처" />

    </FrameLayout>




    MainActivity.java

    /*
      원본 

       https://github.com/josnidhin/Android-Camera-Example


       수정

       webnautes
    */

    package com.tistory.webnautes.androidcameraexample;

    import android.Manifest;
    import android.content.pm.PackageManager;
    import android.hardware.Camera;
    import android.os.Bundle;
    import android.support.annotation.NonNull;
    import android.support.design.widget.Snackbar;
    import android.support.v4.app.ActivityCompat;
    import android.support.v4.content.ContextCompat;
    import android.support.v7.app.AppCompatActivity;
    import android.view.SurfaceView;
    import android.view.View;
    import android.view.WindowManager;
    import android.widget.Button;


    public class MainActivity extends AppCompatActivity
            implements ActivityCompat.OnRequestPermissionsResultCallback {

        private static final String TAG = "android_camera_example";
        private static final int PERMISSIONS_REQUEST_CODE = 100;
        String[] REQUIRED_PERMISSIONS  = {Manifest.permission.CAMERA,
                Manifest.permission.WRITE_EXTERNAL_STORAGE};
        private static final int CAMERA_FACING = Camera.CameraInfo.CAMERA_FACING_BACK; // Camera.CameraInfo.CAMERA_FACING_FRONT

        private SurfaceView surfaceView;
        private CameraPreview mCameraPreview;
        private View mLayout;  // Snackbar 사용하기 위해서는 View가 필요합니다.
                                // (참고로 Toast에서는 Context가 필요했습니다.)


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

            // 상태바를 안보이도록 합니다.
            getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
                    WindowManager.LayoutParams.FLAG_FULLSCREEN);

            // 화면 켜진 상태를 유지합니다.
            getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,
                    WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

            setContentView(R.layout.activity_main);

            mLayout = findViewById(R.id.layout_main);
            surfaceView = findViewById(R.id.camera_preview_main);


            // 런타임 퍼미션 완료될때 까지 화면에서 보이지 않게 해야합니다.
            surfaceView.setVisibility(View.GONE);

            Button button = findViewById(R.id.button_main_capture);
            button.setOnClickListener(new View.OnClickListener() {

                @Override
                public void onClick(View v) {
                    mCameraPreview.takePicture();
                }
            });



            if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA)) {

                int cameraPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA);
                int writeExternalStoragePermission = ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE);


                if ( cameraPermission == PackageManager.PERMISSION_GRANTED
                        && writeExternalStoragePermission == PackageManager.PERMISSION_GRANTED) {
                    startCamera();


                }else {
                    if (ActivityCompat.shouldShowRequestPermissionRationale(this, REQUIRED_PERMISSIONS[0])
                            || ActivityCompat.shouldShowRequestPermissionRationale(this, REQUIRED_PERMISSIONS[1])) {

                        Snackbar.make(mLayout, "이 앱을 실행하려면 카메라와 외부 저장소 접근 권한이 필요합니다.",
                                Snackbar.LENGTH_INDEFINITE).setAction("확인", new View.OnClickListener() {

                            @Override
                            public void onClick(View view) {

                                ActivityCompat.requestPermissions( MainActivity.this, REQUIRED_PERMISSIONS,
                                        PERMISSIONS_REQUEST_CODE);
                            }
                        }).show();


                    } else {
                        // 2. 사용자가 퍼미션 거부를 한 적이 없는 경우에는 퍼미션 요청을 바로 합니다.
                        // 요청 결과는 onRequestPermissionResult에서 수신됩니다.
                        ActivityCompat.requestPermissions( this, REQUIRED_PERMISSIONS,
                                PERMISSIONS_REQUEST_CODE);
                    }

                }

            } else {

                final Snackbar snackbar = Snackbar.make(mLayout, "디바이스가 카메라를 지원하지 않습니다.",
                        Snackbar.LENGTH_INDEFINITE);
                snackbar.setAction("확인", new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        snackbar.dismiss();
                    }
                });
                snackbar.show();
            }


        }



        void startCamera(){

            // Create the Preview view and set it as the content of this Activity.
            mCameraPreview = new CameraPreview(this, this, CAMERA_FACING, surfaceView);

        }



        @Override
        public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grandResults) {

            if ( requestCode == PERMISSIONS_REQUEST_CODE && grandResults.length == REQUIRED_PERMISSIONS.length) {

                boolean check_result = true;

                for (int result : grandResults) {
                    if (result != PackageManager.PERMISSION_GRANTED) {
                        check_result = false;
                        break;
                    }
                }

                if ( check_result ) {

                    startCamera();
                }
                else {

                    if (ActivityCompat.shouldShowRequestPermissionRationale(this, REQUIRED_PERMISSIONS[0])
                            || ActivityCompat.shouldShowRequestPermissionRationale(this, REQUIRED_PERMISSIONS[1])) {

                        Snackbar.make(mLayout, "퍼미션이 거부되었습니다. 앱을 다시 실행하여 퍼미션을 허용해주세요. ",
                                Snackbar.LENGTH_INDEFINITE).setAction("확인", new View.OnClickListener() {

                            @Override
                            public void onClick(View view) {

                                finish();
                            }
                        }).show();

                    }else {

                        Snackbar.make(mLayout, "설정(앱 정보)에서 퍼미션을 허용해야 합니다. ",
                                Snackbar.LENGTH_INDEFINITE).setAction("확인", new View.OnClickListener() {

                            @Override
                            public void onClick(View view) {

                                finish();
                            }
                        }).show();
                    }
                }

            }


        }


    }




    CameraPreview.java


    package com.tistory.webnautes.androidcameraexample;

    import java.io.ByteArrayOutputStream;
    import java.io.File;
    import java.io.FileNotFoundException;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.util.List;


    import android.content.Context;
    import android.content.Intent;
    import android.graphics.Bitmap;
    import android.graphics.BitmapFactory;
    import android.graphics.Matrix;
    import android.hardware.Camera;
    import android.hardware.Camera.Size;
    import android.net.Uri;
    import android.os.AsyncTask;
    import android.os.Environment;
    import android.support.v7.app.AppCompatActivity;
    import android.util.Log;
    import android.view.Surface;
    import android.view.SurfaceHolder;
    import android.view.SurfaceView;
    import android.view.View;
    import android.view.ViewGroup;



    // 카메라에서 가져온 영상을 보여주는 카메라 프리뷰 클래스
    class CameraPreview extends ViewGroup implements SurfaceHolder.Callback {

        private final String TAG = "CameraPreview";

        private int mCameraID;
        private SurfaceView mSurfaceView;
        private SurfaceHolder mHolder;
        private Camera mCamera;
        private Camera.CameraInfo mCameraInfo;
        private int mDisplayOrientation;
        private List<Size> mSupportedPreviewSizes;
        private Size mPreviewSize;
        private boolean isPreview = false;

        private AppCompatActivity mActivity;


        public CameraPreview(Context context, AppCompatActivity activity, int cameraID, SurfaceView surfaceView) {
            super(context);


            Log.d("@@@", "Preview");



            mActivity = activity;
            mCameraID = cameraID;
            mSurfaceView = surfaceView;


            mSurfaceView.setVisibility(View.VISIBLE);


            // SurfaceHolder.Callback를 등록하여 surface의 생성 및 해제 시점을 감지
            mHolder = mSurfaceView.getHolder();
            mHolder.addCallback(this);

        }





        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            // We purposely disregard child measurements because act as a
            // wrapper to a SurfaceView that centers the camera preview instead
            // of stretching it.
            final int width = resolveSize(getSuggestedMinimumWidth(), widthMeasureSpec);
            final int height = resolveSize(getSuggestedMinimumHeight(), heightMeasureSpec);
            setMeasuredDimension(width, height);

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



        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            if (changed && getChildCount() > 0) {
                final View child = getChildAt(0);

                final int width = r - l;
                final int height = b - t;

                int previewWidth = width;
                int previewHeight = height;
                if (mPreviewSize != null) {
                    previewWidth = mPreviewSize.width;
                    previewHeight = mPreviewSize.height;
                }

                // Center the child SurfaceView within the parent.
                if (width * previewHeight > height * previewWidth) {
                    final int scaledChildWidth = previewWidth *