Things take time

[Android] 카메라 사진 찍기 및 앨범에서 사진 가져오기, 크롭(Crop)하기, 프로바이더 설정하기, 이미지뷰에 띄우기 본문

Android(기능)

[Android] 카메라 사진 찍기 및 앨범에서 사진 가져오기, 크롭(Crop)하기, 프로바이더 설정하기, 이미지뷰에 띄우기

겸손할 겸 2017. 8. 28. 16:23

[들어가기 전에]


이 티스토리의 경우,

작업하면서 예제 작업한 소스를 바탕으로 정리만을 하는 용도로만 사용하기 때문에 댓글 알림과 같은 것을 확인하기가 어렵습니다. 

필요할 때만 접속하기때문에.. 어쩌다 보이면 아는 선에서는 답은 하지만요.


그리고 작성날짜가 오래될 수록 변경되었을 확률이 높습니다.


OS는 올라가고, 문법도 바뀌고 있으니 최신 예제를 확인해주시기 바랍니다.. (__)


[카메라]


https://developer.android.com/guide/topics/media/camera.html


안드로이드 내장 기능 중 카메라를 사용하는 방법이다.



[예제]


레이아웃 파일 

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout android:layout_height="match_parent"
    android:layout_width="match_parent"
    android:orientation="vertical"
    xmlns:android="http://schemas.android.com/apk/res/android">
    <Button
        android:id="@+id/btn_capture"
        android:text="사진찍기"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
    <Button
        android:id="@+id/btn_album"
        android:text="앨범열기"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
    <ImageView
        android:id="@+id/iv_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>

매니페스트에 아래의 코드를 기입한다.

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.CAMERA" /> <uses-feature android:name="android.hardware.camera2" />

여기서 READ는 WRITE에 포함되어있어서 WRITE_EXTERNAL_STORAGE만 사용해도 된다.

외장메모리 권한 2개와 카메라 권한, 그리고 하드웨어 기능을 사용하겠다는 uses-feature 태그를 사용하여 기입한다. 만약, 현재 개발중인 앱이 20이하도 포함되어있다면 camera2보단 camera를 사용하길 권장한다. camera2는 21이상부터 지원하는 최신 클래스이다.



