[Android / Java]
Camera Pad 만들기
* 목표 : 안드로이드의 카메라 기능 및 Custom Adapter를 이용해 Camera Pad 기능을 구현해보자
* 사이드 목표 :
- 1) 프래그먼트 화면에서 실행할 수 있도록 구현
- 2) 필요할 때만 메서드를 콜 하는 방식으로 생명주기 관리
내가 원하는 기능을 구현하기엔 구글링에도 한계가 있다. 내가 원하는 복합적인 기능을 만들어보자.
앞선 포스팅은 사실 이 게시글 하나를 위한 준비과정이라 봐도 무방하다. 앞서 연습한 대로 따라온다면 아주 쉽게 만들 수 있을 것이다.
앱이 돌아가는 프로세스를 설명해보자면 실행 버튼을 클릭 시에
1. 권한을 확인하고 (minggu92.tistory.com/7)
2. 새 Fragment를 FrameLayout을 이용해 넣고 (minggu92.tistory.com/10)
2. 버튼을 클릭하면 LinearLayout으로 구성된 Pad를 띄우고 (minggu92.tistory.com/5, minggu92.tistory.com/3)
3. 투명도를 설정해준 상태에서 (minggu92.tistory.com/2)
4. ViewHolder를 이용해 Custom Adapter를 만들어 (minggu92.tistory.com/4)
5. 카메라 기능을 넣어주고 (minggu92.tistory.com/8)
6. 각 아이템을 프래그먼트로 저장 및 불러오기 할 수 있도록 한다. (minggu92.tistory.com/9)
그럼 코딩 속으로 고고 고고! 먼저 권한 설정부터 해주고
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.ming_gu">
<!--Permission-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA" />
<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/Theme.Ming_gu">
<activity android:name=".MainActivity">
<!--앱 최초 실행 시 열릴 Activity 선언-->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<!--파일경로 xml 생성-->
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_path"/>
</provider>
</application>
</manifest>
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<!--메인 프레임 영역 -->
<FrameLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
</FrameLayout>
</LinearLayout>
MainActivity Layout은 건들 필요도 없다.
MainActivity.class
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);
}
}
}
fragment_camera.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal">
<Button
android:id="@+id/btn_add"
android:layout_width="100dp"
android:layout_height="50dp"
android:layout_margin="5dp"
android:gravity="center"
android:text="ADD" />
<Button
android:id="@+id/btn_save"
android:layout_width="100dp"
android:layout_height="50dp"
android:layout_margin="5dp"
android:gravity="center"
android:text="Save" />
</LinearLayout>
<ListView
android:id="@+id/listViewCapture"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
ADD 버튼을 누르면 ListView에 Apdater를 연결하고 아이템을 추가할 것이다. (화면 최초 로드 시에 실행해도 되는 작업이지만 프로세스를 보여주기 위해 분리한 상태)
Save 버튼을 클릭하면 현재 촬영한 사진들을 저장하게 할 것이다.
Load 버튼을 클릭하면 저장되어 있는 사진을 읽어와 Adapter에 추가하고 notifyDataSetChanged()로 데이터를 최신화할 것이다.
CameraFragment.java
package com.example.ming_gu;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ListView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class CameraFragment extends Fragment {
private static final String TAG = "CameraFragment";
private ListView listView;
private CustomCaptureClass customCaptureClass;
private CapturePad capturePad;
public static CustomCaptureAdapter customCaptureAdapter;
public Button bnSave, bnAdd;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_camera, container, false);
capturePad = new CapturePad(getContext());
bnAdd = view.findViewById(R.id.btn_add); // 버튼 선언
bnSave = view.findViewById(R.id.btn_save);
listView = view.findViewById(R.id.listViewCapture); //listview 선언
bnAdd.setOnClickListener(v -> { //버튼 클릭시 어댑터 추가 및 리스트뷰에 세팅
customCaptureAdapter = new CustomCaptureAdapter(getContext());
customCaptureAdapter.addItem("셀카용", "001"); //Item 추가
customCaptureAdapter.addItem("풍경용", "002");
customCaptureAdapter.addItem("음식용", "003");
customCaptureAdapter.addItem("운동용", "004");
customCaptureAdapter.addItem("용용용", "005");
customCaptureAdapter.addItem("짱쎈용", "006");
customCaptureAdapter.addItem("권지용", "007");
listView.setAdapter(customCaptureAdapter); //ListView에 Adapter 연결
loadImgArr(); //이미지 로딩
});
bnSave.setOnClickListener(v -> { //캡쳐파일 저장
saveImgArr();
});
listView.setOnItemClickListener((parent, view1, position, id) -> {
capturePad.openCapturePad((CustomCaptureClass) customCaptureAdapter.getItem(position));
customCaptureAdapter.notifyDataSetChanged(); //Adapter 변경시 최신화
});
return view;
}
@Override
public void onResume() {
super.onResume();
if (customCaptureAdapter != null) {
customCaptureAdapter.notifyDataSetChanged(); //Adapter 변경시 최신화
}
}
/**
* Method Name : loadImgArr
* Param :
* Description : 촬영했던 사진들 로딩
*/
private void loadImgArr() {
for (int i = 0; i < customCaptureAdapter.getCount(); i++) {
try {
customCaptureClass = (CustomCaptureClass) customCaptureAdapter.getItem(i);
File storageDir = new File(getContext().getFilesDir() + "/capture");
String filename = "캡쳐파일" + customCaptureClass.getCategory() + ".jpg";
try {
File file = new File(storageDir, filename);
Bitmap bitmap = BitmapFactory.decodeStream(new FileInputStream(file));
customCaptureClass.setCapture(bitmap);
customCaptureClass.setStatus(true);
} catch (FileNotFoundException e) {
Log.w(TAG, "File Not Found Error ");
}
} catch (Exception e) {
Log.w(TAG, "Capture loading Error!", e);
Toast.makeText(getContext(), "load failed", Toast.LENGTH_SHORT).show();
}
}
customCaptureAdapter.notifyDataSetChanged();
}
/**
* Method Name : saveImgArr
* Param :
* Description : 촬영한 사진 저장
*/
private void saveImgArr() {
for (int i = 0; i < customCaptureAdapter.getCount(); i++) {
try {
customCaptureClass = (CustomCaptureClass) customCaptureAdapter.getItem(i);
if (customCaptureClass.getStatus()) { //Status가 true (사진이 있다면)
//저장할 파일 경로
File storageDir = new File(getContext().getFilesDir() + "/capture");
if (!storageDir.exists()) //폴더가 없으면 생성.
storageDir.mkdirs();
String filename = "캡쳐파일" + customCaptureClass.getCategory() + ".jpg";
// 기존에 있다면 삭제
File file = new File(storageDir, filename);
boolean deleted = file.delete();
Log.w(TAG, "Delete Dup Check : " + deleted);
FileOutputStream output = null;
try {
output = new FileOutputStream(file);
Bitmap bitmap = customCaptureClass.getCapture();
if (bitmap != null)
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, output); //해상도에 맞추어 Compress
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
try {
assert output != null;
output.close();
} catch (IOException e) {
e.printStackTrace();
}
}
Log.e(TAG, "Captured Saved");
Toast.makeText(getContext(), "Capture Saved ", Toast.LENGTH_SHORT).show();
}
} catch (Exception e) {
Log.w(TAG, "Capture Saving Error!", e);
Toast.makeText(getContext(), "Save failed", Toast.LENGTH_SHORT).show();
}
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent intent) {
super.onActivityResult(requestCode, resultCode, intent);
try {
if (requestCode == CapturePad.REQUEST_UPDATE) {
customCaptureAdapter.notifyDataSetChanged();
} else if (requestCode == CapturePad.REQUEST_TAKE_PHOTO) {
capturePad.onActivityResult(requestCode, resultCode, intent);
}
} catch (Exception e) {
Log.w(TAG, "onActivityResult Error !", e);
}
}
}
어댑터는 저번에 만든 것과 비슷하다.
CustomCaptureAdapter.java
package com.example.ming_gu;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.ArrayList;
class CustomCaptureAdapter extends BaseAdapter {
private static final String TAG = "CustomCustomCaptureClass";
private ArrayList<CustomCaptureClass> captureItems = new ArrayList<>();
private ViewHolder mViewHolder;
private Context mContext;
public CustomCaptureAdapter(Context context) {
this.mContext = context;
}
public class ViewHolder {
private ImageView ivCapture;
private CheckBox chkBox;
private TextView titleTextView;
private TextView categoryTextView;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = LayoutInflater.from(mContext).inflate(R.layout.layout_custom_capture_item, parent, false);
mViewHolder = new ViewHolder();
mViewHolder.ivCapture = convertView.findViewById(R.id.iv_photo_item);
mViewHolder.chkBox = convertView.findViewById(R.id.chkStatus);
mViewHolder.titleTextView = convertView.findViewById(R.id.tv_item_title);
mViewHolder.categoryTextView = convertView.findViewById(R.id.tv_item_category);
convertView.setTag(mViewHolder);
} else {
mViewHolder = (ViewHolder) convertView.getTag();
}
CustomCaptureClass captureItem = captureItems.get(position);
// View에 Data 세팅
mViewHolder.chkBox.setChecked(captureItem.getStatus());
mViewHolder.ivCapture.setImageBitmap(captureItem.getCapture());
mViewHolder.titleTextView.setText(captureItem.getTitle());
mViewHolder.categoryTextView.setText(captureItem.getCategory());
return convertView;
}
//전체 사이즈
@Override
public int getCount() {
return captureItems.size();
}
@Override
public long getItemId(int position) {
return position;
}
//해당 position의 객체
@Override
public Object getItem(int position) {
return captureItems.get(position);
}
public void addItem(String strTitle, String strCategory) {
CustomCaptureClass item = new CustomCaptureClass();
item.setTitle(strTitle);
item.setCategory(strCategory);
item.setStatus(false);
captureItems.add(item);
}
}
아이템 레이아웃
layout_custom_capture_item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:descendantFocusability="blocksDescendants"
android:orientation="horizontal"
android:paddingTop="15dp"
android:paddingBottom="15dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="15dp"
android:orientation="horizontal">
<ImageView
android:layout_width="100dp"
android:layout_height="match_parent"
android:id="@+id/iv_photo_item"/>
<CheckBox
android:id="@+id/chkStatus"
android:layout_marginStart="30dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
/>
<TextView
android:id="@+id/tv_item_title"
android:layout_width="0dp"
android:layout_height="45dp"
android:layout_marginStart="30dp"
android:layout_marginEnd="15dp"
android:layout_marginTop="15dp"
android:layout_marginBottom="15dp"
android:layout_weight="1"
android:gravity="center_vertical"
android:lineSpacingExtra="7sp"
android:textColor="#333333"
android:textSize="20sp"
android:text="TITLE"
android:textStyle="bold" />
<TextView
android:id="@+id/tv_item_category"
android:layout_width="0dp"
android:layout_height="45dp"
android:layout_marginTop="15dp"
android:layout_marginBottom="15dp"
android:layout_weight="1"
android:gravity="center_vertical"
android:lineSpacingExtra="7sp"
android:textColor="#333333"
android:textSize="18sp"
android:text="Category"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
CameraFragment에서는 ListView하나와 버튼 몇 개만 두고 리스트 뷰의 아이템을 클릭하면 CapturePad가 열리도록 구현해보려고 한다. 이제 CameraPad라는 자바 클래스 파일을 하나 만들고 그 안에 본격적인 기능을 넣어보자. 먼저 Layout을 그려본다.
layout_capture_pad.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<LinearLayout
android:id="@+id/sign_linear"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_gravity="center"
android:layout_weight="3"
android:orientation="horizontal">
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="2.0"/>
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="5"
android:background="#E3E7E8"
android:orientation="vertical">
<!--Title Bar-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_gravity="center"
android:layout_weight="1"
android:background="#3f3f90"
android:gravity="center_vertical"
app:backgroundTint="@null">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=" CapturePad "
android:textSize="16sp"
android:textColor="@android:color/white"
android:layout_gravity="center_horizontal"
android:layout_margin="5dp"
/>
</FrameLayout>
</LinearLayout>
<!--ImageView-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="8"
android:padding="10dp"
android:background="#E3E7E8"
android:orientation="vertical">
<ImageView
android:id="@+id/iv_capture_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:background="@android:color/white" />
</LinearLayout>
<!--Button Bar-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="center"
android:orientation="horizontal">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:orientation="horizontal">
<Button
android:id="@+id/btn_capture_save"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_margin="3dp"
android:background="#3f3f90"
android:stateListAnimator="@null"
android:text="Save"
android:textColor="@color/white"
android:textSize="15sp"
android:gravity="center"
app:backgroundTint="#3f3f90"
/>
<Button
android:id="@+id/btn_capture_clear"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_margin="3dp"
android:gravity="center"
android:background="@android:color/white"
android:stateListAnimator="@null"
android:text="Clear"
android:textColor="#111111"
android:textSize="15sp"
app:backgroundTint="@null" />
<Button
android:id="@+id/btn_capture_close"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_weight="1"
android:layout_margin="3dp"
android:gravity="center"
android:background="#3f3f90"
android:stateListAnimator="@null"
android:text="Close"
android:textColor="#FFFFFFFF"
android:textSize="15sp"
app:backgroundTint="@null" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="2.0"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
Class를 호출하고 가장먼저 오픈 메서드
/**
* 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(); //클릭이벤트 호출
}
레이아웃을 세팅해서 addContentView()로 보여주는 것. 그동안 여러 번 했다!
레이아웃을 보여주고 바로 클릭 이벤트를 호출해주자.
/**
* 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);
}
}
Fragment에서도 물론
@Override
public void onActivityResult(int requestCode, int resultCode, Intent intent) {
try {
if (requestCode == CapturePad.REQUEST_UPDATE) {
customCaptureAdapter.notifyDataSetChanged();
} else if (requestCode == CapturePad.REQUEST_TAKE_PHOTO) {
capturePad.onActivityResult(requestCode, resultCode, intent);
}
} catch (Exception e) {
Log.w(TAG, "onActivityResult Error !", e);
}
}
마지막으로 CapturePad에서 intent 파라미터를 받아 작업해주면 된다.
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);
}
}
'Andorid' 카테고리의 다른 글
[Android / Java] 앱에 필요한 정보를 저장하는 SharedPreferences (0) | 2020.12.19 |
---|---|
[Android / Java] 재귀함수를 이용한 캐시삭제 (0) | 2020.12.18 |
[Android] Layout / View Group의 종류 2. Frame Layout (1) | 2020.12.11 |
[Android] Activity와 Fragment (2) | 2020.12.09 |
[Android / Java] Camera 촬영 및 내부/외부 저장소에 저장 (8) | 2020.12.06 |