-->

[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);
    }

}

 

 

1. 앱 최초실행 / 2. Add버튼 클릭 시

 

3. 아이템 클릭 시 패드 실행 / 4. 흰 배경 클릭 시 촬영모드

 

5. 촬영 후 / 6. SAVE 버튼 클릭 시

 

7. SAVE 버튼 클릭 하면 캡쳐 저장 / 8. 앱새로 실행 / 9. ADD클릭 시 사진 로드

+ Recent posts