-->

[Android / Java]

재귀 함수를 이용한 캐시 삭제

 

 

* 목표 : 재귀 함수를 이용해서 캐시를 삭제하고 메모리 관리를 하자.

 

전 시간에 만든 Camera Pad를 이용해 앱을 실행하다 보면 캐시 폴더에 이미지가 쌓인다.

일단 카메라 촬영을 하기 위해 임시로 File을 만들어 촬영한 내용 (Uri 혹은 Byte Code)을 담기 때문에 파일 생성은 필수 불가결하다.

그렇지만 캐시 폴더에 만들어놨기 때문에 사실상 언제 지워져도 괜찮은 상태!

따라서 캐시 폴더를 삭제하는 메서드를 만들어보자.

 

    //캐시삭제
    public boolean deleteCache(File dir) {

        try {
            //param File이 Null이 아니여야 하고 & 디렉토리인지 확인
            if (dir != null && dir.isDirectory()) {
                //디렉토리 내 파일 리스트 호출
                String[] children = dir.list();
                //파일 리스트를 반복문으로 호출
                for (String child : children) {
                    //파일 리스트중 디렉토리가 존재할 수 있기 때문에 재귀호출
                    boolean isSuccess = deleteCache(new File(dir, child));
                    if (!isSuccess) {
                        return false;
                    }
                }
            }
        } catch (Exception e) {
            Log.w(TAG, "deleteCache Error!", e);
        }

        return dir.delete();
    }

재귀 함수란 메서드 내에서 다시 호출하는 반복문 형태의 함수이다. 내가 원하는 파일 리스트를 뽑아내거나 몽땅 삭제를 원할 때 유용하게 이용된다. 캐시 삭제는 Activity의 onDestroy를 Overide 하면서 호출하면 될 것이다.

 

    @Override
    public void onDestroy() {
        super.onDestroy();
        deleteCache(getCacheDir());
        android.os.Process.killProcess(android.os.Process.myPid());
    }

 

앱은 종료해도 프로세스가 돌아가는 문제들이 있는데 finish()로도 안 통한다. 위처럼 프로세스 자체를 죽이면 앱이 종료될 때 캐시도 삭제하고 현재 진행되는 프로세스도 종료한다.

 

[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클릭 시 사진 로드

[Android Studio] / [안드로이드 스튜디오]

Layout / View Group의 종류 2. Frame Layout 

 

* 목표 : FrameLayout의 사용법에 대해 알아보자.

 

FrameLayout이란 자식(Children)으로 추가된 여러 뷰(View) 위젯들 중 하나를 Layout의 전면에 표시할 때 사용하는 클래스이다.

 

 

Frame이란 틀, 액자 의 뜻으로 많이 쓰인다. FrameLayout 또한 액자식 구성으로 배치를 하게 된다.

내가 마음에 드는 사진을 액자에 갈아 끼우듯, 내가 보여주고 싶은 View를 Frame안에 갈아 끼워 넣을 수 있다.

따라서 Fragment의 container로 가장 많이 사용된다. 내가 보여주고 싶은 Fragment의 View를 갈아 끼우기 위해서이다.

FrameLayout의 또 다른 특징 중 하나는 중첩이 허용된다는 것이다.

저번에 배운 LinearLayout만을 이용해 레이아웃을 만들다 보면 분명 한계가 찾아온다. 

수평적, 수직적으로 직선 배치만 허용되는 LinearLayout은 같은 Frame내에서 조그마한 중첩도 허용하지 않는다.

물론 margin, padding 값에 - 를 준다면 가능하겠지만... ex) android:marginStart="-10dp"...

레이아웃의 margin, padding 값을 함부로 다루다간 디바이스 간 호환성 문제 때문에 크게 골머리를 앓을 것이다..(내가 그러고 있음 ㅜ)

따라서 이럴 때도 FrameLayout을 사용하게 된다.

 

다음의 예제를 살펴보자

<?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">

    <FrameLayout
        android:layout_width="500dp"
        android:layout_height="500dp"
        android:background="@color/teal_700">

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="end|center"
            android:background="@drawable/ic_launcher_background"
            android:src="@drawable/ic_launcher_foreground" />

    </FrameLayout>


</LinearLayout>

500dp 정사각형 FrameLayout을 만들고 그 안에 이미지 뷰를 배치했다. 기본적으로 자식 View는 항상 왼쪽 최상단에 배치가 되지만 layout_gravity 속성을 이용해 원하는 배치를 할 수 있다.

저번에 만든 LinearLayout 지옥을 FrameLayout을 이용하면 간단하게 표현할 수 있다.

<?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">

    <FrameLayout
        android:layout_width="500dp"
        android:layout_height="500dp"
        android:background="@color/teal_700">

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="start"
            android:layout_margin="10dp"
            android:background="@drawable/ic_launcher_background"
            android:src="@drawable/ic_launcher_foreground" />
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:layout_margin="10dp"
            android:background="@drawable/ic_launcher_background"
            android:src="@drawable/ic_launcher_foreground" />
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="end"
            android:layout_margin="10dp"
            android:background="@drawable/ic_launcher_background"
            android:src="@drawable/ic_launcher_foreground" />
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="start|center"
            android:layout_margin="10dp"
            android:background="@drawable/ic_launcher_background"
            android:src="@drawable/ic_launcher_foreground" />
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_margin="10dp"
            android:background="@drawable/ic_launcher_background"
            android:src="@drawable/ic_launcher_foreground" />
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="end|center"
            android:layout_margin="10dp"
            android:background="@drawable/ic_launcher_background"
            android:src="@drawable/ic_launcher_foreground" />
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="start|bottom"
            android:layout_margin="10dp"
            android:background="@drawable/ic_launcher_background"
            android:src="@drawable/ic_launcher_foreground" />
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center|bottom"
            android:layout_margin="10dp"
            android:background="@drawable/ic_launcher_background"
            android:src="@drawable/ic_launcher_foreground" />
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="end|bottom"
            android:layout_margin="10dp"
            android:background="@drawable/ic_launcher_background"
            android:src="@drawable/ic_launcher_foreground" />
    </FrameLayout>


</LinearLayout>

아름답다..

응용하면 이런 식으로 장난칠 수도 있다.

<?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">

    <FrameLayout
        android:layout_width="500dp"
        android:layout_height="500dp"
        android:background="@color/teal_700">

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="start"
            android:layout_margin="10dp"
            android:background="@drawable/ic_launcher_background"
            android:src="@drawable/ic_launcher_foreground" />
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:layout_margin="10dp"
            android:background="@drawable/ic_launcher_background"
            android:src="@drawable/ic_launcher_foreground" />
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="end"
            android:layout_margin="10dp"
            android:background="@drawable/ic_launcher_background"
            android:src="@drawable/ic_launcher_foreground" />
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="start|center"
            android:layout_margin="10dp"
            android:background="@drawable/ic_launcher_background"
            android:src="@drawable/ic_launcher_foreground" />
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_margin="10dp"
            android:background="@drawable/ic_launcher_background"
            android:src="@drawable/ic_launcher_foreground" />
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="end|center"
            android:layout_margin="10dp"
            android:background="@drawable/ic_launcher_background"
            android:src="@drawable/ic_launcher_foreground" />
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="start|bottom"
            android:layout_margin="10dp"
            android:background="@drawable/ic_launcher_background"
            android:src="@drawable/ic_launcher_foreground" />
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center|bottom"
            android:layout_margin="10dp"
            android:background="@drawable/ic_launcher_background"
            android:src="@drawable/ic_launcher_foreground" />
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="end|bottom"
            android:layout_margin="10dp"
            android:background="@drawable/ic_launcher_background"
            android:src="@drawable/ic_launcher_foreground" />

        
        <LinearLayout
            android:id="@+id/customer_linear1"
            android:layout_width="300dp"
            android:layout_height="300dp"
            android:layout_margin="10dp"
            android:layout_gravity="center"
            android:background="@color/teal_200"
            android:layout_weight="1">

            <Button
                android:layout_width="0dp"
                android:layout_height="100dp"
                android:layout_gravity="center"
                android:layout_margin="10dp"
                android:layout_weight="1"
                android:text="1"
                android:textSize="30sp" />

            <Button
                android:layout_width="0dp"
                android:layout_height="100dp"
                android:layout_gravity="center"
                android:layout_marginStart="15dp"
                android:layout_weight="1"
                android:text="2"
                android:textSize="24sp" />

            <Button
                android:layout_width="0dp"
                android:layout_height="100dp"
                android:layout_gravity="center"
                android:layout_margin="10dp"
                android:layout_weight="1"
                android:text="3"
                android:textSize="24sp" />
            
        </LinearLayout>

    </FrameLayout>


</LinearLayout>

 

 

<참고자료>

abhiandroid.com/ui/framelayout

 

Frame Layout Tutorial With Example In Android Studio | Abhi Android

Frame Layout Tutorial With Example In Android Studio Frame Layout is one of the simplest layout to organize view controls. They are designed to block an area on the screen. Frame Layout should be used to hold child view, because it can be difficult to disp

abhiandroid.com

developer.android.com/reference/android/widget/FrameLayout

 

FrameLayout  |  Android 개발자  |  Android Developers

 

developer.android.com

 

[Android]

Activity와 Fragment

 

* 목표 : Activity와 Fragment의 차이에 대해 알아보자.

 

근본적으로 '안드로이드'란 무엇이냐?

"Android는 Linux 커널을 기반으로 Google에서 개발 한 모바일 운영 체제이며 주로 스마트 폰 및 태블릿과 같은 터치 스크린 모바일 장치 용으로 설계되었습니다. Android의 사용자 인터페이스는 주로 실제 동작에 느슨하게 대응하는 터치 제스처를 사용하는 직접 조작을 기반으로 합니다. 스와이프, 탭, 핀치 등의 텍스트 입력을 위한 가상 키보드와 함께 화면의 개체를 조작할 수 있습니다. " -구글

대부분 아시다시피 안드로이드는 '애플리케이션'을 사용하여 기능을 이용한다.

그리고 Activity와 Fragment는 이 애플리케이션을 만들 때 사용되는 녀석들이다.

 

  1. 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 구성요소를 보관할 창을 만드는 역할이다.
  2. 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 구성 요소의 매핑을 수행하기 때문이다.)

  1. 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위치를 정의한다.

  2. 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로 돌아올 때마다 실행된다.

  3. 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가 이미 액티비티 스택의 맨 위에 있으면 사용자에게 표시된다.

  4. 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 먹는 것들을 멈춘다.

  5. 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 상태 일 때 실행된다.

  6. 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가 더 이상 사용자에게 표시되지 않을 때 호출된다. 

  7. 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에 대해서는 다음에 다시 한번 상세하게 다뤄보도록 하자.

 

<참고자료>

blog.avenuecode.com/android-basics-activities-fragments

