이번 포스트에서 간단하게 알람 앱을 구현해 보겠습니다. Adapter에 대해' 다시 한번 정리해 보자'라는 생각으로 작성한 포스트입니다.
이 앱은 시간을 설정했다고 알람이 울리거나 하지 않습니다. 이를 구현하려면 foreground와 background, 안드로이드 생명주기, 쓰레드 등 다양한 개념이 필요합니다. 특히 구글에서는 RecyclerView를 활용해서 개발하라고 추천하기 때문에 이번 포스트에선 앞서 말한 것처럼 Adapter에 대해 마무리를 짓는 생각으로 내용을 다뤄보겠습니다.(앞서 다뤘던 포스트와는 별 차이 없습니다.) 시작하겠습니다.
앞선 포스트에서도 등장했었지만 기본 레이아웃은 다음과 같습니다.
알람 리스트 |
시간 설정 |
MainActivity에서는 현재시간을 실시간(계속 바뀌는)으로 표시해주고, 알람 리스트를 추가 및 삭제하는 버튼과 알람 리스트를 출력하는 리스트가 존재합니다. TimePickerActivity에서는 시간을 셋팅해주는 TimePicker를 사용했습니다.
자 먼저 새로운 프로젝트를 하나 생성해 주시고, 레이아웃을 꾸미러 가보겠습니다. activity_main.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"
tools:context=".MainActivity"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/current1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal|center_vertical"
android:paddingTop="70dp"
android:textSize="20dp"
android:text="현재시간" />
<TextView
android:id="@+id/current"
android:layout_width="match_parent"
android:layout_height="70dp"
android:gravity="center_vertical|center_horizontal"
android:textStyle="bold"
android:text="현재시간" />
</LinearLayout>
<RelativeLayout
android:id="@+id/relative"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<Button
android:id="@+id/addBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toLeftOf="@+id/removeBtn"
android:text="+"/>
<Button
android:id="@+id/removeBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:text="-"/>
</RelativeLayout>
<ListView
android:id="@+id/list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="@layout/round_theme">
</ListView>
</LinearLayout>
activity_main.xml입니다. 먼가 좀 Layout이 많아서 복잡해 보일 수도 있는데 천천히 보시면 어려운 layout은 아닙니다. 이제 activity_time_picker.xml로 넘어가 보겠습니다. 여기서 ListView에 tools를 저를 따라 하는 것이 아니라면 레이아웃 이름을 고치셔야 합니다.(따라 하시는 분들은 오류가 나도 계속 진행해 주세요.)
activity_time_picker.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=".TimePickerActivity">
<RelativeLayout
android:id="@+id/time_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="90dp">
<TimePicker
android:id="@+id/time_picker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:timePickerMode="spinner"
android:layout_centerInParent="true"/>
</RelativeLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center">
<Button
android:id="@+id/okBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="확인"/>
<Button
android:id="@+id/cancleBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="취소"/>
</LinearLayout>
</LinearLayout>
이 레이아웃은 TimePicker를 활용한 레이아웃입니다. TimePicker는 시간을 설정할 수 있는 아주 좋은 아이템입니다. 나중에 따로 한번 다뤄보겠습니다.
다음은 커스텀 레이아웃을 만들 차례입니다. 앞선 포스트에서 만드는 방법을 올려두었으니 참고해 주시기 바랍니다.
이제 만들었던 커스텀 레이아웃을 적용시켜볼 차례입니다. round_theme.xml입니다.
round_theme.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_margin="6dp"
android:padding="6dp"
android:background="@drawable/layout_background" >
<TextView
android:id="@+id/am_pm"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="오전/오후"
android:textAppearance="@style/TextAppearance.AppCompat.Small"/>
<TextView
android:id="@+id/textTime1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="몇시"
android:paddingLeft="10dp"
android:textAppearance="@style/TextAppearance.AppCompat.Large"/>
<TextView
android:id="@+id/textTime2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="몇분"
android:paddingLeft="10dp"
android:textAppearance="@style/TextAppearance.AppCompat.Large"/>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="match_parent">
<TextView
android:id="@+id/time_month"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="몇월"
android:layout_toLeftOf="@+id/time_day"
android:paddingRight="10dp"
android:textAppearance="@style/TextAppearance.AppCompat.Large"/>
<TextView
android:id="@+id/time_day"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="몇일"
android:layout_toLeftOf="@+id/switchBtn"
android:paddingRight="10dp"
android:textAppearance="@style/TextAppearance.AppCompat.Large"/>
<Switch
android:id="@+id/switchBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:focusable="false"
android:focusableInTouchMode="false"/>
</RelativeLayout>
</LinearLayout>
</LinearLayout>
여기서 weight를 사용하면 더 쉽게 구현할 수 있었을 거 같았는데 이건 다음에 개선점으로 남겨두겠습니다.
자 이제 전반적인 레이아웃 구성을 마치셨으면 이제 이 기능들을 만들러 가봅시다. 먼저 MainActivity입니다.
MainActivity
package hello.world.alamapplication;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import android.app.TimePickerDialog;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.Switch;
import android.widget.TextView;
import android.widget.Toast;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
public class MainActivity extends AppCompatActivity {
public static final int REQUEST_CODE1 = 1000;
public static final int REQUEST_CODE2 = 1001;
private AdapterActivity arrayAdapter;
private Button tpBtn, removeBtn;
private ListView listView;
private TextView textView;
private int hour, minute;
private String month, day, am_pm;
private Handler handler;
private SimpleDateFormat mFormat;
private int adapterPosition;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
/*스위치를 포함한 커스텀 adapterView 리스트 터치 오류 관련 문제 해결(Java code)
switch.setFocusable(false);
switch.setFocusableInTouchMode(false);*/
arrayAdapter = new AdapterActivity();
listView = (ListView) findViewById(R.id.list_view);
listView.setAdapter(arrayAdapter);
//List에 있는 항목들 눌렀을 때 시간변경 가능
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
adapterPosition = position;
arrayAdapter.removeItem(position);
Intent intent = new Intent(MainActivity.this, TimePickerActivity.class);
startActivityForResult(intent,REQUEST_CODE2);
}
});
/*long now = System.currentTimeMillis();
Date date = new Date(now);*/
//쓰레드를 사용해서 실시간으로 시간 출력
handler = new Handler() {
@Override
public void handleMessage(@NonNull Message msg) {
Calendar cal = Calendar.getInstance();
mFormat = new SimpleDateFormat("HH:mm:ss");
String strTime = mFormat.format(cal.getTime());
textView = (TextView) findViewById(R.id.current);
textView.setTextSize(30);
textView.setText(strTime);
}
};
class NewRunnable implements Runnable {
@Override
public void run() {
while(true) {
try {
Thread.sleep(1000);
}catch (Exception e) {
e.printStackTrace();
}
handler.sendEmptyMessage(0);
}
}
}
NewRunnable runnable = new NewRunnable();
Thread thread = new Thread(runnable);
thread.start();
//TimePicker의 시간 셋팅값을 받기 위한 startActivityForResult()
tpBtn = (Button) findViewById(R.id.addBtn);
tpBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent tpIntent = new Intent(MainActivity.this, TimePickerActivity.class);
startActivityForResult(tpIntent, REQUEST_CODE1);
}
});
removeBtn = (Button) findViewById(R.id.removeBtn);
removeBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
arrayAdapter.removeItem();
arrayAdapter.notifyDataSetChanged();
}
});
}
//TimePicker 셋팅값 받아온 결과를 arrayAdapter에 추가
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
//시간 리스트 추가
if(requestCode == REQUEST_CODE1 && resultCode == RESULT_OK && data != null) {
hour = data.getIntExtra("hour", 1);
minute = data.getIntExtra("minute", 2);
am_pm = data.getStringExtra("am_pm");
month = data.getStringExtra("month");
day = data.getStringExtra("day");
arrayAdapter.addItem(hour, minute, am_pm, month, day);
arrayAdapter.notifyDataSetChanged();
}
//시간 리스트 터치 시 변경된 시간값 저장
if(requestCode == REQUEST_CODE2 && resultCode == RESULT_OK && data != null) {
hour = data.getIntExtra("hour", 1);
minute = data.getIntExtra("minute", 2);
am_pm = data.getStringExtra("am_pm");
month = data.getStringExtra("month");
day = data.getStringExtra("day");
arrayAdapter.addItem(hour, minute, am_pm, month, day);
arrayAdapter.notifyDataSetChanged();
}
}
}
코드에 대략적인 주석을 달았으니 제 포스트를 읽으셨던 분들이 시라면 무리 없이 이해하실 수 있을 것 같습니다. 중간에 Thread를 사용한 코드가 보이실 텐데 이것은 화면에 실시간으로 시간을 출력해주는 함수입니다. 이 Thread도 나중에 한 번 다시 다뤄보도록 하겠습니다.
그리고 개발하면서 막혔던 부분이 바로 Switch입니다. 분명 리스트를 터치하는데도 Activity이동이 먹히질 않아 살짝 삽질 좀 했던 기억이 나는데요, 이는 리스트뷰를 터치해도 포커스가 이미 switch에 맞춰져 있어서 리스트 터치가 먹히질 않았던 것입니다. 이를 해결한 방법은 Java 코드와 xml 파일 모두 사용할 수 있습니다. 먼저 Java 코드입니다.
1. Java
switch.setFocusable(false);
switch.setFocusableInTouchMode(false);
스위치 객체를 생성한 다음 저렇게 작성하시면 됩니다.
2. XML
android:focusableInTouchMode="false"
스위치에 딱 이 한 줄 코드만 설정해 놓으시면 됩니다. 자바는 자바 소스에서 스위치 생성 시 주로 사용하는 방법입니다.
이것에 대해 더욱 자세히 알고 싶은 분은 제가 참고한 블로그를 올려둘 테니 참고하시기 바랍니다.
여기까지 완료됐다면 이제 model클래스를 작성해 보겠습니다. Time 액티비티를 하나 생성해 주시고 다음과 같이 만들어줍니다.
Time
package hello.world.alamapplication;
public class Time {
private int hour, minute;
private String month, day, am_pm;
public int getHour() {
return hour;
}
public void setHour(int hour) {
this.hour = hour;
}
public int getMinute() {
return minute;
}
public void setMinute(int minute) {
this.minute = minute;
}
public String getAm_pm() {
return am_pm;
}
public void setAm_pm(String am_pm) {
this.am_pm = am_pm;
}
public String getMonth() {
return month;
}
public void setMonth(String month) {
this.month = month;
}
public String getDay() {
return day;
}
public void setDay(String day) {
this.day = day;
}
@Override
public String toString() {
final StringBuffer sb = new StringBuffer("Time{");
sb.append("hour=").append(hour);
sb.append(", minute=").append(minute);
sb.append('}');
return sb.toString();
}
}
이거 쉽게 만드는 법은 [ Alt + Insert ] 모두 기억하시죠? 기억이 안 나거나 모르시는 분은 BaseAdapter(2) 편을 보시고 오면 되겠습니다.
여기서 toString()은 나중에 디버깅할 때 로그 창에 표시할 수 있어 값이 제대로 전달됐는지 확인하기 위해 만들어 봤습니다. 이것도 위와 같은 방법으로 하면 자동으로 생성됩니다.
이제 TimePickerActivity 액티비티를 생성해주세요
TimePickerActivity
package hello.world.alamapplication;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.TimePicker;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
public class TimePickerActivity extends AppCompatActivity {
private TimePicker timePicker;
private Button okBtn, cancelBtn;
private int hour, minute;
private String am_pm;
private Date currentTime;
private String stMonth, stDay;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_time_picker);
timePicker = (TimePicker)findViewById(R.id.time_picker);
currentTime = Calendar.getInstance().getTime();
SimpleDateFormat day = new SimpleDateFormat("dd", Locale.getDefault());
SimpleDateFormat month = new SimpleDateFormat("MM", Locale.getDefault());
stMonth = month.format(currentTime);
stDay = day.format(currentTime);
okBtn = (Button)findViewById(R.id.okBtn);
okBtn.setOnClickListener(new View.OnClickListener() {
//안드로이드 버전별로 시간값 세팅을 다르게 해주어야 함. 여기선 Android API 23
@Override
public void onClick(View v) {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
hour = timePicker.getHour();
minute = timePicker.getMinute();
}
else {
hour = timePicker.getCurrentHour();
minute = timePicker.getCurrentMinute();
}
am_pm = AM_PM(hour);
hour = timeSet(hour);
Intent sendIntent = new Intent(TimePickerActivity.this, MainActivity.class);
sendIntent.putExtra("hour", hour);
sendIntent.putExtra("minute", minute);
sendIntent.putExtra("am_pm", am_pm);
sendIntent.putExtra("month", stMonth);
sendIntent.putExtra("day", stDay);
setResult(RESULT_OK, sendIntent);
finish();
}
});
//취소버튼 누를 시 TimePickerAcitivity 종료
cancelBtn = (Button) findViewById(R.id.cancleBtn);
cancelBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
}
//24시 시간제 바꿔줌(군대도 아니고..)
private int timeSet(int hour) {
if(hour > 12) {
hour-=12;
}
return hour;
}
//오전, 오후 선택
private String AM_PM(int hour) {
if(hour >= 12) {
am_pm = "오후";
}
else {
am_pm = "오전";
}
return am_pm;
}
}
여기도 적당한 주석을 달았으니 모르는 것이 있으면 질문해주세요.
마지막으로 Adapter를 만들어 봅시다. AdapterAcitivty입니다.
AdapterAcitivty
package hello.world.alamapplication;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;
import java.util.ArrayList;
public class AdapterActivity extends BaseAdapter {
public ArrayList<Time> listviewitem = new ArrayList<Time>();
private ArrayList<Time> arrayList = listviewitem; //백업 arrayList
@Override
public int getCount() {
return arrayList.size();
}
@Override
public Object getItem(int position) {
return arrayList.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if(convertView == null) {
holder = new ViewHolder();
convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.round_theme, parent, false);
TextView hourText = (TextView)convertView.findViewById(R.id.textTime1);
TextView minuteText = (TextView)convertView.findViewById(R.id.textTime2);
TextView am_pm = (TextView)convertView.findViewById(R.id.am_pm);
TextView month = (TextView)convertView.findViewById(R.id.time_month);
TextView day = (TextView)convertView.findViewById(R.id.time_day);
holder.hourText = hourText;
holder.minuteText = minuteText;
holder.am_pm = am_pm;
holder.month = month;
holder.day = day;
convertView.setTag(holder);
}
else {
holder = (ViewHolder)convertView.getTag();
}
Time time = arrayList.get(position);
holder.am_pm.setText(time.getAm_pm());
holder.hourText.setText(time.getHour()+ "시");
holder.minuteText.setText(time.getMinute()+ "분");
holder.month.setText(time.getMonth()+ "월 ");
holder.day.setText(time.getDay()+ "일");
return convertView;
}
public void addItem(int hour, int minute, String am_pm, String month, String day) {
Time time = new Time();
time.setHour(hour);
time.setMinute(minute);
time.setAm_pm(am_pm);
time.setMonth(month);
time.setDay(day);
listviewitem.add(time);
}
//List 삭제 method
public void removeItem(int position) {
if(listviewitem.size() < 1) {
}
else {
listviewitem.remove(position);
}
}
public void removeItem() {
if(listviewitem.size() < 1) {
}
else {
listviewitem.remove(listviewitem.size()-1);
}
}
static class ViewHolder {
TextView hourText, minuteText, am_pm, month, day;
}
}
이것들은 제가 전에 포스트 했던 것들과 많이 비슷하죠? 거의 똑같습니다.
이제 여기까지 오셨으면 다 끝났습니다. 다음은 실행 동영상입니다.
앱이 완성이 되었습니다. 그러나 이 앱은 부족한 점이 너무 많습니다.
1. 실시간 시간을 hoder에 지정을 하지 않아서 다시 로딩된다.
2. 리스트 삭제 시 마지막 리스트부터 삭제가 된다.
3. 새벽이 넘어가면 요일이 바뀌지 않는다.
4. 알람이 울리지 않는다.
5. 정렬이 되지 않는다.
등등 많은 문제점들과 부족한 점이 있습니다. 그러나 지금 이 포스트에선 간단한 레이아웃만을 다루려고 하는 거였지, 완전한 앱을 만드는 것이 목적이 아니었습니다. 나중에 제대로 된 알람 앱을 개발해 보도록 하겠습니다.
총 3편으로 이루어진 Adapter. 잘 보셨는지요? 혹시 아직 배우지 않고 이 글을 보신다면 제 포스트 한 번 참고하시면 좋을 것 같습니다. 감사합니다.