안드로이드에서 카메라에 접근하여 영상을 가져와 화면에 보여주고 버튼을 클릭하면 이미지를 캡쳐하는 예제입니다.
포스트에는 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 * height / previewHeight; child.layout((width - scaledChildWidth) / 2, 0, (width + scaledChildWidth) / 2, height); } else { final int scaledChildHeight = previewHeight * width / previewWidth; child.layout(0, (height - scaledChildHeight) / 2, width, (height + scaledChildHeight) / 2); } } } // Surface가 생성되었을 때 어디에 화면에 프리뷰를 출력할지 알려줘야 한다. public void surfaceCreated(SurfaceHolder holder) { // Open an instance of the camera try { mCamera = Camera.open(mCameraID); // attempt to get a Camera instance } catch (Exception e) { // Camera is not available (in use or does not exist) Log.e(TAG, "Camera " + mCameraID + " is not available: " + e.getMessage()); } // retrieve camera's info. Camera.CameraInfo cameraInfo = new Camera.CameraInfo(); Camera.getCameraInfo(mCameraID, cameraInfo); mCameraInfo = cameraInfo; mDisplayOrientation = mActivity.getWindowManager().getDefaultDisplay().getRotation(); int orientation = calculatePreviewOrientation(mCameraInfo, mDisplayOrientation); mCamera.setDisplayOrientation(orientation); mSupportedPreviewSizes = mCamera.getParameters().getSupportedPreviewSizes(); requestLayout(); // get Camera parameters Camera.Parameters params = mCamera.getParameters(); List<String> focusModes = params.getSupportedFocusModes(); if (focusModes.contains(Camera.Parameters.FOCUS_MODE_AUTO)) { // set the focus mode params.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO); // set Camera parameters mCamera.setParameters(params); } try { mCamera.setPreviewDisplay(holder); // Important: Call startPreview() to start updating the preview // surface. Preview must be started before you can take a picture. mCamera.startPreview(); isPreview = true; Log.d(TAG, "Camera preview started."); } catch (IOException e) { Log.d(TAG, "Error setting camera preview: " + e.getMessage()); } } public void surfaceDestroyed(SurfaceHolder holder) { // Surface will be destroyed when we return, so stop the preview. // Release the camera for other applications. if (mCamera != null) { if (isPreview) mCamera.stopPreview(); mCamera.release(); mCamera = null; isPreview = false; } } private Size getOptimalPreviewSize(List<Size> sizes, int w, int h) { final double ASPECT_TOLERANCE = 0.1; double targetRatio = (double) w / h; if (sizes == null) return null; Size optimalSize = null; double minDiff = Double.MAX_VALUE; int targetHeight = h; // Try to find an size match aspect ratio and size for (Size size : sizes) { double ratio = (double) size.width / size.height; if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE) continue; if (Math.abs(size.height - targetHeight) < minDiff) { optimalSize = size; minDiff = Math.abs(size.height - targetHeight); } } // Cannot find the one match the aspect ratio, ignore the requirement if (optimalSize == null) { minDiff = Double.MAX_VALUE; for (Size size : sizes) { if (Math.abs(size.height - targetHeight) < minDiff) { optimalSize = size; minDiff = Math.abs(size.height - targetHeight); } } } return optimalSize; } 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 Log.d(TAG, "Preview surface does not exist"); return; } // stop preview before making changes try { mCamera.stopPreview(); Log.d(TAG, "Preview stopped."); } catch (Exception e) { // ignore: tried to stop a non-existent preview Log.d(TAG, "Error starting camera preview: " + e.getMessage()); } int orientation = calculatePreviewOrientation(mCameraInfo, mDisplayOrientation); mCamera.setDisplayOrientation(orientation); try { mCamera.setPreviewDisplay(mHolder); mCamera.startPreview(); Log.d(TAG, "Camera preview started."); } catch (Exception e) { Log.d(TAG, "Error starting camera preview: " + e.getMessage()); } } /** * 안드로이드 디바이스 방향에 맞는 카메라 프리뷰를 화면에 보여주기 위해 계산합니다. */ public static int calculatePreviewOrientation(Camera.CameraInfo info, int rotation) { 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; } return result; } public void takePicture(){ mCamera.takePicture(shutterCallback, rawCallback, jpegCallback); } Camera.ShutterCallback shutterCallback = new Camera.ShutterCallback() { public void onShutter() { } }; Camera.PictureCallback rawCallback = new Camera.PictureCallback() { public void onPictureTaken(byte[] data, Camera camera) { } }; //참고 : http://stackoverflow.com/q/37135675 Camera.PictureCallback jpegCallback = new Camera.PictureCallback() { public void onPictureTaken(byte[] data, Camera camera) { //이미지의 너비와 높이 결정 int w = camera.getParameters().getPictureSize().width; int h = camera.getParameters().getPictureSize().height; int orientation = calculatePreviewOrientation(mCameraInfo, mDisplayOrientation); //byte array를 bitmap으로 변환 BitmapFactory.Options options = new BitmapFactory.Options(); options.inPreferredConfig = Bitmap.Config.ARGB_8888; Bitmap bitmap = BitmapFactory.decodeByteArray( data, 0, data.length, options); //이미지를 디바이스 방향으로 회전 Matrix matrix = new Matrix(); matrix.postRotate(orientation); bitmap = Bitmap.createBitmap(bitmap, 0, 0, w, h, matrix, true); //bitmap을 byte array로 변환 ByteArrayOutputStream stream = new ByteArrayOutputStream(); bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream); byte[] currentData = stream.toByteArray(); //파일로 저장 new SaveImageTask().execute(currentData); } }; private class SaveImageTask extends AsyncTask<byte[], Void, Void> { @Override protected Void doInBackground(byte[]... data) { FileOutputStream outStream = null; try { File path = new File (Environment.getExternalStorageDirectory().getAbsolutePath() + "/camtest"); if (!path.exists()) { path.mkdirs(); } String fileName = String.format("%d.jpg", System.currentTimeMillis()); File outputFile = new File(path, fileName); outStream = new FileOutputStream(outputFile); outStream.write(data[0]); outStream.flush(); outStream.close(); Log.d(TAG, "onPictureTaken - wrote bytes: " + data.length + " to " + outputFile.getAbsolutePath()); mCamera.startPreview(); // 갤러리에 반영 Intent mediaScanIntent = new Intent( Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); mediaScanIntent.setData(Uri.fromFile(outputFile)); getContext().sendBroadcast(mediaScanIntent); try { mCamera.setPreviewDisplay(mHolder); mCamera.startPreview(); Log.d(TAG, "Camera preview started."); } catch (Exception e) { Log.d(TAG, "Error starting camera preview: " + e.getMessage()); } } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return null; } } } |
'Android > 개념 및 예제' 카테고리의 다른 글
Andorid 카메라 동영상 녹화 예제 (0) | 2023.10.17 |
---|---|
이벤트 추가와 날짜 선택 가능한 안드로이드 달력 예제 ( CompactCalendarView 라이브러리) (0) | 2023.10.14 |
Android와 PC JAVA 프로그램 간 블루투스 통신 예제 (0) | 2023.10.14 |
Android ViewPager 예제 - 좌우로 스와이프로 화면 전환 (0) | 2023.10.14 |
Android용 HTML 파서(Parser) jsoup 사용해보기 (11) | 2021.01.26 |
시간날때마다 틈틈이 이것저것 해보며 블로그에 글을 남깁니다.
블로그의 문서는 종종 최신 버전으로 업데이트됩니다.
여유 시간이 날때 진행하는 거라 언제 진행될지는 알 수 없습니다.
영화,책, 생각등을 올리는 블로그도 운영하고 있습니다.
https://freewriting2024.tistory.com
제가 쓴 책도 한번 검토해보세요 ^^
그렇게 천천히 걸으면서도 그렇게 빨리 앞으로 나갈 수 있다는 건.
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!