diff --git a/README.md b/README.md index cc0acc5a..541e1612 100644 --- a/README.md +++ b/README.md @@ -1 +1,94 @@ -# android-contacts +# KakaoTechCampus 2기 step2 +## 1주차 1단계 과제 - 연락처 추가 + +### 기능 설명 +1. 전체 폼 디자인 +2. 프로필 이미지 설정 +3. 입력 필드 기능 +4. 전화번호 필수 입력 값 기능 +5. 더보기 토글 기능 +6. 생일 선택 캘린더 팝업 기능 +7. 성별 선택 기능 +8. 취소 기능 +9. 저장 기능 + +--- + +## 1주차 2단계 과제 - 연락처 목록 + +### 화면 3가지 +> main 화면 +> > 오른쪽 하단에 + 블록 버튼이 있는 연락처 조회 및 추가 시작 화면 + +> 연락처 추가 화면 +> > 1단계에서 구현한 화면 + +> 연락처 세부사항 조회 화면 +> > 추가된 연락처를 누룰 시, 해당 연락처 프로필 이미지, 이름, 전화번호 조회 가능 + +### 작동 방법 +1. main 화면에서 오른쪽 하단의 + 버튼을 누르면 '연락처 추가 화면'으로 넘어간다. +2. 해당 '연락처 추가 화면'에서 이름과 전화번호(필수값)을 입력 후 '저장'을 누르면 다시 초기의 main 화면으로 돌아간다. 단, 이때 main 화면에는 추가한 연락처 목록이 조회된다. +3. 해당 연락처 목록을 클릭 시 '연락처 세부사항 조회 화면'으로 넘어간다. +4. 해당 '연락처 세부사항 조회 화면'에는 입력한 이름, 전화번호(필수값)이 있고, 그 외 입력한 값들이 있다면 그 정보들이 추가로 떠 있다. +5. 이후 뒤로가기 버튼을 누르면 다시 main 화면으로 돌아간다. +6. 앱을 종료 시 해당 데이터들은 모두 리셋된다. + +### 기능 구현 +1. 연락처 추가 기능 구현 - 1단계 구현 내용 +2. main 화면 플로팅 버튼 및 텍스트 구현 +3. 추가한 연락처 데이터 - contact 객체 - intent로 전달 기능 구현 +4. 전달받은 객체 데이터를 Recyclerview로 표현 +5. 연락처 추가 시 항목 리스트 조회 기능 구현 +6. 추가된 연락처 스크롤 및 세부사항 화면 클릭 기능 구현 +7. 연락처 세부 사항 조회 기능 구현 +8. 세부 조회 데이터 조건 구현 + +--- + +### 전체 디렉토리 구조 (2단계 기준) +```bash +app +├── build +├── src +│ ├── androidTest +│ │ └── java +│ │ └── contacts +│ │ └── .gitkeep +│ ├── main +│ │ ├── java +│ │ │ └── contacts +│ │ │ ├── Contact.kt +│ │ │ ├── ContactAddActivity.kt +│ │ │ ├── ContactDetailActivity.kt +│ │ │ ├── ContactsAdapter.kt +│ │ │ └── MainActivity.kt +│ │ └── res +│ │ ├── drawable +│ │ │ ├── circle_background.xml +│ │ │ ├── ic_add.xml +│ │ │ ├── ic_launcher_background.xml +│ │ │ ├── ic_launcher_foreground.xml +│ │ │ ├── profile_image.png +│ │ │ ├── round_background.xml +│ │ │ └── round_corners.xml +│ │ └── layout +│ │ ├── activity_main.xml +│ │ ├── contact_add.xml +│ │ ├── contact_detail.xml +│ │ └── contact_item.xml +├── .gitignore +├── build.gradle.kts +└── proguard-rules.pro +``` +### 구현 화면 + + + + + + + + +### 실행 영상 (전체 실행영상) +[[https://youtu.be/re7rHcPNlhE](https://youtu.be/S2kDtQot_4Y)](https://youtu.be/UEXYAcABTMc) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 89dc9d8b..0f8766f7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -21,6 +21,8 @@ + + diff --git a/app/src/main/java/campus/tech/kakao/contacts/Contact.kt b/app/src/main/java/campus/tech/kakao/contacts/Contact.kt new file mode 100644 index 00000000..4333cba4 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/contacts/Contact.kt @@ -0,0 +1,52 @@ +package campus.tech.kakao.contacts + +import android.os.Parcel +import android.os.Parcelable + +//연락처 데이터 객체 클래스 +data class Contact ( + val name: String, + val phone: String, + val email: String?, + val birth: String?, + val gender: String?, + val memo: String? +) : Parcelable { //intent로 전달하는 거 + constructor(parcel: Parcel) : this( + parcel.readString() ?: "", + parcel.readString() ?: "", + parcel.readString(), + parcel.readString(), + parcel.readString(), + parcel.readString() + ) + + //Contact 객체 데이터 내용을 parcel에 쓰기 + override fun writeToParcel(parcel: Parcel, flags: Int){ + parcel.writeString(name) + parcel.writeString(phone) + parcel.writeString(email) + parcel.writeString(birth) + parcel.writeString(gender) + parcel.writeString(memo) + } + + //오류 해결 + //Parcelable 필수 + override fun describeContents(): Int { + return 0 + } + + //Parcelable 객체 생성 + companion object CREATOR: Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): Contact { + return Contact(parcel) + } + + //parcelable 인터페이스의 객체 직렬화 시 필수 - newArray method + override fun newArray(size:Int):Array{ + return arrayOfNulls(size) + } + } + +} diff --git a/app/src/main/java/campus/tech/kakao/contacts/ContactAddActivity.kt b/app/src/main/java/campus/tech/kakao/contacts/ContactAddActivity.kt new file mode 100644 index 00000000..a7181d33 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/contacts/ContactAddActivity.kt @@ -0,0 +1,126 @@ +package campus.tech.kakao.contacts + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import android.app.DatePickerDialog //날짜 선택 +import java.text.SimpleDateFormat // 날짜 형식 지정 +import java.util.* +import android.widget.* +import android.content.Intent + +class ContactAddActivity : AppCompatActivity() { + + private lateinit var nameText: EditText + private lateinit var phoneText: EditText + private lateinit var emailText: EditText + private lateinit var birthText: TextView + private lateinit var memoText: EditText + private lateinit var moreLayout: LinearLayout + private lateinit var moreText: TextView + private lateinit var cancelButton: Button + private lateinit var saveButton: Button + private lateinit var genderRadio: RadioGroup + private lateinit var femaleButton: RadioButton + private lateinit var maleButton: RadioButton + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.contact_add) + + initialViews() + + //호출 + moreText.setOnClickListener{ + toggleMore() + } + + birthText.setOnClickListener{ + showDatePicker() + } + + cancelButton.setOnClickListener { + Toast.makeText(this,getString(R.string.cancel_message), Toast.LENGTH_SHORT).show() + finish() //main 화면으로 돌아가기 + } + + saveButton.setOnClickListener { + val name = nameText.text.toString().trim() + val phone = phoneText.text.toString().trim() + val email = emailText.text.toString().trim() + val birth = birthText.text.toString().trim() + val memo = memoText.text.toString().trim() + + //이름 or 전화번호 미 입력 시 저장 불가 + if(name.isEmpty() || phone.isEmpty()){ + Toast.makeText(this, getString(R.string.notice_message), Toast.LENGTH_SHORT).show() + return@setOnClickListener + } + + //성별 선택 + //아무런 성별도 선택하지 않은 경우 " " 값이 default 이어야 함 + val gender = when(genderRadio.checkedRadioButtonId){ + R.id.femaleButton -> "여성" + R.id.maleButton -> "남성" + else -> " " + } + + //contact 객체 생성 + val contact = Contact(name, phone, email, birth, gender, memo) + + //intent로 결과 반환 + val resultIntent = Intent() + resultIntent.putExtra("contact", contact) + setResult(RESULT_OK, resultIntent) + + Toast.makeText(this, getString(R.string.save_message), Toast.LENGTH_SHORT).show() + finish() + } + } + + private fun initialViews(){ + nameText = findViewById(R.id.nameText) + phoneText = findViewById(R.id.phoneText) + emailText = findViewById(R.id.emailText) + birthText = findViewById(R.id.birthText) + memoText = findViewById(R.id.memoText) + moreLayout = findViewById(R.id.moreLayout) + moreText = findViewById(R.id.moreText) + cancelButton = findViewById(R.id.cancelButton) + saveButton = findViewById(R.id.saveButton) + genderRadio = findViewById(R.id.genderRadio) + femaleButton = findViewById(R.id.femaleButton) + maleButton = findViewById(R.id.maleButton) + } + //더보기 토글 기능 - 표시 및 숨기기 + private fun toggleMore(){ + val moreLayout = findViewById(R.id.moreLayout) + val moreText = findViewById(R.id.moreText) + + if(moreLayout.visibility == LinearLayout.GONE){ + moreLayout.visibility = LinearLayout.VISIBLE + moreText.visibility = TextView.GONE + } + else { + moreLayout.visibility = LinearLayout.GONE + moreText.visibility = TextView.VISIBLE + } + } + + private fun showDatePicker() { + val calendar = Calendar.getInstance() + val datePicker = DatePickerDialog( + this, + { _, year, month, day -> + val selectedDate = Calendar.getInstance() + selectedDate.set(year, month, day) + val setting = SimpleDateFormat("yyyy.MM.dd", Locale.getDefault()) + val formDate = setting.format(selectedDate.time) + birthText.text = formDate + }, + calendar.get(Calendar.YEAR), + calendar.get(Calendar.MONTH), + calendar.get(Calendar.DAY_OF_MONTH) + ) + datePicker.show() + } +} + diff --git a/app/src/main/java/campus/tech/kakao/contacts/ContactDetailActivity.kt b/app/src/main/java/campus/tech/kakao/contacts/ContactDetailActivity.kt new file mode 100644 index 00000000..46866eed --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/contacts/ContactDetailActivity.kt @@ -0,0 +1,87 @@ +package campus.tech.kakao.contacts + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import android.widget.ImageView +import android.widget.TextView + +class ContactDetailActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.contact_detail) + + //view 초기화 보는 거 + val profileView = findViewById(R.id.profileView) + val nameView = findViewById(R.id.nameView) + val phoneView = findViewById(R.id.phoneView) + val emailView = findViewById(R.id.emailView) + val birthView = findViewById(R.id.birthView) + val genderView = findViewById(R.id.genderView) + val memoView = findViewById(R.id.memoView) + + val nameLabelText = findViewById(R.id.nameLabelText) + val phoneLabelText = findViewById(R.id.phoneLabelText) + val emailLabelText = findViewById(R.id.emailLabelText) + val birthLabelText = findViewById(R.id.birthLabelText) + val genderLabelText = findViewById(R.id.genderLabelText) + val memoLabelText = findViewById(R.id.memoLabelText) + + //parcel에 적어서 intent로 전달된 contact 객체 가져오기 + val contact = intent.getParcelableExtra("contact") + + //연락처 정보 설정하기 - 조건 + contact?.let { + + //이름 필수조건 + nameView.text = it.name + nameView.visibility = TextView.VISIBLE + nameLabelText.visibility = TextView.VISIBLE + + //이메일 필수조건 + phoneView.text = it.phone + phoneView.visibility = TextView.VISIBLE + phoneLabelText.visibility = TextView.VISIBLE + + //email + if (!it.email.isNullOrEmpty()) { + emailView.text = it.email + emailView.visibility = TextView.VISIBLE + emailLabelText.visibility = TextView.VISIBLE + } else { + emailView.visibility = TextView.GONE + emailLabelText.visibility = TextView.GONE + } + + //birth + if (!it.birth.isNullOrEmpty()) { + birthView.text = it.birth + birthView.visibility = TextView.VISIBLE + birthLabelText.visibility = TextView.VISIBLE + } else { + birthView.visibility = TextView.GONE + birthLabelText.visibility = TextView.GONE + } + + //gender + //default 값이 아닌 경우 값을 집어 넣도록 설정 - '성별' 문구 안뜨도록 + if (!it.gender.isNullOrEmpty() && it.gender != " ") { + genderView.text = it.gender + genderView.visibility = TextView.VISIBLE + genderLabelText.visibility = TextView.VISIBLE + } else { + genderView.visibility = TextView.GONE + genderLabelText.visibility = TextView.GONE + } + + //memo + if (!it.memo.isNullOrEmpty()) { + memoView.text = it.memo + memoView.visibility = TextView.VISIBLE + memoLabelText.visibility = TextView.VISIBLE + } else { + memoView.visibility = TextView.GONE + memoLabelText.visibility = TextView.GONE + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/contacts/ContactsAdapter.kt b/app/src/main/java/campus/tech/kakao/contacts/ContactsAdapter.kt new file mode 100644 index 00000000..94e6b538 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/contacts/ContactsAdapter.kt @@ -0,0 +1,55 @@ +package campus.tech.kakao.contacts + +import androidx.recyclerview.widget.RecyclerView +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import android.view.LayoutInflater +class ContactsAdapter ( + //contact 객체 데이터 저장 + val contacts : List, + val clickListner : (Contact) -> Unit +) : RecyclerView.Adapter(){ + + class ContactViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){ + val contactInitial = itemView.findViewById(R.id.contactInitial) + val contactName = itemView.findViewById(R.id.contactName) + + //연락처 데이터 view 바인딩 + fun bind(contact: Contact, clickListener: (Contact) -> Unit) { + //연락처 초기 문자 설정 + val contactFirstChar = contact.name.first().toString() + contactInitial.text = contactFirstChar + + //연락처 이름 설정 + val contactFullName = contact.name + contactName.text = contactFullName + + // 클릭 리스너 설정 + itemView.setOnClickListener { + clickListener(contact) + } + } + } + //viewholder 객체 생성 + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ContactViewHolder{ + // layoutinflater 객체 생성 + val inflater = LayoutInflater.from(parent.context) + //xml 레이아웃 파일 -> view 객체 + val itemLayout = R.layout.contact_item + //inflate 메서드 호출 -> view 객체 생성 + val createView = false + val view = inflater.inflate(itemLayout, parent, createView) + + return ContactViewHolder(view) + } + + //데이터 바인딩하기 + override fun onBindViewHolder(holder: ContactViewHolder, position: Int) { + holder.bind(contacts[position], clickListner) + } + + //RecyclerView 개수, contacts 리스트 크기 반환 + override fun getItemCount(): Int = contacts.size + +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/contacts/MainActivity.kt b/app/src/main/java/campus/tech/kakao/contacts/MainActivity.kt index 7aae79fe..4c41681d 100644 --- a/app/src/main/java/campus/tech/kakao/contacts/MainActivity.kt +++ b/app/src/main/java/campus/tech/kakao/contacts/MainActivity.kt @@ -2,10 +2,92 @@ package campus.tech.kakao.contacts import android.os.Bundle import androidx.appcompat.app.AppCompatActivity +import android.content.Intent +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.floatingactionbutton.FloatingActionButton +import android.widget.TextView class MainActivity : AppCompatActivity() { + //전체 + lateinit var noContactsText: TextView + lateinit var contactsRecyclerView: RecyclerView + lateinit var addContactButton: FloatingActionButton + val contactsList = mutableListOf() //list 조회 + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) + + //view 설정 + noContactsText = findViewById(R.id.noContactsText) + contactsRecyclerView = findViewById(R.id.contactsRecyclerView) + addContactButton = findViewById(R.id.addContactButton) + + //Recyclerview - 수직 배열 & 세부 조회 (5,6번 기능 한 번에 commit함) + contactsRecyclerView.layoutManager = LinearLayoutManager(this) + contactsRecyclerView.adapter = + ContactsAdapter(contactsList) { + contact -> + //세부 조회 + val intent = Intent(this, ContactDetailActivity::class.java) + + //intent 담아서 전달 + intent.putExtra("contact", contact) + startActivity(intent) + } + + //연락처 추가 floating click listener + addContactButton.setOnClickListener{ + //새 intent 객체 create + val intent = Intent( + this, + ContactAddActivity::class.java + ) + //추가 후 요청 결과 받기 + startActivityForResult(intent, REQUEST_CODE_ADD) + } + updateUI() + } + + // 연락처 추가 시 비교 + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + + //요청 코드 비교 + if(requestCode == REQUEST_CODE_ADD && resultCode == RESULT_OK) { + val intentData = data + + //parcelable 객체 추출 + val parcelableContact = intentData?.getParcelableExtra("contact") + + //null 여부 확인 + if (parcelableContact != null) { + val contact = parcelableContact + + //연락처 리스트에 추가 + contactsList.add(contact) + + updateUI() + } + } + } + + //요청 코드 + companion object { + const val REQUEST_CODE_ADD = 1 + } + + //UI 업데이트 + private fun updateUI(){ + //contactslist 비어있는 경우 + if (contactsList.isEmpty()) { + noContactsText.visibility = TextView.VISIBLE + contactsRecyclerView.visibility = RecyclerView.GONE + } else { //연락처 하나 이상 있는 경우 + noContactsText.visibility = TextView.GONE + contactsRecyclerView.visibility = RecyclerView.VISIBLE + contactsRecyclerView.adapter?.notifyDataSetChanged() //adapter에 추가된 데이터 변경사항 알리기 + } } } diff --git a/app/src/main/res/drawable/circle_background.xml b/app/src/main/res/drawable/circle_background.xml new file mode 100644 index 00000000..8cf8e4c8 --- /dev/null +++ b/app/src/main/res/drawable/circle_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_add.xml b/app/src/main/res/drawable/ic_add.xml new file mode 100644 index 00000000..5ad8b43d --- /dev/null +++ b/app/src/main/res/drawable/ic_add.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/profile_image.png b/app/src/main/res/drawable/profile_image.png new file mode 100644 index 00000000..19fbda14 Binary files /dev/null and b/app/src/main/res/drawable/profile_image.png differ diff --git a/app/src/main/res/drawable/round_background.xml b/app/src/main/res/drawable/round_background.xml new file mode 100644 index 00000000..2368e727 --- /dev/null +++ b/app/src/main/res/drawable/round_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/round_corners.xml b/app/src/main/res/drawable/round_corners.xml new file mode 100644 index 00000000..56524509 --- /dev/null +++ b/app/src/main/res/drawable/round_corners.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 24d17df2..51a9917a 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -2,18 +2,40 @@ + + + app:layout_constraintEnd_toEndOf="parent" + android:layout_marginTop="16dp" + android:layout_marginBottom="16dp" + android:visibility="gone"/> - + + \ No newline at end of file diff --git a/app/src/main/res/layout/contact_add.xml b/app/src/main/res/layout/contact_add.xml new file mode 100644 index 00000000..2c3ebacc --- /dev/null +++ b/app/src/main/res/layout/contact_add.xml @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +