pt.1~3)안드로이드 스튜디오 계산기 (프로젝트 생성부터 완성까지!)
Android Studio 를 이용해 계산기를 만들어보자!
1. 프로젝트 생성하기
새로운 프로젝트를 생성한다.
이번시간에는 텅 빈 액티비티를 선택한다.
Next를 누르면 프로젝트 이름과 경로를 설정하는 창이 나온다. 설정이 완료되면 [Finish]
실행하면 가장 기본적인 요소 2가지가 보인다.
activity_main.xml은 인터페이스를 구성하는 요소이고 MainActivity는 함수를 컨트롤하는 부분이다.
ASP.NET으로 비유하면 activity_main.xml 과 MainActivity.java는 각각 뷰와 컨트롤에 대응한다.
2. 에뮬레이터 실행하기
AVD Manager를 실행시킨다.
필자는 이미 에뮬레이터를 생성해서 Galaxy S7 API R이 보인다.
+Create Virtual Device를 눌러 가상머신을 생성하자
가상머신의 기종을 선택하자. 원하는 기종이 없을 경우 스펙을 직접 입력하거나 import할 수 있다.
시스템 이미지 설정
여기서 이미지란 일반적으로 뜻하는 사진이 아닌 OS를 구동하기 위한 패키지를 뜻한다.
기타 설정을 마치고 [Finish]
상단의 Run app 버튼을 누른다 (Shift + F10)
에뮬레이터가 실행되고 현재까지 만들어진 어플리케이션이 실행된다.
스마트폰 연결 없이도 테스트 실행을 해볼 수 있는 것이다.
또는 안드로이드 스마트폰을 연결해서 앱을 직접 실행할 수 있다.
그러기 위해서는 개발자 모드를 실행하고 USB연결을 해줘야 한다.
다음 글에 개발자 모드 설정법이 잘 나와있다.
갤럭시 S7 개발자 모드 알아보기 (Galaxy S7 Developer Mode Options)
갤럭시 S7 개발자 모드 알아보기 (Galaxy S7 Developer Mode Options) 이번 포스팅은 개발자 옵션을 적용하여 보고 어떠한 기능을 추가하고 적용할 수 있는지 그리고 개발자 모드로 접근하는 방법에 대하��
nature2public.tistory.com
요약하자면 [설정]-[디바이스 정보] - [소프트웨어 정보] - [빌드 번호] 를 여러번 클릭해준다.
그러면
위와 같이 맨 밑에 개발자 옵션이 생긴다.
들어가서 USB 디버깅을 ON 해주는 것도 잊지 말자.
이까지 하면 기본적인 개발 준비가 완료되었다.
계산기 프로그램 출처: dirrito.tistory.com/13
3. 컨트롤 추가하기
먼저 컨트롤을 추가해 보자
다시 activity_main.xml을 가보면 우측 상단에 3개의 아이콘이 보인다.
각각 코드-분할-디자인 으로 [코드]만 보거나 [코드랑 디자인] 둘다 보거나 [디자인]만 볼 수 있다.
위 사진은 디자인이다.
디자인에서는 컨트롤을 드래그 앤 드롭으로 쉽게 추가할 수 있어서 가시성이 좋다.
Palette의 컨트롤들을 화면 또는 Component tree에 드롭할 수 있다.
드래그 앱 드롭으로 LinearLayout(vertical)을 추가해 보았다.
위와 같이 Component Tree에 추가됨을 알 수 있다.
Component Tree에서 부모개체와 자식개체를 잘 정리해야 컨트롤이 꼬이는 걸 방지할 수 있다.
activity_main.xml 파일에 바로 반영됨을 알 수 있다.
xml 파일에 위 같은 형식으로 입력해도 컨트롤은 똑같이 추가된다.
레이아웃 설정없이 마구잡이로 컨트롤을 추가하면 어플 실행시 좌상단에 컨트롤들이 몰려있는 현상이 생길 수 있다.
다음과 같이 xml 파일을 작성해본다.
[activity_main.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="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>
LinearLayout─LinearLayout─ScrollView─first_textView
└second_textView
└GridLayout─(버튼20개)
의 구조로 되어있다.
버튼은
<Button
android:id="@+id/equal" "equal"로 아이디 설정
android:layout_rowWeight="1"
android:layout_columnWeight="1"
android:layout_margin="-5dp"
android:text="=" /> "="로 텍스트 설정
id와 텍스트를 설정해준다. 이외에 layout_rowWeight, layout_columnWeight 은 열, 행에서 버튼간의 비율을 설정할 때
쓴다. margin은 여백을 나타낸다.
xml을 수정하고 디자인을 확인하면
위와같이 되어있다.
버튼의 text를 설정한대로 보인다.
다음은
1. MainActivity.java를 수정한다.
2. CalculateHelper 클래스를 추가한다.
MainActivity.java.를 다음과 같이 수정한다.
[MainActivity.java]
package com.example.dumb;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
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;
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);
}
Button.OnClickListener numClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
switch(v.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();
}
};
Button.OnClickListener markClickListener = new View.OnClickListener(){
@Override
public void onClick(View v){
switch (v.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);
}
}
여기서는 메인클래스 내에 CalculateHelper, TextView, Button을 선언한다.
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
calculateHelper = new CalculateHelper();
size=0;
setButton();
setTextView();
}
OnCreate 즉 실행되면 가장 먼저 위에 선언한 CalculateHelper, TextView, Button을 인스턴스화 한다.
Button.OnClickListener numClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
switch(v.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();
}
};
숫자 버튼에 OnClickListener를 연결해준다.
View에서 id를 받아서 각 버튼에 맞는 숫자를 textView에 더하도록 하는 함수다.
Button.OnClickListener markClickListener = new View.OnClickListener(){
@Override
public void onClick(View v){
switch (v.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;
}
}
};
기호버튼도 마찬가지로 각각 버튼에 맞게 +,-,*,/ 를 textView에 더해준다.
isPreview는 사칙연산이나 괄호를 눌렀을때 true 그 이외에는 false로 설정된다.
이 isPreview 변수는
textView의 값을 result에 복사하고 CalculatorHelper에서 후위 표기식의 계산을 수행하도록 한다.
그리고 그 결과값을 textView2로 옮긴다.
4. CalculateHelper 클래스 추가
[File]-[New]-[Java Class]를 통해 클래스를 추가한다.
내용은 다음과 같이 작성한다.
[CalculateHelper.java]
package com.example.dumb;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Stack;
public class CalculateHelper {
public static double num1;
public static double num2;
public static double resultNumber;
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;
}
}
CalculatorHelper 클래스는 앞서 입력한 계산식을 후위 표기법으로 바꾼 후 연산을 실행한다.
CalculateHelper에는 splitTokens, infixToPostfix, postFixEval, process, checkNumber 함수가 있다.
[CalculateHelper.splitTokens] : 연산식을 ArrayList에 담아 반환한다.
[Stack]사칙연산 계산기 구현(1) - 후위 표기법
사칙연산 계산기 구현 - 후위 표기법 사칙연산 프로그램을 만들 때 사용하는 수식의 표현법이다. 보통 우리가 사용하는 수식은 중위 표기법으로 표현된다. 중위 표기법은 연산자가 피연산자들��
jamanbbo.tistory.com
이 글을 보면 스택 계산기의 후위 표기법에 대해 아주 잘 나타나 있다.
[CalculateHelper.infixToPostfix] : 중위 표기법을 후위 표기법으로 바꾼다.
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;
}
해쉬맵을 통해 우선순위를 곱셈, 나눗셈은 3, 덧셈 뺄셈은 2, 여는 괄호는 1로 설정해놨다.
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();
}
여는 괄호가 나오면 무조건 스택에 넣고 닫는 괄호가 나오면 여는 괄호까지의 모든 내용을 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());
}
우선순위가 높은게 나오면 pop해서 result에 추가하고 낮은걸 스택에 쌓는다.
[CalculateHelper.postFixEval]: 후위표기법의 식을 계산한다.
후위 표기법의 식을 탐색한다. 숫자가 나올경우 스택에 Push하고 연산자가 나올 경우 첫번째 pop한 수와 두번째로 pop
한 수에 연산을 한다.
[CalculateHelper.process]: 최종적으로 계산값을 리턴하는 함수
public Double process(String equation){
ArrayList postfix = infixToPostfix(splitTokens(equation));
Double result = postFixEval(postfix);
return result;
}
[CalculateHelper.checkNumber]: 아스키코드로 숫자인지 판별해 boolean값을 반환하는 함수
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;
}
5. 앱 아이콘 변경, 빌드
다음으로 앱 아이콘을 바꾸고 빌드해보자.
[File]-[New]-[Image Asset]을 클릭한다.
Select Path로 이미지가 저장된 경로를 연다.
이미지를 선택하고 적당한 크기로 조절한다.
아이콘이 바뀌었음을 확인할 수 있다.
이렇게 에뮬레이터 대신 실제 기기로 실행해보면 어플이 기기에 배포된다. Google Play에 공유, 업로드 할 앱을 빌드하려면 다음과 같이 하면 된다.
[Build]-[Build Bundle(s) / APK(s)]-[Build APK(s)]