QR코드를 이용한 스마트 출석 앱을 만들다가 (과제)

 

로그인 기능과 회원가입 기능에 대한 것을 포함하게 되어 끄적여봅니다.

우선 로그인 화면과 회원가입 화면입니다.

 

로그인 화면에서 username과 password는 데이터베이스에 저장되어 있으며 입력 문자가 너무 짧은 경우 토스트 메시지로 사용자에게 알려줍니다.

 

회원가입 화면에서도, username과 password가 너무 짧다면 거절당하고 password와 confirm이 다를 경우에도 가입이 되지 않습니다.

 

코드

MainActivity.java

 

package com.example.smartattendance;

import android.content.Context;
import android.content.Intent;
import android.database.sqlite.SQLiteDatabase;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.ToggleButton;

public class MainActivity extends AppCompatActivity {

Button button;

SQLiteDatabase newDB;

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

if (newDB == null) {
String dbName = "new";
openDatabase(dbName);
} else {
Toast.makeText(this, "test", Toast.LENGTH_LONG).show();
}

button = (Button) findViewById(R.id.main_button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(getApplicationContext(), LoginActivity.class);
startActivityForResult(intent, 101);
}
});
}

@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);

if (requestCode == 101 && resultCode == RESULT_OK) {
String userId = data.getStringExtra("userId");

Intent intent = new Intent(getApplicationContext(), SubjectActivity.class);
intent.putExtra("userId", userId);
startActivityForResult(intent, 102);
} else {
button.setText("로그인에 실패했습니다.\n 다시 로그인 해주세요");
}
}

public void openDatabase(String dbName) {
DatabaseHelper helper = new DatabaseHelper(this, dbName, null, 1);
newDB = helper.getWritableDatabase();
}
}

 

// onCreate()에서 데이터베이스가 없다면 새로 만들게 됩니다.

 

LoginActivity.java

 

package com.example.smartattendance;

import android.app.Activity;
import android.content.Intent;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

public class LoginActivity extends AppCompatActivity {

Button loginButton;
Button memberButton;

EditText id;
EditText password;

String userId;

boolean userExist;

SQLiteDatabase newDB;
DatabaseHelper helper;

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

helper = new DatabaseHelper(this, "new", null, 1);


loginButton = (Button) findViewById(R.id.login_button);
memberButton = (Button) findViewById(R.id.member_button);

id = (EditText) findViewById(R.id.id_input);
password = (EditText) findViewById(R.id.password_input);

loginButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
userExist = false;

String isId = id.getText().toString().trim();
String isPass = password.getText().toString().trim();
if (isId.length() > 4 && isPass.length() > 4)
searchData(isId, isPass);
else
Toast.makeText(LoginActivity.this, "입력이 잘못되었습니다.", Toast.LENGTH_SHORT);

if (userExist) {
Intent intent = new Intent();
intent.putExtra("userId", userId);
setResult(Activity.RESULT_OK, intent);
finish();
} else {
Toast.makeText(LoginActivity.this, "아이디 혹은 비밀번호가" +
"없거나 잘못되었습니다.", Toast.LENGTH_SHORT).show();
}
}
});

memberButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(getApplicationContext(), MemberActivity.class);
startActivityForResult(intent, 203);
}
});
}

public void searchData(String isId, String isPass) {
newDB = helper.getReadableDatabase();
String sql = ("select userId, password from test");
Cursor cursor = newDB.rawQuery(sql, null);

for (int i = 0; i < cursor.getCount(); i++) {
cursor.moveToNext();
String id = cursor.getString(0);
String password = cursor.getString(1);
if (id.equals(isId) && password.equals(isPass)) {
userId = id;
userExist = true;
break;
}
}
}
}

 

// 데이터베이스는 DatabaseHelper에서 관리합니다.

사용자의 입력을 데이터베이스에 있는 정보와 비교하고 로그인 성공과 실패를 결정합니다.

 

MemberActivity.java

 

package com.example.smartattendance;

