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
117 changes: 117 additions & 0 deletions .idea/codeStyles/Project.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions .idea/codeStyles/codeStyleConfig.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.launch
import ru.otus.cryptosample.CoinsSampleApp
import ru.otus.cryptosample.coins.feature.adapter.CoinsAdapter
import ru.otus.cryptosample.coins.feature.di.DaggerCoinListComponent
import ru.otus.cryptosample.databinding.FragmentCoinListBinding
import javax.inject.Inject
import ru.otus.cryptosample.R
import ru.otus.cryptosample.coins.feature.adapter.CoinItemAnimator

class CoinListFragment : Fragment() {

Expand Down Expand Up @@ -59,14 +62,15 @@ class CoinListFragment : Fragment() {
}

private fun setupRecyclerView() {
coinsAdapter = CoinsAdapter()
coinsAdapter = CoinsAdapter(RecyclerView.RecycledViewPool())

val gridLayoutManager = GridLayoutManager(requireContext(), 2)
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
R.layout.item_category_header -> 2 // Category header spans full width
R.layout.item_coin -> 1 // Coin item spans half-width
R.layout.item_carousel -> 2 // The horizontal row spans full width
else -> 1
}
}
Expand All @@ -75,6 +79,15 @@ class CoinListFragment : Fragment() {
binding.recyclerView.apply {
layoutManager = gridLayoutManager
adapter = coinsAdapter
itemAnimator = CoinItemAnimator()
}

binding.btnAddCoin.setOnClickListener {
viewModel.addRandomCoin()
}

binding.btnRemoveCoins.setOnClickListener {
viewModel.removeRandomCoin()
}
}

Expand All @@ -99,7 +112,7 @@ class CoinListFragment : Fragment() {
}

private fun renderState(state: CoinsScreenState) {
coinsAdapter.setData(state.categories)
coinsAdapter.setData(state.categories, state.showAll)
}

override fun onDestroyView() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,50 @@ class CoinListViewModel(
updateUiState()
}

fun addRandomCoin() {
if (fullCategories.isEmpty()) return

val allCoins = fullCategories.flatMap { it.coins }
if (allCoins.isEmpty()) return

val templateCoin = allCoins.random()

val newCoinId = "new_${System.currentTimeMillis()}"
val newCoin = templateCoin.copy(
id = newCoinId,
name = "New Coin ${(1..100).random()}",
price = "$${(1000..50000).random()}",
)

val randomCategory = fullCategories.random()

fullCategories = fullCategories.map { category ->
if (category.id == randomCategory.id) {
category.copy(coins = category.coins + newCoin)
} else {
category
}
}
updateUiState()
}

fun removeRandomCoin() {
if (fullCategories.isEmpty()) return

val allCoins = fullCategories.flatMap { it.coins }
if (allCoins.isEmpty()) return

val coinsToRemove = allCoins.shuffled().take(5.coerceAtMost(allCoins.size))
val coinsToRemoveIds = coinsToRemove.map { it.id }.toSet()

fullCategories = fullCategories.map { category ->
category.copy(
coins = category.coins.filter { coin -> coin.id !in coinsToRemoveIds }
)
}
updateUiState()
}

private fun requestCoins() {
consumeCoinsUseCase().map { categories ->
categories.map { category -> coinsStateFactory.create(category) }
Expand All @@ -54,20 +98,20 @@ class CoinListViewModel(
}

private fun updateUiState() {
var processedCategories = if (showAll) {
fullCategories
} else {
fullCategories.map { category ->
category.copy(coins = category.coins.take(4))
}
}
var processedCategories = fullCategories

processedCategories = processedCategories.map { category ->
category.copy(coins = category.coins.map { coin ->
coin.copy(highlight = highlightMovers && coin.isHotMover)
})
category.copy(
coins = category.coins.map { coin ->
coin.copy(highlight = highlightMovers && coin.isHotMover)
})
}

_state.update { it.copy(categories = processedCategories) }
_state.update {
it.copy(
categories = processedCategories,
showAll = showAll
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ package ru.otus.cryptosample.coins.feature

data class CoinsScreenState(
val categories: List<CoinCategoryState> = emptyList(),
)
val showAll: Boolean = true,
)

data class CoinCategoryState(
val id: String,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package ru.otus.cryptosample.coins.feature.adapter

import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.view.View
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.RecyclerView

class CoinItemAnimator : DefaultItemAnimator() {

override fun animateAdd(holder: RecyclerView.ViewHolder): Boolean {
holder.itemView.alpha = 0f
holder.itemView.translationX = -holder.itemView.width.toFloat()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Для установки начального сдвига, но на момент вызова view может быть еще не измерен, и width будет равен 0. Из за этого анимация сдвига не сработает при первом появлении элемента. Лучше использовать measuredWidth или ширину экрана как fallback.


val fadeIn = ObjectAnimator.ofFloat(holder.itemView, View.ALPHA, 0f, 1f)
val slideIn = ObjectAnimator.ofFloat(holder.itemView, View.TRANSLATION_X, -holder.itemView.width.toFloat(), 0f)

val animatorSet = AnimatorSet()
animatorSet.playTogether(fadeIn, slideIn)
animatorSet.duration = addDuration
animatorSet.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator) {
dispatchAddStarting(holder)
}

override fun onAnimationEnd(animation: Animator) {
dispatchAddFinished(holder)
holder.itemView.alpha = 1f
holder.itemView.translationX = 0f
}

override fun onAnimationCancel(animation: Animator) {
holder.itemView.alpha = 1f
holder.itemView.translationX = 0f
}
})
animatorSet.start()

return false
}

override fun animateRemove(holder: RecyclerView.ViewHolder): Boolean {
val fadeOut = ObjectAnimator.ofFloat(holder.itemView, View.ALPHA, 1f, 0f)
val slideOut = ObjectAnimator.ofFloat(holder.itemView, View.TRANSLATION_X, 0f, holder.itemView.width.toFloat())

val animatorSet = AnimatorSet()
animatorSet.playTogether(fadeOut, slideOut)
animatorSet.duration = removeDuration
animatorSet.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator) {
dispatchRemoveStarting(holder)
}

override fun onAnimationEnd(animation: Animator) {
dispatchRemoveFinished(holder)
holder.itemView.alpha = 1f
holder.itemView.translationX = 0f
}

override fun onAnimationCancel(animation: Animator) {
holder.itemView.alpha = 1f
holder.itemView.translationX = 0f
}
})
animatorSet.start()

return false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ class CoinViewHolder(
private val binding: ItemCoinBinding
) : RecyclerView.ViewHolder(binding.root) {

sealed class Payload {
data class HighlightChanged(val isHighlighted: Boolean) : Payload()
}

fun bind(coin: CoinState) {
with(binding) {
coinName.text = coin.name
Expand All @@ -33,4 +37,19 @@ class CoinViewHolder(
fireBadge.isVisible = coin.highlight
}
}

fun bind(coin: CoinState, payloads: List<Any>) {
if (payloads.isEmpty()) {
bind(coin)
return
}

payloads.forEach { payload ->
when (payload) {
is Payload.HighlightChanged -> {
binding.fireBadge.isVisible = payload.isHighlighted
}
}
}
}
}
Loading