recipes4dev.tistory.com/58

[Android / Java]

Camera 촬영 및 내부/외부 저장소에 저장

 

* 목표 : 안드로이드의 Camera 기능을 이용하여 촬영을 하고 저장 및 불러오기를 해보자.

 

안드로이드의 내장 기능인 Camera 기능을 이용해 볼 것이다. 

먼저 새 액티비티를 만들어준다.

activity_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:gravity="center"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <ImageView
        android:id="@+id/ivCapture"
        android:layout_width="500dp"
        android:layout_height="500dp"
        android:padding="30dp" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:orientation="horizontal">

        <Button
            android:id="@+id/btnCapture"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="CAMERA" />

        <Button
            android:id="@+id/btnSave"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="SAVE" />
    </LinearLayout>

</LinearLayout>

 

새 액티비티를 만들면 manifest 파일에 액티비티를 만들었다고 알려줘야 한다.

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>

        <activity android:name=".CameraActivity">

            <!--앱 최초 실행 시 열릴 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>

Camera 기능 및 내/외부 저장소에 저장 할 수 있도록 permission을 설정해준다.

또한 <intent-filter> 태그를 이용해 앱 최초 실행 시의 CameraActivity가 열리도록 한다.

fileProvider를 통해 생성한 파일을 앱 내외부로 공유할 수 있도록 선언해준다.

file_path.xml

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <!--Context.getCacheDir() 내부 저장소-->
    <cache-path
        name="cache"
        path="." />

    <!--Context.getFilesDir() 내부 저장소-->
    <files-path
        name="Capture"
        path="." />

    <!--  Environment.getExternalStorageDirectory() 외부 저장소-->
    <external-path
        name="external"
        path="." />

    <!--  Context.getExternalCacheDir() 외부 저장소-->
    <external-cache-path
        name="external-cache"
        path="." />

    <!--  Context.getExternalFilesDir() 외부 저장소-->
    <external-files-path
        name="external-files"
        path="." />
</paths>

 

권한 관련 내용은 앞선 포스팅을 참조하자.

minggu92.tistory.com/7

이제 카메라 버튼을 클릭하면 카메라 기능을 실행할 메서드를 만들어본다.

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

마지막으로 저장버튼을 눌러 파일을 저장한다.

    //이미지저장 메소드
    private void saveImg() {

        try {
            //저장할 파일 경로
            File storageDir = new File(getFilesDir() + "/capture");
            if (!storageDir.exists()) //폴더가 없으면 생성.
                storageDir.mkdirs();

            String filename = "캡쳐파일" + ".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);
                BitmapDrawable drawable = (BitmapDrawable) ivCapture.getDrawable();
                Bitmap bitmap = drawable.getBitmap();
                bitmap.compress(Bitmap.CompressFormat.JPEG, 70, 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(this, "Capture Saved ", Toast.LENGTH_SHORT).show();
        } catch (Exception e) {
            Log.w(TAG, "Capture Saving Error!", e);
            Toast.makeText(this, "Save failed", Toast.LENGTH_SHORT).show();

        }
    }

 

저장 경로는 현재 내부저장소로 세팅했다. 외부저장소는 Path만 getExternalFilesDirs()로 바꿔주면 된다. 

 

Device File Exploer에 보면 내부저장소에 저장되었음을 확인할 수 있다.

 

이미 저장된 파일이 있다면 앱 실행시 이미지 파일을 로드해오는 것까지 만들면 끗

 

    private void loadImgArr() {
        try {

            File storageDir = new File(getFilesDir() + "/capture");
            String filename = "캡쳐파일" + ".jpg";

            File file = new File(storageDir, filename);
            Bitmap bitmap = BitmapFactory.decodeStream(new FileInputStream(file));
            ivCapture.setImageBitmap(bitmap);

        } catch (Exception e) {
            Log.w(TAG, "Capture loading Error!", e);
            Toast.makeText(this, "load failed", Toast.LENGTH_SHORT).show();
        }
    }

 

최종 CameraActivity.java

package com.example.ming_gu;

import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.ColorStateList;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.graphics.drawable.BitmapDrawable;
import android.media.ExifInterface;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.MediaStore;
import android.util.Log;
import android.view.ViewManager;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;
import androidx.core.content.res.ResourcesCompat;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;

public class CameraActivity extends AppCompatActivity {

    private static final String TAG = "CameraActivity";

    public static final int REQUEST_TAKE_PHOTO = 10;
    public static final int REQUEST_PERMISSION = 11;

    private Button btnCamera, btnSave;
    private ImageView ivCapture;
    private String mCurrentPhotoPath;

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

        checkPermission(); //권한체크

        ivCapture = findViewById(R.id.ivCapture); //ImageView 선언
        btnCamera = findViewById(R.id.btnCapture); //Button 선언
        btnSave = findViewById(R.id.btnSave); //Button 선언

        loadImgArr();

        //촬영
        btnCamera.setOnClickListener(v -> captureCamera());

        //저장
        btnSave.setOnClickListener(v -> {

            try {

                BitmapDrawable drawable = (BitmapDrawable) ivCapture.getDrawable();
                Bitmap bitmap = drawable.getBitmap();

                //찍은 사진이 없으면
                if (bitmap == null) {
                    Toast.makeText(this, "저장할 사진이 없습니다.", Toast.LENGTH_SHORT).show();
                } else {
                    //저장
                    saveImg();
                    mCurrentPhotoPath = ""; //initialize
                }

            } catch (Exception e) {
                Log.w(TAG, "SAVE ERROR!", e);
            }
        });
    }

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

    //이미지저장 메소드
    private void saveImg() {

        try {
            //저장할 파일 경로
            File storageDir = new File(getFilesDir() + "/capture");
            if (!storageDir.exists()) //폴더가 없으면 생성.
                storageDir.mkdirs();

            String filename = "캡쳐파일" + ".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);
                BitmapDrawable drawable = (BitmapDrawable) ivCapture.getDrawable();
                Bitmap bitmap = drawable.getBitmap();
                bitmap.compress(Bitmap.CompressFormat.JPEG, 70, 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(this, "Capture Saved ", Toast.LENGTH_SHORT).show();
        } catch (Exception e) {
            Log.w(TAG, "Capture Saving Error!", e);
            Toast.makeText(this, "Save failed", Toast.LENGTH_SHORT).show();

        }
    }

    private void loadImgArr() {
        try {

            File storageDir = new File(getFilesDir() + "/capture");
            String filename = "캡쳐파일" + ".jpg";

            File file = new File(storageDir, filename);
            Bitmap bitmap = BitmapFactory.decodeStream(new FileInputStream(file));
            ivCapture.setImageBitmap(bitmap);

        } catch (Exception e) {
            Log.w(TAG, "Capture loading Error!", e);
            Toast.makeText(this, "load failed", Toast.LENGTH_SHORT).show();
        }
    }

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

    @Override
    public void onResume() {
        super.onResume();
        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(); //권한이 없으면 앱 종료
                }
            }
        }
    }


}

