Skip to content

Main 홈, 스크롤

yunso edited this page Jan 15, 2021 · 5 revisions

🔥 Main 화면 구성 (홈 + 스크롤)

  • 해당 디렉토리로 이동하기
  • viewPager2를 이용해 2개의 프래그먼트를 vertical scroll로 연결하였습니다.
  • ScrollFragment에서 스크롤시 viewPager2가 스크롤되는 것을 막기 위하여 NestedScrollableHost를 이용해 자식 뷰의 스크롤을 우선 인식할 수 있도록 설정했습니다.

fragment_home.xml

<com.example.momo_android.util.ui.NestedScrollableHost
    android:id="@+id/nestedScrollableHost"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView_gradient"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:overScrollMode="never"
        tools:listitem="@layout/item_scroll_gradient" />

</com.example.momo_android.util.ui.NestedScrollableHost>

🔥 HomeFragment

✔️ Day & Night mode

  • 현재 시간을 바탕으로 06:00 ~ 18:59 까지는 Day mode,
    그 이외의 시간에는 Night mode의 UI를 보여줍니다.

HomeFragment.kt

private fun setDayNightStatus() {
    val currentHourDay = Calendar.getInstance().get(Calendar.HOUR_OF_DAY) // 24시간 포맷
    if (currentHourDay in 6..18) { // 06:00 ~ 18:59
        setDayView()
        isDay = true
    } else {
        setNightView()
        isDay = false
    }
    setLoadingViewBackground()
}

✔️ Empty & Diary view

  • 사용자가 현재 일자에 업로드한 일기가 없을 경우, 비어있는 홈 화면을 보여주고
    현재 일자에 이미 일기를 업로드했을 경우 해당 일기 정보를 보여줍니다.

HomeFragment.kt

private fun setServerDiaryData(diaryList: List<ResponseDiaryList.Data>) {
    when (diaryList.size) {
        0 -> {
            setEmptyView()
            DIARY_STATUS = false
        }
        else -> {
            setDiaryView()
            DIARY_STATUS = true
            diaryId = diaryList[0].id
            setEmotionData(diaryList[0].emotionId, isDay)
            setDepthData(diaryList[0].depth)
            setBookDiaryData(diaryList[0])
        }
    }
    fadeOutLoadingView()
}

✔️ Refresh server data

  • 다이어리 상세 뷰에서 수정을 한 후, 다시 홈 화면으로 돌아왔을 때 IS_EDITED boolean 값을 이용해 데이터를 다시 로드합니다.

HomeFragment.kt

override fun onResume() {
    super.onResume()
    if(IS_EDITED) {
        updateByServerData()
        IS_EDITED = false
    }
}


🔥 ScrollFragment

✔️ 그라디언트 및 오브제로 이루어진 깊이 배경 아이템 배치

  • 총 7개의 깊이 단계에 각기 다른 백그라운드를 적용하기 위해 각 단계를 recyclerView의 한 아이템으로 구성했습니다.

ScrollFragment.kt

private fun setGradientRecyclerView(year: Int, month: Int) {
    viewBinding.recyclerViewGradient.apply {
        adapter = ScrollGradientAdapter(year, month)
        layoutManager = LinearLayoutManager(requireContext())
        addOnScrollListener(scrollListener)
    }
}

  • 뷰홀더에서 그라디언트 아이템이 바인딩될 때, viewStub을 이용하여 동적으로 배경 리소스를 교체하였습니다.

ScrollGradientViewHolder.kt

private fun setDepthViews(position: Int) {
    viewBinding.apply {
        constraintLayout.removeAllViews()
        when (position) {
            0 -> {
                textViewDepth.text = "2m"
                viewStubGradient.layoutResource = R.layout.view_stub_depth_1
            }
            1 -> {
                textViewDepth.text = "30m"
                viewStubGradient.layoutResource = R.layout.view_stub_depth_2
            }
            2 -> {
                textViewDepth.text = "100m"
                viewStubGradient.layoutResource = R.layout.view_stub_depth_3
            }
            3 -> {
                textViewDepth.text = "300m"
                viewStubGradient.layoutResource = R.layout.view_stub_depth_4
            }
            4 -> {
                textViewDepth.text = "700m"
                viewStubGradient.layoutResource = R.layout.view_stub_depth_5
            }
            5 -> {
                textViewDepth.text = "1,005m"
                viewStubGradient.layoutResource = R.layout.view_stub_depth_6
            }
            6 -> {
                textViewDepth.text = "심해"
                viewStubGradient.layoutResource = R.layout.view_stub_depth_7
            }
        }
        constraintLayout.addView(viewBinding.viewStubGradient)
        constraintLayout.addView(viewBinding.textViewDepth)
        constraintLayout.addView(viewBinding.recyclerViewOval)
        viewStubGradient.inflate()
    }
}

✔️ 각 깊이 단계에 일기 물방울 아이템 배치

  • 각 깊이 단계에 해당하는 일기 물방울 아이템을 배치하기 위해 recyclerView를 깊이 아이템 뷰에 사용하였습니다. (2중 리사이클러뷰)

ScrollGradientViewHolder.kt

private fun setOvalRecyclerView(depth: Int, wholeDiaryList: List<ResponseDiaryList.Data>) {
    val depthDiaryList = sortServerDiaryData(depth, wholeDiaryList)
    viewBinding.recyclerViewOval.adapter = ScrollOvalAdapter(this, depthDiaryList)
}

