Skip to content

Commit

Permalink
ui: 타임라인 프래그먼트(BottomSheet) 구현 #55 (#71)
Browse files Browse the repository at this point in the history
* ui: 타임라인 View xml 파일 작성

- 타임라인에 나타날 여행 상세 아이템 xml 작성
  - 썸네일 사진 유무에 따라 뷰를 구분
- 타임라인이 나타날 fragment xml 작성

* feat: 타임라인 여행 상세 아이템 UI 모델 생성

* ui: 썸네일이 없는 여행상세 아이템의 margin 조정

* ui: Timeline RecyclerView의 layoutManager 설정

* ui: xml에서의 UiModel 데이터 바인딩 설정

* feat: ViewHolder 작성

- 썸네일 사진 유무에 따라 다른 ViewHolder로 구분
- 공통된 속성을 정의한 TimelineViewHolder 추상클래스 생성

* feat: TimelineRepository Interface 생성

* feat: 임시 TimelineRepository 구현체 생성

* feat: TimelineViewModel 및 Factory 생성

* feat: TimelineViewType 작성

* feat: TimelineAdapter 작성

* feat: TimelineFragment에 ViewModel과 Adapter 구현

* feat: 이미지 로딩 PlaceHolder drawable 추가 및 적용

* feat: 임시 데이터 연결

* ui: Timeline fragment 의 세부 설정 조정

* ui: Timeline의 Item xml 변경

- 뷰 타입을 3개로 분할: 첫 번째 아이템, 중간 아이템, 마지막 아이템
- 이에 따라 xml 파일 추가 및 view 수정

* feat: ViewType 변경에 따른 Adapter 및 ViewHolder 수정

* refactor: 불필요한 View 및 ViewHolder 제거

* feat: 여행 click 에 대한 event handler 생성 및 설정

* refactor: drawable 이름을 네이밍 컨벤션에 맞게 수정

* ui: RecyclerView의 마진 속성을 패딩 속성으로 변경

* feat: 바텀 시트 디자인 변경 및 툴바와의 상호작용 구현

* ui: 타임라인 글귀 추가

* style: ktlint check

* ui: 둥근 모서리의 이미지로 변경
  • Loading branch information
Junyoung-WON authored Jul 25, 2024
1 parent df877a4 commit 8bf48a0
Show file tree
Hide file tree
Showing 23 changed files with 688 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.woowacourse.staccato.domain.repository

import com.woowacourse.staccato.presentation.timeline.TimelineTravelUiModel

