99일차(2)/Android App(65) : 카메라 앱으로 사진 촬영, 저장(2)
2023.03.03 - [국비교육] - 99일차(1)/Android App(64) : 카메라 앱으로 사진 촬영, 저장
- 카메라 앱으로 찍은 사진을 가져오는 방법(2가지)은 이전 게시물에서!
- 카메라 앱으로 찍은 이미지 확대/축소, 사진 방향 회전하지 않도록 처리하기
- ImageView를 가공해서 위와 같은 작업을 하는 ImageView를 만들 수 있다.
TouchImageView 자바클래스 생성
package com.example.step25imagecapture;
import android.content.Context;
import android.graphics.Matrix;
import android.graphics.PointF;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
import androidx.appcompat.widget.AppCompatImageView;
/* 터치 입력에 반응하는 이미지뷰 만들기 */
public class TouchImageView extends AppCompatImageView {
Matrix matrix;
// We can be in one of these 3 states
static final int NONE = 0;
static final int DRAG = 1;
static final int ZOOM = 2;
int mode = NONE;
// Remember some things for zooming
PointF last = new PointF();
PointF start = new PointF();
float minScale = 1f;
float maxScale = 3f;
float[] m;
int viewWidth, viewHeight;
static final int CLICK = 3;
float saveScale = 1f;
protected float origWidth, origHeight;
int oldMeasuredWidth, oldMeasuredHeight;
ScaleGestureDetector mScaleDetector;
Context context;
public TouchImageView(Context context) {
super(context);
sharedConstructing(context);
}
public TouchImageView(Context context, AttributeSet attrs) {
super(context, attrs);
sharedConstructing(context);
}
private void sharedConstructing(Context context) {
super.setClickable(true);
this.context = context;
mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());
matrix = new Matrix();
m = new float[9];
setImageMatrix(matrix);
setScaleType(ScaleType.MATRIX);
setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
mScaleDetector.onTouchEvent(event);
PointF curr = new PointF(event.getX(), event.getY());
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
last.set(curr);
start.set(last);
mode = DRAG;
break;
case MotionEvent.ACTION_MOVE:
if (mode == DRAG) {
float deltaX = curr.x - last.x;
float deltaY = curr.y - last.y;
float fixTransX = getFixDragTrans(deltaX, viewWidth, origWidth * saveScale);
float fixTransY = getFixDragTrans(deltaY, viewHeight, origHeight * saveScale);
matrix.postTranslate(fixTransX, fixTransY);
fixTrans();
last.set(curr.x, curr.y);
}
break;
case MotionEvent.ACTION_UP:
mode = NONE;
int xDiff = (int) Math.abs(curr.x - start.x);
int yDiff = (int) Math.abs(curr.y - start.y);
if (xDiff < CLICK && yDiff < CLICK)
performClick();
break;
case MotionEvent.ACTION_POINTER_UP:
mode = NONE;
break;
}
setImageMatrix(matrix);
invalidate();
return true; // indicate event was handled
}
});
}
public void setMaxZoom(float x) {
maxScale = x;
}
private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
mode = ZOOM;
return true;
}
@Override
public boolean onScale(ScaleGestureDetector detector) {
float mScaleFactor = detector.getScaleFactor();
float origScale = saveScale;
saveScale *= mScaleFactor;
if (saveScale > maxScale) {
saveScale = maxScale;
mScaleFactor = maxScale / origScale;
} else if (saveScale < minScale) {
saveScale = minScale;
mScaleFactor = minScale / origScale;
}
if (origWidth * saveScale <= viewWidth || origHeight * saveScale <= viewHeight)
matrix.postScale(mScaleFactor, mScaleFactor, viewWidth / 2, viewHeight / 2);
else
matrix.postScale(mScaleFactor, mScaleFactor, detector.getFocusX(), detector.getFocusY());
fixTrans();
return true;
}
}
void fixTrans() {
matrix.getValues(m);
float transX = m[Matrix.MTRANS_X];
float transY = m[Matrix.MTRANS_Y];
float fixTransX = getFixTrans(transX, viewWidth, origWidth * saveScale);
float fixTransY = getFixTrans(transY, viewHeight, origHeight * saveScale);
if (fixTransX != 0 || fixTransY != 0)
matrix.postTranslate(fixTransX, fixTransY);
}
float getFixTrans(float trans, float viewSize, float contentSize) {
float minTrans, maxTrans;
if (contentSize <= viewSize) {
minTrans = 0;
maxTrans = viewSize - contentSize;
} else {
minTrans = viewSize - contentSize;
maxTrans = 0;
}
if (trans < minTrans)
return -trans + minTrans;
if (trans > maxTrans)
return -trans + maxTrans;
return 0;
}
float getFixDragTrans(float delta, float viewSize, float contentSize) {
if (contentSize <= viewSize) {
return 0;
}
return delta;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
viewWidth = MeasureSpec.getSize(widthMeasureSpec);
viewHeight = MeasureSpec.getSize(heightMeasureSpec);
//
// Rescales image on rotation
//
if (oldMeasuredHeight == viewWidth && oldMeasuredHeight == viewHeight
|| viewWidth == 0 || viewHeight == 0)
return;
oldMeasuredHeight = viewHeight;
oldMeasuredWidth = viewWidth;
if (saveScale == 1) {
//Fit to screen.
float scale;
Drawable drawable = getDrawable();
if (drawable == null || drawable.getIntrinsicWidth() == 0 || drawable.getIntrinsicHeight() == 0)
return;
int bmWidth = drawable.getIntrinsicWidth();
int bmHeight = drawable.getIntrinsicHeight();
Log.d("bmSize", "bmWidth: " + bmWidth + " bmHeight : " + bmHeight);
float scaleX = (float) viewWidth / (float) bmWidth;
float scaleY = (float) viewHeight / (float) bmHeight;
scale = Math.min(scaleX, scaleY);
matrix.setScale(scale, scale);
// Center the image
float redundantYSpace = (float) viewHeight - (scale * (float) bmHeight);
float redundantXSpace = (float) viewWidth - (scale * (float) bmWidth);
redundantYSpace /= (float) 2;
redundantXSpace /= (float) 2;
matrix.postTranslate(redundantXSpace, redundantYSpace);
origWidth = viewWidth - 2 * redundantXSpace;
origHeight = viewHeight - 2 * redundantYSpace;
setImageMatrix(matrix);
}
fixTrans();
}
}
- 터치 입력에 반응하는 이미지뷰 만들기(코드를 가져와서 사용했다.)
- AppCompatImageView 를 상속했다.
- 이미지를 터치했을 때 어떤 수학적인 계산을 해서 이벤트가 일어난다.
- 사용자의 터치를 파악하고 확대했는지 축소했는지 확인한다.
- 그냥 터치가 되는 기능을 가진 ImageView를 만들었다고 생각하면 된다.
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"
tools:context=".MainActivity"
android:orientation="vertical">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="사진찍기"
android:id="@+id/takePicture"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="사진찍기(원본사진)"
android:id="@+id/takePicture2"/>
<com.example.step25imagecapture.TouchImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/imageView"/>
</LinearLayout>
- 레이아웃에서 새로 만든 TouchImageView로 imageView를 바꾸어준다.
- 따로 만들어둔 클래스를 사용할 경우 패키지명을 붙여서 적어주면 된다.
- 터치이미지뷰로 바꿔주기
- 가상기기에서는 ctrl 을 누른 상태로 움직이면 된다.
- 촬영한 이미지가 확대/축소되고, 위치 이동이 가능하다.
- 이전에는 가로/세로로 찍은 사진이 imageView에서 세로/가로로 회전되는 경우가 있었는데,
사진이 회전되지 않도록 출력하는 기능 추가
MainActivity 수정
package com.example.step25imagecapture;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.media.ExifInterface;
import android.net.Uri;
import android.os.Bundle;
import android.provider.MediaStore;
import android.util.Log;
import android.widget.Button;
import android.widget.ImageView;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.FileProvider;
import java.io.File;
import java.io.IOException;
import java.util.UUID;
public class MainActivity extends AppCompatActivity {
ImageView imageView;
//저장된 이미지의 전체 경로
String imagePath;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//사진을 출력할 ImageView 의 참조값 필드에 저장하기
imageView=findViewById(R.id.imageView);
Button takePicture=findViewById(R.id.takePicture);
takePicture.setOnClickListener(v->{
//사진을 찍고 싶다는 Intent 객체 작성하기
Intent intent=new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
//운영체제에 해당 인턴트를 처리할수 있는 App 을 실행시켜 달라고 하고 결과 값도 받아올수 있도록 한다.
startActivityForResult(intent, 0);
});
Button takePicutre2=findViewById(R.id.takePicture2);
takePicutre2.setOnClickListener(v->{
//사진을 찍고 싶다는 Intent 객체 작성하기
Intent intent=new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
//외부 저장 장치의 절대 경로
String absolutePath=getExternalFilesDir(null).getAbsolutePath();
//파일명 구성
String fileName= UUID.randomUUID().toString()+".jpg";
//생성할 이미지의 전체 경로
imagePath=absolutePath+"/"+fileName;
//이미지 파일을 저장할 File 객체
File photoFile=new File(imagePath);
//File 객체를 Uri 로 포장을 한다.
//Uri uri= Uri.fromFile(photoFile);
Uri uri=FileProvider.getUriForFile(this,
"com.example.step25imagecapture.fileprovider",
photoFile);
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
startActivityForResult(intent, 1);
});
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
//만일 위에서 요청한 요청과 같고 결과가 성공적이라면
if(requestCode == 0 && resultCode == RESULT_OK){
//data Intent 객체에 결과값(섬네일 이미지 데이터) 가 들어 있다.
Bitmap image=(Bitmap)data.getExtras().get("data");
//ImageView 에 출력하기
imageView.setImageBitmap(image);
}else if(requestCode == 1 && resultCode == RESULT_OK){
//만일 여기가 실행된다면 imagePath 경로에 이미지 파일이 성공적으로 만들어진 것이다
//Bitmap image=BitmapFactory.decodeFile(imagePath);
//imageView.setImageBitmap(image);
fitToImageView(imageView, imagePath);
}
}
//이미지 뷰의 크기에 맞게 이미지를 출력하는 메소드
public static void fitToImageView(ImageView imageView, String absolutePath){
//출력할 이미지 뷰의 크기를 얻어온다.
int targetW = imageView.getWidth();
int targetH = imageView.getHeight();
// Get the dimensions of the bitmap
BitmapFactory.Options bmOptions = new BitmapFactory.Options();
bmOptions.inJustDecodeBounds = true;
BitmapFactory.decodeFile(absolutePath, bmOptions);
int photoW = bmOptions.outWidth;
int photoH = bmOptions.outHeight;
// Determine how much to scale down the image
int scaleFactor = Math.min(photoW/targetW, photoH/targetH);
// Decode the image file into a Bitmap sized to fill the View
bmOptions.inJustDecodeBounds = false;
bmOptions.inSampleSize = scaleFactor;
bmOptions.inPurgeable = true;
Bitmap bitmap = BitmapFactory.decodeFile(absolutePath, bmOptions);
/* 사진이 세로로 촬영했을때 회전하지 않도록 */
try {
ExifInterface ei = new ExifInterface(absolutePath);
int orientation = ei.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED);
switch(orientation) {
case ExifInterface.ORIENTATION_ROTATE_90:
bitmap = rotateImage(bitmap, 90);
break;
case ExifInterface.ORIENTATION_ROTATE_180:
bitmap = rotateImage(bitmap, 180);
break;
// etc.
}
}catch(IOException ie){
Log.e("####", ie.getMessage());
}
imageView.setImageBitmap(bitmap);
}
//Bitmap 이미지 회전시켜서 리턴하는 메소드
public static Bitmap rotateImage(Bitmap source, float angle) {
Bitmap retVal;
Matrix matrix = new Matrix();
matrix.postRotate(angle);
retVal = Bitmap.createBitmap(source, 0, 0, source.getWidth(), source.getHeight(), matrix, true);
return retVal;
}
}
- Matrix는 graphics 패키지에 있는 것을 import
- Matrix는 java의 행렬 알고리즘이다.
- 이 메소드에서 전달받은 경로(absolutePath)에 있는 사진을 인코딩해서 출력해준다.
- 가상기기에서는 화면을 돌릴 수 없어 확인이 되지 않지만, 이런 기능이 있다는 것을 확인..ㅎ
'국비교육(22-23)' 카테고리의 다른 글
102일차(1)/Android App(67) : 모바일 갤러리 기능 구현(1) (0) | 2023.03.08 |
---|---|
101일차(1)/Android App(66) : 카메라 앱으로 사진 촬영, 저장(3) / 서버 전송 (0) | 2023.03.07 |
99일차(1)/Android App(64) : 카메라 앱으로 사진 촬영, 저장 (0) | 2023.03.03 |
98일차(1)/Android App(63) : Internal, External Storage에서 읽기 (read) (1) | 2023.03.02 |
97일차(2)/Android App(62) : Internal, External Storage 저장 (write) (0) | 2023.02.25 |