2023.03.07 - 안드로이드 앱개발자 과정

CHA·2023년 3월 8일
0

Android




ActivityResultLauncher 설정하기

ActivityResultLauncher<Intent> launcher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), new ActivityResultCallback<ActivityResult>() {
    @Override
    public void onActivityResult(ActivityResult result) {
        if(result.getResultCode() != RESULT_CANCELED){
            Intent intent = result.getData();
            Uri uri = intent.getData();
            Glide.with(MainActivity.this).load(uri).into(iv);
        }
    }
});

ActivityResultLauncher 객체를 선언하는 법은 액티비티 전환 시 했던 작업과 동일합니다. 런쳐를 통해 인텐트를 보내고, 결과를 받아오면, onActivityResult() 콜백 메서드가 실행됩니다.

  • result.getResultCode() != RESULT_CANCELED
    결과값의 코드가 RESULT_CANCELED 가 아니라면, 즉, 결과값이 정상적으로 도착했다면 if 문을 실행합니다.

  • Intent intent = result.getData();
    result 의 getData() 를 실행하면 리턴값으로 Intent 를 리턴합니다. 인텐트 객체에 결과값을 넣어줍니다.

  • Uri uri = intent.getData();
    인텐트에게 getData() 를 요청하면 Uri 객체를 리턴합니다. 여기서는 우리가 찍은 사진의 정보의 Uri 데이터 입니다.

  • Glide.with(MainActivity.this).load(uri).into(iv);
    Glide 를 이용하여 Uri 값을 이미지 뷰에 뿌려줍니다.


ACTION_PICK

사진 앱이나 갤러리 앱에서 사진을 가져오는 작업입니다. 인텐트를 하나 설정하고, ActivityResultLauncher 에게 사진을 가져와달라고 요청하면 됩니다.

void clickBtn1(){
    Intent intent = new Intent(Intent.ACTION_PICK);
    intent.setType("image/*"); 
    launcher.launch(intent);
}
  • Intent intent = new Intent(Intent.ACTION_PICK);
    인텐트 객체를 하나 설정합시다. 그리고 우리는 사진 앱 혹은 갤러리 앱에서 사진을 가져와야 하므로, 인텐트 생성자 파라미터로 Intent.ACTION_PICK 을 전달하여 사진을 가져온다는 액션값을 설정해주어야 합니다.

  • intent.setType("image/*");
    사진을 가져오기 위해서는 꼭 설정해주어야 하는 속성입니다. 인텐트가 가져올 데이터의 타입이 어떤것인지를 명시합니다.


ACTION_GET_CONTENT

ACTION_PICK 이 갤러리나 포토앱에서만 선택할 수 있는 인텐트 액션이었다면, ACTION_GET_CONTENT 은 문서 파일에서 사진 데이터를 읽어올 수 있습니다.

void clickBtn2(){
    Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
    intent.setType("image/*");
    launcher.launch(intent);
}


ACTION_OPEN_DOCUMENT

ACTION_OPEN_DOCUMENTACTION_GET_CONTENT 와 마찬가지로, 문서에서 사진 데이터를 읽어 올 수 있습니다. 두 액션값의 차이는 아래쪽 사진으로 첨부해두었습니다. 참고합시다.

void clickBtn3(){
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT).setType("image/*");
    launcher.launch(intent);
}


Jetpack PickVisualMedia

앞선 테스트들에서 명시적 인텐트를 생성하여 사진선택을 했던것과는 다르게, BottomSheet 형식으로 사진선택 화면을 보여줄 수 있는 JetPack 클래스 입니다. pickMediaLauncher 객체는 이러한 형식으로 보여줄 수 있도록 지원해줍니다.

void clickBtn4(){
    pickMediaLauncher.launch(new PickVisualMediaRequest());
}

ActivityResultLauncher<PickVisualMediaRequest> pickMediaLauncher = registerForActivityResult(new ActivityResultContracts.PickVisualMedia(), new ActivityResultCallback<Uri>() {
    @Override
    public void onActivityResult(Uri result) {
        Glide.with(MainActivity.this).load(result).into(iv);
    }
});


action MediaStore pick

PickVisualMedia 의 객체는 명시적 인텐트의 생성 없이 BottomSheet 형식으로 보여줄 수 있는 객체였습니다. 인텐트의 액션값으로 MediaStore.ACTION_PICK_IMAGES 을 전달하면, 인텐트를 가지고도 BottomSheet 형식으로 만들 수 있습니다.

void clickBtn5(){
    Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
    launcher.launch(intent);
}


이번에는 갤러리 또는 포토 앱에서 사진을 여러장 선택할 수 있는 방법에 대해 알아봅시다. 사진을 여러장을 가져와 어댑터를 통해 뷰페이져에 뿌려주어 여러장이 잘 받아지는지 테스트 해봅시다. 다만 어댑터 구현이나 뷰페이저 구현은 코드만 봅시다.


action open document multiple

인텐트에게 알려주기

일단, 사진 가져오기 위해서는 인텐트를 사진앱으로 보내고 촬영된 사진 정보를 다시 가져오도록 요청해야합니다. 그리고 여러장을 선택하겠다는 정보를 인텐트에게 전달해주어야 여러장의 사진을 선택할 수 있습니다.

void clickBtn1(){
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT).setType("image/*").putExtra(Intent.EXTRA_ALLOW_MULTIPLE,true);
    launcher.launch(intent);
}

인텐트에게 ACTION_OPEN_DOCUMENT 액션값을 설정하여 문서파일에서 사진을 얻어와봅시다. setType() 을 이용하여 파일 타입 또한 설정해주었습니다. 여기서 putExtra(Intent.EXTRA_ALLOW_MULTIPLE,true) 을 설정해주면 사진을 여러장 가져올 수 있게 됩니다.

ActivityResultLauncher 설정하기

이제 인텐트를 사진앱으로 보내줄 런쳐를 설정해봅시다.

ActivityResultLauncher<Intent> launcher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
            result -> {
                if(result.getResultCode() != RESULT_CANCELED){
                    Intent intent = result.getData();
                    ClipData clipData = intent.getClipData();
                    int size = clipData.getItemCount();

                    for(int i =0;i<size;i++){
                        images.add(clipData.getItemAt(i).getUri());
                    }
                    adapter.notifyDataSetChanged();
                }
            }
    );
  • ClipData clipData = intent.getClipData();
    앞선 테스트에서 했던 런쳐와 거의 동일합니다만, 여러장의 사진을 가져와야 하기 때문에 ClipData 클래스를 이용해주어야 합니다. 결과값을 받아온 인텐트 객체에게 getClipData() 를 요청하여 ClipData 객체를 리턴받아둡시다. 이 객체에는 여러장의 사진의 정보가 들어있으므로, 이 정보를 이용하여 뷰페이져에 뿌려주기만 하면 됩니다.


Jetpack PickMultipleVisualMedia

앞선 테스트에서 PickVisualMedia 을 이용하여 명시적 인텐트 없이 BottomSheet 의 형식으로 사진을 선택할 수 있었던것과 동일하게 여러장의 사진을 BottomSheet 의 형식으로 선택할 수 있게 해주는 클래스가 PickMultipleVisualMedia 입니다.

void clickBtn2(){
    multiplePickLauncher.launch(new PickVisualMediaRequest());
}
ActivityResultLauncher<PickVisualMediaRequest> multiplePickLauncher = registerForActivityResult(new ActivityResultContracts.PickMultipleVisualMedia(), new ActivityResultCallback<List<Uri>>() {
    @Override
    public void onActivityResult(List<Uri> result) {
        for( Uri uri : result){
            images.add(uri);
        }
        adapter.notifyDataSetChanged();
    }
});

역시 앞선 테스트와 동일합니다. 다만, 한장의 사진을 Uri 형태로 가져오는 앞선 테스트와 달리 여러개의 사진을 가져와야 하기 때문에, 결과값의 데이터 타입이 List<Uri> 의 형태 입니다.


action MediaStore PICK multiple

인텐트를 이용하여 BottomSheet 의 형태로 사진 선택창을 보여줄 수 있습니다. ACTION_OPEN_DOCUMENT 액션값을 이용하여 사진선택창을 띄워줄 때, putExtra() 의 파라미터로 Intent.EXTRA_ALLOW_MULTIPLE 을 이용해주었지만, 이번에는 MediaStore.EXTRA_PICK_IMAGES_MAX 을 이용하여 가져올 이미지의 최대 개수를 지정해주어야 합니다.

void clickBtn3(){
    Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES).putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX,10);
    launcher.launch(intent);
}


Camera App 으로 사진 및 비디오 가져오기

앞선 테스트들은 모두 갤러리 혹은 사진앱으로 사진을 선택해보는 테스트 였습니다. 이번 테스트에서는 카메라 앱으로 사진과 비디오를 선택해봅시다.


