본문 바로가기

Android

[Android] 홍드로이드 기초 강의 - Camera 촬영 및 Image 가져오기_MainActivity

이전 Android 포스팅인 "[Android] 홍드로이드 기초 강의 - Camera 촬영 및 Image 가져오기_환경설정"에 이어서 MainActivity에 대해 작성하겠습니다.

 

1. 애플리케이션 동작

기본적인 동작은 간단합니다.

 

1) 사용자로부터 권한을 요청한다.

2) 촬영 버튼을 눌러 카메라를 실행한다.

3) 사진을 촬영하고, 앱 메인화면으로 가져온다.

 

1)

애플리케이션의 첫 화면인 MainActivity가 나타나면 권한이 허용되었는지 확인해야 합니다.

권한이 거부 상태이면 사용자에게 권한 요청 메시지가 보입니다.

 

2)

권한을 허용하면 앱 하단의 '촬영' 버튼을 이용해서 카메라를 사용할 수 있습니다.

 

3)

촬영한 이미지는 메인화면에 표시됩니다.

이때 어떻게 스마트폰을 들고 있느냐에 따라서 이미지가 90˚, 180˚ 등으로 돌아갈 것입니다.

회전된 이미지를 되돌리기 위해서 ExifInterface[각주:1]로 부터 회전각도를 가져옵니다.

 

2. 권한 체크

권한을 체크하는 방법은 다음의 코드와 같습니다.

 

// 권한 체크
TedPermission.with(getApplicationContext())                 
        .setPermissionListener(permissionListener)          
        .setRationaleMessage("카메라 권한이 필요합니다.")    
        .setDeniedMessage("거부하셨습니다.")                
        .setPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.CAMERA) 
        .check();

 

권한 체크를 수행하는 데에 필요한 매개변수가 많아서 '빌더 패턴(Builder pattern)'[각주:2]을 활용한 코드입니다.

상세 설명이 필요한 부분을 하나씩 뜯어보겠습니다.

 

TedPermission.with(getApplicationContext())

 

1) with()가 context를 매개변수로 받아서 빌더(Builder)를 반환해주는 TedPermission 라이브러리의 메소드입니다.

2) getApplicationContext()은 애플리케이션이 종료되기 전까지 사용될 수 있는 context[각주:3]를 반환해줍니다.

 

카메라 기능을 이용할 때마다 사용자에게 권한 요청 메시지를 보여줄 필요가 없습니다.

게다가 권한을 허용한 상태라면 불편만 늘어날 겁니다.

따라서 권한 체크는 애플리케이션을 처음 이용할 때 단 한 번만 요청하면 되므로 Application Context를 사용합니다.

※ Activity 생존 주기를 지니는 context가 필요하면 getContext()

 

.setPermissionListener(permissionListener)          // permissionListener 호출
.setRationaleMessage("카메라 권한이 필요합니다.")    // 권한 요청 시에 나타나는 메시지
.setDeniedMessage("거부하셨습니다.")                // 거부하면 나타나는 메시지
.setPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.CAMERA) // 외부 저장소 쓰기 권한 및 카메라 권한
.check();

 

1) setPermissionListener(permissionListener)는 권한이 허가되거나 거부됐을 때 결과를 리턴해주는 리스너를 설정합니다.

2) setPermissions()는 필요한 권한들을 설정합니다. 현재 외부 저장소 쓰기 권한과 카메라 권한을 요청하고 있습니다.

※ 외부 저장소 쓰기가 암묵적으로 읽기 권한도 허용

 

끝으로 아래의 PermissionListener를 정의해주면 권한 체크는 마무리됩니다.

 

