사전준비
주제, 프로젝트명 정하기
연락처 앱 프로젝트는 연락처의 본질을 지키되, 기존 연락처앱과의 차별성을 요구하는 프로젝트이다. 팀원분들의 의견을 조합해서 낸 나의 '선물' 아이디어가 채택됐고 팀원분이 찾아본 '산타' 키워드가 앱의 특징을 잘 드러내므로 앱 이름은 산타의 리스트로 채택됐다.
역할 정하기
바쁜 기간이 끝난 직후라 진도도 쫓기고 있었고 잠도 잘 못자서 고민됐지만 그래서 더 욕심 부리고 싶었다. 공연 때문에 뒤쳐진 느낌이었고 팀프로젝트는 배울 게 많은 기회이기 때문이다. 선택사항인 알림을 하고싶다고 제일 먼저 이야기했고 그 뒤엔 남는 자리 채워서 알림과 디테일페이지 담당이 됐다.
와이어프레임 만들기
현업 하시던 분들이 계신터라 와이어프레임에서부터 배울 게 많았다. 글씨크기는 12 14 16을 기본으로 쓰고 2의 배수로 조절하는 것이 좋다. 패딩은 내부에 얼마나 담길지 모르니 아이템의 크기를 고정하지 말고 패딩을 줘서 크기를 지정하는 것이 좋은데 4의 배수를 주로 쓴다.
Git & Github
프로젝트의 시작
'레포지토리 팔게요!' 이제 두번째 팀플인 나로서 시도는 좋았으나 너무 느렸다. 브랜치 파다가 팀플 끝날 것 같아서 저번에 리사이클러뷰 강의해주신 엘리트 팀원분이 마법 부리듯이 만들어주셨다. 스포하는데 팀플 내내 많이 도와주시고 가르쳐주신 은인이시다.
충돌관리
이번 팀플하면서 크게 꼬인 건 없었는데 이상하게 push하면 레포지토리에 push완료 메시지는 뜨지만 내 코드가 반영되지 않았고 다른 사람 코드를 pull하면 내가 새로 만든 코드들은 자동 merge때문에 사라진다. 덕분에 stash와 apply도 배웠고 진짜 유용하고 많이 쓴 건 기능브랜치에서 개발브랜치로 체크아웃 한 뒤, 개발브랜치에서 origin develop을 pull받은 다음, 기능브랜치로 돌아와 merge하는 방법이다. 수동으로 merge하는데 내 코드와 다른 사람 코드 모두 안전하게 지킬 수 있는 방법이다.
commit 메시지로 정리한 제작과정
2024. 07. 22 : Feature/alert 기능브랜치 생성 - 프로필 사진 추가 - 디테일페이지 UI 완성 - 디테일페이지 상단 텍스트 생성
2024. 07. 23 : 디테일페이지 선물버튼 생성 - 디테일페이지 알림버튼 생성, 선물버튼 보완
2024. 07. 24 : 스크롤뷰 에러 개선
2024. 07. 25 : 알람매니저 설정 - 알람 라디오버튼 다이얼로그 추가, 테마 변경 - 알람 코드개선 - 알람 현재시간기준 경과시간 설정
2024. 07. 26 : 알림버튼 아이콘, 텍스트 동시클릭되도록 수정 - 디테일페이지 dialog 연결 - 디테일페이지 툴바 다이얼로그 - 다이얼로그 커스텀으로 변경 - 알림다이얼로그 프래그먼트로 생성
2024. 07. 27 : 디테일페이지 이미지 문제 해결 - 전체 UI보완 - 로고이미지 생성 - 코드정리
Notification
하나의 뷰로 아이콘과 텍스트 생성하는 방법
isChecked의 Boolean으로 설정이 가능하고 단순히 클릭이 아니라 클릭된 상태를 유지해줘야 할 때 체크박스가 적합하다고 생각하는데 체크박스로 각각 아이콘과 텍스트를 만들었더니 같이 변경되지 않는게 문제였다. 이벤트마다 레이아웃을 변경하려니 코드 중복이 너무 많아지기도 하고 selector의 의미도 소실된다.
1. 레이아웃으로 감싸기
다른 분들의 자문을 구해봤는데 가장 먼저 나온 의견은 역시 레이아웃으로 감싸는 것이었지만 depth가 깊어져 지양하고 싶은 방법이라 시도하지 않았다.
2. flow로 묶기
늘 아이디어가 넘치는 말감님의 나눔으로 새로운 위젯을 새로 알게됐다. 그런데 적용해보니 referenced 뷰 외에 인접한 뷰들과의 간격이 같이 조절돼서 아쉽지만 다른 방법을 찾아야했다.
<androidx.constraintlayout.helper.widget.Flow
android:id="@+id/detail_info_flow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="horizontal" //수평흐름 vertical은 수직
app:constraint_referenced_ids="뷰,뷰" //흐름에 묶을 뷰들을 ,콤마로 구분해서 적는다. 띄어쓰기 금지
app:flow_horizontalGap="14dp" //수평간격 verticalgap은 수직
3. drawableCompat 속성쓰기
우연히 외국문서에서 발견한 한줄기의 빛을 발견했다.
<CheckBox
android:button="@android:color/transparent" //기본 체크박스 숨기기
android:gravity="center"
android:text="@string/alert_off"
android:textColor="@drawable/sel_alert_detail_text"
android:textSize="14sp"
app:drawableStartCompat="@drawable/sel_detail_alert_ic" //아이콘!
app:drawable[위치]Compat로 텍스트와 아이콘을 같이 표시할 수 있어 이벤트처리도 편해졌고 체크박스도 하나로 통합했다. 아이콘의 크기는 drawable에서 layout-list로 조절해 참조해주면 된다. 이후 다른 곳에서도 텍스트와 아이콘을 동시 구현해야 할 때 쉽게 응용할 수 있었다.
그런데 다이얼로그 외부를 클릭해 행동을 취소하면 버튼 상태가 변경되지 못한다. alertDialog.isCancelable = false로 외부 클릭을 막았는데 사용자에게 좀 더 자유를 주고 싶다면 외부 클릭이 true일 때를 감지해서 버튼 상태를 변경해주면 되겠다.
Dialog
알림버튼을 누르면 사용자의 상태에 따라 세가지 중 알맞는 다이얼로그를 출력한다.
권한이 없는 경우 -> 권한설정여부 확인하는 다이얼로그 출력
이미 예약한 경우 -> 취소확인하는 다이얼로그 출력
권한 허용했고 예약하지 않은 경우 -> 알림 예약 다이얼로그 출력
//알림버튼 : 세가지 기능 중 사용자 상태에 따라 실행 (알림 권한 요청 / 알림 취소 / 알림 예약)
_binding?.detailCbAlert?.setOnClickListener {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !NotificationManagerCompat.from(
requireContext()
).areNotificationsEnabled()
)
requireAlarmPermission()
else if (selectedAlarm == 4) cancelAlarm()
else {
isCheck()
val alertDialog = AlertDialogFragment.newInstance(friend)
alertDialog.isCancelable = false
alertDialog.show(requireFragmentManager(), "DialogFragment")
}
}
다이얼로그 만드는 방법
1. 프래그먼트로 만들기
번거로운만큼 예쁘고 보다 섬세한 처리가 가능한 방법은 역시 xml을 자유롭게 만들 수 있는 다이얼로그 프래그먼트다. 알림 옵션을 선택하는 다이얼로그를 만들 때 라디오그룹으로 입력도 편하게 받고 입력받은 값의 처리도 깔끔했다.
class AlertDialogFragment : DialogFragment() {//상속 받을 때 주의
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//5초뒤
_binding?.btnAlertDialog1?.setOnClickListener {
selectedAlarm = 1
}
//하루 전
_binding?.btnAlertDialog2?.setOnClickListener {
selectedAlarm = 2
}
//당일
_binding?.btnAlertDialog3?.setOnClickListener {
selectedAlarm = 3
}
//완료
_binding?.alertBtnDialogComplete?.setOnClickListener {
exit()
}
//취소
_binding?.alertBtnDialogBack?.setOnClickListener {
selectedAlarm = 0
exit()
}
}
//디테일페이지로 돌아가는 함수
private fun exit() {
val dialogResult =
ContactDetailFragment.newInstance(friend!!, selectedAlarm)
requireActivity().supportFragmentManager.beginTransaction()
.replace(R.id.frame_layout, dialogResult).addToBackStack(null).commit()
dismiss()
}
2. Material로 만들기
제공하는 다이얼로그로 편하게 만들 수 있는데 style로 커스터마이징이 가능하다는 장점과 지정된 부분은 수정이 어렵다는 단점이 있다. 간단하게 필요할 때 쓰면 좋다. 마감기한 앞두고 빨리 만들어야 될 때 잘 썼다.
알림 권한여부를 확인하는 다이얼로그라 같이 정리해보자면 권한 설정하러 가기를 클릭했을 때, intent가 설정으로 접근한다. 지금까지는 보통 this나 context를 써왔는데 은인님의 가르침 덕분에 프래그먼트에는 requireContext()로 컨텍스트를 지정하면 된다는 것을 배웠다. 권한 필요성은 https://hhyun-s2.tistory.com/118 참고
private fun requireAlarmPermission() {
MaterialAlertDialogBuilder(
requireContext(), R.style.detail_dialog_alert
)
.setTitle(getString(R.string.alarm_ask_permission))
.setNegativeButton(getString(R.string.move)) { dialog, which ->
val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply {
putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName)
}
startActivity(intent)
}
.setPositiveButton(getString(R.string.cancel)) { dialog, which ->
Toast.makeText(
requireContext(),
getString(R.string.alarm_need_permission),
Toast.LENGTH_LONG
).show()
}
.show()
isNotCheck()
}
스타일은 이렇게 부분적으로 지정이 가능해 생각보다 자유도가 있는 편이다. 하지만 범위가 좁아서 진짜 앱을 만들 때는 쓰지 못하겠다.
<style name="detail_dialog_alert" parent="ThemeOverlay.MaterialComponents.MaterialAlertDialog">
<item name="android:background">@color/white</item>
<item name="materialAlertDialogTitleTextStyle">@style/TitleTextStyle</item>
<item name="buttonBarNegativeButtonStyle">@style/NegativeButtonStyle</item>
<item name="buttonBarPositiveButtonStyle">@style/PositiveButtonStyle</item>
<item name="android:dialogCornerRadius">15dp</item>
</style>
<style name="TitleTextStyle" parent="MaterialAlertDialog.MaterialComponents.Title.Text">
<item name="android:textColor">@color/black</item>
<item name="android:textSize">18sp</item>
</style>
<style name="NegativeButtonStyle" parent="Widget.MaterialComponents.Button.TextButton.Dialog">
<item name="android:textColor">@color/primary</item>
<item name="android:textSize">16sp</item>
<item name="android:textStyle">bold</item>
<item name="rippleColor">@color/primary</item>
</style>
<style name="PositiveButtonStyle" parent="Widget.MaterialComponents.Button.TextButton.Dialog">
<item name="android:textColor">@color/black</item>
<item name="android:textSize">16sp</item>
<item name="rippleColor">@color/white</item>
</style>
3. 기본으로 만들기
https://hhyun-s2.tistory.com/112 참고
LocalDateTime
날짜와 시간 편리하게 다루는 방법
.minusDays(1) 처럼 LocalDateTime에 제공되는 편리한 기능들이 많다!
달을 숫자로 받고 싶을 땐 .monthValue로 String으로 받고 싶을 땐 .month
date.format(DateTimeFormatter.ofPattern("yyyy년 M월 dd일"))으로 String을 만들기도 하고
LocalDateTime.of(selectDate[0], selectDate[1], selectDate[2], 0, 0, 0)로 다시 쪼갤 수도 있다.
calendar.apply {
set(Calendar.YEAR, alarm_date.year)
set(Calendar.MONTH, alarm_date.monthValue - 1) //0부터 시작한다
set(Calendar.DAY_OF_MONTH, alarm_date.dayOfMonth)
set(Calendar.HOUR_OF_DAY, 0) //24시간으로 지정한다
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
}
참고로 캘린더 색을 바꾸고 싶다면 themes.xml
<style name="Base. 안에 <item name="colorAccent">색깔</item>을 넣으면 된다.
Alarm
알람 만드는 방법
이 기능은 명확히 구분하자면 '알림'은 간단하지만 '알람'이 까다로워 선택사항이었다.
1. manifast에 receiver 추가
<receiver
android:name=".AlarmReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.SANTA" />
</intent-filter>
</receiver>
2. 프래그먼트에서 pendingIntent로 알람리시버에 전달하기
alarmManager에 정의하는 부분에서 경고가 나타났는데 어노테이션을 추가하면 사라지고 기능도 정상작동한다.
//알람리시버에 알림을 예약하는 함수
@SuppressLint("ScheduleExactAlarm")
private fun reserveAlarm() {
val alarmManager = requireContext().getSystemService(ALARM_SERVICE) as AlarmManager
val intent = Intent(requireContext(), AlarmReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast(
requireContext(),
0,
intent,
PendingIntent.FLAG_MUTABLE
)
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
calendar.timeInMillis, pendingIntent
)
}
3. 알람리시버에 알림 정의하기
사실상 알림은 여기서 정의된다. 알림뿐만 아니라 같이 적어둔 기능도 예약된 시간에 출력된다.
class AlarmReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent != null) {
val activityIntent = Intent(context, ContactActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val pendingIntent = PendingIntent.getActivity(
context, 0, activityIntent, PendingIntent.FLAG_UPDATE_CURRENT or
PendingIntent.FLAG_IMMUTABLE
)
val manager =
context?.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
val builder: NotificationCompat.Builder
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val channelId = "one-channel"
val channelName = "산타데이 알림"
val importance = NotificationManager.IMPORTANCE_DEFAULT
val channel = NotificationChannel(
channelId,
channelName,
importance
)
manager.createNotificationChannel(channel)
builder = NotificationCompat.Builder(context, channelId)
} else {
builder = NotificationCompat.Builder(context)
}
builder.run {
setSmallIcon(R.drawable.ic_alert_on)
setWhen(System.currentTimeMillis())
setContentTitle("산타로서 선물할 때가 됐어요!")
setContentText("소중한 사람에게 마음을 전하러 가볼까요?")
setContentIntent(pendingIntent)
setAutoCancel(true)
}
manager.notify(1, builder.build())
Toast.makeText(context, "산타로서 선물할 때가 됐어요!", Toast.LENGTH_SHORT).show()
}
}
}
Animation
쏟아부은 알림에 비해 선물이 너무 초라해보이기도 하고 FAB 배울 때 들었던 AlphaAnimation, ScaleAnimation, TraslateAnimation을 익혀보고 싶어서 효과를 넣어보기로 했다. 튀어나오는 그런 느낌을 내고 싶어 scale로 선택했는데 MaterialDialog처럼 쉽게 쓸 수 있지만 디테일한 설정은 어려워 xml파일로 만들기로 했다.
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<scale
android:pivotX="110%"
android:pivotY="120%"
android:fromXScale="1"
android:fromYScale="1"
android:toXScale="6"
android:toYScale="6"
android:duration="300" />
</set>
fun btnGiftAnimation() {
val scaleOut = AnimationUtils.loadAnimation(requireContext(), R.anim.ainm_detail_gift)
_binding?.detailIvGift?.startAnimation(scaleOut)
}
애니메이션과 다이얼로그 출력이 이어지므로 지연시키는 방법을 써서 둘의 시간을 조절해 자연스럽게 조정할 수 있었다.
//지연시키는 방법
Handler(Looper.getMainLooper()).postDelayed({
//실행할 코드
}, 1000)// 1000 = 1초
그 외 추가적으로 알게 된 점
- 캘린더의 달은 0부터 시작한다.
- 맥북 import정리하는 키 : option + ctrl + O
- pull 받으면 gradle에 입력은 되는데 sync는 각자 해야된다.
회고
도와줄 곳 없는지 적극적으로 물어봐주시고 아는 부분도 꾸준히 나눠주신 팀원분들의 배려덕분에 이번 팀프로젝트도 많이 배울 수 있었다. 특히 주말 내내 같이 4조에서 함께해주신 팀원님 덕분에 웃으며 즐겁게, 또 존경스러워하며 유익하게 끝까지 가득채워 보낼 수 있어 좋았다. 발표도 실수 없이 튜터님과 많은 분들께 칭찬 받아 뿌듯 ᖜ ‿ᖜ
아쉬웠던 점
어려울 때마다 팀원분들이 해결하도록 도움 주신 건 감사했지만 아예 해결해주신 부분도 많아 아쉬웠다. 내가 스스로 해결할만큼 얼른 성장하고 싶다.
알림 클릭하면 어플 실행뿐만 아니라 해당 디테일 프레그먼트로 되돌아가거나, 삭제하는 것까지 구현하고 싶었는데 못한게 아쉽다.
String, Style 등 파일관리와 컨벤션, commit, 기록 좀 더 꼼꼼하고 확실히 하고 싶었는데 뒤로 갈수록 못해서 아쉽다.
다음에 적용할 점
여러 곳에 애니메이션 추가해보자.
기능먼저! UI는 나중에 보완하자.
'안드로이드와 앱 > 프로젝트' 카테고리의 다른 글
개인 프로잭트 [이미지 검색 앱] (0) | 2024.07.30 |
---|---|
팀프로젝트 [산타의 리스트] 발표대본 (0) | 2024.07.28 |
개인 프로젝트 [에코마켓 앱] (0) | 2024.07.11 |
팀프로젝트 [MenuJo] (0) | 2024.07.02 |
개인 프로젝트 [회원가입 앱] (0) | 2024.06.19 |