Add header to EpisodeListAdapter

This commit is contained in:
Jarne Demeulemeester 2021-07-06 18:19:37 +02:00
parent a70d154eca
commit 4bbf40bc22
No known key found for this signature in database
GPG key ID: 60884A0C1EBA43E5
7 changed files with 249 additions and 162 deletions

View file

@ -113,7 +113,7 @@ fun bindPersonImage(imageView: ImageView, person: BaseItemPerson) {
}
@BindingAdapter("episodes")
fun bindEpisodes(recyclerView: RecyclerView, data: List<BaseItemDto>?) {
fun bindEpisodes(recyclerView: RecyclerView, data: List<EpisodeItem>?) {
val adapter = recyclerView.adapter as EpisodeListAdapter
adapter.submitList(data)
}

View file

@ -8,10 +8,37 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import dev.jdtech.jellyfin.databinding.EpisodeItemBinding
import dev.jdtech.jellyfin.databinding.SeasonHeaderBinding
import org.jellyfin.sdk.model.api.BaseItemDto
import java.util.*
class EpisodeListAdapter(private val onClickListener: OnClickListener) :
ListAdapter<BaseItemDto, EpisodeListAdapter.EpisodeViewHolder>(DiffCallback) {
private const val ITEM_VIEW_TYPE_HEADER = 0
private const val ITEM_VIEW_TYPE_EPISODE = 1
class EpisodeListAdapter(
private val onClickListener: OnClickListener,
private val seriesId: UUID,
private val seriesName: String?,
private val seasonId: UUID,
private val seasonName: String?
) :
ListAdapter<EpisodeItem, RecyclerView.ViewHolder>(DiffCallback) {
class HeaderViewHolder(private var binding: SeasonHeaderBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(
seriesId: UUID,
seriesName: String?,
seasonId: UUID,
seasonName: String?
) {
binding.seriesId = seriesId
binding.seasonId = seasonId
binding.seasonName.text = seasonName
binding.seriesName.text = seriesName
binding.executePendingBindings()
}
}
class EpisodeViewHolder(private var binding: EpisodeItemBinding) :
RecyclerView.ViewHolder(binding.root) {
@ -20,7 +47,9 @@ class EpisodeListAdapter(private val onClickListener: OnClickListener) :
if (episode.userData?.playedPercentage != null) {
binding.progressBar.layoutParams.width = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
(episode.userData?.playedPercentage?.times(.84))!!.toFloat(), binding.progressBar.context.resources.displayMetrics).toInt()
(episode.userData?.playedPercentage?.times(.84))!!.toFloat(),
binding.progressBar.context.resources.displayMetrics
).toInt()
binding.progressBar.visibility = View.VISIBLE
} else {
binding.progressBar.visibility = View.GONE
@ -29,35 +58,75 @@ class EpisodeListAdapter(private val onClickListener: OnClickListener) :
}
}
companion object DiffCallback : DiffUtil.ItemCallback<BaseItemDto>() {
override fun areItemsTheSame(oldItem: BaseItemDto, newItem: BaseItemDto): Boolean {
companion object DiffCallback : DiffUtil.ItemCallback<EpisodeItem>() {
override fun areItemsTheSame(oldItem: EpisodeItem, newItem: EpisodeItem): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: BaseItemDto, newItem: BaseItemDto): Boolean {
override fun areContentsTheSame(oldItem: EpisodeItem, newItem: EpisodeItem): Boolean {
return oldItem == newItem
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EpisodeViewHolder {
return EpisodeViewHolder(
EpisodeItemBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
ITEM_VIEW_TYPE_HEADER -> {
HeaderViewHolder(
SeasonHeaderBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
ITEM_VIEW_TYPE_EPISODE -> {
EpisodeViewHolder(
EpisodeItemBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
else -> throw ClassCastException("Unknown viewType $viewType")
}
}
override fun onBindViewHolder(holder: EpisodeViewHolder, position: Int) {
val item = getItem(position)
holder.itemView.setOnClickListener {
onClickListener.onClick(item)
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder.itemViewType) {
ITEM_VIEW_TYPE_HEADER -> {
(holder as HeaderViewHolder).bind(seriesId, seriesName, seasonId, seasonName)
}
ITEM_VIEW_TYPE_EPISODE -> {
val item = getItem(position) as EpisodeItem.Episode
holder.itemView.setOnClickListener {
onClickListener.onClick(item.episode)
}
(holder as EpisodeViewHolder).bind(item.episode)
}
}
}
override fun getItemViewType(position: Int): Int {
return when (getItem(position)) {
is EpisodeItem.Header -> ITEM_VIEW_TYPE_HEADER
is EpisodeItem.Episode -> ITEM_VIEW_TYPE_EPISODE
}
holder.bind(item)
}
class OnClickListener(val clickListener: (item: BaseItemDto) -> Unit) {
fun onClick(item: BaseItemDto) = clickListener(item)
}
}
sealed class EpisodeItem {
abstract val id: UUID
object Header : EpisodeItem() {
override val id: UUID = UUID.randomUUID()
}
data class Episode(val episode: BaseItemDto) : EpisodeItem() {
override val id = episode.id
}
}

View file

@ -37,11 +37,7 @@ class SeasonFragment : Fragment() {
binding.episodesRecyclerView.adapter =
EpisodeListAdapter(EpisodeListAdapter.OnClickListener { episode ->
navigateToEpisodeBottomSheetFragment(episode)
})
binding.seriesName.text = args.seriesName
binding.seasonName.text = args.seasonName
binding.seriesId = args.seriesId
binding.seasonId = args.seasonId
}, args.seriesId, args.seriesName, args.seasonId, args.seasonName)
viewModel.loadEpisodes(args.seriesId, args.seasonId)
}

View file

@ -1,10 +1,13 @@
package dev.jdtech.jellyfin.viewmodels
import androidx.lifecycle.*
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import dev.jdtech.jellyfin.adapters.EpisodeItem
import dev.jdtech.jellyfin.repository.JellyfinRepository
import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.ItemFields
import java.util.*
import javax.inject.Inject
@ -14,12 +17,17 @@ class SeasonViewModel
@Inject
constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() {
private val _episodes = MutableLiveData<List<BaseItemDto>>()
val episodes: LiveData<List<BaseItemDto>> = _episodes
private val _episodes = MutableLiveData<List<EpisodeItem>>()
val episodes: LiveData<List<EpisodeItem>> = _episodes
fun loadEpisodes(seriesId: UUID, seasonId: UUID) {
viewModelScope.launch {
_episodes.value = jellyfinRepository.getEpisodes(seriesId, seasonId, fields = listOf(ItemFields.OVERVIEW))
_episodes.value = getEpisodes(seriesId, seasonId)
}
}
private suspend fun getEpisodes(seriesId: UUID, seasonId: UUID): List<EpisodeItem> {
val episodes = jellyfinRepository.getEpisodes(seriesId, seasonId, fields = listOf(ItemFields.OVERVIEW))
return listOf(EpisodeItem.Header) + episodes.map { EpisodeItem.Episode(it) }
}
}

View file

@ -15,6 +15,7 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_marginHorizontal="24dp"
android:layout_marginBottom="24dp"
android:foreground="?android:attr/selectableItemBackground">

View file

@ -10,154 +10,23 @@
name="viewModel"
type="dev.jdtech.jellyfin.viewmodels.SeasonViewModel" />
<variable
name="seriesId"
type="java.util.UUID" />
<variable
name="seasonId"
type="java.util.UUID" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/header"
android:layout_width="match_parent"
android:layout_height="200dp"
android:layout_marginBottom="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/item_banner"
android:layout_width="match_parent"
android:layout_height="200dp"
android:scaleType="centerCrop"
app:itemBackdropById="@{seriesId}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:contentDescription="@string/series_poster"/>
<FrameLayout
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/header_gradient"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/season_poster"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="24dp"
android:layout_marginTop="24dp"
android:elevation="8dp"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,3:2"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:seasonPoster="@{seasonId}"
app:shapeAppearanceOverlay="@style/roundedImageView" />
<TextView
android:id="@+id/series_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
app:layout_constraintBottom_toTopOf="@id/season_name"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/season_poster"
tools:text="Attack on Titan" />
<TextView
android:id="@+id/season_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/season_poster"
tools:text="Season 1" />
</androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout
android:id="@+id/buttons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:layout_marginTop="24dp"
android:layout_marginBottom="24dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/header">
<ImageButton
android:id="@+id/play_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
android:background="@drawable/button_setup_background"
android:contentDescription="@string/play_button_description"
android:paddingHorizontal="24dp"
android:paddingVertical="12dp"
android:src="@drawable/ic_play" />
<ImageButton
android:id="@+id/shuffle_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
android:background="@drawable/button_accent_background"
android:contentDescription="@string/shuffle_button_description"
android:padding="12dp"
android:src="@drawable/ic_shuffle" />
<ImageButton
android:id="@+id/check_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
android:background="@drawable/button_accent_background"
android:contentDescription="@string/check_button_description"
android:padding="12dp"
android:src="@drawable/ic_check" />
<ImageButton
android:id="@+id/favorite_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/button_accent_background"
android:contentDescription="@string/favorite_button_description"
android:padding="12dp"
android:src="@drawable/ic_heart" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/episodes_recycler_view"
android:layout_width="0dp"
android:layout_height="0dp"
android:clipToPadding="false"
android:paddingHorizontal="24dp"
android:paddingTop="16dp"
app:episodes="@{viewModel.episodes}"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/buttons"
app:layout_constraintTop_toTopOf="parent"
tools:itemCount="4"
tools:listitem="@layout/episode_item" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,144 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="seriesId"
type="java.util.UUID" />
<variable
name="seasonId"
type="java.util.UUID" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/header"
android:layout_width="match_parent"
android:layout_height="200dp"
android:layout_marginBottom="24dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/item_banner"
android:layout_width="match_parent"
android:layout_height="200dp"
android:contentDescription="@string/series_poster"
android:scaleType="centerCrop"
app:itemBackdropById="@{seriesId}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<FrameLayout
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/header_gradient"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/season_poster"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="24dp"
android:layout_marginTop="24dp"
android:elevation="8dp"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,3:2"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:seasonPoster="@{seasonId}"
app:shapeAppearanceOverlay="@style/roundedImageView" />
<TextView
android:id="@+id/series_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
app:layout_constraintBottom_toTopOf="@id/season_name"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/season_poster"
tools:text="Attack on Titan" />
<TextView
android:id="@+id/season_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/season_poster"
tools:text="Season 1" />
</androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout
android:id="@+id/buttons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:layout_marginBottom="24dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/header">
<ImageButton
android:id="@+id/play_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
android:background="@drawable/button_setup_background"
android:contentDescription="@string/play_button_description"
android:paddingHorizontal="24dp"
android:paddingVertical="12dp"
android:src="@drawable/ic_play" />
<ImageButton
android:id="@+id/shuffle_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
android:background="@drawable/button_accent_background"
android:contentDescription="@string/shuffle_button_description"
android:padding="12dp"
android:src="@drawable/ic_shuffle" />
<ImageButton
android:id="@+id/check_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
android:background="@drawable/button_accent_background"
android:contentDescription="@string/check_button_description"
android:padding="12dp"
android:src="@drawable/ic_check" />
<ImageButton
android:id="@+id/favorite_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/button_accent_background"
android:contentDescription="@string/favorite_button_description"
android:padding="12dp"
android:src="@drawable/ic_heart" />
</LinearLayout>
</LinearLayout>
</layout>