public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (findViewById(R.id.fragment_container) != null) {
if (savedInstanceState != null) {
return;
}
FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.beginTransaction()
.replace(R.id.fragment_container, new CameraFragment()) //현재 프레임을 프라그먼트로 교체
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
.addToBackStack(null)
.commit();
}
}
}
메인 액티비티 자체도 onCreate()시에 View를 Fragment로 보여주는 메서드만 갖고 있다. 저번에 설명했다시피 Activity는 무겁기 때문에 많은 코드를 집어넣지 않는 게 좋다. FragmentManager 클래스를 호출해서 현재 컨테이너를 새 Fragment로 replace 했다. replace는 단순 컨테이너의 view를 교체하는 작업이고 add 한다면 현재 뷰 위에 선언한 fragment view가 스택에 위로 쌓인다. addToBackStack()은 해당 Fragment를 스택에 쌓아서 Back 버튼 클릭 시 이전 Fragment로 돌아갈 수 있게 한다. 모든 작업은 CameraFragment.java 에서 진행하도록 하겠다. 앗 권한 체크를 안 넣었다. 권한 체크 관련 내용은 지난 번 포스팅 내용을 고대로 가져온다.
package com.example.ming_gu;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
public static final int REQUEST_PERMISSION = 11;
private static final String TAG = "MainActivitiy";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
checkPermission(); //권한체크
if (findViewById(R.id.fragment_container) != null) {
if (savedInstanceState != null) {
return;
}
FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.beginTransaction()
.replace(R.id.fragment_container, new CameraFragment()) //현재 프레임을 프라그먼트로 교체
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
.addToBackStack(null)
.commit();
}
}
//권한 확인
public void checkPermission() {
int permissionCamera = ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA);
int permissionRead = ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE);
int permissionWrite = ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE);
//권한이 없으면 권한 요청
if (permissionCamera != PackageManager.PERMISSION_GRANTED
|| permissionRead != PackageManager.PERMISSION_GRANTED
|| permissionWrite != PackageManager.PERMISSION_GRANTED) {
if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)) {
Toast.makeText(this, "이 앱을 실행하기 위해 권한이 필요합니다.", Toast.LENGTH_SHORT).show();
}
ActivityCompat.requestPermissions(this, new String[]{
Manifest.permission.CAMERA, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_PERMISSION);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
switch (requestCode) {
case REQUEST_PERMISSION: {
// 권한이 취소되면 result 배열은 비어있다.
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "권한 확인", Toast.LENGTH_LONG).show();
} else {
Toast.makeText(this, "권한 없음", Toast.LENGTH_LONG).show();
finish(); //권한이 없으면 앱 종료
}
}
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
for (Fragment fragment : getSupportFragmentManager().getFragments()) { //fragment 로 전달
fragment.onActivityResult(requestCode, resultCode, data);
}
}
}
CameraFragment에서는 ListView하나와 버튼 몇 개만 두고 리스트 뷰의 아이템을 클릭하면 CapturePad가 열리도록 구현해보려고 한다. 이제 CameraPad라는 자바 클래스 파일을 하나 만들고 그 안에 본격적인 기능을 넣어보자. 먼저 Layout을 그려본다.
/**
* Method Name : setClickListener
* Param :
* Description : onClickListener 매핑
*/
private void setClickListener() {
View.OnClickListener Listener = v -> {
switch (v.getId()) {
case R.id.iv_capture_view: //이미지 클릭 시 카메라 촬영모드로
captureCamera();
break;
case R.id.btn_capture_save: //저장 클릭 시 이미지 저장
saveCapture();
break;
case R.id.btn_capture_clear: //클리어 클릭 시 이미지 삭제
clearCapture();
break;
case R.id.btn_capture_close: //닫기 클릭 시 캡쳐패드 종료
closeCapture();
break;
}
};
ivCapture.setOnClickListener(Listener);
bnCaptureSave.setOnClickListener(Listener);
bnCaptureClear.setOnClickListener(Listener);
bnCaptureClose.setOnClickListener(Listener);
}
onClick 이벤트가 여러 개 있을 때 이런 식으로 메서드 하나를 만들어 switch문으로 감싸주면 확실히 직관적으로 코드 관리를 할 수 있다. 이제 각각에 들어갈 메서드를 만들어주도록 하자.
/**
* Method Name : captureCamera
* Param :
* Description : 카메라 촬영 모드 오픈
*/
private void captureCamera() {
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
// 인텐트를 처리 할 카메라 액티비티가 있는지 확인
if (takePictureIntent.resolveActivity(mContext.getPackageManager()) != null) {
// 촬영한 사진을 저장할 파일 생성
File photoFile = null;
try {
//임시로 사용할 파일이므로 경로는 캐시폴더로
File tempDir = mContext.getCacheDir();
//임시촬영파일 세팅
String timeStamp = new SimpleDateFormat("yyyyMMdd", Locale.getDefault()).format(new Date());
String imageFileName = "Capture_" + timeStamp + "_"; //ex) Capture_20201206_
File tempImage = File.createTempFile(
imageFileName, /* 파일이름 */
".jpg", /* 파일형식 */
tempDir /* 경로 */
);
// ACTION_VIEW 인텐트를 사용할 경로 (임시파일의 경로)
mCurrentPhotoPath = tempImage.getAbsolutePath();
photoFile = tempImage;
} catch (IOException e) {
//에러 로그는 이렇게 관리하는 편이 좋다.
Log.w(TAG, "파일 생성 에러!", e);
}
//파일이 정상적으로 생성되었다면 계속 진행
if (photoFile != null) {
//Uri 가져오기
Uri photoURI = FileProvider.getUriForFile(mContext,
mContext.getPackageName() + ".fileprovider",
photoFile);
//인텐트에 Uri담기
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);
Log.e(TAG, "go Intent! : " + takePictureIntent);
//인텐트 실행
((Activity) mContext).startActivityForResult(takePictureIntent, REQUEST_TAKE_PHOTO);
}
}
}
저번에 만든 것 재활용! mContext가 어디에 들어가는지 확인을 잘해주면 된다. SimpleDateFormat에서 자꾸 알림이 떠서 안드로이드 스튜디오가 원하는 대로 Locale을 추가해줬다. 또구또구한 개발자라면 여기서 의문이 들 것이다. 마지막 줄을 보면 카메라를 열 때 startActivityForResult()로 Intent를 실행하는데 그러려면 반드시 onActivityResult()로 Activity가 리턴 값을 받아와야 한다는 것을... 그러나 Acitivity 클래스를 상속받지 않는 Fragment의 Context로는 바로 오버라이드 해서 쓸 수 없다. 따라서 짭 onActivityResult()를 만들어야 한다.
그래서 MainActivity에 다음과 같은 로직이 있었던 것.
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
for (Fragment fragment : getSupportFragmentManager().getFragments()) { //fragment 로 전달
fragment.onActivityResult(requestCode, resultCode, data);
}
}
public void onActivityResult(int requestCode, int resultCode, Intent intent) {
try {
//after capture
switch (requestCode) {
case REQUEST_TAKE_PHOTO: {
if (resultCode == RESULT_OK) {
File file = new File(mCurrentPhotoPath);
Bitmap bitmap = MediaStore.Images.Media
.getBitmap(((Activity) mContext).getContentResolver(), Uri.fromFile(file));
if (bitmap != null) {
ExifInterface ei = new ExifInterface(mCurrentPhotoPath);
int orientation = ei.getAttributeInt(ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_UNDEFINED);
Bitmap rotatedBitmap = null;
switch (orientation) {
case ExifInterface.ORIENTATION_ROTATE_90:
rotatedBitmap = rotateImage(bitmap, 90);
break;
case ExifInterface.ORIENTATION_ROTATE_180:
rotatedBitmap = rotateImage(bitmap, 180);
break;
case ExifInterface.ORIENTATION_ROTATE_270:
rotatedBitmap = rotateImage(bitmap, 270);
break;
case ExifInterface.ORIENTATION_NORMAL:
default:
rotatedBitmap = bitmap;
}
outputBitmap = rotatedBitmap;
ivCapture.setImageBitmap(rotatedBitmap);
bnCaptureSave.setEnabled(true);
bnCaptureClear.setEnabled(true);
}
}
break;
}
}
} catch (Exception e) {
Log.w(TAG, "onActivityResult Error !", e);
}
}
//카메라에 맞게 이미지 로테이션
public static Bitmap rotateImage(Bitmap source, float angle) {
Matrix matrix = new Matrix();
matrix.postRotate(angle);
return Bitmap.createBitmap(source, 0, 0, source.getWidth(), source.getHeight(),
matrix, true);
}
그다음은 사진의 저장!
/**
* Method Name : saveCapture
* Param :
* Description : 촬영한 사진을 객체에 담기
*/
private void saveCapture() {
try {
BitmapDrawable drawable = (BitmapDrawable) ivCapture.getDrawable();
Bitmap bitmap = drawable.getBitmap();
customCaptureClass.setCapture(bitmap);
customCaptureClass.setStatus(true);
// ((Activity)mContext).onActivityResult(REQUEST_UPDATE, 10, null); //update item //todo Context만으로는 Result를 보낼 수 없다!
CameraFragment.customCaptureAdapter.notifyDataSetChanged(); //결국 Fragment를 직접 Param 으로 받아 올 수 밖에..
closeCapture(); //종료
} catch (Exception e) {
Log.w(TAG, "Capture Saving Error!", e);
Toast.makeText(mContext, "Save failed", Toast.LENGTH_SHORT).show();
}
}
이번에 해보니까 Context만으로는 onAcitivityResult()를 호출할 수가 없다.... 그리고 찾아보니 startActivityForResult랑 같이 Deprecate 됐다더라.. 이제 다른 라이브러리를 공부해야겠더라.. 결국 Pad를 열 때 Context만 받아오는 게 아니라 Fragment를 파라미터로 받아오면 해결이야 되겠지만... 그렇게 하지 않으려고 힘들게 만드는 이유가 무색해지는구먼.
아무튼 저 메서드는 실제로 저장하지는 않고 CustomCaptureClass 객체에 촬영한 bitmap을 전달만 해준다.
/**
* Method Name : clearCapture
* Param :
* Description : 촬영한 사진 클리어
*/
private void clearCapture() {
ivCapture.setImageBitmap(null);
customCaptureClass.setStatus(false);
}
/**
* Method Name : closeCapture
* Param :
* Description : 캡쳐패드 종료
*/
private void closeCapture() {
((ViewManager) linearLayout.getParent()).removeView(linearLayout); //View 삭제
}
이렇게 클리어와 종료까지 만들어주면 끗!!
전체 코드
CapturePad.java
package com.example.ming_gu;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Matrix;
import android.graphics.drawable.BitmapDrawable;
import android.media.ExifInterface;
import android.net.Uri;
import android.provider.MediaStore;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewManager;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.Toast;
import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import static android.app.Activity.RESULT_OK;
public class CapturePad {
private static final String TAG = "CustomCapturePad";
public static final int REQUEST_TAKE_PHOTO = 10;
public static final int REQUEST_UPDATE = 11;
public Button bnCaptureSave, bnCaptureClear, bnCaptureClose;
public String mCurrentPhotoPath;
public ImageView ivCapture;
public Bitmap inputBitmap, outputBitmap;
private File file;
private LinearLayout linearLayout;
private CustomCaptureClass customCaptureClass;
private Context mContext;
public CapturePad(Context context) {
this.mContext = context;
}
/**
* Method Name : openCapturePad
* Param : customCaptureClass
* Description : 캡쳐패드를 실행한다.
*
* @param customCaptureClass
*/
public void openCapturePad(CustomCaptureClass customCaptureClass) {
//새 inflater 생성
LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
//새 레이아웃 객체생성
linearLayout = (LinearLayout) inflater.inflate(R.layout.layout_capture_pad, null);
//레이아웃 배경 투명도 주기
int myColor = ContextCompat.getColor(mContext, R.color.o60);
linearLayout.setBackgroundColor(myColor);
//레이아웃 위에 겹치기
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams
(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT);
//기존레이아웃 터치 안되게
linearLayout.setClickable(true);
linearLayout.setFocusable(true);
((Activity) mContext).addContentView(linearLayout, params);
//버튼
ivCapture = linearLayout.findViewById(R.id.iv_capture_view);
bnCaptureSave = linearLayout.findViewById(R.id.btn_capture_save);
bnCaptureClear = linearLayout.findViewById(R.id.btn_capture_clear);
bnCaptureClose = linearLayout.findViewById(R.id.btn_capture_close);
ivCapture.setImageBitmap(customCaptureClass.getCapture()); //최초 로드시 기존 Capture 세팅
this.customCaptureClass = customCaptureClass;
setClickListener(); //클릭이벤트 호출
}
/**
* Method Name : setClickListener
* Param :
* Description : onClickListener 매핑
*/
private void setClickListener() {
View.OnClickListener Listener = v -> {
switch (v.getId()) {
case R.id.iv_capture_view: //이미지 클릭 시 카메라 촬영모드로
captureCamera();
break;
case R.id.btn_capture_save: //저장 클릭 시 이미지 저장
saveCapture();
break;
case R.id.btn_capture_clear: //클리어 클릭 시 이미지 삭제
clearCapture();
break;
case R.id.btn_capture_close: //닫기 클릭 시 캡쳐패드 종료
closeCapture();
break;
}
};
ivCapture.setOnClickListener(Listener);
bnCaptureSave.setOnClickListener(Listener);
bnCaptureClear.setOnClickListener(Listener);
bnCaptureClose.setOnClickListener(Listener);
}
/**
* Method Name : captureCamera
* Param :
* Description : 카메라 촬영 모드 오픈
*/
private void captureCamera() {
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
// 인텐트를 처리 할 카메라 액티비티가 있는지 확인
if (takePictureIntent.resolveActivity(mContext.getPackageManager()) != null) {
// 촬영한 사진을 저장할 파일 생성
File photoFile = null;
try {
//임시로 사용할 파일이므로 경로는 캐시폴더로
File tempDir = mContext.getCacheDir();
//임시촬영파일 세팅
String timeStamp = new SimpleDateFormat("yyyyMMdd", Locale.getDefault()).format(new Date());
String imageFileName = "Capture_" + timeStamp + "_"; //ex) Capture_20201206_
File tempImage = File.createTempFile(
imageFileName, /* 파일이름 */
".jpg", /* 파일형식 */
tempDir /* 경로 */
);
// ACTION_VIEW 인텐트를 사용할 경로 (임시파일의 경로)
mCurrentPhotoPath = tempImage.getAbsolutePath();
photoFile = tempImage;
} catch (IOException e) {
//에러 로그는 이렇게 관리하는 편이 좋다.
Log.w(TAG, "파일 생성 에러!", e);
}
//파일이 정상적으로 생성되었다면 계속 진행
if (photoFile != null) {
//Uri 가져오기
Uri photoURI = FileProvider.getUriForFile(mContext,
mContext.getPackageName() + ".fileprovider",
photoFile);
//인텐트에 Uri담기
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);
Log.e(TAG, "go Intent! : " + takePictureIntent);
//인텐트 실행
((Activity) mContext).startActivityForResult(takePictureIntent, REQUEST_TAKE_PHOTO);
}
}
}
/**
* Method Name : saveCapture
* Param :
* Description : 촬영한 사진을 객체에 담기
*/
private void saveCapture() {
try {
BitmapDrawable drawable = (BitmapDrawable) ivCapture.getDrawable();
Bitmap bitmap = drawable.getBitmap();
customCaptureClass.setCapture(bitmap);
customCaptureClass.setStatus(true);
// ((Activity)mContext).onActivityResult(REQUEST_UPDATE, 10, null); //update item //todo Context만으로는 Result를 보낼 수 없다!
CameraFragment.customCaptureAdapter.notifyDataSetChanged(); //결국 Fragment를 직접 Param 으로 받아 올 수 밖에..
closeCapture(); //종료
} catch (Exception e) {
Log.w(TAG, "Capture Saving Error!", e);
Toast.makeText(mContext, "Save failed", Toast.LENGTH_SHORT).show();
}
}
/**
* Method Name : clearCapture
* Param :
* Description : 촬영한 사진 클리어
*/
private void clearCapture() {
ivCapture.setImageBitmap(null);
customCaptureClass.setStatus(false);
}
/**
* Method Name : closeCapture
* Param :
* Description : 캡쳐패드 종료
*/
private void closeCapture() {
((ViewManager) linearLayout.getParent()).removeView(linearLayout); //View 삭제
}
public void onActivityResult(int requestCode, int resultCode, Intent intent) {
try {
//after capture
switch (requestCode) {
case REQUEST_TAKE_PHOTO: {
if (resultCode == RESULT_OK) {
File file = new File(mCurrentPhotoPath);
Bitmap bitmap = MediaStore.Images.Media
.getBitmap(((Activity) mContext).getContentResolver(), Uri.fromFile(file));
if (bitmap != null) {
ExifInterface ei = new ExifInterface(mCurrentPhotoPath);
int orientation = ei.getAttributeInt(ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_UNDEFINED);
Bitmap rotatedBitmap = null;
switch (orientation) {
case ExifInterface.ORIENTATION_ROTATE_90:
rotatedBitmap = rotateImage(bitmap, 90);
break;
case ExifInterface.ORIENTATION_ROTATE_180:
rotatedBitmap = rotateImage(bitmap, 180);
break;
case ExifInterface.ORIENTATION_ROTATE_270:
rotatedBitmap = rotateImage(bitmap, 270);
break;
case ExifInterface.ORIENTATION_NORMAL:
default:
rotatedBitmap = bitmap;
}
outputBitmap = rotatedBitmap;
ivCapture.setImageBitmap(rotatedBitmap);
bnCaptureSave.setEnabled(true);
bnCaptureClear.setEnabled(true);
}
}
break;
}
}
} catch (Exception e) {
Log.w(TAG, "onActivityResult Error !", e);
}
}
//카메라에 맞게 이미지 로테이션
public static Bitmap rotateImage(Bitmap source, float angle) {
Matrix matrix = new Matrix();
matrix.postRotate(angle);
return Bitmap.createBitmap(source, 0, 0, source.getWidth(), source.getHeight(),
matrix, true);
}
}
"Android는 Linux 커널을 기반으로 Google에서 개발 한 모바일 운영 체제이며 주로 스마트 폰 및 태블릿과 같은 터치 스크린 모바일 장치 용으로 설계되었습니다. Android의 사용자 인터페이스는 주로 실제 동작에 느슨하게 대응하는 터치 제스처를 사용하는 직접 조작을 기반으로 합니다. 스와이프, 탭, 핀치 등의 텍스트 입력을 위한 가상 키보드와 함께 화면의 개체를 조작할 수 있습니다. " -구글
대부분 아시다시피 안드로이드는 '애플리케이션'을 사용하여 기능을 이용한다.
그리고 Activity와 Fragment는 이 애플리케이션을 만들 때 사용되는 녀석들이다.
Activity is the part where the user will interacts with your application. In other words, it is responsible for creating a window to hold your UI components. (UI components and how to build a layout will be discussed in another article).
Activity는 사용자가 애플리케이션과 상호 작용하는 부분이다. UI 구성요소를 보관할 창을 만드는 역할이다.
Fragment represents a behavior or a portion of user interface in an Activity. You can combine multiple fragments in a single activity to build a multi-pane UI and reuse a fragment in multiple activities. You can think of a fragment as a modular section of an activity, which has its own lifecycle, receives its own input events, and to which you can add or remove while the activity is running (sort of like a "sub activity" that you can reuse in different activities). And it must always be embedded in an activity. The fragment's lifecycle is directly affected by the host activity's lifecycle - in other words, a fragment cannot be instantiated alone!
Fragment는 Activity에서 동작 또는 사용자 인터페이스의 일부를 나타낸다. 단일 Activity에서 여러 Fragment를 결합하여 다중 창 UI를 빌드하고 여러 활동에서 Fragment를 재사용 할 수 있다. Fragment는 자체 생명주기를 갖고 있고 자체 입력 이벤트를 수신하며 Activity가 실행되는 동안 추가, 제거할 수 있는 Activity의 모듈러 섹션이라고 생각할 수 있다. (마치 다른 Activity에서 재사용할 수 있는 서브 Activity와 같다.) 그리고 Fragment는 항상 Activity에 포함되어 있어야 한다. Fragment의 생명주기는 호스트 Activity의 생명주기에 직접적인 영향을 받는다. 즉, Fragment는 단독으로 인스턴스화 될 수 없다.
애플리케이션이 열리고 첫 번째 Activity가 표시되면 특정 라이프 사이클을 따른다. 여기에서 Activity가 파괴(onDestory()), 일시 중지(onPause()), 재개(onResume()) 또는 생성(onCreate())되는시기를 처리할 수 있다. Fragment 생명주기는 Activity의 생명주기 내에 포함되지만 몇 가지 세부 정보와 추가 단계가 있다.
The only mandatory callback that we should override is onCreate for the activity and onCreateView for fragment, because we define the layout and do the mapping of the UI components inside of them.
(우리가 재정의해야하는 유일한 필수 콜백은 Activity에 대해서는 onCreate, Fragment에 대해서는 onCreateView이다. 레이아웃을 정의하고 내부에 있는 UI 구성 요소의 매핑을 수행하기 때문이다.)
onCreate(activities) / onCreateView(fragments): The only required method (that is, it needs to be overwritten by the class that extends Activity) defines what the layout of that activity is (with the "setContentView" method) and where the XML components are mapped to the code. - 유일한 필수 메소드는 (Acitivity를 확장하는 클래스로 덮어써야 함) 해당 Acitivity의 레이아웃과 XML위치를 정의한다.
onStart: This method is always executed right after the onCreate and whenever the activity returns from background to foreground through onRestart. - 이 메소드는 항상 onCreate 직후와 onRestart를 통해 활동이 background에서 foreground로 돌아올 때마다 실행된다.
onResume: It always runs before the application appears to the user. At that moment this activity is already at the top of the activity stack and is visible to the user. - 항상 애플리케이션이 사용자에게 표시되기 전에 실행된다. onResume 될 때 Activity가 이미 액티비티 스택의 맨 위에 있으면 사용자에게 표시된다.
onPause: Called when the system is about to start resuming another activity. This method is generally used to confirm unsaved data changes, stop animations among other things that may be consuming CPU, and so on. - 시스템이 다른 Activity를 재개하려고 할 때 호출된다. 일반적으로 저장되지 않은 데이터 변경 사항을 확인하고 애니메이션 같이 CPU 먹는 것들을 멈춘다.
onRestart: Called when an activity that was stopped in the background is returning to the foreground because the activity that was visible above it is being destroyed or called. It always runs when an activity is in the STOP state. - 백그라운드에서 중지 된 Activity의 상위 Activity가 소멸되거나 호출되어 foreground로 돌아갈 때 호출된다. Activity가 항상 STOP 상태 일 때 실행된다.
onStop: Called when the activity is no longer visible to the user. This can happen because it is being destroyed or is going to background so that another activity takes its place at the top of the stack and is visible to the user. - Activity가 더 이상 사용자에게 표시되지 않을 때 호출된다.
onDestroy: Called before destroying the activity. This is the last call that activity will take when it is leaving the activity stack - that is, it is being destroyed - it is called because the activity is terminating (someone called finish ()), or because the system is temporarily destroying this activity instance to save space. - Activiy를 파괴하기 전에 호출된다.
이상이 교과서적인 설명이고 내가 실제 사용해보고 이해한 내용은 다음과 같다.
Activity는 하나의 화면이다. 앱의 모든 화면을 Activity로만 구성할 수 있다. 그러나 크고 무겁다. 따라서 리소스 관리, 메모리 관리 측면에서 부담이 된다. 그래서 사용하는 것이 Fragment이다. Activity와 비슷한 생명주기를 갖지만 엄연히 다르다. Fragment는 하나의 View 껍데기이다. 고객이라는 기능을 구현할 때 고객관리라는 Activity를 만들고 그 안에 고객 검색, 고객 등록, 고객 삭제 등의 세부화면은 Fragment로 구성할 수 있는 것이다.
Acitivity와 Fragment의 차이는 생명주기 말고도 Context라는 측면에서의 차이도 있다. Context는 쉽게 말해 참조할 객체를 말하는데, 예를 들어 getResources() 라는 메서드를 통해 Res폴더 안의 String 값이나 Image를 불러오려고 한다고 가정해보자. Activity 내에서는 단순하게 getResources(). getDrawable(R.drawable.icon_1024_001)라는 이름으로 해당 이미지 객체를 갖고 올 수 있다.
그러나 Fragment에서는 getResources()만 사용하면 NulPointerException 에러가 뜬다.
@Override
public Resources getResources() {
if (mResources == null && VectorEnabledTintResources.shouldBeUsed()) {
mResources = new VectorEnabledTintResources(this, super.getResources());
}
return mResources == null ? super.getResources() : mResources;
}
getResources() 메소드 자체는 AppCompatActivity라는 클래스를 상속받고 있기 때문이다.
따라서 Fragment에서는 getContext().getResources() 혹은 getActivity(). getResources()의 방식으로 호출하여야 한다.
Fragment는 Activity가 아니기 때문에 Context를 갖지 않는다.
안드로이드 내에서 알림메세지를 띄우는 Toast 같은 클래스에서는 Context를 필수로 입력하여야 하는데, Activity에서 토스트를 사용하면 Toast.makeText(this, "message", Toast.LENGTH_SHORT). show(); 등으로 이용해도 되지만 Fragment에서는 this 대신 getContext()를 이용해야 한다.
안드로이드를 이제 접하는 초보들은 구글링을 통해 얻은 샘플 코드가 Activity에서 바로 사용 할 수 있는지, Fragment에서는 어떤 방식으로 콜 해야 하는지 시행착오를 겪을 것이다. 내가 호출하려는 함수가 Activity안에서 사용해야 하는지, Fragment내에서도 사용가능한지 알기 위해서는 약간의 공부가 필요한 시점이다. Context에 대해서는 다음에 다시 한번 상세하게 다뤄보도록 하자.
private void captureCamera() {
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
// 인텐트를 처리 할 카메라 액티비티가 있는지 확인
if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
// 촬영한 사진을 저장할 파일 생성
File photoFile = null;
try {
//임시로 사용할 파일이므로 경로는 캐시폴더로
File tempDir = getCacheDir();
//임시촬영파일 세팅
String timeStamp = new SimpleDateFormat("yyyyMMdd").format(new Date());
String imageFileName = "Capture_" + timeStamp + "_"; //ex) Capture_20201206_
File tempImage = File.createTempFile(
imageFileName, /* 파일이름 */
".jpg", /* 파일형식 */
tempDir /* 경로 */
);
// ACTION_VIEW 인텐트를 사용할 경로 (임시파일의 경로)
mCurrentPhotoPath = tempImage.getAbsolutePath();
photoFile = tempImage;
} catch (IOException e) {
//에러 로그는 이렇게 관리하는 편이 좋다.
Log.w(TAG, "파일 생성 에러!", e);
}
//파일이 정상적으로 생성되었다면 계속 진행
if (photoFile != null) {
//Uri 가져오기
Uri photoURI = FileProvider.getUriForFile(this,
getPackageName() + ".fileprovider",
photoFile);
//인텐트에 Uri담기
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);
//인텐트 실행
startActivityForResult(takePictureIntent, REQUEST_TAKE_PHOTO);
}
}
}
카메라 촬영을 하기 위해선 임시 파일을 만들어야 한다. 만든 임시파일에 촬영한 사진을 저장하고 최종적으로 startActivityForResult를 실행하여 액티비티로 다시 Request를 보내고, onActivityResult를 통해 요청받은 Request 내용을 받아오도록 하면 된다.
@Override
public void onActivityResult(int requestCode, int resultCode, Intent intent) {
super.onActivityResult(requestCode, resultCode, intent);
try {
//after capture
switch (requestCode) {
case REQUEST_TAKE_PHOTO: {
if (resultCode == RESULT_OK) {
File file = new File(mCurrentPhotoPath);
Bitmap bitmap = MediaStore.Images.Media
.getBitmap(getContentResolver(), Uri.fromFile(file));
if (bitmap != null) {
ExifInterface ei = new ExifInterface(mCurrentPhotoPath);
int orientation = ei.getAttributeInt(ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_UNDEFINED);
// //사진해상도가 너무 높으면 비트맵으로 로딩
// BitmapFactory.Options options = new BitmapFactory.Options();
// options.inSampleSize = 8; //8분의 1크기로 비트맵 객체 생성
// Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath(), options);
Bitmap rotatedBitmap = null;
switch (orientation) {
case ExifInterface.ORIENTATION_ROTATE_90:
rotatedBitmap = rotateImage(bitmap, 90);
break;
case ExifInterface.ORIENTATION_ROTATE_180:
rotatedBitmap = rotateImage(bitmap, 180);
break;
case ExifInterface.ORIENTATION_ROTATE_270:
rotatedBitmap = rotateImage(bitmap, 270);
break;
case ExifInterface.ORIENTATION_NORMAL:
default:
rotatedBitmap = bitmap;
}
//Rotate한 bitmap을 ImageView에 저장
ivCapture.setImageBitmap(rotatedBitmap);
}
}
break;
}
}
} catch (Exception e) {
Log.w(TAG, "onActivityResult Error !", e);
}
}
//카메라에 맞게 이미지 로테이션
public static Bitmap rotateImage(Bitmap source, float angle) {
Matrix matrix = new Matrix();
matrix.postRotate(angle);
return Bitmap.createBitmap(source, 0, 0, source.getWidth(), source.getHeight(),
matrix, true);
}
Camera 기능 및 내/외부 저장소에 저장할 수 있도록 permission을 설정해준다.
앱의 권한 문제는 굉장히 깐깐하고 예민하게 작업해야 하는 부분이다. 이와 관련해 TedPermission 같은 라이브러리를 사용하면 편하지만 지금은 매뉴얼로 쓱싹 하는 과정을 보여주겠다.
앱 실행 시 권한을 확인하고, 권한이 없으면 권한을 요청하고, 요청이 거부되면 앱을 종료하는 프로세스로 작업을 진행한다.
//권한 확인
public void checkPermission() {
int permissionCamera = ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA);
int permissionRead = ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE);
int permissionWrite = ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE);
//권한이 없으면 권한 요청
if (permissionCamera != PackageManager.PERMISSION_GRANTED
|| permissionRead != PackageManager.PERMISSION_GRANTED
|| permissionWrite != PackageManager.PERMISSION_GRANTED) {
if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)) {
Toast.makeText(this, "이 앱을 실행하기 위해 권한이 필요합니다.", Toast.LENGTH_SHORT).show();
}
ActivityCompat.requestPermissions(this, new String[]{
Manifest.permission.CAMERA, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_PERMISSION);
}
}
@Override
public void onRequestPermissionsResult(int requestCode,
String permissions[], int[] grantResults) {
switch (requestCode) {
case REQUEST_PERMISSION: {
// 권한이 취소되면 result 배열은 비어있다.
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "권한 확인", Toast.LENGTH_LONG).show();
} else {
Toast.makeText(this, "권한 없음", Toast.LENGTH_LONG).show();
finish(); //권한이 없으면 앱 종료
}
}
}
}
앱을 실행할 때는 권한을 줘놓고 애플리케이션 관리자에서 권한을 뺏어가는 변태들이 있을 수 있으니 onResume시에 권한을 다시 한번 체크하도록 하자.
@Override
public void onResume() {
super.onResume();
checkPermission(); //권한체크
}
MainActivity.java
package com.example.ming_gu;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import android.Manifest;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
public static final int REQUEST_PERMISSION = 11;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
checkPermission(); //권한체크
}
//권한 확인
public void checkPermission() {
int permissionCamera = ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA);
int permissionRead = ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE);
int permissionWrite = ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE);
//권한이 없으면 권한 요청
if (permissionCamera != PackageManager.PERMISSION_GRANTED
|| permissionRead != PackageManager.PERMISSION_GRANTED
|| permissionWrite != PackageManager.PERMISSION_GRANTED) {
if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)) {
Toast.makeText(this, "이 앱을 실행하기 위해 권한이 필요합니다.", Toast.LENGTH_SHORT).show();
}
ActivityCompat.requestPermissions(this, new String[]{
Manifest.permission.CAMERA, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_PERMISSION);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
switch (requestCode) {
case REQUEST_PERMISSION: {
// 권한이 취소되면 result 배열은 비어있다.
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "권한 확인", Toast.LENGTH_LONG).show();
} else {
Toast.makeText(this, "권한 없음", Toast.LENGTH_LONG).show();
finish(); //권한이 없으면 앱 종료
}
}
}
}
@Override
public void onResume() {
super.onResume();
checkPermission(); //권한체크
}
}
//String 으로 입력
layout.setBackgroundColor(Color.parseColor("#83000000"));
//또구또구하게 value값 이용
int myColor = ContextCompat.getColor(getContext(), R.color.o70);
layout.setBackgroundColor(myColor);