✔️ 랜덤 x좌표에 물방울 아이템 배치

  • 서버에서 넘어오는 0~9의 랜덤값에 따른 x좌표에 물방울 아이템을 배치하였습니다.

ScrollOvalViewHolder.kt

fun onBind(diaryData: ResponseDiaryList.Data) {
    getItemAreaWidth()
    setOvalXPosition(diaryData.position)
    setDiaryData(diaryData)
}

private fun getItemAreaWidth() {
    // (디바이스 너비 - 아이템 너비 - 좌우여백) / (한 행당 아이템 최대 개수 - 1)
    val deviceWidthPixels = displayMetrics.widthPixels
    val itemWidthPixels = ITEM_SIZE * displayMetrics.density
    val horizontalMarginPixels = HORIZONTAL_MARGIN * 2 * displayMetrics.density
    itemDistance =
        (deviceWidthPixels.toFloat() - itemWidthPixels - horizontalMarginPixels) / (ITEM_AMOUNT - 1)
}

private fun setOvalXPosition(xPosition: Int) {
    // itemDistance * 아이템 랜덤값 + 좌측 여백 px
    val leftMargin = ((itemDistance * xPosition) + (HORIZONTAL_MARGIN * displayMetrics.density))
    val layoutParams = viewBinding.imageButtonOval.layoutParams as ConstraintLayout.LayoutParams
    layoutParams.marginStart = leftMargin.toInt()
    viewBinding.imageButtonOval.layoutParams = layoutParams
}

✔️ 스크롤에 따른 리소스 상태 설정

  • 스크롤 중에는 스와이프 아이콘이 보이지 않다가 스크롤을 멈췄을 때 스와이프 아이콘이 보여집니다.

ScrollFragment.kt

private val scrollListener = object : RecyclerView.OnScrollListener() {
    @RequiresApi(Build.VERSION_CODES.N)
    override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
        super.onScrolled(recyclerView, dx, dy)
        val visibleItemPosition = getVisibleItemPosition()
        checkHomeButtonStatus(visibleItemPosition)
        updateVerticalSeekBar(visibleItemPosition)
    }
    override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
        super.onScrollStateChanged(recyclerView, newState)
        when (newState) {
            SCROLL_STATE_IDLE -> fadeInSwipeImage()
            SCROLL_STATE_DRAGGING -> fadeOutSwipeImage()
        }
    }
}

private fun fadeInSwipeUpImage() {
    viewBinding.imageViewSwipeUp.apply {
        visibility = View.VISIBLE
        alpha = 0f
        animate()
            .alpha(1f)
            .setDuration(resources.getInteger(android.R.integer.config_longAnimTime).toLong())
            .setListener(null)
    }
}

private fun fadeOutSwipeUpImage() {
    viewBinding.imageViewSwipeUp.apply {
        visibility = View.VISIBLE
        alpha = 1f
        animate()
            .alpha(0f)
            .setDuration(resources.getInteger(android.R.integer.config_longAnimTime).toLong())
            .setListener(object : AnimatorListenerAdapter() {
                override fun onAnimationEnd(animation: Animator) {
                    viewBinding.viewLoading.visibility = View.INVISIBLE
                }
            })
    }
}

  • 스크롤 시, 현재 보여지는 recyclerView itemPosition에 따라 우측의 seekBar가 이동합니다.

ScrollFragment.kt

@RequiresApi(Build.VERSION_CODES.N)
private fun updateVerticalSeekBar(visibleItemPosition: Int) {
    when (visibleItemPosition) {
        0 -> {}
        else -> {
            ObjectAnimator
                .ofInt(
                    viewBinding.verticalSeekBarDepth,
                    "progress",
                    (visibleItemPosition - 1) * 80
                )
                .setDuration(1000)
                .start()
        }
    }
}

✔️ Scroll to top

  • 스크롤 뷰에서 홈 버튼을 눌렀을 경우 아이템 0번째 포지션까지 스크롤이 된 후 홈 화면으로 이동합니다.

ScrollFragment.kt

private val fragmentOnClickListener = View.OnClickListener {
    viewBinding.apply {
        when (it.id) {
            imageButtonMy.id -> setIntentToSettingActivity()
            imageButtonCalendar.id -> setIntentToDatePicker()
            imageButtonHome.id -> scrollToTop()
            imageButtonUpload.id -> setIntentToUploadActivity()
            imageButtonList.id -> setIntentToListActivity()
        }
    }
}

private fun scrollToTop() {
    if (getVisibleItemPosition() == 0) {
        requireActivity().onBackPressed()
    }
    viewBinding.recyclerViewGradient.smoothScrollToPosition(0)
    isHomeButtonClicked = true
}

private fun checkHomeButtonStatus(visibleItemPosition: Int) {
    if (visibleItemPosition == 0 && isHomeButtonClicked) {
        IS_FROM_SCROLL = true
        requireActivity().onBackPressed()
        isHomeButtonClicked = false
    }
}

✔️ Refresh server data

  • 일기 수정 후 다시 스크롤 뷰로 돌아왔을 경우 데이터를 다시 로드한 후 해당 깊이를 보여줍니다.

ScrollFragment.kt

override fun onResume() {
    super.onResume()
    if(IS_EDITED) {
        setLoadingViewBackground()
        viewBinding.recyclerViewGradient.adapter!!.notifyDataSetChanged()
        viewBinding.recyclerViewGradient.scrollToPosition(EDITED_DEPTH + 1)
        fadeOutLoadingView()
        IS_EDITED = false
    }
}