Skip to content
Open

HW #1

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.ItemAnimator
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
CoinsAdapter.VIEW_TYPE_CATEGORY -> 2 // Category header spans full width
CoinsAdapter.VIEW_TYPE_COIN -> 1 // Coin item spans half width
CoinsAdapter.VIEW_TYPE_CAROUSEL -> 2
else -> 1
}
}
Expand All @@ -75,6 +77,8 @@ class CoinListFragment : Fragment() {
binding.recyclerView.apply {
layoutManager = gridLayoutManager
adapter = coinsAdapter
itemAnimator = ItemAnimator()
setRecycledViewPool(CoinsAdapter.sharedPool)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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.CoinState
import ru.otus.cryptosample.databinding.ItemCoinBinding

class CarouselAdapter: ListAdapter<CoinState, CoinViewHolder>(CarouselDiff) {

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CoinViewHolder {
val binding = ItemCoinBinding.inflate(LayoutInflater.from(parent.context), parent, false)
val params = binding.root.layoutParams
?: RecyclerView.LayoutParams(
RecyclerView.LayoutParams.WRAP_CONTENT,
RecyclerView.LayoutParams.WRAP_CONTENT
)
val screenWidth = parent.resources.displayMetrics.widthPixels
params.width = screenWidth / 2 - 16

binding.root.layoutParams = params

return CoinViewHolder(binding)
}

override fun getItemViewType(position: Int): Int = CoinsAdapter.VIEW_TYPE_COIN

override fun onBindViewHolder(
holder: CoinViewHolder,
position: Int
) {
holder.bind(getItem(position))
}

override fun onBindViewHolder(
holder: CoinViewHolder,
position: Int,
payloads: List<Any>
) {
holder.bind(getItem(position), payloads)
}
}

object CarouselDiff : DiffUtil.ItemCallback<CoinState>() {

override fun areItemsTheSame(old: CoinState, new: CoinState) = old.id == new.id

override fun areContentsTheSame(old: CoinState, new: CoinState) = old == new

override fun getChangePayload(oldItem: CoinState, newItem: CoinState): Any? =
if (oldItem.highlight != newItem.highlight) {
Payload.HOT_MOVER_CHANGED
} else null

object Payload {
const val HOT_MOVER_CHANGED = "HOT_MOVER_CHANGED"
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package ru.otus.cryptosample.coins.feature.adapter

import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import ru.otus.cryptosample.coins.feature.CoinState
import ru.otus.cryptosample.databinding.ItemCarouselBinding

class CarouselViewHolder(
binding: ItemCarouselBinding,
sharedPool: RecyclerView.RecycledViewPool
): RecyclerView.ViewHolder(binding.root) {

private val carouselAdapter = CarouselAdapter()

init {
binding.recyclerView.apply {
layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
adapter = carouselAdapter
setRecycledViewPool(sharedPool)
isNestedScrollingEnabled = false
setHasFixedSize(true)
itemAnimator = ItemAnimator()
}
}

fun bind(coins: List<CoinState>) {
carouselAdapter.submitList(coins)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,12 @@ class CoinViewHolder(
fireBadge.isVisible = coin.highlight
}
}

fun bind(coin: CoinState, payloads: List<Any>) {
if (CoinDiff.Payload.HOT_MOVER_CHANGED in payloads) {
binding.fireBadge.isVisible = coin.highlight
} else {
bind(coin)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,50 @@ 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.ItemCarouselBinding
import ru.otus.cryptosample.databinding.ItemCategoryHeaderBinding
import ru.otus.cryptosample.databinding.ItemCoinBinding

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

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_CAROUSEL = 2

val sharedPool = RecyclerView.RecycledViewPool()
}

private var items = listOf<CoinsAdapterItem>()


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))
if (category.coins.size <= 10) {
category.coins.forEach { coin ->
adapterItems.add(CoinsAdapterItem.CoinItem(coin))
}
} else {
adapterItems.add(CoinsAdapterItem.Carousel(category.coins))
}
}

items = adapterItems
notifyDataSetChanged()
submitList(adapterItems)
}
override fun getItemCount(): Int = items.size

override fun getItemCount(): Int = currentList.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.Carousel -> VIEW_TYPE_CAROUSEL
}
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
VIEW_TYPE_CATEGORY -> CategoryHeaderViewHolder(
Expand All @@ -48,25 +55,105 @@ class CoinsAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
false
)
)
VIEW_TYPE_COIN -> CoinViewHolder(
ItemCoinBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
VIEW_TYPE_COIN -> {
val binding = ItemCoinBinding.inflate(LayoutInflater.from(parent.context), parent, false)
val params = binding.root.layoutParams
?: RecyclerView.LayoutParams(
RecyclerView.LayoutParams.WRAP_CONTENT,
RecyclerView.LayoutParams.WRAP_CONTENT
)
val screenWidth = parent.resources.displayMetrics.widthPixels
params.width = screenWidth / 2 - 16

binding.root.layoutParams = params
CoinViewHolder(binding)
}
VIEW_TYPE_CAROUSEL -> {
CarouselViewHolder(
ItemCarouselBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
),
sharedPool
)
)
}
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.Carousel -> {
(holder as CarouselViewHolder).bind(item.coins)
}
}
}

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: List<Any>) {
when (val item = getItem(position)) {
is CoinsAdapterItem.CategoryHeader -> {
(holder as CategoryHeaderViewHolder).bind(item.categoryName)
}
is CoinsAdapterItem.CoinItem -> {
if (payloads.isEmpty()) {
(holder as CoinViewHolder).bind(item.coin)
} else {
(holder as CoinViewHolder).bind(item.coin, payloads)
}

}
is CoinsAdapterItem.Carousel -> {
(holder as CarouselViewHolder).bind(item.coins)
}
}
}
}