// 권한 승인/거절 여부에 따른 안내 메시지
PermissionListener permissionListener = new PermissionListener() {
    @Override
    public void onPermissionGranted() {
        Toast.makeText(getApplicationContext(), "권한이 허용됨",Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onPermissionDenied(List<String> deniedPermissions) {
        Toast.makeText(getApplicationContext(), "권한이 거부됨",Toast.LENGTH_SHORT).show();
    }
};

 

3. 캐시 파일 생성 및 카메라 실행

카메라로 사진을 촬영하므로 공용 외부 저장소에 접근해야 하고 갤러리에 저장할 필요는 없으므로 임시 디렉터리를 이용합니다.

그런 다음에는 중복되지 않는 파일 이름을 만들어야 합니다.

또한 나중에 사용할 수 있도록 멤버 변수에 경로를 저장할 수도 있습니다.

아래의 코드는 날짜-시간 스탬프를 활용해서 사진의 고유한 파일 이름을 반환하는 메소드입니다.

 

private File createImageFile() throws IOException {
    String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
    String imageFileName = "TEST_" + timeStamp + "_";
    File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES);  
    File image = File.createTempFile(       
            imageFileName,
            ".jpg",
            storageDir
    );
    imageFilePath = image.getAbsolutePath();    
    return image;                              
}

 

 

1) getExternalFilesDir (Environment.DIRECTORY_PICTURES[각주:4])는 사진을 앱 이외에는 비공개로 설정합니다.

2) File.createTempFile()는 임시파일을 생성할 대 사용합니다. 매개변수로 파일명, 확장자, 경로를 가집니다.

3) imageFilePath는 Bitmap 이미지를 만들 때 사용될 절대 경로입니다.

 

위의 코드를 통해 캐시 파일을 만들고 나면 카메라 앱을 실행할 준비를 해야 합니다.

아래의 코드가 바로 카메라를 실행하는 코드입니다.

 

// 촬영 버튼 눌렀을 때
findViewById(R.id.btn_capture).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);    // 기본 카메라 생성
        
        if(intent.resolveActivity(getPackageManager()) != null) {	// 기본 카메라를 이용할 수 있다면
            File photoFile = null;
            try {
                photoFile = createImageFile();  // 캐시 파일을 받아옴
            } catch(IOException e) {

            }

            if(photoFile != null) {
                photoUri = FileProvider.getUriForFile(getApplicationContext(), getPackageName(), photoFile);
                intent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
                startActivityForResult(intent, REQUEST_IMAGE_CAPTURE);
            }
        }
    }
});

 

1) resolveActivity()는 매개변수로 주어진 Activity가 애플리케이션 목록에서 실행 가능한지 확인하는 메소드입니다.

2) getPackageManager()는 스마트폰에서 사용 가능한 앱들의 목록을 가져옵니다.

 

첫 조건문은 위의 설명에 의해 기본 카메라를 이용할 수 있는지 판단하는 조건문입니다.

카메라를 사용할 수 있다면 캐시 파일을 생성해서 저장합니다.

이후에 애플리케이션이 캐시 파일에 대한 시스템 사용 권한을 uri로 생성하고 카메라 intent에 저장합니다.

 

4. 이미지 조정 및 앱으로 가져오기

카메라로 촬영한 이미지를 앱으로 가져오는 마지막 단계입니다.

촬영하는 방법에 따라 사진이 돌아가는 경우가 있습니다.

회전된 이미지를 원래대로 돌리기 위해선 얼마큼 회전됐는지 알아야 합니다.

 

private int exifOrientationToDegress(int exifOrientation) {
    if(exifOrientation == ExifInterface.ORIENTATION_ROTATE_90) {
        return 90;
    } else if(exifOrientation == ExifInterface.ORIENTATION_ROTATE_180) {
        return 180;
    } else if(exifOrientation == ExifInterface.ORIENTATION_ROTATE_270) {
        return 270;
    }
    return 0;
}

 

Exifinterface[각주:5]가 제공하는 회전 각도를 매개변수로 가져와서 회전된 정도를 반환해줍니다.

 

알아낸 회전각도로 이미지를 조정하고 반환해줍니다.

 

private Bitmap rotate(Bitmap bitmap, float degree) {
    Matrix matrix = new Matrix();
    matrix.postRotate(degree);
    return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
}

 