import android.app.Activity;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;

import java.sql.Time;

public class MemberActivity extends AppCompatActivity {

EditText userId;
EditText userPassword;

SQLiteDatabase newDB;
DatabaseHelper helper;

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

helper = new DatabaseHelper(this, "new", null, 1);

userId = (EditText) findViewById(R.id.member_id);
userPassword = (EditText) findViewById(R.id.member_password);

Button joinButton = (Button) findViewById(R.id.join_button);
joinButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String id = userId.getText().toString().trim();
String password = userPassword.getText().toString().trim();

if (id.length() < 5 || password.length() < 5) {
Toast.makeText(MemberActivity.this, "아이디 다섯 글자 이상 \n" +
"비밀번호 다섯 글자 이상" +
"\n 입력해주세요.", Toast.LENGTH_LONG).show();
} else {
insertData(id, password);
setResult(Activity.RESULT_OK);
finish();
}
}
});

Button cancelButton = (Button) findViewById(R.id.cancel_button);
cancelButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
setResult(Activity.RESULT_CANCELED);
finish();
}
});
}

public void insertData(String id, String password) {
newDB = helper.getWritableDatabase();

String sql = ("insert into test(userId, password) values " +
"(" + "'" + id + "'" + "," + "'" + password + "'" + ")");

newDB.execSQL(sql);
}
}

 

// 조건을 만족한다면 아이디와 패스워드를 데이터베이스에 기록합니다. 각 테이블에서 두 데이터는 같은 id를 가집니다.

 

DatabaseHelper.java

 

package com.example.smartattendance;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

public class DatabaseHelper extends SQLiteOpenHelper {
public DatabaseHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
super(context, name, factory, version);
}

@Override
public void onCreate(SQLiteDatabase db) {

String userTable = "test";
db.execSQL("create table if not exists " + userTable + " ("
+ " _id integer PRIMARY KEY autoincrement, "
+ " userId text, "
+ " password text);");

String userSubjectTable = "test2";
db.execSQL("create table if not exists " + userSubjectTable + " ("
+ " _id integer PRIMARY KEY autoincrement, "
+ " userId text, "
+ " sub text, "
+ " day text, "
+ " start_time text, "
+ " finish_time text, "
+ " FOREIGN KEY (userId) REFERENCES test (userId));");

}

@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
onCreate(db);
}
}

 

마지막으로 DatabaseHelper 클래스입니다. 데이터베이스 생성. 데이터 기록밖에 하지 않기 때문에 간단하고 짧습니다.

 

부스트코스 안드로이드 프로그래밍에서 만들었던 DatabaseHelper에 비하면 십분의 일 수준이네요.

 

기능으로 묵는다면 LoginActivity.java의 searchData() 메소드를 DatabaseHelper로 옮기는 것도 괜찮겠네요.

일부 코드만 살펴보도록 하겠습니다.

 

서버 통신에 대한 자세한 정보는 구글링하면 많이 나오고, 저 같은 경우 파이썬과 연동하기 위해서 온갖 자료를 찾다가, 겨우 찾은 동영상에서 엄청난 도움을 받았습니다.

 

https://www.youtube.com/watch?v=b7VkbAUqMqMhttps://youtu.be/b7VkbAUqMqM

 

해당 링크입니다. 저는 Face Detect를 Measuring size of object로 바꿔 응용했을 뿐입니다.

 

1. 파일 선택, 저장을 위한 메소드

서버로 파일, 그러니까 사진을 전송하기 전 저장소에 접근하고 가져와야합니다.

 

private void onCamera() {
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);

if (intent.resolveActivity(getPackageManager()) != null) {
File photoFile = null;
try {
photoFile = saveImageFile();
} catch (IOException e) {
// Error occurred while creating the File
e.printStackTrace();
println("저장 실패");
}
// 파일 생성에 성공했을 경우에만 동작
if (photoFile != null) {
Uri photoURI = FileProvider.getUriForFile(this,
"com.example.upload.fileprovider",
photoFile);
intent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);
startActivityForResult(intent, CAMERA_REQUEST);
}
}
}

 

