티스토리 뷰

앱개발 숙련 과제

 

스크롤 상단 이동

 

구현 사항

- 스크롤을 최상단으로 이동시키는 플로팅 버튼 기능 추가
- 플로팅 버튼은 스크롤을 아래로 내릴 때 나타나며, 스크롤이 최상단일때 사라집니다.
- 플로팅 버튼을 누르면 스크롤을 최상단으로 이동시킵니다.
- 플로팅 버튼은 나타나고 사라질때 fade 효과가 있습니다.
- 플로팅 버튼을 클릭하면 아이콘 색이 변경됩니다.

 

플로팅 버튼 추가

<com.google.android.material.floatingactionbutton.FloatingActionButton
    android:id="@+id/fb_scrollUp"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginEnd="20dp"
    android:layout_marginBottom="20dp"
    android:backgroundTint="@color/white"
    app:fabCustomSize="50dp"
    android:src="@drawable/ic_arrow_up" 
    app:tint="@null" 
    android:visibility="invisible"
    app:borderWidth="0dp"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
/>

메인 레이아웃의 우측하단에 플로팅 버튼을 배치한다.

스크롤에 따른 애니메이션을 구현하기 위해 초기에는 안보이게 설정한다.

 

플로팅 버튼의 아이콘은 기본적으로 검은색이지만

app:tint="@null" 속성을 추가해 원래 아이콘 색상으로 설정할 수 있다.

 

 

플로팅 애니메이션 구현

private fun fbAnimation(){
    // 플로팅 버튼 누르면 화면 최상단으로 이동
    binding.fbScrollUp.setOnClickListener {
        binding.recyclerView.smoothScrollToPosition(0)
    }
	
    // 0.5초 동안 투명도를 조절해 페이드 인/아웃 애니메이션을 구현한다.
    val fadeIn = AlphaAnimation(0f, 1f).apply { duration = 500 }
    val fadeOut = AlphaAnimation(1f, 0f).apply { duration = 500 }
    
    var isTop = true // 현재 스크롤 위치가 최상단인지 감지
    
    // 스크롤 위치에 따라 애니메이션을 정의한다.
    binding.recyclerView.addOnScrollListener(object: RecyclerView.OnScrollListener() {
        override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
            super.onScrollStateChanged(recyclerView, newState)
            
            // 스크롤이 최상단에 위치하고, 현재 스크롤 중이 아니라면
            if (!binding.recyclerView.canScrollVertically(-1)
                && newState == RecyclerView.SCROLL_STATE_IDLE) {
                // 페이드 아웃 애니메이션 적용
                binding.fbScrollUp.startAnimation(fadeOut)
                binding.fbScrollUp.visibility = View.GONE
                isTop = true
            } else { 
                // 스크롤이 최상단에 위치한다면
                if(isTop) { 
                    // 페이드 인 애니메이션 적용
                    binding.fbScrollUp.visibility = View.VISIBLE
                    binding.fbScrollUp.startAnimation(fadeIn)
                    isTop = false
                }
            }
        }
    })
}

플로팅 버튼을 눌렀을 때 화면이 최상단으로 스크롤 되고,

스크롤 위치에 따라 플로팅 버튼이 페이드 인/아웃 되도록 설정한다.

 

 

버튼 클릭시 아이콘 색상 변경

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    // 버튼을 눌렀을 때의 아이콘 색상
    <item android:color="@color/black" android:state_pressed="true" /> 
    // 기본 아이콘 색상
    <item android:color="#7A7A7A" android:state_enabled="true" />
</selector>

color라는 res의 하위폴더를 만들어준 뒤 해당 xml파일을 추가한다.

selector를 통해 버튼 상태에 따라 아이콘에 다른 색상을 할당시킨다.

 

app:tint="@color/fab_color_selector"

 플로팅 버튼의 아이콘 색상을 해당 xml파일로 지정한다.

 

 

 

상품 삭제하기

 

구현 사항

- 상품을 롱클릭 했을때 삭제 여부를 묻는 다이얼로그를 띄우고
- 확인을 선택 시 해당 항목을 삭제하고 리스트를 업데이트한다.

 

어댑터 설정

interface ItemLongClick {
    fun onLongClick(view : View, position : Int)
}

var itemLongClick : ItemLongClick? = null

override fun onBindViewHolder(holder: Holder, position: Int) {
    ...
    holder.itemView.setOnLongClickListener() OnLongClickListener@{
        itemLongClick?.onLongClick(it, position)
        return@OnLongClickListener true
    }
}

onClick과 마찬가지로 인터페이스를 통해 메인 액티비티로 클릭 이벤트를 넘겨준다.

 

롱클릭 리스너를 설정구문에서 return@OnLongClickListener true

클릭 이벤트 처리가 완료되었음을 전달하는 역할을 한다.

 

 

인터페이스 구현

adapter.itemLongClick = object : ItemAdapter.ItemLongClick {
     override fun onLongClick(view: View, position: Int) {
        val dialog = AlertDialog.Builder(this@MainActivity)
        dialog.setIcon(R.drawable.ic_chat)
        dialog.setTitle("상품 삭제")
        dialog.setMessage("상품을 정말로 삭제하시겠습니까?")
        dialog.setPositiveButton("확인") { dialog, _ ->
            dataList.removeAt(position)
            adapter.notifyItemRemoved(position)
        }
        dialog.setNegativeButton("취소"){ dialog,_ ->
            dialog.dismiss()
        }
        ad.show()
    }
}