object CoinDiff : DiffUtil.ItemCallback<CoinsAdapterItem>() {

override fun areItemsTheSame(oldItem: CoinsAdapterItem, newItem: CoinsAdapterItem): Boolean =
when {
oldItem is CoinsAdapterItem.CategoryHeader &&
newItem is CoinsAdapterItem.CategoryHeader ->
oldItem.categoryName == newItem.categoryName

oldItem is CoinsAdapterItem.CoinItem &&
newItem is CoinsAdapterItem.CoinItem ->
oldItem.coin.id == newItem.coin.id

oldItem is CoinsAdapterItem.Carousel &&
newItem is CoinsAdapterItem.Carousel ->
// каждая карусель привязана к своей категории и стоит на том же месте
oldItem.coins.firstOrNull()?.id == newItem.coins.firstOrNull()?.id

else -> false
}

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

override fun getChangePayload(oldItem: CoinsAdapterItem, newItem: CoinsAdapterItem): Any? {
return when (oldItem) {
is CoinsAdapterItem.CoinItem -> {
val oldCoin = oldItem.coin
val newCoin = (newItem as CoinsAdapterItem.CoinItem).coin
if (oldCoin.highlight != newCoin.highlight) {
Payload.HOT_MOVER_CHANGED
} else {
null
}
}
else -> null
}
}
}

object Payload {
const val HOT_MOVER_CHANGED = "HOT_MOVER_CHANGED"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ import ru.otus.cryptosample.coins.feature.CoinState
sealed class CoinsAdapterItem {
data class CategoryHeader(val categoryName: String) : CoinsAdapterItem()
data class CoinItem(val coin: CoinState) : CoinsAdapterItem()
}
data class Carousel(val coins: List<CoinState>) : CoinsAdapterItem()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package ru.otus.cryptosample.coins.feature.adapter

import android.view.View
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.RecyclerView

class ItemAnimator : DefaultItemAnimator() {

override fun animateAdd(holder: RecyclerView.ViewHolder): Boolean {
val view: View = holder.itemView
addDuration = when (holder) {
is CoinViewHolder -> 400L
is CategoryHeaderViewHolder -> 50L
else -> 200L
}

view.alpha = 0f
view.translationX = view.width * -1f
view.scaleX = 0.9f
view.scaleY = 0.9f

val animator = view.animate()
.alpha(1f)
.translationX(0f)
.scaleX(1f)
.scaleY(1f)
.setDuration(addDuration)
.setListener(null)

animator.withStartAction {
dispatchAddStarting(holder)
}

animator.withEndAction {
view.alpha = 1f
view.translationX = 0f
view.scaleX = 1f
view.scaleY = 1f
dispatchAddFinished(holder)
view.animate().setListener(null)
}

animator.start()
return true
}

override fun runPendingAnimations() {
super.runPendingAnimations()
}

override fun endAnimation(item: RecyclerView.ViewHolder) {
item.itemView.animate().cancel()
super.endAnimation(item)
}

override fun endAnimations() {
super.endAnimations()
}
}
6 changes: 6 additions & 0 deletions app/src/main/res/layout/item_carousel.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" />
Loading