//이미지 파일을 내부 저장소에 저장하는 메소드
private File saveImageFile() throws IOException {
File storageDir = new File("/sdcard/DCIM/Camera");
println("sdir : " + storageDir.toString());

current = System.currentTimeMillis();
date = new Date(current);
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_hhmmss");
String name = sdf.format(date);

File image = File.createTempFile(
name, /* prefix */
".jpg", /* suffix */
storageDir /* directory */
);

currentImagePath = image.getAbsolutePath();

return image;
}

 

onCamera는 앱에서 카메라를 사용하는 메소드입니다. 실패했을 경우 log를 남깁니다.

try구문에서 saveImageFile() 메소드로 이동하게 되는데 이것은 이미지 파일을 저장하는 메소드입니다.

 

/sdcard/DCIM/Camera 경로에 name(현재시간)으로 파일을 저장합니다.

 

2. 사진을 서버에 업로드

private void uploadImage() {
if (selectedFileUri != null) {
dialog = new ProgressDialog(MeasureActivity.this);
dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
dialog.setMessage("Uploading....");
dialog.setIndeterminate(false);
dialog.setMax(100);
dialog.setCancelable(false);
dialog.show();

File file = null;
try {
file = new File(Common.getFilePath(this, selectedFileUri));
} catch (URISyntaxException e) {
e.printStackTrace();
}

if (file != null) {
final ProgressRequestBody requestBody = new ProgressRequestBody(file, this);

final MultipartBody.Part body = MultipartBody.Part.createFormData("image", file.getName(), requestBody);

new Thread(new Runnable() {
@Override
public void run() {
mService.uploadFile(body)
.enqueue(new Callback<String>() {
@Override
public void onResponse(Call<String> call, Response<String> response) {
dialog.dismiss();

String image_processed_link;

image_processed_link = new StringBuffer("http://10.11.6.53:8000/" +
response.body().replace("\\", "")).toString();


String[] str = image_processed_link.split("\"");
String imageLink = "";
for (int i = 0; i < str.length; i++)
imageLink += str[i];

Picasso.get().load(imageLink).into(imageView);
basicsImage.setImageBitmap(null);

}

@Override
public void onFailure(Call<String> call, Throwable t) {
dialog.dismiss();
t.printStackTrace();
Toast.makeText(MeasureActivity.this, "" + t.getMessage(), Toast.LENGTH_SHORT).show();
}
});
}
}).start();
}
} else {
Toast.makeText(this, "Cannot upload this file!!", Toast.LENGTH_SHORT).show();
}
}

 

업로드 버튼을 눌렀을 때 동작하는 메소드입니다.

ImageView에 이미지가 있을 경우 업로드를 시도하며, dialog로 확인할 수 있습니다. 하지만 제대로 동작한다면 dialog는 보이지 않을 정도로 순식간에 지나가고, 문제가 있을 경우 dialog 화면이 지속되다가 오류 메시지를 보게 됩니다.

 

중요한 부분은

 