촬영한 사진 가져오기

버튼을 누르면 카메라 앱이 켜지고, 카메라 앱을 이용하여 촬영한 사진을 다시 가져와 이미지뷰에 띄워주는 테스트입니다.

void clickBtn(){
    Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    launcher.launch(intent);
}

카메라 앱을 띄운다는것은 다른 화면을 가져온다는 의미입니다. 즉, 인텐트를 활용해주어야 합니다. 인텐트의 액션값으로 MediaStore.ACTION_IMAGE_CAPTURE 을 전달하면 카메라 앱을 사용할 수 있습니다.

ActivityResultLauncher<Intent> launcher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
        result ->{
            if(result.getResultCode() != RESULT_CANCELED){
                Intent intent = result.getData();
                Uri uri = intent.getData();

                Bundle bundle = intent.getExtras();
                Bitmap bm = (Bitmap) bundle.get("data");
                iv.setImageBitmap(bm);
            }
        }
);}

역시 이번에도 인텐트를 이용하여 찍은 사진의 정보를 다시 원래의 액티비티로 가져와야 하므로, 런처를 이용합니다. 단, 프로그램 내에서 실행한 카메라 앱으로 사진을 촬영하게 되면 그 촬영된 사진은 디바이스에 파일로 저장이 되지않도록 안드로이드 정책이 바뀌었습니다. 마구잡이로 사진이 스토리지에 저장되는것을 방지하기 위함으로 보여집니다.

그래서 우리는 일단 촬영한 사진정보를 Bitmap 객체로 만들어 이미지뷰에 setImageBitmap() 을 해주어야 합니다. 이제 코드를 한줄씩 살펴봅시다.

  • Intent intent = result.getData(); & Uri uri = intent.getData();
    받아온 result 결과값을 이용하여 사진의 Uri 정보를 받아올 수 있습니다. 단, 바뀐 안드로이드 정책에 의해 uri 의 값은 null 입니다.

  • Bundle bundle = intent.getExtras();
    결과값을 가지고 있는 intent 객체에게 getExtras() 를 요청하고 리턴값으로 Bundle 객체를 리턴합니다.

  • Bitmap bm = (Bitmap) bundle.get("data");
    Bundle 객체의 get() 메소드를 이용하면 비트맵 객체를 하나 만들수 있으며, get() 메소드의 파라미터로 data 를 전달해주어야 사진 정보를 비트맵에 저장할 수 있습니다.

우리는 사진을 찍었지만, 사진은 저장되지 않았습니다. 만약, 카메라앱을 프로그램 내부에서 사용하면서, 사진을 저장하고 싶다면 어떻게 해야할까요?


Contents Provider

우리는 카메라 앱을 프로그램 내부에서 사용하여 사진을 디바이스에 저장하고 싶습니다. 그러기 위해서는 몇가지의 단계가 필요합니다. 일단 전체적인 맥락을 생각해봅시다. 우리는 찍은 사진을 디바이스에 파일로 저장하고 싶습니다. 그러기 위해서는 추가적인 데이터가 필요하며, 여기에서 저장할 이미지의 Uri 값이 필요합니다.

단, 앞서 이야기하기로, 프로그램의 내부에서 카메라 앱으로 사진을 찍으면 파일로 저장이 되지 않는다고 하였습니다. 즉, 찍은 사진의 Uri 정보를 알 수 없게 되었습니다. 그래서 Uri 값을 얻어오기 위해 사진이 저장된 실제 파일의 경로를 알아온 뒤, 이 경로를 기반으로 Uri 값으로 변환을 시켜주어야 합니다. 그러면 변환된 Uri 값을 기반으로 추가적인 데이터에게 전달할 수 있으며, 그러면 우리는 찍은 사진의 파일 저장이 가능해집니다.

자 그러면 단계별로 알아봅시다.

1. 촬영된 이미지를 파일로 저장하기 위한 추가 데이터 설정

버튼을 누르면 카메라 앱으로 이동합니다.

Uri imgUri = null;