[Android / Java] 권한 설정

 

* 목표 : 권한을 요청하고 관리해보자

 

안드로이드의 카메라 기능, 외부 저장소 읽고 쓰는 기능, 주소록, 인터넷 등 앱에서 필요한 기능을 구현하기 위해서는 권한을 갖고 있어야 한다. 

요새 개인정보로 민감한 시기에 최소한 권한을 요청하고 뺏는 방법은 알고 있도록 하자!

먼저 manifest 파일에 권한을 요청한다.

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">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

    </application>

</manifest>

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(); //권한체크
    }
}

 

권한 거부 시 앱종료

 

권한을 뺏으려면 Setting에 애플리케이션 관리자로 들어가서 해당 앱의 Permission을 거부하면 된다.

<참고자료>

eunplay.tistory.com/81

 

 

[Android Studio] / [안드로이드 스튜디오]

ListView Adapter return always 0 (index) / 리스트뷰에 한 개의 아이템만 나올 때

 

Adapter에 두개의 객체를 담아 보여주는 짓을 마무리 하는데 ListView에 아이템이 한개만 노출되는 현상을 목격...

아무리 로그를 찍어도 왜 한개만 노출되는지 모르겠다..안 보여줄거면 다 안나오던가... 왜 1개만이냐고...

어댑터에서 문제가 생긴 것 같아서 어댑터에 getView() 메소드 로그를 찍어봤는데

12-05 14:14:48.870 8419-8419/com.m3s.skylark E/DashBoardAdapter: getView ( position ) : 0

어댑터 자체에서 한 번만 돌았다는 사실을 알게 됐고 로직 문제가 아니라 판단하여 혹시 리스트뷰의 attribute 때문인가!?

그렇게 구글링 하던 중에 이유를 알게 된다.

 

ScrollView 안에 ListView를 쓰지 마라!

심지어 구글에서 저렇게 말할 정도라니 ㅜ

ScrollView 안에서는 ListView의 높이를 파악할 수 없기 때문에 생겨나는 문제이다. 

ListView 자체도 Scroll 기능을 포함하고 있으니 이 속성끼리 부딪혀 생겨나는 모양이다. 첫 아이템이 세팅되고 스크롤이 내려가지 못해 다음 아이템 자체를 읽어오지 못하는 것..

따라서 해결하려면 ListView의 높이를 재측정하여 파라미터로 ScrollView에 알려줘야 한다.

일단 스크롤 뷰에 ViewPort속성을 True로 주고 

<ScrollView 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:fillViewport="true"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    
    .....
    
    
    <ListView
    android:id="@+id/lvGroup"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_weight="1" />
    
</ScrollView>

java class 내에서 리스트 뷰의 높이를 다시 세팅해주는 함수를 만든다.

    public static void setListViewHeight(ListView listView) {

        ListAdapter listAdapter = listView.getAdapter(); //현재 세팅되어 있는 어댑터 정보 갖고오기

        int totalHeight = 0;

        for (int i = 0; i < listAdapter.getCount(); i++) {
            View listItem = listAdapter.getView(i, null, listView); //어댑터에 세팅된 아이템정보 가지고 오기.
            listItem.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
                    View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
            totalHeight += listItem.getMeasuredHeight(); //현재 측정된 아이템 높이 만큼 총높이를 증가
        }

        ViewGroup.LayoutParams params = listView.getLayoutParams(); //현재 리스트뷰의 파라미터값들
        params.height = totalHeight + (listView.getDividerHeight() * (listAdapter.getCount() - 1)); //현재 리스트뷰의 높이를 측정한 총높이(ListView 자체의 height)에 아이템 수만큼 + 
        listView.setLayoutParams(params); //리스트뷰에 파라미터 세팅
        listView.requestLayout();
    }

그리고 어댑터를 세팅한 다음에 바로 이 함수를 호출해주면 끗 -

 

'Satisfaction' 님의 블로그에 따르면 ListView자체에 padding 옵션을 준 경우, 저 함수를 불러줘도 height가 모자란 상태로 조정된다고 한다.