if (file != null) {
final ProgressRequestBody requestBody = new ProgressRequestBody(file, this);

 

부터입니다. 스레드가 실행되면서 mService.uploadFile(업로드API)가 body를 변수로 받아 서버로 보내면

응답(response)을 받게 되는데 그것을 Picasso를 이용하여 imageView에 set합니다.

 

여기서 주의할 점은 영상을 따라가다 보면 RetrofitClient 클래스를 만들게 되는데

해당 클래스의 Retrofit의 baseUrI와 uploadImage()의 image_processed_link의 Uri가 같아야 한다는 겁니다.

 

둘이 다르면 절대로 동작하지 않습니다. 추가로 설명하면 영상에서는 10.0.2.2를 사용하는데, 실제 디바이스의 경우 PC의 IP를 사용해야합니다.

 

또 하나 주의할 점은 이 코드는 http를 기준으로 동작한다는 겁니다. 서버가 https일 경우 오류가 발생할 겁니다.

 

 

다른 코드의 경우 링크 영상에 전부 있는 내용이라 생략하겠습니다.

개요

사물의 사이즈를 측정하는 앱 만들기

내용

규격화 된 물체(A4용지, 동전 등)의 사이즈를 이용하여 다른 물건의 사이즈 측정.

 

본래 구글 AR줄자 같은 앱을 만들려고 했지만 제 수준으로는 불가능한 일이었습니다...

결국 차선책으로 opencv를 사용하여 크기를 측정하는 python 코드를 안드로이드 앱과 연동하는 것을 선택했습니다.

(이는 팀 프로젝트이지만 모든 코드와 디자인을 제가 짰다고 보시면 됩니다.)

 

간략한 구조는 이렇습니다.

 

1. 파이썬 flask를 이용하여 만든 간단한 서버를 오픈한다.

2. 모바일 앱에서 조건에 맞는 사진을 서버로 전송한다.

3. opencv를 이용하여 사진에 있는 물건의 사이즈를 측정한다.

4. 측정된 사진을 서버에서 모바일 앱으로 전송한다.

 

opencv를 사용하여 물체의 크기를 측정 - 이곳의 오픈 소스를 사용했습니다.

https://www.pyimagesearch.com/2016/03/28/measuring-size-of-objects-in-an-image-with-opencv/

 

Measuring size of objects in an image with OpenCV - PyImageSearch

Today, I'll demonstrate how you can compute the size of objects in an image using OpenCV, Python, and computer vision + image processing techniques.

www.pyimagesearch.com

 

오픈소스를 19번 라인(ap = argparse.ArgumentParser())에서 118번 라인(cv2.waitKey(0))까지 함수로 만든 후

return하도록 변경했고, 추가로 flask를 사용하여 간단한 서버를 구현했습니다.

 

앱에서는 http통신을 위하여 RetrofitClient를 사용했으며 이것으로 서버에 접속했습니다.

 

자세한 코드는 다음 글에 소개하겠습니다.

 

성능

성능 자체는 좋았습니다. 사진의 각도에 따라 조금씩 다르지만 큰 오차를 보이지 않았습니다. 오차는 실제 크기에서 대략 ±3cm 수준입니다. 하지만 단점이 너무 명확하기 때문에 실제로 사용하기엔 힘들 것으로 보입니다.

 

단점

1. 먼저 규격화된 물건이 필요하다.

아래 before - after를 보면 창문 좌측에 검은색 A4용지가 붙어있음을 확인할 수 있습니다. 네, 바로 그것이 규격화된 물건입니다. 앞에 명시한 링크에서도 확인할 수 있듯 기본적으로 좌측 상단에 기준 물체가 있어야만 합니다. 또한 기준 물체의 사이즈를 알고 있어야 합니다.

 

이는 매우 치명적인 단점으로 앱이 가질 수 있는 편리함을 없앤다고 볼 수 있습니다.

 

2. flask를 사용한다는 것

flask를 사용했기 때문에 서버의 ip주소와 모바일에서 사용하는 네트워크의 ip주소가 일치해야만 합니다. 같은 네트워크(와이파이 등)에 접속한 상태가 아닌, 서로 다른 네트워크를 사용하고 있다면 모바일 앱은 서버에 접속할 수 없습니다.

 

이는 톰캣 등으로 서버를 새로 구현한다면 해결이 가능한 문제로 보입니다.

 

3. 이미지 인식 불안정

측정하려는 사진(이미지)가 어둡거나 선명하지 않다면 제대로 측정되지 않습니다. 실제 스마트폰에서 즉석으로 사진을 찍어 실험한 결과 사진이 조금 어둡거나 좌측 상단의 배경이 조금 다르다면 기준 물체를 잘못 인식하는 경우가 발생합니다.

어느 정도 완성된 모습

 

Splash
Allow
Main
Measure
before
after

 

수식을 계산할 때 곱셈과 덧셈 중 곱셈을 먼저하는 등, 우선순위가 있는 기호들이 있다는 건 기본이죠.그런데 그것을 알고리즘으로 구현하려고 하니까, 쉽게 되지 않고 코드만 복잡해지고 결과는 실패하는 일이 많았습니다.

 

그때 떠오른 것이 바로 자료구조!!

 

자료구조 수업을 들었던 게 몇 년 전인데 신기하게 떠오르더군요. 자료구조에서 후위표기식이라는 것이 있는데, 그것을 적용하여 입력을 계산하니 옳바른 결과가 나왔습니다.

 

후위표기식에 대해서 더 자세히 알아보시려면 아래를 참고하세요.

https://jamanbbo.tistory.com/54

 

[Stack]사칙연산 계산기 구현(2) - 후위 표기 수식 계산

저번 포스팅에서는 사칙연산 계산기 프로그램을 만들기 위한 중위 표기식을 후위 표기법을 이용해 수식을 표현하는 방법을 알아보았다. [Stack]사칙연산 계산기 구현(1) - 후위 표기법 이제 후위 표기 수식을 계산..

jamanbbo.tistory.com

 

코드

CalculateHelper.java

 

package com.example.calculator;

import android.util.Log;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Stack;

//https://jamanbbo.tistory.com/54의 소스코드를 참조, 수정하였음
public class CalculateHelper {
public static double num1;
public static double num2;
public static double resultNumber;

//사용자의 input을 각각 구분하여 ArrayList에 저장하는 메소드
private ArrayList splitTokens(String equation) {
String[] constant = equation.split(" "); //공백을 기준

ArrayList constantList = new ArrayList();
double number = 0;

boolean flag = false;
for (String data : constant) {
if (data.equals(" ")) {
continue;
}
if (checkNumber(data)) {
number = number * 10 + Double.parseDouble(data);
flag = true;
} else {
if (flag) {
constantList.add(number);
number = 0;
}
flag = false;
constantList.add(data);
}
}

if (flag) {
constantList.add(number);
}

return constantList;
}

//후위 표기식으로 변형
private ArrayList infixToPostfix(ArrayList constant) {
ArrayList result = new ArrayList();
HashMap level = new HashMap();
Stack stack = new Stack();

 

//각 기호의 우선순위 레벨. 곱하기, 나누기 > 더하기, 빼기 > 기타
level.put("*", 3);
level.put("/", 3);
level.put("+", 2);
level.put("-", 2);
level.put("(", 1);

for (Object object : constant) {
if (object.equals("(")) {
stack.push(object);
} else if (object.equals(")")) {
while (!stack.peek().equals("(")) {
Object val = stack.pop();
if (!val.equals("(")) {
result.add(val);
}
}
stack.pop();
} else if (level.containsKey(object)) {
if (stack.isEmpty()) {
stack.push(object);
} else {
if (Double.parseDouble(level.get(stack.peek()).toString()) >= Double.parseDouble(level.get(object).toString())) {
result.add(stack.pop());
stack.push(object);
} else {
stack.push(object);
}
}
} else {
result.add(object);
}
}

while (!stack.isEmpty()) {
result.add(stack.pop());
}

return result;
}

//후위 표기식을 계산
private Double postFixEval(ArrayList expr) {
Stack numberStack = new Stack();
for (Object o : expr) {
if (o instanceof Double) {
numberStack.push(o);
} else if (o.equals("+")) {
num1 = (Double) numberStack.pop();
num2 = (Double) numberStack.pop();
numberStack.push(num2 + num1);
} else if (o.equals("-")) {
num1 = (Double) numberStack.pop();
num2 = (Double) numberStack.pop();
numberStack.push(num2 - num1);
} else if (o.equals("*")) {
num1 = (Double) numberStack.pop();
num2 = (Double) numberStack.pop();
numberStack.push(num2 * num1);
} else if (o.equals("/")) {
num1 = (Double) numberStack.pop();
num2 = (Double) numberStack.pop();
numberStack.push(num2 / num1);
}
}

resultNumber = (Double) numberStack.pop();

return resultNumber;
}

public Double process(String equation) {
ArrayList postfix = infixToPostfix(splitTokens(equation));
Double result = postFixEval(postfix);
return result;
}

public boolean checkNumber(String str) {
char check;

if (str.equals(""))
return false;

for (int i = 0; i < str.length(); i++) {
check = str.charAt(i);
if (check < 48 || check > 58) {
if (check != '.')
return false;
}
}
return true;
}
}

 

설명

각 메소드에 대한 것은 주석을 읽어주세요!

 

크게 복잡한 것은 없습니다. MainActivity에서 사용자의 입력이 기록된 textView의 텍스트를 CalculateHelper로 전달하면, CalculateHelper에서는 그것을 후위표기식으로 변환 후 계산까지 하고 MainActivity로 리턴합니다.

 

궁금하신 점이 있으시다면 댓글로 남겨주세요.

 

 

xml코드

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

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

<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1">

<TextView
android:id="@+id/first_textView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="10dp"
android:text=""
android:textSize="30dp" />
</ScrollView>

<TextView
android:id="@+id/second_textView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="10dp"
android:layout_weight="2"
android:text=""
android:textSize="30dp" />

</LinearLayout>

<GridLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:columnCount="4"
android:orientation="horizontal"
android:rowCount="5">

<Button
android:id="@+id/clear"
android:layout_rowWeight="1"
android:layout_columnWeight="1"
android:layout_margin="-5dp"
android:text="C" />

<Button
android:id="@+id/bracket"
android:layout_rowWeight="1"
android:layout_columnWeight="1"
android:layout_margin="-5dp"
android:text="( )" />

<Button
android:id="@+id/percent"
android:layout_rowWeight="1"
android:layout_columnWeight="1"
android:layout_margin="-5dp"
android:text="%" />

<Button
android:id="@+id/div"
android:layout_rowWeight="1"
android:layout_columnWeight="1"
android:layout_margin="-5dp"
android:text="÷" />


<Button
android:id="@+id/num7"
android:layout_rowWeight="1"
android:layout_columnWeight="1"
android:layout_margin="-5dp"
android:text="7" />

<Button
android:id="@+id/num8"
android:layout_rowWeight="1"
android:layout_columnWeight="1"
android:layout_margin="-5dp"
android:text="8" />

<Button
android:id="@+id/num9"
android:layout_rowWeight="1"
android:layout_columnWeight="1"
android:layout_margin="-5dp"
android:text="9" />

<Button
android:id="@+id/mul"
android:layout_rowWeight="1"
android:layout_columnWeight="1"
android:layout_margin="-5dp"
android:text="X" />


<Button
android:id="@+id/num4"
android:layout_rowWeight="1"
android:layout_columnWeight="1"
android:layout_margin="-5dp"
android:text="4" />

<Button
android:id="@+id/num5"
android:layout_rowWeight="1"
android:layout_columnWeight="1"
android:layout_margin="-5dp"
android:text="5" />

<Button
android:id="@+id/num6"
android:layout_rowWeight="1"
android:layout_columnWeight="1"
android:layout_margin="-5dp"
android:text="6" />

<Button
android:id="@+id/sub"
android:layout_rowWeight="1"
android:layout_columnWeight="1"
android:layout_margin="-5dp"
android:text="-" />


<Button
android:id="@+id/num1"
android:layout_rowWeight="1"
android:layout_columnWeight="1"
android:layout_margin="-5dp"
android:text="1" />

<Button
android:id="@+id/num2"
android:layout_rowWeight="1"
android:layout_columnWeight="1"
android:layout_margin="-5dp"
android:text="2" />

<Button
android:id="@+id/num3"
android:layout_rowWeight="1"
android:layout_columnWeight="1"
android:layout_margin="-5dp"
android:text="3" />

<Button
android:id="@+id/add"
android:layout_rowWeight="1"
android:layout_columnWeight="1"
android:layout_margin="-5dp"
android:text="+" />


<Button
android:id="@+id/back"
android:layout_rowWeight="1"
android:layout_columnWeight="1"
android:layout_margin="-5dp"
android:text="back" />

<Button
android:id="@+id/num0"
android:layout_rowWeight="1"
android:layout_columnWeight="1"
android:layout_margin="-5dp"
android:text="0" />

<Button
android:id="@+id/dot"
android:layout_rowWeight="1"
android:layout_columnWeight="1"
android:layout_margin="-5dp"
android:text="." />

<Button
android:id="@+id/equal"
android:layout_rowWeight="1"
android:layout_columnWeight="1"
android:layout_margin="-5dp"
android:text="=" />

</GridLayout>
</LinearLayout>

이번에는 안드로이드 스튜디오를 이용해서 만든 간단한 계산기 앱을 보여드리려고 합니다.

 

원래 부스트코스 에이스 과정 중 기업연계 프로젝트로 진행한 주제였는데, ReactNative로 만들어야 하는 걸 제가 시간이 없는 관계로 이틀 동안 스튜디오로 만들었습니다. (하루 정도 코드를 rn으로 변환해보려고 했는데 쉽지가 않아서 포기했습니다. rn을 안드로이드 코드로 바꾸는 것보다 안드로이드 코드를 rn으로 바꾸는 게 더 어렵다는 자료를 본 거 같아요.)

 

먼저 완성된 것부터 보여드리고 코드로 넘어가겠습니다.

 

 

실행 예시

개요

더하기, 빼기, 곱하기, 나누기, 괄호 등으로 구성된 간단한 계산기 구현

 

구성

다른 앱과 기본 계산기 앱 등을 참고하여 사용자의 입력을 표기할 상단 레이아웃, 계산식에 필요한 20개의 버튼이 있는 하단 레이아웃으로 구성.

 

상단 레이아웃 : 사용자 입력을 표기할 텍스트뷰(+스크롤뷰) + 계산 결과 미리보기를 제공하는 텍스트뷰

하단 레이아웃 : 그리드 레이아웃과 거기에 속한 20개의 버튼

 

코드

MainAcitivity.java

 

package com.example.calculator;

import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {
CalculateHelper calculateHelper;

boolean isDot;
boolean isBracket;
boolean isPreview;

TextView textView;
TextView textView2;

int size;

String result;

Button num0;
Button num1;
Button num2;
Button num3;
Button num4;
Button num5;
Button num6;
Button num7;
Button num8;
Button num9;

Button add;
Button sub;
Button mul;
Button div;
Button clear;
Button bracket;
Button percent;
Button back;
Button dot;

Button equal;

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

calculateHelper = new CalculateHelper();

size = 0;
int number = 25;
int t = String.valueOf(Math.sqrt(number)).length();
Log.d("test", "" + t + " ? " + String.valueOf(Math.sqrt(number)));

isPreview = false;
isBracket = false;
isDot = false;

int[][] test = new int[5][4];
setButton();
setTextView();
}

private void setButton() {
num0 = findViewById(R.id.num0);
num1 = findViewById(R.id.num1);
num2 = findViewById(R.id.num2);
num3 = findViewById(R.id.num3);
num4 = findViewById(R.id.num4);
num5 = findViewById(R.id.num5);
num6 = findViewById(R.id.num6);
num7 = findViewById(R.id.num7);
num8 = findViewById(R.id.num8);
num9 = findViewById(R.id.num9);

add = findViewById(R.id.add);
sub = findViewById(R.id.sub);
mul = findViewById(R.id.mul);
div = findViewById(R.id.div);
clear = findViewById(R.id.clear);
bracket = findViewById(R.id.bracket);
percent = findViewById(R.id.percent);
back = findViewById(R.id.back);
dot = findViewById(R.id.dot);

equal = findViewById(R.id.equal);

num0.setOnClickListener(numClickListener);
num1.setOnClickListener(numClickListener);
num2.setOnClickListener(numClickListener);
num3.setOnClickListener(numClickListener);
num4.setOnClickListener(numClickListener);
num5.setOnClickListener(numClickListener);
num6.setOnClickListener(numClickListener);
num7.setOnClickListener(numClickListener);
num8.setOnClickListener(numClickListener);
num9.setOnClickListener(numClickListener);

add.setOnClickListener(markClickListener);
sub.setOnClickListener(markClickListener);
mul.setOnClickListener(markClickListener);
div.setOnClickListener(markClickListener);
clear.setOnClickListener(markClickListener);
bracket.setOnClickListener(markClickListener);
percent.setOnClickListener(markClickListener);
back.setOnClickListener(markClickListener);
dot.setOnClickListener(markClickListener);

equal.setOnClickListener(markClickListener);
}

//숫자 버튼이 눌렸을 경우
private Button.OnClickListener numClickListener = new View.OnClickListener() {
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.num0:
textView.append("0");
break;
case R.id.num1:
textView.append("1");
break;
case R.id.num2:
textView.append("2");
break;
case R.id.num3:
textView.append("3");
break;
case R.id.num4:
textView.append("4");
break;
case R.id.num5:
textView.append("5");
break;
case R.id.num6:
textView.append("6");
break;
case R.id.num7:
textView.append("7");
break;
case R.id.num8:
textView.append("8");
break;
case R.id.num9:
textView.append("9");
break;
}

preview();
}
};