마무리로 촬영이 끝났을 때 이미지를 앱으로 가져오도록 onActivityResult()를 정의해줍니다.

 

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if(requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) {
        Bitmap bitmap = BitmapFactory.decodeFile(imageFilePath);	// 캐시 파일에 대한 bitmap 생성
        ExifInterface exif = null;

        try{
            exif = new ExifInterface(imageFilePath);
        }catch(IOException e) {
            e.printStackTrace();
        }

        int exifOrientation;
        int exifDegree;

        if(exif != null) {
            exifOrientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
            exifDegree = exifOrientationToDegress(exifOrientation);	// 회전 각도
        } else {
            exifDegree = 0;
        }
        
        // 이미지를 조정하고 화면에 띄우기
        ((ImageView) findViewById(R.id.iv_result)).setImageBitmap(rotate(bitmap, exifDegree));
    }
}

 

5. 전체코드 및 실행

package com.example.cameraexample_fixed;

import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.FileProvider;

import android.Manifest;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.media.ExifInterface;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.provider.MediaStore;
import android.view.View;
import android.widget.ImageView;
import android.widget.Toast;

import java.io.File;
import java.io.IOException;
import java.security.Permission;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import com.gun0912.tedpermission.PermissionListener;
import com.gun0912.tedpermission.TedPermission;

public class MainActivity extends AppCompatActivity {

    private static final int REQUEST_IMAGE_CAPTURE = 672;   // 다수의 activity가 호출되었을 때, 이를 구별하기 위한 상수값
    private String imageFilePath;
    private Uri photoUri;

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

        // 권한 체크
        TedPermission.with(getApplicationContext())    
                .setPermissionListener(permissionListener) 
                .setRationaleMessage("카메라 권한이 필요합니다.")  
                .setDeniedMessage("거부하셨습니다.")   
                .setPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.CAMERA) 
                .check();   // 설정한 권한 체크

        // 촬영 버튼 눌렀을 때
        findViewById(R.id.btn_capture).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);    // 기본 카메라 생성
                
                if(intent.resolveActivity(getPackageManager()) != null) {
                    File photoFile = null;
                    try {
                        photoFile = createImageFile();  // 캐시 파일을 받아옴
                    } catch(IOException e) {

                    }

                    if(photoFile != null) {
                        photoUri = FileProvider.getUriForFile(getApplicationContext(), getPackageName(), photoFile);
                        intent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
                        startActivityForResult(intent, REQUEST_IMAGE_CAPTURE);
                    }
                }
            }
        });
    }

    private File createImageFile() throws IOException {
        String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
        String imageFileName = "TEST_" + timeStamp + "_";
        File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES);
        File image = File.createTempFile(       // 캐시된 파일 생성
                imageFileName,
                ".jpg",
                storageDir
        );
        imageFilePath = image.getAbsolutePath();    // 생성된 캐시 파일의 절대 경로
        return image;                               // 캐시 파일 반환
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        //super.onActivityResult(requestCode, resultCode, data);

        if(requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) {
            Bitmap bitmap = BitmapFactory.decodeFile(imageFilePath);
            ExifInterface exif = null;

            try{
                exif = new ExifInterface(imageFilePath);
            }catch(IOException e) {
                e.printStackTrace();
            }

            int exifOrientation;
            int exifDegree;

            if(exif != null) {
                exifOrientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
                exifDegree = exifOrientationToDegress(exifOrientation);
            } else {
                exifDegree = 0;
            }

            ((ImageView) findViewById(R.id.iv_result)).setImageBitmap(rotate(bitmap, exifDegree));
        }
    }

    // Rotate된 이미지를 촬영 당시 시점으로 되돌릴 각도 반환
    private int exifOrientationToDegress(int exifOrientation) {
        if(exifOrientation == ExifInterface.ORIENTATION_ROTATE_90) {
            return 90;
        } else if(exifOrientation == ExifInterface.ORIENTATION_ROTATE_180) {
            return 180;
        } else if(exifOrientation == ExifInterface.ORIENTATION_ROTATE_270) {
            return 270;
        }
        return 0;
    }

    // 화면 각도 변환
    private Bitmap rotate(Bitmap bitmap, float degree) {
        Matrix matrix = new Matrix();
        matrix.postRotate(degree);
        return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
    }

    // 권한 승인/거절 여부에 따른 안내 메시지
    PermissionListener permissionListener = new PermissionListener() {
        @Override
        public void onPermissionGranted() {
            Toast.makeText(getApplicationContext(), "권한이 허용됨",Toast.LENGTH_SHORT).show();

        }

        @Override
        public void onPermissionDenied(List<String> deniedPermissions) {
            Toast.makeText(getApplicationContext(), "권한이 거부됨",Toast.LENGTH_SHORT).show();
        }
    };

}

 

 

