※ 이 게시글은 크레이의 IT개발 관련 성장기를 다루고 있습니다. 관련지식이 약간 있어야 이해되실 수 있습니다. 가벼운 마음으로 읽어보시면서 흥미가 생기고 의욕이 생긴다면? 개발자의 자질이 있으신 겁니다 :)
- 안드로이드 스튜디오 돌핀 2021. 3. 1 버전을 사용중입니다. -
지난 시간에는 카메라로 사진을 촬영하고 앱에 불러오는 부분을 진행했었는데요.
https://itadventure.tistory.com/583
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. 앱 실행!
앱을 실행하면 촬영 버튼이 나오는 것은 지난번과 동일합니다.
그리고 스마트폰 기본 카메라 앱이 실행되면 촬영해서 확인 버튼을 누르는 부분도 동일한데요.
최종결과 화면에서 갑자기 없었던 저장 버튼이 짜잔~ 하고 나타나는 부분이 다릅니다.
저장 버튼을 터치하면 사진이 저장되지요.
아직은 만족스럽지 못한건 화질이 떨어진다는 것인데요.
앱의 내부 저장공간을 오픈해서 사진을 찍는 방법을 사용하면 고해상도 사진을 얻을 수 있다고 합니다.
기본 카메라 앱을 실행하지 않고 직접 카메라 찍는 기능을 앱에 내장하는 방법도 있다고 하는데요.
모바일웹과 사용 방법이 달라서 그렇지 정주행해서 학습하다 보면 분명 할 수 있을거라 생각됩니다 :)
카메라는 이쯤에서 마무리하고 나중에 더 고~오급 기능을 다뤄보겠습니다.
우선 기본 과정부터 정주행하면서 말이지요.
오늘도 방문해주시는 모든 분들께 감사드립니다.
크리스마스가 얼마 남지 않았군요.
모든 분들깨 미~리 크리스마스~ 입니다 !
'코딩과 알고리즘' 카테고리의 다른 글
크레이의 앱개발 도전 #6. 프래그먼트?(Fragment) 와우~ (0) | 2022.12.06 |
---|---|
크레이의 앱개발 도전기 #5. 리싸이클뷰 (0) | 2022.12.05 |
크레이의 앱개발 도전기 #3. startActivityForResult 가 없어졌다구?! (2) | 2022.11.26 |
크레이의 앱개발 도전기 #2. 네비게이션 메뉴 (2) | 2022.11.21 |
웹퐁 파이스크립트 PDF 책 (0) | 2022.11.20 |