Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import androidx.recyclerview.widget.GridLayoutManager
import kotlinx.coroutines.launch
import ru.otus.cryptosample.CoinsSampleApp
import ru.otus.cryptosample.coins.feature.adapter.CoinsAdapter
import ru.otus.cryptosample.coins.feature.adapter.CustomItemAnimator
import ru.otus.cryptosample.coins.feature.di.DaggerCoinListComponent
import ru.otus.cryptosample.databinding.FragmentCoinListBinding
import javax.inject.Inject
Expand Down Expand Up @@ -65,8 +66,9 @@ class CoinListFragment : Fragment() {
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
return when (coinsAdapter.getItemViewType(position)) {
0 -> 2 // Category header spans full width
1 -> 1 // Coin item spans half width
0 -> 2
1 -> 1
2 -> 2
else -> 1
}
}
Expand All @@ -75,6 +77,10 @@ class CoinListFragment : Fragment() {
binding.recyclerView.apply {
layoutManager = gridLayoutManager
adapter = coinsAdapter
itemAnimator = CustomItemAnimator().apply {
addDuration = 300
removeDuration = 300
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ class CategoryHeaderViewHolder(
) : RecyclerView.ViewHolder(binding.root) {

fun bind(categoryName: String) {
itemView.alpha = 1f
itemView.scaleX = 1f
itemView.scaleY = 1f
itemView.translationY = 0f

binding.categoryTitle.text = categoryName
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ class CoinViewHolder(
) : RecyclerView.ViewHolder(binding.root) {

fun bind(coin: CoinState) {
itemView.alpha = 1f
itemView.scaleX = 1f
itemView.scaleY = 1f
itemView.translationY = 0f

with(binding) {
coinName.text = coin.name
coinPrice.text = coin.price
Expand All @@ -33,4 +38,8 @@ class CoinViewHolder(
fireBadge.isVisible = coin.highlight
}
}

fun bindPayload(coin: CoinState) {
binding.fireBadge.isVisible = coin.highlight
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,71 +2,122 @@ package ru.otus.cryptosample.coins.feature.adapter

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import ru.otus.cryptosample.coins.feature.CoinCategoryState
import ru.otus.cryptosample.databinding.ItemCategoryHeaderBinding
import ru.otus.cryptosample.databinding.ItemCoinBinding
import ru.otus.cryptosample.databinding.ItemHorizontalCategoryBinding

class CoinsAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
class CoinsAdapter : ListAdapter<CoinsAdapterItem, RecyclerView.ViewHolder>(ItemDiffCallback) {

companion object {
private const val VIEW_TYPE_CATEGORY = 0
private const val VIEW_TYPE_COIN = 1
const val VIEW_TYPE_CATEGORY = 0
const val VIEW_TYPE_COIN = 1
const val VIEW_TYPE_HORIZONTAL_CATEGORY = 2
const val PAYLOAD_HIGHLIGHT = "PAYLOAD_HIGHLIGHT"
}
private var items = listOf<CoinsAdapterItem>()

private val sharedViewPool = RecyclerView.RecycledViewPool()

fun setData(categories: List<CoinCategoryState>) {
val adapterItems = mutableListOf<CoinsAdapterItem>()

categories.forEach { category ->
adapterItems.add(CoinsAdapterItem.CategoryHeader(category.name))
category.coins.forEach { coin ->
adapterItems.add(CoinsAdapterItem.CoinItem(coin))
adapterItems.add(CoinsAdapterItem.CategoryHeader(category.id, category.name))
if (category.coins.size > 10) {
adapterItems.add(CoinsAdapterItem.HorizontalCategory(category.id, category.coins))
} else {
category.coins.forEach { coin ->
adapterItems.add(CoinsAdapterItem.CoinItem(coin))
}
}
}

items = adapterItems
notifyDataSetChanged()
submitList(adapterItems)
}

override fun getItemCount(): Int = items.size

override fun getItemViewType(position: Int): Int {
return when (items[position]) {
return when (getItem(position)) {
is CoinsAdapterItem.CategoryHeader -> VIEW_TYPE_CATEGORY
is CoinsAdapterItem.CoinItem -> VIEW_TYPE_COIN
is CoinsAdapterItem.HorizontalCategory -> VIEW_TYPE_HORIZONTAL_CATEGORY
}
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
return when (viewType) {
VIEW_TYPE_CATEGORY -> CategoryHeaderViewHolder(
ItemCategoryHeaderBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
ItemCategoryHeaderBinding.inflate(layoutInflater, parent, false)
)
VIEW_TYPE_COIN -> CoinViewHolder(
ItemCoinBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
ItemCoinBinding.inflate(layoutInflater, parent, false)
)
VIEW_TYPE_HORIZONTAL_CATEGORY -> HorizontalCategoryViewHolder(
ItemHorizontalCategoryBinding.inflate(layoutInflater, parent, false),
sharedViewPool
)
else -> throw IllegalArgumentException("Unknown view type: $viewType")
}
}

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (val item = items[position]) {
when (val item = getItem(position)) {
is CoinsAdapterItem.CategoryHeader -> {
(holder as CategoryHeaderViewHolder).bind(item.categoryName)
}
is CoinsAdapterItem.CoinItem -> {
(holder as CoinViewHolder).bind(item.coin)
}
is CoinsAdapterItem.HorizontalCategory -> {
(holder as HorizontalCategoryViewHolder).bind(item.coins)
}
}
}

override fun onBindViewHolder(
holder: RecyclerView.ViewHolder,
position: Int,
payloads: MutableList<Any>
) {
if (payloads.isEmpty()) {
super.onBindViewHolder(holder, position, payloads)
} else {
val item = getItem(position)
if (payloads.contains(PAYLOAD_HIGHLIGHT)) {
when (item) {
is CoinsAdapterItem.CoinItem -> {
(holder as CoinViewHolder).bindPayload(item.coin)
}
is CoinsAdapterItem.HorizontalCategory -> {
(holder as HorizontalCategoryViewHolder).bind(item.coins)
}
else -> Unit
}
}
}
}

object ItemDiffCallback : DiffUtil.ItemCallback<CoinsAdapterItem>() {
override fun areItemsTheSame(oldItem: CoinsAdapterItem, newItem: CoinsAdapterItem): Boolean {
return oldItem.id == newItem.id
}

override fun areContentsTheSame(oldItem: CoinsAdapterItem, newItem: CoinsAdapterItem): Boolean {
return oldItem == newItem
}

override fun getChangePayload(oldItem: CoinsAdapterItem, newItem: CoinsAdapterItem): Any? {
if (oldItem is CoinsAdapterItem.CoinItem && newItem is CoinsAdapterItem.CoinItem) {
if (oldItem.coin.copy(highlight = newItem.coin.highlight) == newItem.coin) {
return PAYLOAD_HIGHLIGHT
}
} else if (oldItem is CoinsAdapterItem.HorizontalCategory && newItem is CoinsAdapterItem.HorizontalCategory) {
return PAYLOAD_HIGHLIGHT
}
return super.getChangePayload(oldItem, newItem)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ package ru.otus.cryptosample.coins.feature.adapter

import ru.otus.cryptosample.coins.feature.CoinState

sealed class CoinsAdapterItem {
data class CategoryHeader(val categoryName: String) : CoinsAdapterItem()
data class CoinItem(val coin: CoinState) : CoinsAdapterItem()
sealed class CoinsAdapterItem(val id: String) {
data class CategoryHeader(val categoryId: String, val categoryName: String) : CoinsAdapterItem(categoryId)
data class CoinItem(val coin: CoinState) : CoinsAdapterItem(coin.id)
data class HorizontalCategory(val categoryId: String, val coins: List<CoinState>) : CoinsAdapterItem(categoryId + "_horizontal")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package ru.otus.cryptosample.coins.feature.adapter

import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ObjectAnimator
import android.animation.PropertyValuesHolder
import android.view.animation.AnticipateInterpolator
import android.view.animation.OvershootInterpolator
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.RecyclerView

class CustomItemAnimator : DefaultItemAnimator() {

private val customAnimators = mutableMapOf<RecyclerView.ViewHolder, Animator>()

override fun animateAdd(holder: RecyclerView.ViewHolder): Boolean {
holder.itemView.alpha = 0f
holder.itemView.scaleX = 0f
holder.itemView.scaleY = 0f
holder.itemView.translationY = 100f
return super.animateAdd(holder)
}

override fun onAddStarting(item: RecyclerView.ViewHolder) {
super.onAddStarting(item)

val pvhScaleX = PropertyValuesHolder.ofFloat("scaleX", 1f)
val pvhScaleY = PropertyValuesHolder.ofFloat("scaleY", 1f)
val pvhTranslationY = PropertyValuesHolder.ofFloat("translationY", 0f)

val anim = ObjectAnimator.ofPropertyValuesHolder(item.itemView, pvhScaleX, pvhScaleY, pvhTranslationY)
anim.duration = addDuration
anim.interpolator = OvershootInterpolator(1.5f)
anim.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
customAnimators.remove(item)
}
})
customAnimators[item] = anim
anim.start()
}

override fun animateRemove(holder: RecyclerView.ViewHolder): Boolean {
return super.animateRemove(holder)
}

override fun onRemoveStarting(item: RecyclerView.ViewHolder) {
super.onRemoveStarting(item)

val pvhScaleX = PropertyValuesHolder.ofFloat("scaleX", 0f)
val pvhScaleY = PropertyValuesHolder.ofFloat("scaleY", 0f)
val pvhTranslationY = PropertyValuesHolder.ofFloat("translationY", 100f)

val anim = ObjectAnimator.ofPropertyValuesHolder(item.itemView, pvhScaleX, pvhScaleY, pvhTranslationY)
anim.duration = removeDuration
anim.interpolator = AnticipateInterpolator(1.5f)
anim.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
customAnimators.remove(item)
}
})
customAnimators[item] = anim
anim.start()
}

override fun endAnimation(item: RecyclerView.ViewHolder) {
super.endAnimation(item)
customAnimators[item]?.cancel()
customAnimators.remove(item)
resetState(item)
}

override fun endAnimations() {
super.endAnimations()
customAnimators.values.toList().forEach { it.cancel() }
customAnimators.clear()
}

override fun onAddFinished(item: RecyclerView.ViewHolder) {
super.onAddFinished(item)
resetState(item)
}

override fun onRemoveFinished(item: RecyclerView.ViewHolder) {
super.onRemoveFinished(item)
resetState(item)
}

private fun resetState(item: RecyclerView.ViewHolder) {
item.itemView.alpha = 1f
item.itemView.scaleX = 1f
item.itemView.scaleY = 1f
item.itemView.translationY = 0f
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package ru.otus.cryptosample.coins.feature.adapter

import androidx.recyclerview.widget.RecyclerView
import ru.otus.cryptosample.coins.feature.CoinState
import ru.otus.cryptosample.databinding.ItemHorizontalCategoryBinding

class HorizontalCategoryViewHolder(
private val binding: ItemHorizontalCategoryBinding,
viewPool: RecyclerView.RecycledViewPool
) : RecyclerView.ViewHolder(binding.root) {

private val adapter = HorizontalCoinsAdapter()

init {
binding.horizontalRecyclerView.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(
binding.root.context,
RecyclerView.HORIZONTAL,
false
)
binding.horizontalRecyclerView.setRecycledViewPool(viewPool)
binding.horizontalRecyclerView.adapter = adapter
}

fun bind(coins: List<CoinState>) {
itemView.alpha = 1f
itemView.scaleX = 1f
itemView.scaleY = 1f
itemView.translationY = 0f

adapter.submitList(coins)
}
}
Loading