diff --git a/android/app/src/main/java/poke/rogue/helper/presentation/dex/AnalyticsExtensions.kt b/android/app/src/main/java/poke/rogue/helper/presentation/dex/AnalyticsExtensions.kt index fe515189..eb2a74ca 100644 --- a/android/app/src/main/java/poke/rogue/helper/presentation/dex/AnalyticsExtensions.kt +++ b/android/app/src/main/java/poke/rogue/helper/presentation/dex/AnalyticsExtensions.kt @@ -2,6 +2,7 @@ package poke.rogue.helper.presentation.dex import poke.rogue.helper.analytics.AnalyticsEvent import poke.rogue.helper.analytics.AnalyticsLogger +import poke.rogue.helper.presentation.dex.detail.NavigateToBattleEvent import poke.rogue.helper.presentation.dex.filter.PokeFilterUiModel import poke.rogue.helper.presentation.dex.sort.PokemonSortUiModel @@ -36,3 +37,27 @@ private fun PokeFilterUiModel.toParams(): List { AnalyticsEvent.Param(key = "type", value = it.name) } + AnalyticsEvent.Param(key = "generation", value = selectedGeneration.name) } + +fun AnalyticsLogger.logPokemonDetailToBattle(event: NavigateToBattleEvent) { + val eventType = "pokemon_detail_to_battle_directly" + logEvent( + AnalyticsEvent( + type = eventType, + extras = event.toParams(), + ), + ) +} + +private fun NavigateToBattleEvent.toParams(): List { + val (battleRoleValue, pokemon) = + when (this) { + is NavigateToBattleEvent.WithMyPokemon -> Pair("MyPokemon", pokemon) + is NavigateToBattleEvent.WithOpponentPokemon -> Pair("EnemyPokemon", pokemon) + } + + return listOf( + AnalyticsEvent.Param(key = "battle_role", value = battleRoleValue), + AnalyticsEvent.Param(key = "pokemon_id", value = pokemon.id), + AnalyticsEvent.Param(key = "pokemon_name", value = pokemon.name), + ) +} diff --git a/android/app/src/main/java/poke/rogue/helper/presentation/dex/detail/NavigateToBattleEvent.kt b/android/app/src/main/java/poke/rogue/helper/presentation/dex/detail/NavigateToBattleEvent.kt new file mode 100644 index 00000000..f443223e --- /dev/null +++ b/android/app/src/main/java/poke/rogue/helper/presentation/dex/detail/NavigateToBattleEvent.kt @@ -0,0 +1,9 @@ +package poke.rogue.helper.presentation.dex.detail + +import poke.rogue.helper.presentation.dex.model.PokemonUiModel + +sealed class NavigateToBattleEvent { + data class WithMyPokemon(val pokemon: PokemonUiModel) : NavigateToBattleEvent() + + data class WithOpponentPokemon(val pokemon: PokemonUiModel) : NavigateToBattleEvent() +} diff --git a/android/app/src/main/java/poke/rogue/helper/presentation/dex/detail/PokemonDetailActivity.kt b/android/app/src/main/java/poke/rogue/helper/presentation/dex/detail/PokemonDetailActivity.kt index c09ca2bb..cbfbbbe1 100644 --- a/android/app/src/main/java/poke/rogue/helper/presentation/dex/detail/PokemonDetailActivity.kt +++ b/android/app/src/main/java/poke/rogue/helper/presentation/dex/detail/PokemonDetailActivity.kt @@ -3,6 +3,9 @@ package poke.rogue.helper.presentation.dex.detail import android.content.Context import android.content.Intent import android.os.Bundle +import android.view.View +import android.view.animation.Animation +import android.view.animation.AnimationUtils import android.widget.LinearLayout.LayoutParams import androidx.activity.viewModels import androidx.appcompat.widget.Toolbar @@ -21,6 +24,7 @@ import poke.rogue.helper.presentation.util.context.stringOf import poke.rogue.helper.presentation.util.repeatOnStarted import poke.rogue.helper.presentation.util.view.dp import poke.rogue.helper.presentation.util.view.loadImageWithProgress +import timber.log.Timber class PokemonDetailActivity : ToolbarActivity(R.layout.activity_pokemon_detail) { @@ -31,6 +35,8 @@ class PokemonDetailActivity : private lateinit var pokemonTypesAdapter: PokemonTypesAdapter private lateinit var pokemonDetailPagerAdapter: PokemonDetailPagerAdapter + private var isExpanded = false + override val toolbar: Toolbar get() = binding.toolbarPokemonDetail @@ -40,9 +46,22 @@ class PokemonDetailActivity : binding.eventHandler = viewModel binding.lifecycleOwner = this + binding.vm = viewModel initAdapter() initObservers() + initFloatingButtonsHandler() + } + + override fun onSaveInstanceState(outState: Bundle) { + outState.putBoolean(IS_EXPANDED, isExpanded) + super.onSaveInstanceState(outState) + } + + override fun onRestoreInstanceState(savedInstanceState: Bundle) { + isExpanded = savedInstanceState.getBoolean(IS_EXPANDED) + super.onRestoreInstanceState(savedInstanceState) + updateFloatingButtonsState() } private fun initAdapter() { @@ -69,6 +88,13 @@ class PokemonDetailActivity : observeNavigateToAbilityDetailEvent() observeNavigateToBiomeDetailEvent() observeNavigateToPokemonDetailEvent() + observeNavigateToBattleEvent() + } + + private fun initFloatingButtonsHandler() { + binding.fabPokemonDetailBattle.setOnClickListener { + toggleFloatingButtons() + } } private fun observePokemonDetailUi() { @@ -118,6 +144,24 @@ class PokemonDetailActivity : } } + // TODO: 예니 여기서 하면 될 Battle Activity 로 이동하면 될 것 같아요 + private fun observeNavigateToBattleEvent() { + repeatOnStarted { + viewModel.navigateToBattleEvent.collect { battleEvent -> + when (battleEvent) { + is NavigateToBattleEvent.WithMyPokemon -> { + Timber.d("내 포켓몬으로 배틀 액티비티로 이동 pokemon: ${battleEvent.pokemon}") + } + + is NavigateToBattleEvent.WithOpponentPokemon -> { + Timber.d("상대 포켓몬으로 배틀 액티비티로 이동 pokemon: ${battleEvent.pokemon}") + // TODO() + } + } + } + } + } + private fun bindPokemonDetail(pokemonDetail: PokemonDetailUiState.Success) { with(binding) { ivPokemonDetailPokemon.loadImageWithProgress( @@ -140,8 +184,44 @@ class PokemonDetailActivity : ) } + private fun toggleFloatingButtons() { + val rotateOpen: Animation = AnimationUtils.loadAnimation(this, R.anim.rotate_open) + val rotateClose: Animation = AnimationUtils.loadAnimation(this, R.anim.rotate_close) + val fromBottom: Animation = AnimationUtils.loadAnimation(this, R.anim.from_bottom) + val toBottom: Animation = AnimationUtils.loadAnimation(this, R.anim.to_bottom) + + updateFloatingButtonsState() + with(binding) { + if (!isExpanded) { + fabPokemonDetailBattle.startAnimation(rotateOpen) + efabPokemonDetailBattleWithMine.startAnimation(fromBottom) + efabPokemonDetailBattleWithOpponent.startAnimation(fromBottom) + } else { + fabPokemonDetailBattle.startAnimation(rotateClose) + efabPokemonDetailBattleWithMine.startAnimation(toBottom) + efabPokemonDetailBattleWithOpponent.startAnimation(toBottom) + } + } + + isExpanded = !isExpanded + } + + private fun updateFloatingButtonsState() { + with(binding) { + if (isExpanded) { + efabPokemonDetailBattleWithMine.visibility = View.VISIBLE + efabPokemonDetailBattleWithOpponent.visibility = View.VISIBLE + } else { + efabPokemonDetailBattleWithMine.visibility = View.INVISIBLE + efabPokemonDetailBattleWithOpponent.visibility = View.INVISIBLE + } + } + } + companion object { private const val POKEMON_ID = "pokemonId" + private const val IS_EXPANDED = "isExpanded" + val TAG: String = PokemonDetailActivity::class.java.simpleName private val typesUiConfig = diff --git a/android/app/src/main/java/poke/rogue/helper/presentation/dex/detail/PokemonDetailNavigateHandler.kt b/android/app/src/main/java/poke/rogue/helper/presentation/dex/detail/PokemonDetailNavigateHandler.kt index 48bd17bd..581ff43c 100644 --- a/android/app/src/main/java/poke/rogue/helper/presentation/dex/detail/PokemonDetailNavigateHandler.kt +++ b/android/app/src/main/java/poke/rogue/helper/presentation/dex/detail/PokemonDetailNavigateHandler.kt @@ -8,4 +8,8 @@ interface PokemonDetailNavigateHandler { fun navigateToHome() fun navigateToPokemonDetail(pokemonId: String) + + fun navigateToBattleWithMine() + + fun navigateToBattleWithOpponent() } diff --git a/android/app/src/main/java/poke/rogue/helper/presentation/dex/detail/PokemonDetailViewModel.kt b/android/app/src/main/java/poke/rogue/helper/presentation/dex/detail/PokemonDetailViewModel.kt index 343b147c..c9e70062 100644 --- a/android/app/src/main/java/poke/rogue/helper/presentation/dex/detail/PokemonDetailViewModel.kt +++ b/android/app/src/main/java/poke/rogue/helper/presentation/dex/detail/PokemonDetailViewModel.kt @@ -2,13 +2,12 @@ package poke.rogue.helper.presentation.dex.detail import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -17,31 +16,37 @@ import poke.rogue.helper.analytics.analyticsLogger import poke.rogue.helper.data.repository.DexRepository import poke.rogue.helper.presentation.base.BaseViewModelFactory import poke.rogue.helper.presentation.base.error.ErrorHandleViewModel +import poke.rogue.helper.presentation.dex.logPokemonDetailToBattle +import poke.rogue.helper.presentation.util.event.MutableEventFlow +import poke.rogue.helper.presentation.util.event.asEventFlow class PokemonDetailViewModel( private val dexRepository: DexRepository, - logger: AnalyticsLogger = analyticsLogger(), + private val logger: AnalyticsLogger = analyticsLogger(), ) : ErrorHandleViewModel(logger), PokemonDetailNavigateHandler { private val _uiState: MutableStateFlow = MutableStateFlow(PokemonDetailUiState.IsLoading) val uiState = _uiState.asStateFlow() - val isEmpty: StateFlow = + val isLoading: StateFlow = uiState.map { it is PokemonDetailUiState.IsLoading } .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000L), true) - private val _navigationToAbilityDetailEvent = MutableSharedFlow() - val navigationToAbilityDetailEvent: SharedFlow = _navigationToAbilityDetailEvent.asSharedFlow() + private val _navigationToAbilityDetailEvent = MutableEventFlow() + val navigationToAbilityDetailEvent = _navigationToAbilityDetailEvent.asEventFlow() - private val _navigationToBiomeDetailEvent = MutableSharedFlow() - val navigationToBiomeDetailEvent: SharedFlow = _navigationToBiomeDetailEvent.asSharedFlow() + private val _navigationToBiomeDetailEvent = MutableEventFlow() + val navigationToBiomeDetailEvent = _navigationToBiomeDetailEvent.asEventFlow() - private val _navigateToHomeEvent = MutableSharedFlow() - val navigateToHomeEvent = _navigateToHomeEvent.asSharedFlow() + private val _navigateToHomeEvent = MutableEventFlow() + val navigateToHomeEvent = _navigateToHomeEvent.asEventFlow() - private val _navigateToPokemonDetailEvent = MutableSharedFlow() - val navigateToPokemonDetailEvent = _navigateToPokemonDetailEvent.asSharedFlow() + private val _navigateToPokemonDetailEvent = MutableEventFlow() + val navigateToPokemonDetailEvent = _navigateToPokemonDetailEvent.asEventFlow() + + private val _navigateToBattleEvent = MutableEventFlow() + val navigateToBattleEvent = _navigateToBattleEvent.asEventFlow() fun updatePokemonDetail(pokemonId: String?) { requireNotNull(pokemonId) { "Pokemon ID must not be null" } @@ -74,6 +79,27 @@ class PokemonDetailViewModel( } } + override fun navigateToBattleWithMine() { + viewModelScope.launch { + val navigation = NavigateToBattleEvent.WithMyPokemon(pokemonUiModel()) + _navigateToBattleEvent.emit(navigation) + logger.logPokemonDetailToBattle(navigation) + } + } + + override fun navigateToBattleWithOpponent() { + viewModelScope.launch { + val navigation = NavigateToBattleEvent.WithOpponentPokemon(pokemonUiModel()) + _navigateToBattleEvent.emit(navigation) + logger.logPokemonDetailToBattle(navigation) + } + } + + private suspend fun pokemonUiModel() = + uiState + .filterIsInstance() + .first().pokemon + companion object { fun factory(dexRepository: DexRepository): ViewModelProvider.Factory = BaseViewModelFactory { PokemonDetailViewModel(dexRepository) } diff --git a/android/app/src/main/java/poke/rogue/helper/presentation/util/context/ContextExtension.kt b/android/app/src/main/java/poke/rogue/helper/presentation/util/context/ContextExtension.kt index f27b107e..24fef177 100644 --- a/android/app/src/main/java/poke/rogue/helper/presentation/util/context/ContextExtension.kt +++ b/android/app/src/main/java/poke/rogue/helper/presentation/util/context/ContextExtension.kt @@ -64,7 +64,7 @@ fun Context.stringOf( fun Context.stringArrayOf( @ArrayRes resId: Int, -) = resources.getStringArray(resId) +): Array = resources.getStringArray(resId) fun Context.colorOf( @ColorRes resId: Int, diff --git a/android/app/src/main/res/anim/from_bottom.xml b/android/app/src/main/res/anim/from_bottom.xml new file mode 100644 index 00000000..9b794187 --- /dev/null +++ b/android/app/src/main/res/anim/from_bottom.xml @@ -0,0 +1,20 @@ + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/anim/rotate_close.xml b/android/app/src/main/res/anim/rotate_close.xml new file mode 100644 index 00000000..c96ecbed --- /dev/null +++ b/android/app/src/main/res/anim/rotate_close.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/anim/rotate_open.xml b/android/app/src/main/res/anim/rotate_open.xml new file mode 100644 index 00000000..cdc7a2ae --- /dev/null +++ b/android/app/src/main/res/anim/rotate_open.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/android/app/src/main/res/anim/to_bottom.xml b/android/app/src/main/res/anim/to_bottom.xml new file mode 100644 index 00000000..6bc46599 --- /dev/null +++ b/android/app/src/main/res/anim/to_bottom.xml @@ -0,0 +1,23 @@ + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/ic_pokemon_battle_enemy.png b/android/app/src/main/res/drawable/ic_pokemon_battle_enemy.png new file mode 100644 index 00000000..b1c3d39b Binary files /dev/null and b/android/app/src/main/res/drawable/ic_pokemon_battle_enemy.png differ diff --git a/android/app/src/main/res/drawable/ic_pokemon_battle_mine.png b/android/app/src/main/res/drawable/ic_pokemon_battle_mine.png new file mode 100644 index 00000000..e66693a9 Binary files /dev/null and b/android/app/src/main/res/drawable/ic_pokemon_battle_mine.png differ diff --git a/android/app/src/main/res/drawable/shape_red_20_fill_20_rect.xml b/android/app/src/main/res/drawable/shape_red_20_fill_20_rect.xml new file mode 100644 index 00000000..7459b9e4 --- /dev/null +++ b/android/app/src/main/res/drawable/shape_red_20_fill_20_rect.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/android/app/src/main/res/layout/activity_pokemon_detail.xml b/android/app/src/main/res/layout/activity_pokemon_detail.xml index 2aa8dde7..689ec88b 100644 --- a/android/app/src/main/res/layout/activity_pokemon_detail.xml +++ b/android/app/src/main/res/layout/activity_pokemon_detail.xml @@ -9,6 +9,10 @@ name="eventHandler" type="poke.rogue.helper.presentation.dex.detail.PokemonDetailNavigateHandler" /> + + + + + + + + + + + + + - \ No newline at end of file + diff --git a/android/app/src/main/res/values/dimensions.xml b/android/app/src/main/res/values/dimensions.xml index 81b45198..16bd80d3 100644 --- a/android/app/src/main/res/values/dimensions.xml +++ b/android/app/src/main/res/values/dimensions.xml @@ -1,4 +1,7 @@ 8dp + + 12dp + \ No newline at end of file diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 518d845b..205511ea 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -59,6 +59,9 @@ 정보 + 상대 포켓몬으로 + 내 포켓몬으로 + 레벨 기술명