저 함수는 View의 높이 자체만 계산하기 때문에 외부적 요소에 관해서는 알아채지 못하기 때문이다.

따라서 RecyclerVIew를 사용하는 방법을 추천한다

 

<참고자료>

stackoverflow.com/questions/29783732/android-customadapter-basesadapter-getview-is-always-returning-0

 

Android customAdapter (BasesAdapter) getView is always returning 0

I have made a customadapter for a listview, but for some reason the getView method's parameter "position" is always 0, which makes the method pretty useless. Here is the code of my customAdapter,

stackoverflow.com

wefu.tistory.com/64

 

Android ScrollView 안에 ListView 2개 스크롤 하기

ListView 2개를 ScrollView로 감싸서 ListView2개가 ScrollView 높이를 초과하게 되면 스크롤 되어 보여지게 UI를 구성했습니다. 문제는 ListView 2개의 heigh를 wrap_content나 match_parent를 주고 ScrollVie..

wefu.tistory.com

medium.com/@sinyakinv/your-solution-doesnt-solve-problem-fully-e5f97cad2a12

 

Your solution doesn’t solve problem fully.

for (int i = 0; i < listAdapter.getCount(); i++) { view = listAdapter.getView(i, view, listView); if (i == 0) view.setLayoutParams(new…

medium.com

satisfactoryplace.tistory.com/77

[Android]

Layout / View Group의 종류 1. Linear Layout 

 

* 목표 : View와 ViewGroup의 정의, LinearLayout의 사용법에 대해 알아보자.

 

안드로이드 화면에서 유저와 터치 등을 통해 상호작용하는 것들을 View라고 한다.

예를 들면 글자를 보여주는 TextView, 이미지를 보여주는 ImageView, 누르는 Button 등..

각 View 들은 View Class를 상속받아 사용해야 하며 View는 단독으로 사용할 수 없다.

반드시 View Group / View Container를 통해 화면에 나타난다. View Class를 상속받는 ViewGroup 역시 다양한데 

수직, 수평으로 위젯을 배치하는 LinearLayout, 제약을 통한 ConstraintLayout, 상하좌우 관계를 통해 배치되는 RelativeLayout, 중첩으로 배치되는 FrameLayout 등.. 종류만 해도 수십 가지다.

제목은 Layout의 종류라고 했지만 View Group의 종류라고 말해야 정확한 셈이다.

오늘은 그중에 가장 많이 사용되는 LinearLayout에 대해 알아보겠다.

 

출처 : 네이버 영어사전

 

Linear의 사전적 정의이다. LinearLayout 또한 속한 View들을 직선으로 배치한다는 점이 동일하다.

수평 혹은 수직으로 배치한다는 점에서 유저가 보기에 매우 직관적이며, 사각형의 안드로이드 화면에 가장 적합한 형태라고 볼 수 있다. 관련된 속성은 orientation의 vertical, horizontal이다. 다음의 코드 예제를 보며 확인해보자.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent">


    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@color/design_default_color_error"
        android:text="텍스트뷰1"
        />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@android:color/darker_gray"
        android:text="텍스트뷰2"
        />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@android:color/holo_blue_bright"
        android:text="텍스트뷰3"
        />


</LinearLayout>

좌 : android:orientaion="horizontal" , 우 :android:orientaion="vertical"

최상위 View Group은 LinearLayout으로 설정했고 orientation속성을 따로 했다. 따라서 그림과 같이 배치된다. 

width와 height 값에 wrap_content를 준 것은 View의 content 크기만큼을 너비와 높이로 잡겠다는 뜻이다. match_parent로 하면 부모 ViewGroup의 크기로 잡힌다.

근데 여기서 내가 만약 각 View의 크기를 동일한 비율로 주고 싶다면? 그러면 가중치(Weight)를 이용하면 된다.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent">


    <TextView
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:background="@color/design_default_color_error"
        android:text="텍스트뷰1"
        />

    <TextView
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:background="@android:color/darker_gray"
        android:text="텍스트뷰2"
        />

    <TextView
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:background="@android:color/holo_blue_bright"
        android:text="텍스트뷰3"
        />


</LinearLayout>

각 TextView의 너비를 0dp로 설정하고 android:layout_weight를 1로 설정했다.

에뮬레이터를 태블릿 크기로 설정해놔서 ㅜ 돋보기를 주고싶누..

그럼 그림과 같이 같은 1:1:1의 비율로 View가 배치된다.

만약 화면의 반만 1:1:1로 배치하고 나머지 반을 여백으로 남기고 싶다면...? 

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:weightSum="6">


    <TextView
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:background="@color/design_default_color_error"
        android:textSize="25sp"
        android:text="텍스트뷰1"
        />

    <TextView
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:background="@android:color/darker_gray"
        android:textSize="25sp"
        android:text="텍스트뷰2"
        />

    <TextView
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:background="@android:color/holo_blue_bright"
        android:textSize="25sp"
        android:text="텍스트뷰3"
        />


</LinearLayout>

그 사이 글자크기를 키웠다능..

바로 weightSum 속성을 부모 ViewGroup에 주면 된다. 현재 가중치의 합이 6이고 각 TextView에 1씩 배분했으니 나머지 가중치 3의 여백을 갖게 되는 것이다. (이해가 안된다면 실습뿐이다)

 

 

사실 LinearLayout은 이 것만 알면 된다.. 나머지는 이를 통한 응용일 뿐이다. 응용 샘플 하나 보는 것으로 오늘 포스팅을 마무리하겠다.

 

<?xml version="1.0" encoding="utf-8"?>
<ScrollView 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">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <LinearLayout
            android:id="@+id/customer_linear1"
            android:layout_width="match_parent"
            android:layout_height="300dp"
            android:layout_marginTop="15dp"
            android:layout_weight="1">

            <Button
                android:layout_width="0dp"
                android:layout_height="300dp"
                android:layout_gravity="center"
                android:layout_marginStart="15dp"
                android:layout_weight="1"
                android:padding="30dp"
                android:text="1"
                android:textSize="30sp" />

            <Button
                android:layout_width="0dp"
                android:layout_height="300dp"
                android:layout_gravity="center"
                android:layout_marginStart="15dp"
                android:layout_weight="1"
                android:padding="30dp"
                android:text="2"
                android:textSize="24sp" />

            <Button
                android:layout_width="0dp"
                android:layout_height="300dp"
                android:layout_gravity="center"
                android:layout_marginStart="15dp"
                android:layout_weight="1"
                android:padding="30dp"
                android:text="3"
                android:textSize="24sp" />
        </LinearLayout>

        <LinearLayout
            android:id="@+id/customer_linear2"
            android:layout_width="match_parent"
            android:layout_height="300dp"
            android:layout_marginTop="15dp"
            android:layout_weight="1">

            <Button
                android:layout_width="0dp"
                android:layout_height="300dp"
                android:layout_gravity="center"
                android:layout_marginStart="15dp"
                android:layout_weight="1"
                android:padding="30dp"
                android:text="4"
                android:textSize="24sp" />

            <Button
                android:layout_width="0dp"
                android:layout_height="300dp"
                android:layout_gravity="center"
                android:layout_marginStart="15dp"
                android:layout_weight="1"
                android:padding="30dp"
                android:text="5"
                android:textSize="24sp" />

            <Button
                android:layout_width="0dp"
                android:layout_height="300dp"
                android:layout_gravity="center"
                android:layout_marginStart="15dp"
                android:layout_weight="1"
                android:padding="30dp"
                android:text="6"
                android:textSize="24sp"
                android:visibility="invisible" />

        </LinearLayout>

        <LinearLayout
            android:id="@+id/customer_linear3"
            android:layout_width="match_parent"
            android:layout_height="300dp"
            android:layout_marginTop="15dp"
            android:layout_weight="1">

            <Button
                android:id="@+id/bn_search_customer"
                android:layout_width="0dp"
                android:layout_height="300dp"
                android:layout_gravity="center"
                android:layout_marginStart="15dp"
                android:layout_weight="1"
                android:padding="30dp"
                android:text="7"
                android:textSize="24sp" />

            <Button
                android:id="@+id/bn_manage_customer"
                android:layout_width="0dp"
                android:layout_height="300dp"
                android:layout_gravity="center"
                android:layout_marginStart="15dp"
                android:layout_weight="1"
                android:padding="30dp"
                android:text="8"
                android:textSize="24sp" />

            <LinearLayout
                android:layout_width="0dp"
                android:layout_height="300dp"
                android:layout_gravity="center"
                android:layout_marginStart="15dp"
                android:layout_weight="1"
                android:orientation="vertical">

                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="0dp"
                    android:layout_marginTop="15dp"
                    android:orientation="horizontal"
                    android:layout_weight="1">

                    <Button
                        android:layout_width="0dp"
                        android:layout_height="wrap_content"
                        android:layout_gravity="center"
                        android:layout_marginStart="15dp"
                        android:layout_weight="1"
                        android:padding="30dp"
                        android:text="1"
                        android:textSize="30sp" />

                    <Button
                        android:layout_width="0dp"
                        android:layout_height="wrap_content"
                        android:layout_gravity="center"
                        android:layout_marginStart="15dp"
                        android:layout_weight="1"
                        android:padding="30dp"
                        android:text="2"
                        android:textSize="24sp" />

                    <Button
                        android:layout_width="0dp"
                        android:layout_height="wrap_content"
                        android:layout_gravity="center"
                        android:layout_marginStart="15dp"
                        android:layout_weight="1"
                        android:padding="30dp"
                        android:text="3"
                        android:textSize="24sp" />
                </LinearLayout>

                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="0dp"
                    android:layout_marginTop="15dp"
                    android:layout_weight="1"
                    android:orientation="horizontal"
                    android:weightSum="3">

                    <Button
                        android:layout_width="0dp"
                        android:layout_height="wrap_content"
                        android:layout_gravity="center"
                        android:layout_marginStart="15dp"
                        android:layout_weight="1"
                        android:padding="30dp"
                        android:text="4"
                        android:textSize="24sp" />

                    <Button
                        android:layout_width="0dp"
                        android:layout_height="wrap_content"
                        android:layout_gravity="center"
                        android:layout_marginStart="15dp"
                        android:layout_weight="1"
                        android:padding="30dp"
                        android:text="5"
                        android:textSize="24sp" />

                    <Button
                        android:layout_width="0dp"
                        android:layout_height="wrap_content"
                        android:layout_gravity="center"
                        android:layout_marginStart="15dp"
                        android:layout_weight="1"
                        android:padding="30dp"
                        android:text="5"
                        android:textSize="24sp" />

                </LinearLayout>

                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="0dp"
                    android:layout_marginTop="15dp"
                    android:orientation="horizontal"
                    android:layout_weight="1">

                    <Button
                        android:layout_width="0dp"
                        android:layout_height="wrap_content"
                        android:layout_gravity="center"
                        android:layout_marginStart="15dp"
                        android:layout_weight="1"
                        android:padding="30dp"
                        android:text="7"
                        android:textSize="24sp" />

                    <Button
                        android:layout_width="0dp"
                        android:layout_height="wrap_content"
                        android:layout_gravity="center"
                        android:layout_marginStart="15dp"
                        android:layout_weight="1"
                        android:padding="30dp"
                        android:text="8"
                        android:textSize="24sp" />

                    <Button
                        android:layout_width="0dp"
                        android:layout_height="wrap_content"
                        android:layout_gravity="center"
                        android:layout_marginStart="15dp"
                        android:layout_weight="1"
                        android:padding="30dp"
                        android:text="8"
                        android:textSize="24sp" />

                </LinearLayout>

            </LinearLayout>

        </LinearLayout>

    </LinearLayout>
</ScrollView>

 

<참고자료>

developer.android.com/reference/android/view/ViewGroup

 

[Android / Java]

ViewHolder를 이용한 Custom Adapter 만들기

 

* 목표 : Custom Item Class를 만들어 ViewHolder를 이용한 Custom Adapter를 구현해 매핑해보자.

 

Adapter는 보통 스크롤로 표현되는 ListView 에 많이 사용된다. 

내가 원하는 아이템의 POJO 클래스를 만들어 해당 클래스에 아이템을 추가하면 어댑터가 컨버팅을 해 뷰로 보이는 원리다.

일단 가볍게 클래스를 만들어주자.

package com.example.ming_gu;

import android.graphics.Bitmap;

public class CustomCaptureClass  {

    private String title; //이름
    private String realPath; //실제경로
    private String category; //분류
    private Bitmap capture; //이미지파일
    private Boolean status; //상태
    private String serialNo; //Serial


    public String getCategory() {
        return category;
    }

    public void setCategory(String category) {
        this.category = category;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getRealPath() {
        return realPath;
    }

    public void setRealPath(String realPath) {
        this.realPath = realPath;
    }

    public Bitmap getCapture() {
        return capture;
    }

    public void setCapture(Bitmap capture) {
        this.capture = capture;
    }

    public Boolean getStatus() {
        return status;
    }

    public void setStatus(Boolean status) {
        this.status = status;
    }

    public String getSerialNo() {
        return serialNo;
    }

    public void setSerialNo(String serialNo) {
        this.serialNo = serialNo;
    }

    
    @Override
    public String toString(){
        return "CustomCaptureClass [ +  title : "+ title
                +  ", category : " + category
                +  ", realPath : " + realPath
                + ", capture : " + capture
                + ", status : " + status
                + ", serialNo : " + serialNo;
    }

}

 

코드가 길어보이지만 자바 짬이 되는 사람은 getter and setter generate에 대해 들어봤을 것이다..

내가 변수만 지정해주면 generate 를 자동으로 해준다. 단축키는 Alt + Insert

 

그리고 @Overide로 toString 을 해주면 Log 찍을 때 클래스만 적으면 해당 아이템 정보가 쫘르륵 나오니 POJO 클래스를 만들 때 사용해주면 좋다. ex) Log.e(TAG, "CaptureItem" + captureItem);

그 뒤 캡쳐아이템을 담을 Layout을 만들어주자.

<?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>

 

클래스에서 정의한 것처럼 ImageView, CheckBox, TextView 등을 만들어줬다. 

안드로이드는 UI/UX 디자인 단이 매우 매우 중요한 개발 플랫폼이다. margin 1px에도 예민한 사람이 되어보자.

이제 본격적인  Custom Adapter 만드는 작업을 시작하겠다.

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.ivCapture.setImageBitmap(captureItem.getCapture());
        mViewHolder.chkBox.setChecked(false);
        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;
    }

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

    

보자마자 바로 현기증이 오지만 실제로 뜯어보면 별 것 아닌 내용이다. 

일단 ViewHolder가 무엇이냐면 'View들을 넣어두는 Holder 객체'를 말한다.

ListView에 들어갈 아이템이 적을 땐 상관 없지만 만약 1,000개, 10,000개가 들어간다면..? 그만큼의 객체를 findViewById() 메서드를 통해 일일이 지정해 줘야 한다. 

그러나 ViewHolder를 이용하면 최초에 화면에 나타나는 갯수만큼의 객체를 지정해주고 스크롤을 내리면 가장 첫 번째 아이템이 마지막 위치로 이동한다. 

아이템 갯수가 무한히 늘어남에도 이미 만들어둔 View를 재활용하기 위한 퍼포먼스 전략이라고 볼 수 있다.

왼쪽은 길이가 긴 ListView를 일일히 객체지정 하는 것, 오른 쪽은 ViewHolder라는 컨베이어를 이용해 재활용 하는 것

따라서 ListView 를 이용한다면 반드시 알아야 하는 항목이다. 

Adapter 안에 ViewHolder Class를 만들고 화면에서 보일 객체를 저장해주기 때문에 사용하지 않는 필드는 정의할 필요 없다. ex) POJO클래스에는 정의했지만 내부적으로 사용할 serialNo 등

