본문 바로가기
코딩과 알고리즘

크레이의 앱개발 도전기 #4. 갤러리에 사진 저장

※ 이 게시글은 크레이의 IT개발 관련 성장기를 다루고 있습니다. 관련지식이 약간 있어야 이해되실 수 있습니다. 가벼운 마음으로 읽어보시면서 흥미가 생기고 의욕이 생긴다면? 개발자의 자질이 있으신 겁니다 :)

- 안드로이드 스튜디오 돌핀 2021. 3. 1 버전을 사용중입니다. -

지난 시간에는 카메라로 사진을 촬영하고 앱에 불러오는 부분을 진행했었는데요.

https://itadventure.tistory.com/583

 

크레이 안드 도전기 #3. startActivityResult 가 없어졌다구?!

※ 이 게시글은 크레이의 IT개발 관련 성장기를 다루고 있습니다. 관련지식이 약간 있어야 이해되실 수 있습니다. 가벼운 마음으로 읽어보시면서 흥미가 생기고 의욕이 생긴다면? 개발자의 자질

itadventure.tistory.com

startActivityForResult 라는 명령어가 deprecated(삭제)되어서 registerForActivityResult 명령어를 대신 사용하는 방법을 알아보았습니다.

이번시간에는 이렇게 불러온 사진을,
스마트폰의 내장 앱 갤러리에 저장하는 부분을 진행해 보았는데요.

갤러리에 파일을 저장하는 공개 함수를 조금 수정해 보았습니다.
여기저기 왔다갔댜 하다보니 출처가 어디인지 기억도 안 나네요 ㅎ..

자, 그럼 렛츠 고~


1. 엠프티 액티비티 생성 

뭐 이건 기본이니 설명은 넘어가겠습니다. :)

 


2. 매니페스트 파일 수정

사실 이 부분이 아리송합니다.
크레이의 스마트폰에서는 이 부분을 설정하지 않아도 잘 되었거든요.
하지만 디버깅이 아니라면 제한이 있을 수 있으니 적어 봅니다.

app/manifests/AndroidManifest.xml 파일에 아래 코드를 추가해줍니다.
이 설정값은 앱의 외부 저장공간 ( 갤러리같은 ) 에 접근할 수 있는 권한을 추가해 줍니다.
갤러리에 사진을 저장해주기 위해 필요하다 하더라구요.

<?xml version="1.0" encoding="utf-8"?> 
<manifest
  xmlns:android="http://schemas.android.com/apk/res/android"    
  xmlns:tools="http://schemas.android.com/tools">  
  <!-- 외부 저장소 퍼일 저장 권한 --> 
  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />  
  <
application 
    android:usesCleartextTraffic="true"
       :


3. 액티비티 화면 수정

다음으로 메인화면을 아래처럼 바꿔보았는데요,

메인 화면에 해당하는 res/layout/activity_main.xml 파일을 수정합니다.
기본 레이아웃을 LinearLayout 으로 바꿔 주고 레이아웃에 들어갈 요소들의 배치될 방향을 세로 방향으로 설정해줍니다. 

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

그리고 기본 제공된 TextView 를 삭제하고 버튼 2개와 이미지 뷰를 배치해보았는데요.
아래가 전체소스입니다. 강조된 부분은 추가된 버튼과 이미지 뷰이지요.
화면요소는 처음에는 이것 저것 할게 많아서 혼동했었는데 자주 수정하다 보니 익숙해 지더라구요.
굵은 글씨가 추가된 부분입니다.

<?xml version="1.0" encoding="utf-8"?>   
  <LinearLayout
    android:orientation="vertical" 
    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" 
    tools:context=".MainActivity">  

    <
Button 
      android:id="@+id/btn_picture" 
      android:layout_width="wrap_content" 
      android:layout_height="wrap_content" 
      android:text=" 촬영 " 
      android:textSize="40sp" 
      android:layout_gravity="center_horizontal" 
      android:layout_marginTop="55dp"/> 

    <