코드에 권한을 요청한 것을 기본적으로 수행했다 가정한다. (http://g-y-e-o-m.tistory.com/47)


권한 요청을 하지 않고 이 포스팅에 있는 것만을 가져다 쓴다면 문제가 발생할 것이다. 매니페스트나 설정파일에서도 꼭 해야할 일이 있으니 확인하길. 



[중요]

사진찍을 때 필요한 프로바이더를 작성한다. 프로바이더란 앱과 앱과의 데이터를 주고받기위한 규약 및 컴포넌트다.

 

그러므로 안드로이드에서 파일의 경로(Uri)를 공유할 때는 content://로 시작되는 content provider를 사용하게 한다. 

예를 들어, 카메라 실행시 Uri.fromFile(photoFile)과 같은 함수를 사용하면 리턴값이 file://로 나오는 경우가 생겨서, 이를 사용하지 말고 프로바이더를 이용하여 해당 경로를 content://로 바꿔서 사용하란 뜻이다.


그런데 모든 부분에서 content://로 사용하란 뜻은 아니다. 카메라를 킬 때 사용하는 부분에서는 content://로 사용하란 뜻이다. 코드를 자세히 분석하면, Crop으로 넘기는 부분에서는 file://로 시작하는 값인 albumURI라는 변수를 크롭 함수에 넘겨서 잘 사용하기 때문이다.



프로바이더 작업은 다음과 같다.


1. 매니페스트에 provider 작성 (<application> </application> 사이)

        <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="com.shuvic.alumni.cameraalbum"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths" />
        </provider>

2. res폴더 밑에 xml 폴더 및 file_paths란 xml파일 생성






3. file_paths.xml

<?xml version="1.0" encoding="utf-8"?> <paths xmlns:android="http://schemas.android.com/apk/res/android"> <external-path name="hidden" path="/Pictures/gyeom" /> </paths>

xml에 기록한 name은 임시 값, path가 실제 값이다.

이 후 코드를 보면 알겠지만 중요한 것은 gyeom이며, hidden은 겉으로 보여주기만 할 뿐, 실제 파일이 생성된 경로를 의미하는 것은 gyeom이다.


** 중요 : 댓글에도 있지만, 파일이 정상적으로 로그에도 잡히는 데도 불구하고, 업로드할 때나 해당 파일을 저장할때 해당 위치에 파일이 없다는 File Directory관련 에러가 발생한다. 이 이유는 프로바이더! 2주간 삽질한결과이다.


위의 xml;에서 path를 바로 잡지말고, Pictures/를 넣어줘야한다.



[액티비티 풀소스]

public class MainActivity extends AppCompatActivity { private static final int MY_PERMISSION_CAMERA = 1111; private static final int REQUEST_TAKE_PHOTO = 2222; private static final int REQUEST_TAKE_ALBUM = 3333; private static final int REQUEST_IMAGE_CROP = 4444; Button btn_capture, btn_album; ImageView iv_view; String mCurrentPhotoPath; Uri imageUri; Uri photoURI, albumURI; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); btn_capture = (Button) findViewById(R.id.btn_capture); btn_album = (Button) findViewById(R.id.btn_album); iv_view = (ImageView) findViewById(R.id.iv_view); btn_capture.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { captureCamera(); } }); btn_album.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { getAlbum(); } }); checkPermission(); } private void captureCamera(){ String state = Environment.getExternalStorageState(); // 외장 메모리 검사 if (Environment.MEDIA_MOUNTED.equals(state)) { Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); if (takePictureIntent.resolveActivity(getPackageManager()) != null) { File photoFile = null; try { photoFile = createImageFile(); } catch (IOException ex) { Log.e("captureCamera Error", ex.toString()); } if (photoFile != null) { // getUriForFile의 두 번째 인자는 Manifest provier의 authorites와 일치해야 함 Uri providerURI = FileProvider.getUriForFile(this, getPackageName(), photoFile); imageUri = providerURI; // 인텐트에 전달할 때는 FileProvier의 Return값인 content://로만!!, providerURI의 값에 카메라 데이터를 넣어 보냄 takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, providerURI); startActivityForResult(takePictureIntent, REQUEST_TAKE_PHOTO); } } } else { Toast.makeText(this, "저장공간이 접근 불가능한 기기입니다", Toast.LENGTH_SHORT).show(); return; } } public File createImageFile() throws IOException { // Create an image file name String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); String imageFileName = "JPEG_" + timeStamp + ".jpg"; File imageFile = null; File storageDir = new File(Environment.getExternalStorageDirectory() + "/Pictures", "gyeom"); if (!storageDir.exists()) { Log.i("mCurrentPhotoPath1", storageDir.toString()); storageDir.mkdirs(); } imageFile = new File(storageDir, imageFileName); mCurrentPhotoPath = imageFile.getAbsolutePath(); return imageFile; } private void getAlbum(){ Log.i("getAlbum", "Call"); Intent intent = new Intent(Intent.ACTION_PICK); intent.setType("image/*"); intent.setType(android.provider.MediaStore.Images.Media.CONTENT_TYPE); startActivityForResult(intent, REQUEST_TAKE_ALBUM); } private void galleryAddPic(){ Log.i("galleryAddPic", "Call"); Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); // 해당 경로에 있는 파일을 객체화(새로 파일을 만든다는 것으로 이해하면 안 됨) File f = new File(mCurrentPhotoPath); Uri contentUri = Uri.fromFile(f); mediaScanIntent.setData(contentUri); sendBroadcast(mediaScanIntent); Toast.makeText(this, "사진이 앨범에 저장되었습니다.", Toast.LENGTH_SHORT).show(); } // 카메라 전용 크랍 public void cropImage(){ Log.i("cropImage", "Call"); Log.i("cropImage", "photoURI : " + photoURI + " / albumURI : " + albumURI); Intent cropIntent = new Intent("com.android.camera.action.CROP"); // 50x50픽셀미만은 편집할 수 없다는 문구 처리 + 갤러리, 포토 둘다 호환하는 방법 cropIntent.setFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); cropIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); cropIntent.setDataAndType(photoURI, "image/*"); //cropIntent.putExtra("outputX", 200); // crop한 이미지의 x축 크기, 결과물의 크기 //cropIntent.putExtra("outputY", 200); // crop한 이미지의 y축 크기 cropIntent.putExtra("aspectX", 1); // crop 박스의 x축 비율, 1&1이면 정사각형 cropIntent.putExtra("aspectY", 1); // crop 박스의 y축 비율 cropIntent.putExtra("scale", true); cropIntent.putExtra("output", albumURI); // 크랍된 이미지를 해당 경로에 저장 startActivityForResult(cropIntent, REQUEST_IMAGE_CROP); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { case REQUEST_TAKE_PHOTO: if (resultCode == Activity.RESULT_OK) { try { Log.i("REQUEST_TAKE_PHOTO", "OK"); galleryAddPic(); iv_view.setImageURI(imageUri); } catch (Exception e) { Log.e("REQUEST_TAKE_PHOTO", e.toString()); } } else { Toast.makeText(MainActivity.this, "사진찍기를 취소하였습니다.", Toast.LENGTH_SHORT).show(); } break; case REQUEST_TAKE_ALBUM: if (resultCode == Activity.RESULT_OK) { if(data.getData() != null){ try { File albumFile = null; albumFile = createImageFile(); photoURI = data.getData(); albumURI = Uri.fromFile(albumFile); cropImage(); }catch (Exception e){ Log.e("TAKE_ALBUM_SINGLE ERROR", e.toString()); } } } break; case REQUEST_IMAGE_CROP: if (resultCode == Activity.RESULT_OK) { galleryAddPic(); iv_view.setImageURI(albumURI); } break; } } private void checkPermission(){ if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { // 처음 호출시엔 if()안의 부분은 false로 리턴 됨 -> else{..}의 요청으로 넘어감 if ((ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) || (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA))) { new AlertDialog.Builder(this) .setTitle("알림") .setMessage("저장소 권한이 거부되었습니다. 사용을 원하시면 설정에서 해당 권한을 직접 허용하셔야 합니다.") .setNeutralButton("설정", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); intent.setData(Uri.parse("package:" + getPackageName())); startActivity(intent); } }) .setPositiveButton("확인", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { finish(); } }) .setCancelable(false) .create() .show(); } else { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.CAMERA}, MY_PERMISSION_CAMERA); } } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { switch (requestCode) { case MY_PERMISSION_CAMERA: for (int i = 0; i < grantResults.length; i++) { // grantResults[] : 허용된 권한은 0, 거부한 권한은 -1 if (grantResults[i] < 0) { Toast.makeText(MainActivity.this, "해당 권한을 활성화 하셔야 합니다.", Toast.LENGTH_SHORT).show(); return; } } // 허용했다면 이 부분에서.. break; } } }


[로직]


권한 및 카메라, 앨범을 정상적으로 가져왔을 경우를 가정한다.


카메라 : 권한 검사 -> 파일 생성(빈 껍데기) -> 생성된 파일의 경로(Uri)와 함께 카메라 실행 -> 해당 경로에 사진찍은 이 후 데이터를 저장 -> 동기화 -> 이미지뷰에 뿌림


앨범 : 권한 검사 -> 앨범 호출 -> 앨범에서 가져온 데이터 Uri값을 가져옴 -> 해당 Uri와 크롭할 이미지를 저장할 파일을 생성 및 경로를 설정함 -> 

Crop -> 이미지뷰에 뿌림



[참고]


captureCamera()에서 사용하는 FileProvider.getUriForFile함수가 contentURI를 얻는 방법이고

createImageFile()에서 imageFile을 new File 할 때 중간에 들어가는 gyeom은 프로바이더에 들어가는 path랑 동일하게 작성해야한다.


new File은 해당 위치의 파일의 경로를 객체화 시킨다. 만약 그 위치에 파일이 없으면 생성하는 것도 포함되나 가끔 파일을 못찾는다는 에러가 있는 기기 때문에 mkdir()을 통해 해당 파일의 바로 상위폴더를 먼저 생성하고 createNewFile()로 확실히 생성시킨다. 



중간에 cropIntent에 setFlag로 들어가는 URI관련 인텐트에 대한 보충 설명(출처: 공식)



간략히 말해, 안드로이드 내장 크롭기능은 우리가 만든 기능이 아니기때문에 직접 건드릴수 있는 방법은 없다. 다만 크롭기능을 호출할때 전달자 역할을 하는 인텐트에 해당 옵션을 넣어보내면 수신하는 액티비티(여기서는 안드로이드 내장 크롭을 의미)에 특정 URI 데이터를 넣어보낸다고 알려주는 것이다.



** 크롭 관련


위의 로직은 사진 찍고 -> 저장 -> 크롭하고 -> 저장의 두 번 저장하는 방식이다. (파일 2개 생성)

만약 사진을찍고 해당 사진을 크롭한것을 최종적으로 저장하고 싶다면 소스가 바뀐다. (파일 1개 생성)

// 카메라 전용 크랍(앨범엔 크롭된 이미지만 저장시키기 위해) public void cropSingleImage(Uri photoUriPath){ Log.i("cropSingleImage", "Call"); Log.i("cropSingleImage", "photoUriPath : " + photoUriPath); Intent cropIntent = new Intent("com.android.camera.action.CROP"); // 50x50픽셀미만은 편집할 수 없다는 문구 처리 + 갤러리, 포토 둘다 호환하는 방법, addFlags로도 에러 나서 setFlags // 누가 버전 처리방법임 cropIntent.setFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); cropIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); cropIntent.setDataAndType(photoUriPath, "image/*"); //cropIntent.putExtra("outputX", 200); // crop한 이미지의 x축 크기, 결과물의 크기 //cropIntent.putExtra("outputY", 200); // crop한 이미지의 y축 크기 cropIntent.putExtra("aspectX", 1); // crop 박스의 x축 비율, 1&1이면 정사각형 cropIntent.putExtra("aspectY", 1); // crop 박스의 y축 비율 Log.i("cropSingleImage", "photoUriPath22 : " + photoUriPath);

cropIntent.putExtra("scale", true); cropIntent.putExtra("output", photoUriPath); // 크랍된 이미지를 해당 경로에 저장 // 같은 photoUriPath에 저장하려면 아래가 있어야함(왜지) List

list = getPackageManager().queryIntentActivities(cropIntent, 0); grantUriPermission(list.get(0).activityInfo.packageName, photoUriPath,Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION); Intent i = new Intent(cropIntent); ResolveInfo res = list.get(0); i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); i.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); grantUriPermission(res.activityInfo.packageName, photoUriPath, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION); i.setComponent(new ComponentName(res.activityInfo.packageName, res.activityInfo.name)); startActivityForResult(i, REQUEST_IMAGE_CROP); }


[결과]





Q. 이미지 뷰에 사진 돌아가는 것은 어떻게? 

A. ImageView의 속성이나 매트릭스를 건드려야하는 것으로 알고 있다. 현재 나는 이미지 뷰를 사용하지 않고 있고, 예제를 위해 넣은 거라 별 큰 의미 없이 사용했다. 답은 구글링이다. (검색어 추천 : ImageView rotate)



[테스트 기기]


1. G스타일러스 - Android 5.0.2 / API 21

2. Nexus 5X - Android 7.1.2 / API 25