1. 설계
-contactMethods에 contact정보를 가져오는 메소드들을 작성
-ContactDataSource는 싱글톤 패턴으로 구현하며 ContactEntities에 연락처 정보를 가지고 있음
-ContactRepositoryImpl에서 데이터 소스를 캡슐화하며 접근 메소드들을 구현
-SeviceLocator은 싱글톤으로 구현하며 ContactRepository에 필요한 Context를 제공
-motionLayout을 이용하여 슬라이드 버튼 구현
2. 코드
1)SNS
class SNS(){
var instagram = ArrayList<String>()
var github = ArrayList<String>()
var discord = ArrayList<String>()
}
-SNS정보가 담겨있는 클래스
2)ContactEntity
data class ContactEntity(
//필수 입력 사항
var name: String,
var convertedName: String,
var num: String,
var tag: Int, //0일반, 1즐겨찾기, 2차단
//선택 입력 사항
var img: Uri?,
var birth: String?,
var email: String?,
var sns: SNS?
){
init {
sns=SNS()
}
}
-연락처 정보가 담겨있는 클래스
3)contactMethods
fun contactList(context: Context):ArrayList<ContactEntity>{
val list = ArrayList<ContactEntity>()
val projection=arrayOf(ContactsContract.CommonDataKinds.Phone.CONTACT_ID, ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
ContactsContract.CommonDataKinds.Phone.NUMBER,ContactsContract.CommonDataKinds.Photo.PHOTO_URI, ContactsContract.Contacts.STARRED)
val cursor = context.contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,projection,null,null,null)
?: return list
while(cursor.moveToNext()){
val nameidx=cursor.getColumnIndex(projection[1])
val numberidx=cursor.getColumnIndex(projection[2])
val photoidx=cursor.getColumnIndex(projection[3])
val starredidx=cursor.getColumnIndex(projection[4])
val name=cursor.getString(nameidx)
val number=cursor.getString(numberidx)
val photoUri=cursor.getString(photoidx)?.toUri()
val starred = cursor.getInt(starredidx)
list+=ContactEntity(name, convertString(name),number,starred,photoUri,null,null,null)
}
return list.sortBy{it.name} as ArrayList<ContactEntity>
}
fun convertString(s:String):String{
val result=StringBuilder()
for(i in s){
if(checkKorean(i)) result.append(getFirstAlphabetKorean(i))
else result.append(i)
}
return result.toString()
}
fun checkKorean(c:Char):Boolean{
val char=c.toString()
if(Pattern.matches("^[ㄱ-ㅎ가-힣]*$",char)) return true
else return false
}
fun getFirstAlphabetKorean(c:Char):Char{
return ((c.digitToInt()-0xAC00)/28/21).toChar()
}
-연락처 정보를 가져오고 가공하는 역할을 하는 메소드들이 담겨 있는 파일
-초성 검색을 지원하기 위해 한글인 경우 초성들을 뽑아서 함께 저장
4)ContactDataSource
class ContactDataSource(application: Application) {
companion object{
@Volatile
private var INSTANCE: ContactDataSource? = null
fun getContactDataSource(application: Application): ContactDataSource {
return synchronized(this) {
val newInstance = INSTANCE ?: ContactDataSource(application)
INSTANCE = newInstance
newInstance
}
}
}
val ContactEntities by lazy { contactList(application) }
}
-싱글톤 패턴으로 구현
-연락처 리스트를 저장해둔다.
5)ContactRepository
interface ContactRepository {
fun getContactList() : ArrayList<ContactEntity>
fun addContactList(contact: ContactEntity)
fun modifyContact(idx:Int, contact:ContactEntity)
fun removeContact(idx:Int)
fun search(str:String) :ArrayList<ContactEntity>
}
-레포지토리 구현을 위한 인터페이스
6)ContactRepositoryImple
class ContactRepositoryImpl(private val contactDataSource: ContactDataSource):ContactRepository {
override fun getContactList(): ArrayList<ContactEntity> {
return contactDataSource.ContactEntities
}
override fun addContactList(contact: ContactEntity) {
contactDataSource.ContactEntities.add(contact)
contactDataSource.ContactEntities.sortBy { it.name }
}
override fun modifyContact(idx:Int, contact:ContactEntity) {
contactDataSource.ContactEntities.set(idx, contact)
}
override fun removeContact(idx:Int){
contactDataSource.ContactEntities.removeAt(idx)
}
override fun search(str: String):ArrayList<ContactEntity> {
val result=ArrayList<ContactEntity>()
for(i in contactDataSource.ContactEntities){
if(str.equals(i.name.slice(0..minOf(str.length-1,i.name.length-1)))
||str.equals(i.convertedName.slice(0..minOf(str.length-1,i.name.length-1)))){
result+=i
}
}
return result
}
}
-데이터 소스를 private로 받아와 외부에서 데이터 소스에 접근을 막음
-리스트받아오기, 수정,추가,삭제,검색을 구현
7)MainActivity
class MainActivity : AppCompatActivity() {
val binding by lazy {
ActivityMainBinding.inflate(layoutInflater)
}
var isGrid = false
var isContact = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(binding.main) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
val serviceLocator=ServiceLocator.getInstance(application)
getPermission()
}
fun getPermission(){
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return
val permissions = arrayOf(android.Manifest.permission.READ_CONTACTS, android.Manifest.permission.CALL_PHONE,
android.Manifest.permission.POST_NOTIFICATIONS,android.Manifest.permission.SEND_SMS, android.Manifest.permission.INTERNET)
var flag=false
for(i in permissions){
if(checkSelfPermission(i)== PackageManager.PERMISSION_DENIED){
flag=true
}
}
if(flag) requestPermissions(permissions,0)
else initView()
}
fun initView(){
with(binding){
mainLlGridlist.setOnClickListener{
binding.mainViewWhitebtn.callOnClick()
isGrid=!isGrid
lifecycleScope.launch {
EventBus.produceEvent(isGrid)
}
}
mainBtnContact.setOnClickListener {
if(isContact) return@setOnClickListener
isContact=!isContact
setFragment(isContact)
}
mainBtnMypage.setOnClickListener {
if(!isContact) return@setOnClickListener
isContact=!isContact
setFragment(isContact)
}
mainFbtnAdd.setOnClickListener{
}
mainFbtnAddalarm.setOnClickListener {
}
}
}
fun setFragment(isContact: Boolean){
if(isContact){
}else{
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if(requestCode ==0){
var flag=true
for(i in grantResults){
if(i!=PackageManager.PERMISSION_GRANTED) flag=false
}
if(flag) initView()
}
}
}
-앱 시작시 권한 체크후 권한을 받고 권한을 모두 수락한 경우에만 리스트가 뜨도록 함.
-initView()로 초기화 및 클릭 이벤트 설정
8)메인 액티비티 레이아웃
<FrameLayout
android:id="@+id/main_framelayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/buttonBackground"
android:orientation="horizontal"
android:padding="2dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.constraintlayout.motion.widget.MotionLayout
app:layoutDescription="@xml/main_motion"
android:layout_width="match_parent"
android:layout_height="match_parent">
<View
android:id="@+id/main_view_whitebtn"
app:layout_constraintHorizontal_weight="1"
android:layout_width="0dp"
android:layout_height="30sp"
android:background="@drawable/gridlist_shape"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.motion.widget.MotionLayout>
<LinearLayout
android:id="@+id/main_ll_gridlist"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/main_btn_grid"
android:layout_width="0dp"
android:layout_height="30sp"
android:layout_weight="1"
android:background="@android:color/transparent"
android:text="@string/main_gridviewbtn"
android:textAllCaps="false"
android:textColor="@color/black"
android:textSize="18sp"
android:textStyle="bold"
android:clickable="false"/>
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/main_btn_list"
android:layout_width="0dp"
android:layout_height="30sp"
android:layout_weight="1"
android:background="@android:color/transparent"
android:gravity="center"
android:text="@string/main_listviewbtn"
android:textAllCaps="false"
android:textColor="@color/black"
android:textSize="18sp"
android:clickable="false"/>
</LinearLayout>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:motion="http://schemas.android.com/apk/res-auto">
<Transition
motion:constraintSetStart="@+id/start"
motion:constraintSetEnd="@+id/end"
motion:duration="300">
<OnClick
motion:targetId="@+id/main_view_whitebtn"
motion:clickAction="transitionToEnd" />
</Transition>
<Transition
motion:constraintSetStart="@+id/end"
motion:constraintSetEnd="@+id/start"
motion:duration="300">
<OnClick
motion:targetId="@id/main_view_whitebtn"
motion:clickAction="transitionToEnd"/>
</Transition>
<ConstraintSet android:id="@+id/start">
<Constraint
android:id="@+id/main_view_whitebtn"
android:layout_width="200dp"
android:layout_height="30sp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintTop_toTopOf="parent"/>
</ConstraintSet>
<ConstraintSet android:id="@+id/end">
<Constraint
android:id="@+id/main_view_whitebtn"
android:layout_width="200dp"
android:layout_height="30sp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintTop_toTopOf="parent"/>
</ConstraintSet>
</MotionScene>
-MotionScene에 동작에 대해 정의
-MotionLayout으로 애니메이션 구현
'코틀린 팀플3-연락처 어플 만들기' 카테고리의 다른 글
서비스 로케이터를 MVVM으로 변경, 알람 기능 확장 (0) | 2024.07.26 |
---|---|
알람, 권한 부여, 애니매이션, 착발신 기록 가져오기 (0) | 2024.07.24 |