ImageView 
      android:id="@+id/imageView" 
      android:layout_width="250dp" 
      android:layout_height="250dp" 
      android:layout_gravity="center_horizontal" 
      android:src="@mipmap/ic_launcher" 
      android:layout_marginTop="20dp"/> 

    <
Button 
      android:id="@+id/btn_save" 
      android:layout_marginTop="20dp" 
      android:layout_width="wrap_content" 
      android:layout_height="wrap_content" 
      android:layout_gravity="center_horizontal" 
      android:text="저장" 
      android:textSize="40sp"/>  

</LinearLayout>


4. 액티비티 자바 코드 수정

이제 액티비티 요소들의 기능을 심는 부분인데요.
먼저 버튼 2개와 이미지 뷰를 연결할 변수를 선언와 이미지를 저장할 비트맵 변수를 선언합니다.

public class MainActivity extends AppCompatActivity {  

  // 레이아웃의 버튼과 이미지뷰를 연결할 변수 선언 
  private Button btn_picture; 
  private ImageView imageView; 
  public Button btn_save;  

  // 그림을 받아올 비트맵 변수를 선언 
  private Bitmap bitmap =null;
      :

이어서 onCreate() 메소드에서 변수와 화면요소를 연결합니다.

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

  imageView = findViewById(R.id.imageView); 
  btn_picture = findViewById(R.id.btn_picture); 
  btn_save = findViewById(R.id.btn_save);

그리고 촬영하기 전에는 저장 버튼을 감추기 위해 숨기는 코드를 실행합니다.

  btn_save.setVisibility(View.INVISIBLE); 

이제 촬영 버튼을 터치하면 실행하는 함수는 지난 게시글과 동일한 데요.
촬영이 성공한 후에, 저장 버튼을 보이게 하는 부분을 추가하였습니다.
바로 아래 촬영 버튼을 누르면 실행할 콜백함수를 추가하고

btn_picture.setOnClickListener(new View.OnClickListener() {   
  @Override 
  public void onClick(View view) { 
    camera_app.launch(new Intent( 
      MediaStore.ACTION_IMAGE_CAPTURE 
    )); 
  }
 
});

기본 카메라를 실행할 camera_app 을 런쳐를 추가할 때, onCreate 함수 안쪽이 아닌, 동일한 깊이에 추가해주어야 합니다.

@Override protected void onCreate(Bundle savedInstanceState) {     
    : 
}  

ActivityResultLauncher<Intent> camera_app = 
  registerForActivityResult(
 
    new ActivityResultContracts.StartActivityForResult(), 
    new ActivityResultCallback<ActivityResult>() { 
      @Override 
      public void onActivityResult(ActivityResult result) { 
        if( result.getResultCode() == RESULT_OK  && result.getData() != null){ 
          Bundle extras = result.getData().getExtras(); 
          bitmap = (Bitmap) extras.get("data"); 
          imageView.setImageBitmap(bitmap); 
          btn_save.setVisibility(View.VISIBLE);  // 지난 코드에서 추가된 부분
        }
 
      }
 
    }
 
  );
 

저장 버튼을 터치하는 액션 또한 onCreate 함수 안쪽에 구현하고
실제 파일을 저장하는 것은 별도로 구현할  saveFile() 함수를 호출하였는데요. ( 바로 이어서 나옵니다 )

protected void onCreate(Bundle savedInstanceState) { 
        :
  btn_save
.setOnClickListener(new View.OnClickListener() {   
    @Override 
    public void onClick(View view) { 
      String filename="photo.JPG"; 
      saveFile(
filename); 
    }
 
  }); 
       :

샘플 소스를 약간 변형하여 크레이 입맛에 맞췄습니다.
비트맵 이미지를 바이트 배열로 바꾸는 함수는 그냥 그대로 사용했지만,

         :
public byte[] bitmapToByteArray( Bitmap $bitmap ) { 
  ByteArrayOutputStream stream = new ByteArrayOutputStream() ; 
  $bitmap.compress(
Bitmap.CompressFormat.JPEG, 100, stream) ; 
  return stream.toByteArray() ; 
} 
 
        :

토스트 메시지는 좀 더 사용하기 쉽게 별도로 함수로 뺐습니다.
아래처럼 함수를 선언해놓으면 이 코드 내에서는그냥 ToastMsg("블라블라~"); 만 사용하면 되지요 :)
공통모듈로 빼놓으면 관리하기 아주 편할것 같은데 진행하면서 알아봐야 할것 같습니다.

         :
private void ToastMsg(String msg) { 
  Toast.makeText( 
    this.getApplicationContext(), 
    msg,
 
    Toast.LENGTH_SHORT).show(); 
} 
 
       :

앞의 2개 함수를 이용해서 갤러리에 저장하는 소스입니다.
saveFile("파일명"); 이라고 적어주면 되는데요.
동일한 파일명이 있으면 알아서 photo(2).jpg, photo(3).jpg와 같은 식으로 번호를 매겨주더군요.

private void saveFile(String filename)
{
   
if(bitmap == null)
    {
        ToastMsg(
"먼저 촬영을 하세요");
       
return;
    }

   
ContentValues values = new ContentValues();
   
values.put(
       
MediaStore.Images.Media.DISPLAY_NAME,
        filename);
   
values.put(
       
MediaStore.Images.Media.MIME_TYPE,
       
"image/*");

   
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
       
values.put(
           
MediaStore.Images.Media.IS_PENDING,
           
1);
    }

   
ContentResolver contentResolver = getContentResolver();
   
Uri item = contentResolver.insert(
       
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
       
values);

   
try {
       
ParcelFileDescriptor pdf =
            
contentResolver.openFileDescriptor(
               
item,
               
"w",
               
null);

       
if (pdf == null) {
            ToastMsg(
"파일 디스크립션 생성에 실패하였습니다.");
           
return;
        }
       
byte[] strToByte = bitmapToByteArray(bitmap);
       
FileOutputStream fos = new FileOutputStream(pdf.getFileDescriptor());
       
fos.write(strToByte);
       
fos.close();

       
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
           
values.clear();
           
values.put(
                
MediaStore.Images.Media.IS_PENDING,
               
0);
           
contentResolver.update(item, values, null, null);
        }
        ToastMsg(
"갤러리에 파일을 저장하였습니다.");
    }
catch (FileNotFoundException e) {
        e.printStackTrace();
        ToastMsg(e.getMessage());
    }
catch (IOException e) {
        e.printStackTrace();
        ToastMsg(e.getMessage());
    }
}
 

위 수정사항이 모두 반영된 MainActivity 자바코드는 아래와 같습니다.


package com.example.myapplication;

import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AppCompatActivity;

import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.provider.MediaStore;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.Toast;

import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

public class MainActivity extends AppCompatActivity {

   
// 레이아웃의 버튼과 이미지뷰를 연결할 변수 선언
   
private Button btn_picture;
   
private ImageView imageView;
   
public Button btn_save;

   
// 그림을 받아올 비트맵 변수를 선언
   
private Bitmap bitmap =null;

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

       
imageView = findViewById(R.id.imageView);
       
btn_picture = findViewById(R.id.btn_picture);
       
btn_save = findViewById(R.id.btn_save);
       
btn_save.setVisibility(View.INVISIBLE);

       
btn_picture.setOnClickListener(new View.OnClickListener() {
           
@Override
           
public void onClick(View view) {
                
camera_app.launch(new Intent(
                   
MediaStore.ACTION_IMAGE_CAPTURE
               
));
            }
        });

       
btn_save.setOnClickListener(new View.OnClickListener() {
           
@Override
           
public void onClick(View view) {
               
String filename="photo.JPG";
                saveFile(
filename);
            }
        });
    }

   