아까 만들어 둔 Layout을 inflate하고 최초에 convertView가 없다면 새로 만들고 있다면 원래 것을 사용하도록 만들어준다. 

BaseAdapter를 extend 함에 따라 Overide 된 메서드들이 보이는데 보이는 그대로 사이즈나 position을 통한 객체 확인 용도이다.

여기까지 됐으면 이제 끝난 것이나 다름없다. 

MainActivity에서 ListView을 적용하고 아이템을 추가해보자.

<?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">

    <ListView
        android:id="@+id/listViewCapture"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        />

</LinearLayout>
package com.example.ming_gu;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.widget.ListView;

public class MainActivity extends AppCompatActivity {

    private ListView listView;

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

        listView = findViewById(R.id.listViewCapture); //listview 선언

        CustomCaptureAdapter customCaptureAdapter = new CustomCaptureAdapter(MainActivity.this); //CustomAdapter
        listView.setAdapter(customCaptureAdapter); //ListView에 Adapter 연결
        
        customCaptureAdapter.addItem("셀카용","001"); //Item 추가
        customCaptureAdapter.addItem("풍경용","002");
        customCaptureAdapter.addItem("음식용","003");
        customCaptureAdapter.addItem("운동용","004");
        customCaptureAdapter.addItem("용용용","005");
        customCaptureAdapter.addItem("짱쎈용","006");
        customCaptureAdapter.addItem("권지용","007");

        customCaptureAdapter.notifyDataSetChanged(); //Adapter 변경시 적용 

    }
}

 

ListView를 선언, Adapter도 선언해주고 setAdapter를 통해 연결 지어준다. 

아이템을 추가한 뒤에는 notifyDataSetChanged()를 호출하면 Adapter의 변경 사항을 바로 적용해 ListView에서 보여준다.

 

 

 

[Android / Java]

Programmatic 하게 레이아웃을 만들기

 

 

* 목표 : 현재 보이는 뷰에 새로운 레이아웃을 직접 만들어 띄워 보자

 

        //새 inflater 생성
        LayoutInflater inflater = (LayoutInflater) getActivity().getSystemService(mActivity.LAYOUT_INFLATER_SERVICE);

        //새 레이아웃 객체생성
        LinearLayout linearLayout = (LinearLayout) inflater.inflate(R.layout.my_layout, null);

        //레이아웃 배경 투명도 주기
        int myColor = ContextCompat.getColor(mFragment.getContext(), 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);
        
        getActivity().addContentView(linearLayout, params);

 

웬만하면 Activity에서 코딩하지 않고 Fragment에서 작업하려고 한다.  (Activity에서 작업하는 것이 더 쉽다!)

* LayoutInfalter는 XML로 정의된 레이아웃을 View로 실체화해주는 녀석이다.  