interface TimelineRepository {
fun loadTravels(): List<TimelineTravelUiModel>
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.woowacourse.staccato.presentation.main

import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.addCallback
import androidx.activity.result.contract.ActivityResultContracts
Expand Down Expand Up @@ -69,6 +70,8 @@ class MainActivity : BindingActivity<ActivityMainBinding>() {
setupBottomSheetController()
setupBottomSheetNavigation()
setupBackPressedHandler()
setUpBottomSheetBehaviorAction()
setUpToolbar()
}

private fun setupBackPressedHandler() {
Expand Down Expand Up @@ -134,4 +137,51 @@ class MainActivity : BindingActivity<ActivityMainBinding>() {
.setLaunchSingleTop(true)
.setPopUpTo(popUpToId, false)
.build()

private fun setUpToolbar() {
binding.toolbarMain.setNavigationOnClickListener {
if (navController.currentDestination?.id == R.id.timelineFragment) {
behavior.state = STATE_COLLAPSED
} else {
navController.popBackStack()
}
}
}

private fun setUpBottomSheetBehaviorAction() {
behavior.apply {
addBottomSheetCallback(
object : BottomSheetBehavior.BottomSheetCallback() {
override fun onStateChanged(
bottomSheet: View,
newState: Int,
) {
when (newState) {
STATE_COLLAPSED -> {
binding.toolbarMain.visibility = View.INVISIBLE
}

STATE_EXPANDED -> {
binding.btnTimeline.visibility = View.INVISIBLE
}

else -> {
binding.toolbarMain.visibility = View.VISIBLE
binding.btnTimeline.visibility = View.VISIBLE
}
}
}

override fun onSlide(
bottomSheet: View,
slideOffset: Float,
) {
binding.tvBottomSheetRemindYourMemories.alpha = 1 - slideOffset
binding.btnTimeline.alpha = 1 - slideOffset
binding.toolbarMain.alpha = slideOffset
}
},
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.woowacourse.staccato.presentation.timeline

import com.woowacourse.staccato.domain.repository.TimelineRepository

class TempTimelineRepository : TimelineRepository {
private val travels =
listOf(
TimelineTravelUiModel(
travelId = 0L,
travelThumbnail = null,
travelPeriod = "2024.07.23",
travelTitle = "우테코 선릉캠 탐방",
),
TimelineTravelUiModel(
travelId = 1L,
travelThumbnail = "https://cdn.tourtoctoc.com/news/photo/202305/520_2742_5617.jpg",
travelPeriod = "2024.06.30 - 07.04",
travelTitle = "제주도 여행",
),
TimelineTravelUiModel(
travelId = 2L,
travelThumbnail = null,
travelPeriod = "2024.06.28",
travelTitle = "파리 여행",
),
TimelineTravelUiModel(
travelId = 3L,
travelThumbnail =
"https://pds.joongang.co.kr/news/component/htmlphoto_mmdata/" +
"202203/11/97c3e727-0d4c-4fba-83c7-7558d9455651.jpg",
travelPeriod = "2024.06.26",
travelTitle = "포항 영일대 당일치기",
),
TimelineTravelUiModel(
travelId = 4L,
travelThumbnail =
"https://triptogo.world/web/product/big/" +
"202104/e827b41e2d22aeddc8015b018df9aa5b.png",
travelPeriod = "2024.05.28 - 29",
travelTitle = "서울 나들이",
),
TimelineTravelUiModel(
travelId = 5L,
travelThumbnail =
"https://triptogo.world/web/product/big/" +
"202104/e827b41e2d22aeddc8015b018df9aa5b.png",
travelPeriod = "2024.05.28 - 29",
travelTitle = "서울 나들이",
),
TimelineTravelUiModel(
travelId = 6L,
travelThumbnail =
"https://triptogo.world/web/product/big/" +
"202104/e827b41e2d22aeddc8015b018df9aa5b.png",
travelPeriod = "2024.05.28 - 29",
travelTitle = "서울 나들이",
),
TimelineTravelUiModel(
travelId = 7L,
travelThumbnail =
"https://triptogo.world/web/product/big/" +
"202104/e827b41e2d22aeddc8015b018df9aa5b.png",
travelPeriod = "2024.05.28 - 29",
travelTitle = "서울 나들이",
),
)

override fun loadTravels(): List<TimelineTravelUiModel> {
return travels
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,51 @@ package com.woowacourse.staccato.presentation.timeline

import android.os.Bundle
import android.view.View
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import com.woowacourse.staccato.R
import com.woowacourse.staccato.databinding.FragmentTimelineBinding
import com.woowacourse.staccato.presentation.base.BindingFragment
import com.woowacourse.staccato.presentation.timeline.adapter.TimelineAdapter

class TimelineFragment :
BindingFragment<FragmentTimelineBinding>(R.layout.fragment_timeline),
TimelineHandler {
private lateinit var viewModel: TimelineViewModel
private lateinit var adapter: TimelineAdapter

class TimelineFragment : BindingFragment<FragmentTimelineBinding>(R.layout.fragment_timeline) {
override fun onViewCreated(
view: View,
savedInstanceState: Bundle?,
) {
binding.btnTimeline.setOnClickListener {
findNavController().navigate(R.id.action_timelineFragment_to_travelFragment)
setUpViewModel()
setUpAdapter()
setUpObserving()
viewModel.loadTimeline()
}

private fun setUpViewModel() {
val viewModelFactory = TimelineViewModelFactory(TempTimelineRepository())
viewModel = ViewModelProvider(this, viewModelFactory)[TimelineViewModel::class.java]
}

private fun setUpAdapter() {
adapter = TimelineAdapter(this)
binding.rvTimeline.adapter = adapter
}

private fun setUpObserving() {
viewModel.travels.observe(viewLifecycleOwner) { timeline ->
adapter.setTravels(timeline)
}
}

private fun navigateToTravel() {
findNavController().navigate(R.id.action_timelineFragment_to_travelFragment)
}

override fun onTravelClicked(travelId: Long) {
// Log.d("ㅌㅅㅌ", "clicked item: $travelId")
navigateToTravel()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.woowacourse.staccato.presentation.timeline

interface TimelineHandler {
fun onTravelClicked(travelId: Long)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.woowacourse.staccato.presentation.timeline

data class TimelineTravelUiModel(
val travelId: Long,
val travelThumbnail: String? = null,
val travelPeriod: String,
val travelTitle: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.woowacourse.staccato.presentation.timeline

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.woowacourse.staccato.domain.repository.TimelineRepository

class TimelineViewModel(private val repository: TimelineRepository) : ViewModel() {
private val _travels = MutableLiveData<List<TimelineTravelUiModel>>()
val travels: LiveData<List<TimelineTravelUiModel>>
get() = _travels

fun loadTimeline() {
_travels.value = repository.loadTravels()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.woowacourse.staccato.presentation.timeline

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.woowacourse.staccato.domain.repository.TimelineRepository

class TimelineViewModelFactory(private val repository: TimelineRepository) :
ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(TimelineViewModel::class.java)) {
return TimelineViewModel(repository) as T
} else {
throw IllegalArgumentException("확인되지 않은 ViewModel 클래스입니다.")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.woowacourse.staccato.presentation.timeline.adapter

import com.woowacourse.staccato.databinding.LayoutItemFragmentTimelineFirstBinding
import com.woowacourse.staccato.presentation.timeline.TimelineHandler
import com.woowacourse.staccato.presentation.timeline.TimelineTravelUiModel

class FirstTravelViewHolder(
private val binding: LayoutItemFragmentTimelineFirstBinding,
private val eventHandler: TimelineHandler,
) : TimelineViewHolder(binding, eventHandler) {
override fun bind(item: TimelineTravelUiModel) {
binding.travel = item
binding.eventHandler = eventHandler
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.woowacourse.staccato.presentation.timeline.adapter

import com.woowacourse.staccato.databinding.LayoutItemFragmentTimelineLastBinding
import com.woowacourse.staccato.presentation.timeline.TimelineHandler
import com.woowacourse.staccato.presentation.timeline.TimelineTravelUiModel

class LastTravelViewHolder(
private val binding: LayoutItemFragmentTimelineLastBinding,
private val eventHandler: TimelineHandler,
) : TimelineViewHolder(binding, eventHandler) {
override fun bind(item: TimelineTravelUiModel) {
binding.travel = item
binding.eventHandler = eventHandler
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.woowacourse.staccato.presentation.timeline.adapter

import com.woowacourse.staccato.databinding.LayoutItemFragmentTimelineMiddleBinding
import com.woowacourse.staccato.presentation.timeline.TimelineHandler
import com.woowacourse.staccato.presentation.timeline.TimelineTravelUiModel

class MiddleTravelViewHolder(
private val binding: LayoutItemFragmentTimelineMiddleBinding,
private val eventHandler: TimelineHandler,
) : TimelineViewHolder(binding, eventHandler) {
override fun bind(item: TimelineTravelUiModel) {
binding.travel = item
binding.eventHandler = eventHandler
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.woowacourse.staccato.presentation.timeline.adapter

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.woowacourse.staccato.databinding.LayoutItemFragmentTimelineFirstBinding
import com.woowacourse.staccato.databinding.LayoutItemFragmentTimelineLastBinding
import com.woowacourse.staccato.databinding.LayoutItemFragmentTimelineMiddleBinding
import com.woowacourse.staccato.presentation.timeline.TimelineHandler
import com.woowacourse.staccato.presentation.timeline.TimelineTravelUiModel

class TimelineAdapter(private val eventHandler: TimelineHandler) :
RecyclerView.Adapter<TimelineViewHolder>() {
private var travels = emptyList<TimelineTravelUiModel>()

override fun getItemViewType(position: Int): Int {
return TimelineViewType.fromPosition(position, itemCount).ordinal
}

override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int,
): TimelineViewHolder {
return when (viewType) {
TimelineViewType.FIRST_ITEM.ordinal -> {
val binding =
LayoutItemFragmentTimelineFirstBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false,
)
FirstTravelViewHolder(binding, eventHandler)
}

TimelineViewType.MIDDLE_ITEM.ordinal -> {
val binding =
LayoutItemFragmentTimelineMiddleBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false,
)
MiddleTravelViewHolder(binding, eventHandler)
}

else -> {
val binding =
LayoutItemFragmentTimelineLastBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false,
)
LastTravelViewHolder(binding, eventHandler)
}
}
}

override fun getItemCount(): Int = travels.size

override fun onBindViewHolder(
holder: TimelineViewHolder,
position: Int,
) {
holder.bind(travels[position])
}

fun setTravels(newTravels: List<TimelineTravelUiModel>) {
travels = newTravels
notifyItemRangeInserted(0, newTravels.size)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.woowacourse.staccato.presentation.timeline.adapter

import androidx.databinding.ViewDataBinding
import androidx.recyclerview.widget.RecyclerView
import com.woowacourse.staccato.presentation.timeline.TimelineHandler
import com.woowacourse.staccato.presentation.timeline.TimelineTravelUiModel

abstract class TimelineViewHolder(
binding: ViewDataBinding,
private val eventHandler: TimelineHandler,
) :
RecyclerView.ViewHolder(binding.root) {
abstract fun bind(item: TimelineTravelUiModel)
}
Loading

0 comments on commit 8bf48a0

Please sign in to comment.