ActivityResultLauncher<Intent> camera_app =
        registerForActivityResult(
           
new ActivityResultContracts.StartActivityForResult(),
           
new ActivityResultCallback<ActivityResult>() {
               
@Override
               
public void onActivityResult(ActivityResult result) {
                   
if( result.getResultCode() == RESULT_OK
                      
&& result.getData() != null){
                       
Bundle extras = result.getData().getExtras();
                       
bitmap = (Bitmap) extras.get("data");
                       
imageView.setImageBitmap(bitmap);
                       
btn_save.setVisibility(View.VISIBLE);
                    }
                }
            }
        );

   
public byte[] bitmapToByteArray( Bitmap $bitmap ) {
       
ByteArrayOutputStream stream = new ByteArrayOutputStream() ;
        $bitmap.compress(
Bitmap.CompressFormat.JPEG, 100, stream) ;
       
return stream.toByteArray()  ;
    }

   
private void ToastMsg(String msg)
    {
       
Toast.makeText(
           
this.getApplicationContext(),
            msg,
           
Toast.LENGTH_SHORT).show();
    }

   
private void saveFile(String filename)
    {
       
if(bitmap == null)
        {
            ToastMsg(
"먼저 촬영을 하세요");
           
return;
        }

       
ContentValues values = new ContentValues();
       
values.put(
           
MediaStore.Images.Media.DISPLAY_NAME,
            filename);
       
values.put(
           
MediaStore.Images.Media.MIME_TYPE,
           
"image/*");

       
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
           
values.put(
               
MediaStore.Images.Media.IS_PENDING,
               
1);
        }

       
ContentResolver contentResolver = getContentResolver();
       
Uri item = contentResolver.insert(
           
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
           
values);

       
try {
           
ParcelFileDescriptor pdf =
               
contentResolver.openFileDescriptor(
                   
item,
                   
"w",
                   
null);

           
if (pdf == null) {
                ToastMsg(
"파일 디스크립션 생성에 실패하였습니다.");
                
return;
            }
           
byte[] strToByte = bitmapToByteArray(bitmap);
           
FileOutputStream fos = new FileOutputStream(pdf.getFileDescriptor());
           
fos.write(strToByte);
           
fos.close();

           
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
               
values.clear();
               
values.put(
                   
MediaStore.Images.Media.IS_PENDING,
                   
0);
               
contentResolver.update(item, values, null, null);
            }
            ToastMsg(
"갤러리에 파일을 저장하였습니다.");
        }
catch (FileNotFoundException e) {
            e.printStackTrace();
            ToastMsg(e.getMessage());
        }
catch (IOException e) {
            e.printStackTrace();
            ToastMsg(e.getMessage());
        }
    }
}


5. 앱 실행!

앱을 실행하면 촬영 버튼이 나오는 것은 지난번과 동일합니다.

그리고 스마트폰 기본 카메라 앱이 실행되면 촬영해서 확인 버튼을 누르는 부분도 동일한데요.

최종결과 화면에서 갑자기 없었던 저장 버튼이 짜잔~ 하고 나타나는 부분이 다릅니다.
저장 버튼을 터치하면 사진이 저장되지요.

아직은 만족스럽지 못한건 화질이 떨어진다는 것인데요.

앱의 내부 저장공간을 오픈해서 사진을 찍는 방법을 사용하면 고해상도 사진을 얻을 수 있다고 합니다.
기본 카메라 앱을 실행하지 않고 직접 카메라 찍는 기능을 앱에 내장하는 방법도 있다고 하는데요.

모바일웹과 사용 방법이 달라서 그렇지 정주행해서 학습하다 보면 분명 할 수 있을거라 생각됩니다 :)

카메라는 이쯤에서 마무리하고 나중에 더 고~오급 기능을 다뤄보겠습니다.
우선 기본 과정부터 정주행하면서 말이지요.


오늘도 방문해주시는 모든 분들께 감사드립니다.
크리스마스가 얼마 남지 않았군요.
모든 분들깨 미~리 크리스마스~ 입니다 !