void clickBtn(){
    Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    createImageUri();
    
    if(imgUri != null) intent.putExtra(MediaStore.EXTRA_OUTPUT,imgUri);
    launcher.launch(intent);
}
  • launcher.launch(intent);
    런쳐를 이용하여 인텐트를 카메라 앱으로 보내고, 찍은 사진의 정보를 가져오게 합니다.

  • if(imgUri != null) intent.putExtra(MediaStore.EXTRA_OUTPUT,imgUri);
    우리가 찍은 사진을 디바이스에 저장하기 위해서는 추가 데이터를 설정해주어야 합니다. 그 추가 데이터가 putExtra(MediaStore.EXTRA_OUTPUT,imgUri) 입니다. 액션값은 MediaStore.EXTRA_OUTPUT 으로 설정해주면 되는데, 문제는 두번째 파라미터 입니다. 이 두번째 파라미터가 추가적인 데이터가 필요로 하는 Uri 값입니다.

  • createImageUri();
    이제 이 메소드 내부에서 찍은 사진의 실제 경로를 기반으로 Uri 값으로 변환해줍시다. 그러면 멤버변수로 설정된 imgUri 에 넣어줄 수 있습니다.

2. 사진의 실제 경로구하기

Uri 로 변환하기 위해서는 먼저 사진이 저장된 파일이 필요합니다. 즉, 파일객체를 하나 생성해주어야 하며, 파일 객체를 생성하기 위해서는 사진이 저장될 경로, 그리고 파일명이 필요합니다.

void createImageUri(){
    File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);

    SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
    String fileName = "EX72_IMG_"+sdf.format(new Date())+".jpg";

    File file = new File(path,fileName);

    imgUri = FileProvider.getUriForFile(this,"sam",file);
    tv.setText(imgUri.toString());
}
  • File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
    카메라 앱으로 사진을 찍게되면, 디바이스에 실제로 저장이 되지 않기 때문에 저장을 시켜줘야 합니다. 그래서 사진이 저장되는 실제 경로를 먼저 지정해줍시다.

  • String fileName = "EX72_IMG_"+sdf.format(new Date())+".jpg";
    파일이름을 만들어주는 작업입니다. 보통 사진파일의 경우 동일한 이름이 있다면 덮어쓰기가 되기 때문에 절대로 동일한 이름을 사용해서는 안됩니다. 그래서 보통 찍은 날짜와 시간을 파일의 이름으로 사용합니다. SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss"); 을 이용하여 사진을 찍은 현재 시간을 받아오고, 그것을 기반으로 파일명을 만들어줍니다.

  • File file = new File(path,fileName);
    만들어진 경로와 파일명을 이용하여 파일 객체 하나를 생성해주었습니다. 이제 여기까지 했다면, 파일의 실제 경로는 구한셈입니다. 이제 이 경로를 기반으로 Uri 값을 구해야 합니다.

3. 구해진 경로를 기반으로 Uri 변환하기

카메라 앱을 통해 찍은 사진을 저장시키기 위해서는 이미지의 실제경로가 아닌 DB 주소에 해당하는 콘텐츠 경로, 즉 , Uri 의 정보가 필요합니다. 그리고 그 Uri 는 DB 에 저장이 되어있으며, 그 DB는 내 앱의 DB 입니다. 그리고 다른 앱에서 내 앱의 DB 에 접근하기 위해서는 안드로이드의 4대 컴포넌트 중 하나인 Contents Provider 가 필요합니다. 원래라면, Provider 클래스를 설계하여 사용해야 하나, 파일에 대한 경로를 제공하는 Provider 는 이미 FileProvider 라는 클래스로 설계되어있습니다. 그러면 이제 구해진 경로를 기반으로 Uri 를 구해봅시다.

먼저, Contents Provider 는 4대 컴포넌트 이므로, Manifest 파일에 등록을 시켜주어야 합니다.

<provider
    android:authorities="sam"
    android:name="androidx.core.content.FileProvider"
    android:exported="false"
    android:grantUriPermissions="true">

</provider>
  • android:authorities="sam"
    authorities 속성은 Provider 의 종류를 판별하기 위한 속성입니다. 이 속성은 중복이 되면 안되므로, 보통은 패키지명을 사용하여 지정합니다.

  • android:name="androidx.core.content.FileProvider"
    name 속성은 어떠한 종류의 Provider 를 사용할지 결정합니다. 우리는 사진 파일의 정보를 제공해야하므로 FileProvider 를 사용합니다.

  • android:exported="false"
    exported 속성의 경우 외부에서 이 Provider 의 사용여부를 결정합니다. 원래 Contents Provider 는 앱과 앱 사이에서 정보를 제공해야 하기때문에 true 로 지정해주는 경우가 있으나, 우리는 카메라 앱에게만 정보를 제공해야하므로 false 로 지정해줍시다.

  • android:grantUriPermissions="true"
    Uri 에 관한 정보를 이용하고자 할 때에 지정해주어야 하는 속성입니다. 사용할 예정이므로 true 를 속성값으로 지정합시다.