메인에서 어댑터의 itemLongClick 인터페이스를 구현한다.

position값을 이용해 선택한 항목을 삭제하고 리스트뷰를 업데이트한다.

 

 

 

좋아요 처리

 

구현 사항

- 상품 상세 화면에서 좋아요 선택시 아이콘 변경 및 Snackbar 메세지 표시
- 메인 화면으로 돌아오면 해당 상품에 좋아요 표시 및 좋아요 카운트 +1
- 상세 화면에서 좋아요 해제시 이전 상태로 되돌림

 

프로퍼티 추가

val isLike: Boolean

Item 데이터 클래스에 Boolean형 프로퍼티를 하나 추가한다.

 

 

private fun initData(){
    ...
    dataList.add(Item(..., false))
}

테스트를 위해 더미 데이터의 isLike값은 모두 false로 설정한다.

 

 

어댑터 설정

inner class Holder(binding: ItemBinding) : RecyclerView.ViewHolder(binding.root) {
    ...
    val ivLike = binding.ivLike
}

ivLike는 아이템 레이아웃의 좋아요 아이콘에 해당한다.

 

override fun onBindViewHolder(holder: Holder, position: Int) {
    ...
    if(mItems[position].isLike) holder.ivLike.setImageResource(R.drawable.ic_like_filled)
    else holder.ivLike.setImageResource(R.drawable.ic_like_border)
}

Item클래스의 isLike값에 따라 홀더의 좋아요 아이콘을 초기화한다.

 

 

디테일 액티비티

private var isLike = false // 좋아요 클릭 여부

// 인텐트에서 아이템 위치를 받아옴
private val itemPosition: Int by lazy {
    intent.getIntExtra(Constants.ITEM_INDEX, 0)
}

디테일 액티비티에서 필요한 변수들을 선언한다.

 

private fun setLike(){
    // Item의 isLike 초기값에 따라 아이콘을 초기화한다.
    isLike = item?.isLike == true
    binding.ivDetailLike.setImageResource(
        if (isLike) {
            R.drawable.ic_like_filled
        } else {
            R.drawable.ic_like_border
        }
    )
	
    // 좋아요 아이콘을 눌렀을 때의 동작 정의
    binding.ivDetailLike.setOnClickListener {
        if (!isLike) {
            binding.ivDetailLike.setImageResource(R.drawable.ic_like_filled)
            Snackbar.make(binding.constLayout, "관심 목록에 추가되었습니다.", Snackbar.LENGTH_SHORT).show()
            isLike = true
        } else {
            binding.ivDetailLike.setImageResource(R.drawable.ic_like_border)
            isLike = false
        }
    }
}

Item의 isLike값에 따라 해당 아이콘을 초기화하고,

좋아요 아이콘을 눌렀을 때의 동작을 정의하는 함수이다.

 

object Constants {
   ...
    const val IS_LIKE = "is_like"
}

좋아요 여부를 메인액티비티에 넘겨주기 위해 해당 키네임을 추가해준다.

 

private fun exit() {
    val intent = Intent(this, MainActivity::class.java).apply {
        putExtra(Constants.ITEM_INDEX, itemPosition)
        putExtra(Constants.IS_LIKE, isLike)
    }
    setResult(RESULT_OK, intent) // 이전 액티비티로 인텐트 전달 후
    finish() // 현재 액티비티 종료
}

private val callback = object : OnBackPressedCallback(true) {
    override fun handleOnBackPressed() {
        exit()
    }
}

exit 메소드는 현재 디테일 액티비티를 종료했을 때 인텐트를 이용해

메인 엑티비티로 해당 아이템의 인덱스와 좋아요 여부를 넘겨준다. 

 

binding.ivBack.setOnClickListener { exit() }
this.onBackPressedDispatcher.addCallback(this, callback)

뒤로가기를 눌렀을 때 exit 메소드가 호출되도록 한다.

 

 

메인 액티비티

lateinit var activityResultLauncher: ActivityResultLauncher<Intent>

액티비티의 실행결과를 얻어오기 위해 activityResultLauncher라는 것을 사용한다.

 

activityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
    if (it.resultCode == RESULT_OK) {
        val itemIndex = it.data?.getIntExtra(Constants.ITEM_INDEX,0) as Int
        val isLike = it.data?.getBooleanExtra(Constants.IS_LIKE,false) as Boolean
		
        if(isLike) { 
            dataList[itemIndex].isLike = true 
            dataList[itemIndex].LikeCnt += 1
        }else {
            if(dataList[itemIndex].isLike) {
                dataList[itemIndex].isLike = false
                dataList[itemIndex].LikeCnt -= 1
             }
         }

        adapter.notifyItemChanged(itemIndex)
    }
}

디테일에서 넘겨받은 데이터를 이용해 해당 아이템의 좋아요 카운트와 아이콘을 초기화한다.

 

디테일에서 좋아요를 눌렀다면 메인에서 해당 아이템의 좋아요 처리를 진행하고,

좋아요 표시가 되있는 상태에서 좋아요를 취소했다면 해당 아이템의 좋아요 처리를 취소한다.

 

마지막으로 아이템 인덱스를 통해 아이템의 상태변경을 어댑터에 알린다.

 

 

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
글 보관함