불러올 XML 레이아웃 파일을 지정하여 LinearLayout 객체를 생성했다.

전 시간에 한 것처럼반투명한 상태로 레이아웃이 올라오도록 구현해 보았다.

또 바깥 레이아웃은 터치가 안되도록 포커스를 새 레이아웃에 줬다.

 

 

 

<참고 자료>

developer.android.com/reference/android/view/LayoutInflater

 

LayoutInflater  |  Android 개발자  |  Android Developers

 

developer.android.com

 

[Android]

배경(XML)에 투명도(Opacity) 적용

 

* 목표 : xml을 이용해 레이아웃에 Opacity를 적용하도록 한다.

 

안드로이드 색상 형식은 #AARRGGBB이고 앞에 'AA' 부분이 알파채널 16진수이다. 

이 부분을 바꿔서 투명도를 적용시킬 수 있다. 

 

알파 채널의 범위는 8비트 이기 때문에 0~255 값을 갖는다.

1. 내가 적용시킬 투명도에 255를 곱해 반올림한다. (255 * 0.7 = 178.5 179 )

2. 나온 수를 16진수 헥사코드로 변경한다. (Google에서 '179 to Hexa' 검색 시 '0xB3' )

3. 이제 나온 값으로 알파채널을 변경해주면 된다. (#B3000000)

 

아래는 16진수 투명도 값 목록이다.

100% : FF
 95% : F2
 90% : E6
 85% : D9
 80% : CC
 75% : BF
 70% : B3
 65% : A6
 60% : 99
 55% : 8C
 50% : 80
 45% : 73
 40% : 66
 35% : 59
 30% : 4D
 25% : 40
 20% : 33
 15% : 26
 10% : 1A
  5% : 0D
  0% : 00

안드로이드 스튜디오를 쓰는 또구또구잉이라면 value/colors.xml 에 미리 등록해주자.

    <!--Opacity-->
    
    <color name="o5">#0D000000</color>
    <color name="o10">#1A000000</color>
    <color name="o15">#26000000</color>
    <color name="o20">#33000000</color>
    <color name="o25">#40000000</color>
    <color name="o30">#4D000000</color>
    <color name="o35">#59000000</color>
    <color name="o40">#66000000</color>
    <color name="o45">#73000000</color>
    <color name="o50">#80000000</color>
    <color name="o55">#8C000000</color>
    <color name="o60">#99000000</color>
    <color name="o65">#A6000000</color>
    <color name="o70">#B3000000</color>
    <color name="o75">#BF000000</color>
    <color name="o80">#CC000000</color>
    <color name="o85">#D9000000</color>
    <color name="o90">#E6000000</color>
    <color name="o95">#F2000000</color>

xml에서 불러올 때는 android:background = "@color/o70"

코드에서 programmatic 하게 불러올 때는

//String 으로 입력
layout.setBackgroundColor(Color.parseColor("#83000000"));

//또구또구하게 value값 이용
int myColor = ContextCompat.getColor(getContext(), R.color.o70);
layout.setBackgroundColor(myColor);

 

 

 

 

+ Recent posts