//기호 버튼이 눌렸을 경우
private Button.OnClickListener markClickListener = new View.OnClickListener() {
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.add:
textView.append(" + ");
isPreview = true;
break;
case R.id.sub:
textView.append(" - ");
isPreview = true;
break;
case R.id.mul:
textView.append(" * ");
isPreview = true;
break;
case R.id.div:
textView.append(" / ");
isPreview = true;
break;
case R.id.percent:
textView.append(" % ");
isPreview = true;
break;
case R.id.clear:
textView.setText("");
textView2.setText("");

calculateHelper = new CalculateHelper();

isPreview = false;

break;
case R.id.bracket:
if (!isBracket) {
textView.append("( ");
isBracket = true;
} else {
textView.append(" )");
isBracket = false;
}

isPreview = true;

break;
case R.id.back:
size = textView.getText().length();

if (size != 0)
textView.setText(textView.getText().toString().substring(0, size - 1));

if (size > 1) {
if (calculateHelper.checkNumber(textView.getText().toString().substring(size - 2)))
preview();
else {
isPreview = false;
textView2.setText("");
}
}

break;
case R.id.dot:
textView.append(".");
isDot = true;
break;
case R.id.equal:
result = textView.getText().toString();
double r = calculateHelper.process(result);

if (!isDot)
textView.setText(String.valueOf((int) r));
else
textView.setText(String.valueOf(r));

textView2.setText("");
isDot = false;
isPreview = false;
break;
}
}
};

private void preview() {
if (isPreview) {
result = textView.getText().toString();
double r = calculateHelper.process(result);

if (!isDot)
textView2.setText(String.valueOf((int) r));
else
textView2.setText(String.valueOf(r));
}
}

private void setTextView() {
textView = findViewById(R.id.first_textView);
textView2 = findViewById(R.id.second_textView);
}

 

설명

버튼 클릭은 숫자, 기호로 구분되어 동작하며 버튼이 눌렸을 경우 append()를 사용하여 textView에 표시

textView2는 결과 미리보기로 1)숫자 입력 2)기호 입력 3)숫자 입력 순으로 앞에 숫자와 기호가 존재하고 다시 숫자가 입력되는 경우 미리보기를 제공한다. 계산은 CalculateHelper에서 진행 후 리턴.

 

CalculateHelper에 대한 것은 다음 글에서 설명하겠습니다.

+ Recent posts