Provider 를 등록해주었다면 이제 Provider 가 제공할 정보의 경로를 지정해주어야 합니다. 경로를 지정하는 방법은 다음 코드와 같습니다.

<provider ... 중략 ">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/paths"/>
</provider>

<meta-data> 를 이용하여 경로 지정을 합니다. 메타데이터의 name 속성은 android:name="android.support.FILE_PROVIDER_PATHS" 와 같이 지정해주어야 합니다.

또한 resource 속성으로 xml 파일을 지정해야 합니다. FileProvider 는 이 xml 파일에 작성된 파일들에 대해서만 Uri 생성이 가능합니다. xml 파일은 res 폴더 안에 xml 폴더에 지정해주어야 하며, Root Element 는 path 로 지정해주어야 합니다.파일의 내용은 다음과 같습니다.

<paths xmlns:android="http://schemas.android.com/apk/res/android">

    <external-files-path
        name="external_file"
        path="."/>
    <external-path
        name="external"
        path="."/>
    <files-path
        name="files"
        path="."/>
</paths>

위 xml 코드처럼 여러개의 파일이 올 수 있으며, FileProvider 는 위에 명시된 파일들에 대한 Uri 생성이 가능합니다. <external-files-path> 은 외부 저장소의 앱 전용 영역의 데이터를 의미하며, <external-path> 은 외부 저장소의 공용 영역을 의미합니다. 또한 <files-path> 는 내부 저장소 데이터를 의미합니다. 위 명시된 영역들 이외에도 여러 종류가 있으니 참고합시다.

이제 FileProvider 의 설정들이 끝났으니 FileProvider 를 이용하여 우리가 넘겨주고자 하는 데이터의 Uri 를 가져와봅시다.

imgUri = FileProvider.getUriForFile(this,"sam",file);
  • imgUri = FileProvider.getUriForFile(this,"sam",file);
    FileProvider.getUriForFile() 의 파라미터로 Context 객체하나와 앞서 지정해주었던 FileProvider 의 authorities 속성값, 그리고 제공하고자 하는 File 객체를 전달합니다. 그러면 리턴값으로 Uri 를 반환하는데, 이 Uri 가 우리가 전달하고자 하는 Uri 입니다.

비디오 가져오기

카메라 앱을 이용하여 비디오를 가져오는 방법은 사진을 가져오는 방법과 동일합니다. 다만, 사진과는 다르게 비디오의 경우 용량이 매우 크기 때문에 자동으로 파일에 저장시켜 준다는 점이 다릅니다.

버튼을 누르면 카메라 앱으로 이동하고, 영상을 촬영하면 영상 정보를 가져와 VideoView 에 뿌려주는 테스트를 해봅시다. VideoView 는 영상정보를 담을 Container 를 의미하는 뷰입니다.

void clickBtn(){
    Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
    launcher.launch(intent);
}

ActivityResultLauncher<Intent> launcher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),result -> {
    if(result.getResultCode() == RESULT_CANCELED) return;

    Uri uri = result.getData().getData();
    vv.setVideoURI(uri);
    vv.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
        @Override
        public void onPrepared(MediaPlayer mediaPlayer) {
            vv.start();
        }
    });
});
  • Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
    인텐트를 활용하여 카메라 앱으로 이동합시다. 단, ACTION_IMAGE_CAPTURE 가 아닌 ACTION_VIDEO_CAPTURE 으로 액션값을 설정해주어야 영상촬영이 가능합니다.

ActivityResultLauncher 을 이용하여 인텐트를 보내고 다시 돌아온 인텐트를 이용하여 Uri 를 생성합니다. 생성된 Uri 객체를 vv.setVideoURI() 의 파라미터로 전달합시다. 그러면 영상을 실행할 준비는 끝납니다.

다만, 비디오가 로딩이 되는데 시간이 걸리기 때문에, vv.start() 로 바로 실행하게 되면 지연이 됩니다. 그래서 setOnPreparedListener() 을 이용하여 로딩이 완료되었을 때 실행되는 콜백메서드 onPrepared() 를 이용합시다. 이 메소드 내부에 vv.start() 를 작성하면 비디오의 로딩이 끝난 뒤, 실행 시킬 수 있습니다.

profile
Developer

0개의 댓글