Skip to content

Commit

Permalink
Animations (#38)
Browse files Browse the repository at this point in the history
* Add animation to text in Player Board

* Clean up animation

* Add animation for round score insertion

* Add logic to insert single rounds to Scoreboard instead of resetting entire score

* Animate EndGame screen transitions
  • Loading branch information
askariya authored Aug 17, 2024
1 parent d3af7e7 commit b8e1c8e
Show file tree
Hide file tree
Showing 16 changed files with 184 additions and 23 deletions.
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
package com.askariya.lostcitiesscorecalculator

import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4

import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith

import org.junit.Assert.*

/**
* Instrumented test, which will execute on an Android device.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,18 @@ class MainActivity : AppCompatActivity() {

private fun showEndGameFragment() {
val endGameFragment = EndGameFragment()
supportFragmentManager.beginTransaction()
.replace(R.id.endgame_fragment_container, endGameFragment)
.commit()
val transaction = supportFragmentManager.beginTransaction()

// Apply rotate animations
transaction.setCustomAnimations(
R.anim.rotate_in, // Animation for fragment entering
R.anim.rotate_out // Animation for fragment exiting
)

transaction.replace(R.id.endgame_fragment_container, endGameFragment)
transaction.addToBackStack(null) // Optional: to allow navigation back
transaction.commit()

// Hide ViewPager2 and TabLayout
viewPager.visibility = View.GONE
tabLayout.visibility = View.GONE
Expand All @@ -148,9 +157,16 @@ class MainActivity : AppCompatActivity() {
private fun hideEndGameFragment() {
val endGameFragment = supportFragmentManager.findFragmentById(R.id.endgame_fragment_container)
if (endGameFragment != null) {
supportFragmentManager.beginTransaction()
.remove(endGameFragment)
.commit()
val transaction = supportFragmentManager.beginTransaction()

// Apply rotate-out and slide-out animations
transaction.setCustomAnimations(
R.anim.rotate_in, // Animation for fragment entering
R.anim.slide_out // Animation for fragment exiting
)

transaction.remove(endGameFragment)
transaction.commit()
}

// Show ViewPager2 and TabLayout
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,11 +226,10 @@ class PlayerBoardFragment : Fragment() {

private val roundCounterObserver = Observer<Int> { round ->
viewModel.resetBoardCommand()
//TODO add a notification badge to score tab
}
private val totalScoreObserver = Observer<Int> { totalScore ->
DialogUtils.flashTextColor(binding.totalScoreValue, R.color.white, R.color.color_primary)
binding.totalScoreValue.text = totalScore.toString()
//TODO make field flash
}

private val scoreObserver = Observer<Int> { score ->
Expand All @@ -242,6 +241,7 @@ class PlayerBoardFragment : Fragment() {
GameStateManager.setPlayer2CurrentPoints(score)
}

DialogUtils.flashTextColor(binding.currentScoreValue, R.color.white, R.color.color_primary)
binding.currentScoreValue.text = score.toString()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ class EndGameFragment : Fragment() {

handleWinner()

// Clear the submitted round score to counteract the Scoreboard observer incorrectly
// being triggered when observer is attached.
GameStateManager.clearSubmittedRoundScore()
val fragment = ScoreboardFragment() // Replace with your fragment class
childFragmentManager.beginTransaction()
.replace(R.id.fragment_container, fragment)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.DecelerateInterpolator
import android.widget.Button
import android.widget.GridLayout
import android.widget.TextView
Expand Down Expand Up @@ -59,8 +60,8 @@ class ScoreboardFragment : Fragment() {
GameStateManager.player2CurrentPoints.observe(viewLifecycleOwner, player2CurrentScoreObserver)
GameStateManager.player1TotalPoints.observe(viewLifecycleOwner, player1TotalScoreObserver)
GameStateManager.player2TotalPoints.observe(viewLifecycleOwner, player2TotalScoreObserver)
GameStateManager.roundScores.observe(viewLifecycleOwner, roundScoreObserver)
GameStateManager.roundScores.observe(viewLifecycleOwner, roundScoreObserver)
GameStateManager.roundScores.observe(viewLifecycleOwner, roundScoresObserver)
GameStateManager.submittedRoundScore.observe(viewLifecycleOwner, submittedRoundScoreObserver)
GameStateManager.player1Name.observe(viewLifecycleOwner, player1NameObserver)
GameStateManager.player2Name.observe(viewLifecycleOwner, player2NameObserver)

Expand Down Expand Up @@ -97,7 +98,6 @@ class ScoreboardFragment : Fragment() {
// Create TextView for Round number
val textSize = 20f
val textColor = ContextCompat.getColor(requireContext(), R.color.white)
val colorPrimary = ContextCompat.getColor(requireContext(), R.color.color_primary)
val colorPrimaryVariant = ContextCompat.getColor(requireContext(), R.color.color_primary_variant)
var backgroundColor = ContextCompat.getColor(requireContext(), R.color.background_color)

Expand Down Expand Up @@ -125,8 +125,12 @@ class ScoreboardFragment : Fragment() {
setTextColor(textColor)
setBackgroundColor(colorPrimaryVariant)
setTypeface(typeface, android.graphics.Typeface.BOLD)
setPadding(12,12,12,12)
setPadding(12, 12, 12, 12)
gravity = android.view.Gravity.CENTER

// Initial state for animation
alpha = 0f
translationY = 50f
}

// Create TextView for Player 1 Score
Expand All @@ -141,8 +145,12 @@ class ScoreboardFragment : Fragment() {
setTextSize(TypedValue.COMPLEX_UNIT_SP, textSize)
setTextColor(textColor)
setBackgroundColor(backgroundColor)
setPadding(12,12,12,12)
setPadding(12, 12, 12, 12)
gravity = android.view.Gravity.CENTER

// Initial state for animation
alpha = 0f
translationY = 50f
}

// Create TextView for Player 2 Score
Expand All @@ -157,15 +165,47 @@ class ScoreboardFragment : Fragment() {
setTextSize(TypedValue.COMPLEX_UNIT_SP, textSize)
setTextColor(textColor)
setBackgroundColor(backgroundColor)
setPadding(12,12,12,12)
setPadding(12, 12, 12, 12)
gravity = android.view.Gravity.CENTER

// Initial state for animation
alpha = 0f
translationY = 50f
}

// Add the views to the GridLayout
val gridLayout = binding.scoreGridLayout
gridLayout.addView(roundNumber)
gridLayout.addView(player1ScoreView)
gridLayout.addView(player2ScoreView)

// Animate the views into place with a 1-second delay
val delay = 350L // 1 second in milliseconds

roundNumber.animate()
.alpha(1f)
.translationY(0f)
.setStartDelay(delay)
.setDuration(300)
.setInterpolator(DecelerateInterpolator())
.start()

player1ScoreView.animate()
.alpha(1f)
.translationY(0f)
.setStartDelay(delay)
.setDuration(300)
.setInterpolator(DecelerateInterpolator())
.start()

player2ScoreView.animate()
.alpha(1f)
.translationY(0f)
.setStartDelay(delay)
.setDuration(300)
.setInterpolator(DecelerateInterpolator())
.start()

checkEmptyViewVisibility()
}

Expand Down Expand Up @@ -221,7 +261,7 @@ class ScoreboardFragment : Fragment() {
player2Score.text = totalScore.toString()
}

private val roundScoreObserver = Observer<MutableMap<Int, Pair<Int, Int>>> { roundScores ->
private val roundScoresObserver = Observer<MutableMap<Int, Pair<Int, Int>>> { roundScores ->
val roundNumbers = roundScores.keys.sorted()
val scoreGrid = binding.scoreGridLayout
scoreGrid.removeAllViews()
Expand All @@ -235,6 +275,18 @@ class ScoreboardFragment : Fragment() {
checkEmptyViewVisibility()
}

private val submittedRoundScoreObserver = Observer<Pair<Int, Int>> { roundScores ->
val player1Score = roundScores.first
val player2Score = roundScores.second

if (player1Score != -500 && player2Score != -500) {
val roundNum = GameStateManager.roundCounter.value ?: 1
addNewRound(roundNum, player1Score, player2Score)

checkEmptyViewVisibility()
}
}

private val player1NameObserver = Observer<String> { name ->
binding.player1ColumnHeader.text = name
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package com.askariya.lostcitiesscorecalculator.ui.utils

import android.animation.ArgbEvaluator
import android.animation.ValueAnimator
import android.content.Context
import android.text.Html
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat

object DialogUtils {
fun showConfirmationDialog(context: Context,
Expand Down Expand Up @@ -52,4 +56,21 @@ object DialogUtils {
fun showGameLoadedNotification(context: Context){
Toast.makeText(context, "Loaded Save", Toast.LENGTH_SHORT).show()
}

// Function to make the text field flash
fun flashTextColor(textView: TextView, fromColorId: Int, toColorId: Int) {
val colorFrom = ContextCompat.getColor(textView.context, fromColorId)
val colorTo = ContextCompat.getColor(textView.context, toColorId) // Flash color

val animator = ValueAnimator.ofObject(ArgbEvaluator(), colorFrom, colorTo)
animator.duration = 250 // Half a second total for flashing
animator.repeatCount = 1
animator.repeatMode = ValueAnimator.REVERSE

animator.addUpdateListener { animation ->
textView.setTextColor(animation.animatedValue as Int)
}

animator.start()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ object GameStateManager {
private val _player2TotalPoints = MutableLiveData<Int>()
private val _roundCounter = MutableLiveData<Int>()
private val _roundScores: MutableLiveData<MutableMap<Int, Pair<Int, Int>>> = MutableLiveData(mutableMapOf())
private val _submittedRoundScore: MutableLiveData<Pair<Int, Int>> = MutableLiveData<Pair<Int, Int>>()

val gameOver: LiveData<Boolean> get() = _gameOver
val player1CurrentPoints: LiveData<Int> get() = _player1CurrentPoints
Expand All @@ -43,13 +44,16 @@ object GameStateManager {
val player2TotalPoints: LiveData<Int> get() = _player2TotalPoints
val roundCounter: LiveData<Int> get() = _roundCounter
val roundScores: MutableLiveData<MutableMap<Int, Pair<Int, Int>>> get() = _roundScores
val submittedRoundScore: MutableLiveData<Pair<Int, Int>> get() = _submittedRoundScore

private val sharedPreferencesListener =
SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
// Handle changes based on the key
onSharedPreferencesChanged(key)
}

private var roundScoresLocal: MutableMap<Int, Pair<Int, Int>> = mutableMapOf()

fun initialize(context: Context) {
// initialize custom game state shared preference manager
gameStateSharedPreferences = context.getSharedPreferences("game_state_preferences", Context.MODE_PRIVATE)
Expand Down Expand Up @@ -101,6 +105,18 @@ object GameStateManager {
_roundScores.value = scores
}

private fun setRoundScoresLocal(scores: MutableMap<Int, Pair<Int, Int>>) {
roundScoresLocal = scores
}

private fun setSubmittedRoundScore(roundScore: Pair<Int, Int>) {
_submittedRoundScore.value = roundScore
}

fun clearSubmittedRoundScore() {
_submittedRoundScore.value = Pair(-500, -500)
}

private fun incrementRoundCounter() {
_roundCounter.value = (_roundCounter.value ?: 1) + 1
}
Expand Down Expand Up @@ -184,13 +200,16 @@ object GameStateManager {
{
val roundScoreMap = roundScores.value ?: mutableMapOf()
roundScoreMap[round] = Pair(player1RoundScore, player2RoundScore)
setRoundScores(roundScoreMap)
// We only want to set the local score so that the subscribers don't get an update.
setRoundScoresLocal(roundScoreMap)
setSubmittedRoundScore(Pair(player1RoundScore, player2RoundScore))
setPlayer1TotalPoints((player1TotalPoints.value ?: 0) + player1RoundScore)
setPlayer2TotalPoints((player2TotalPoints.value ?: 0) + player2RoundScore)
}

private fun resetRoundScores()
{
setRoundScoresLocal(mutableMapOf())
setRoundScores(mutableMapOf())
setPlayer1TotalPoints(0)
setPlayer2TotalPoints(0)
Expand All @@ -210,6 +229,7 @@ object GameStateManager {
// Should only be used by the MainActivity for loading the scores
private fun loadGameScores(scores: MutableMap<Int, Pair<Int, Int>>, player1TotalScore: Int, player2TotalScore: Int, roundNum: Int)
{
setRoundScoresLocal(scores)
setRoundScores(scores)
setPlayer1TotalPoints(player1TotalScore)
setPlayer2TotalPoints(player2TotalScore)
Expand All @@ -220,7 +240,7 @@ object GameStateManager {
val editor = gameStateSharedPreferences.edit()
// Convert map to JSON string
val gson = Gson()
val jsonString = gson.toJson(roundScores.value)
val jsonString = gson.toJson(roundScoresLocal)

// Save JSON string to SharedPreferences
editor.putString("roundScores", jsonString)
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/res/anim/fade_in.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:fromAlpha="0.0"
android:toAlpha="1.0"
android:duration="1000" />
5 changes: 5 additions & 0 deletions app/src/main/res/anim/fade_out.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:fromAlpha="1.0"
android:toAlpha="0.0"
android:duration="1000" />
7 changes: 7 additions & 0 deletions app/src/main/res/anim/rotate_in.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:fromDegrees="90"
android:toDegrees="0"
android:pivotX="50%"
android:pivotY="50%"
android:duration="300" />
7 changes: 7 additions & 0 deletions app/src/main/res/anim/rotate_out.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:fromDegrees="0"
android:toDegrees="90"
android:pivotX="50%"
android:pivotY="50%"
android:duration="300" />
9 changes: 9 additions & 0 deletions app/src/main/res/anim/scale_in.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<scale xmlns:android="http://schemas.android.com/apk/res/android"
android:fromXScale="0.8"
android:toXScale="1.0"
android:fromYScale="0.8"
android:toYScale="1.0"
android:pivotX="50%"
android:pivotY="50%"
android:duration="300" />
9 changes: 9 additions & 0 deletions app/src/main/res/anim/scale_out.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<scale xmlns:android="http://schemas.android.com/apk/res/android"
android:fromXScale="1.0"
android:toXScale="0.8"
android:fromYScale="1.0"
android:toYScale="0.8"
android:pivotX="50%"
android:pivotY="50%"
android:duration="300" />
5 changes: 5 additions & 0 deletions app/src/main/res/anim/slide_in.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:fromXDelta="100%p"
android:toXDelta="0"
android:duration="300" />
5 changes: 5 additions & 0 deletions app/src/main/res/anim/slide_out.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:fromXDelta="0"
android:toXDelta="-100%p"
android:duration="300" />
Loading

0 comments on commit b8e1c8e

Please sign in to comment.