정상적으로 동작함을 알 수 있습니다.

기초 강의라고 하기엔 많은 부분들이 설명이 생략된 채로 강의가 진행되어 정리하기 바빴던 예제였습니다.

프로젝트는 GitHub에도 업로드했습니다.

 

※참고

https://bictoselfdev.blogspot.com/2021/02/builderPattern.html

 

[안드로이드] 빌더 패턴 (Builder pattern)

빌더 패턴(Builder Pattern) 주요 내용 요약 정리

bictoselfdev.blogspot.com

https://4z7l.github.io/2020/09/17/android-context.html

 

[Android] Context란? getContext(), getApplicationContext(), getBaseContext()의 차이점 - HERSTORY

Context란? 먼저 공식문서에 의하면 다음과 같다. Interface to global information about an application environment. This is an abstract class whose implementation is provided by the Android system. It allows access to application-specific reso

4z7l.github.io

https://mainia.tistory.com/1179

 

안드로이드(Android) 사진의 EXIF 정보 가져오기

안드로이드(Android) 사진의 EXIF 정보 가져오기 개발환경 : window 7 64bit, Eclipse Mars, Android 4.2.2 EXIF 란 디지털 사진의 이미지 정보입니다. 아주 다양한 정보가 저장되는데 이미지의 기본값, 크기, 화..

mainia.tistory.com

https://developer.android.com/reference/android/media/ExifInterface#constants

 

ExifInterface  |  Android Developers

android.net.wifi.hotspot2.omadm

developer.android.com

https://justbobby.tistory.com/12

 

Android 카메라 사진 캐시영역(Cache-path) 저장하기

 앱에 카메라 기능을 넣다보면 File영역에 저장하여 갤러리에 보이게끔 하는 경우도 있지만 카톡의 프로필 사진을 위해 사진을 찍을때는 사진을 찍고 편집하고 업로드 하면나면 찍고 작업한 사

justbobby.tistory.com

https://developer.android.google.cn/reference/android/content/Context?hl=ko#getExternalFilesDir(java.lang.String) 

 

Context  |  Android Developers

android.net.wifi.hotspot2.omadm

developer.android.google.cn

https://codechacha.com/ko/check-if-startable-activity/

 

안드로이드 - 실행가능한 Activity인지 확인하기

Android에서 실행 가능한 액티비티는 말그대로 실행할 수 있는 액티비티를 말합니다. 어떤 액티비티를 실행했을 때 실행이 안될 수 있는데요. 미리 체크를 한다면 다른 예외처리를 할 수 있기 때

codechacha.com

https://billcorea.tistory.com/12

 

안드로이드 앱 만들기 getPackageManager() 란

오늘은 내폰에 설치된 앱 목록을 추출해 볼까요? PackageManager pkgMgr = getPackageManager(); List mApps; ImageView logoImage ; mApps = pkgMgr.queryIntentActivities(mainIntent,0); // 실행가능한 Package..

billcorea.tistory.com

 

  1. 다양한 이미지 파일 형식의 Exif 정보를 읽고 쓰기 위한 클래스 [본문으로]
  2. 생성자 인자가 많아지면서 가동성이 떨어지고 필요에 따라 서로 다른 생성자를 생성되는 문제를 개선하는 패턴 [본문으로]
  3. 애플리케이션 환경에 대한 인터페이스 겸 추상 클래스로, 애플리케이션의 현재 상태를 나타냄 [본문으로]
  4. 사용자가 사용할 수 있는 사진을 저장할 표준 디렉터리 [본문으로]
  5. 다양한 이미지 파일 형식의 Exif 태그를 읽고 쓰기 위한 클래스 [본문으로]