diff --git a/app/build.gradle b/app/build.gradle index 6166c369e..f3e57d22d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -39,6 +39,18 @@ android { buildFeatures { dataBinding true } + + // Configure only for each module that uses Java 8 + // language features (either in its source code or + // through dependencies). + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + // For Kotlin projects + kotlinOptions { + jvmTarget = "1.8" + } } dependencies { @@ -53,6 +65,7 @@ dependencies { // KTX implementation 'androidx.core:core-ktx:1.3.1' + implementation "androidx.fragment:fragment-ktx:1.2.5" // Navigation implementation "android.arch.navigation:navigation-fragment-ktx:1.0.0-rc02" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ebac42795..3122546fb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -18,6 +18,8 @@ xmlns:tools="http://schemas.android.com/tools" package="com.example.android.guesstheword"> + + - - + diff --git a/app/src/main/java/com/example/android/guesstheword/screens/game/GameFragment.kt b/app/src/main/java/com/example/android/guesstheword/screens/game/GameFragment.kt index 0dbe12118..3a39c6aec 100644 --- a/app/src/main/java/com/example/android/guesstheword/screens/game/GameFragment.kt +++ b/app/src/main/java/com/example/android/guesstheword/screens/game/GameFragment.kt @@ -17,13 +17,16 @@ package com.example.android.guesstheword.screens.game import android.os.Bundle +import android.os.VibrationEffect +import android.os.Vibrator import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.databinding.DataBindingUtil +import androidx.core.content.getSystemService import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.Observer import androidx.navigation.fragment.NavHostFragment.findNavController -import com.example.android.guesstheword.R import com.example.android.guesstheword.databinding.GameFragmentBinding /** @@ -31,111 +34,59 @@ import com.example.android.guesstheword.databinding.GameFragmentBinding */ class GameFragment : Fragment() { - // The current word - private var word = "" - - // The current score - private var score = 0 - - // The list of words - the front of the list is the next word to guess - private lateinit var wordList: MutableList - - private lateinit var binding: GameFragmentBinding - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle?): View? { - - // Inflate view and obtain an instance of the binding class - binding = DataBindingUtil.inflate( - inflater, - R.layout.game_fragment, - container, - false - ) - - resetList() - nextWord() - - binding.correctButton.setOnClickListener { onCorrect() } - binding.skipButton.setOnClickListener { onSkip() } - updateScoreText() - updateWordText() - return binding.root - - } - - /** - * Resets the list of words and randomizes the order - */ - private fun resetList() { - wordList = mutableListOf( - "queen", - "hospital", - "basketball", - "cat", - "change", - "snail", - "soup", - "calendar", - "sad", - "desk", - "guitar", - "home", - "railway", - "zebra", - "jelly", - "car", - "crow", - "trade", - "bag", - "roll", - "bubble" - ) - wordList.shuffle() - } + private val viewModel by viewModels() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? = GameFragmentBinding.inflate(inflater) + .apply { + this.viewmodel = viewModel + this.lifecycleOwner = viewLifecycleOwner + } + .also { + viewModel.run { + eventGameFinish.observe(viewLifecycleOwner, Observer { hasFinished -> + handleEventGameFinish(hasFinished) + }) + eventBuzz.observe(viewLifecycleOwner, Observer { buzzType -> + handleEventBuzz(buzzType) + }) + } + } + .root /** * Called when the game is finished */ - private fun gameFinished() { - val action = GameFragmentDirections.actionGameToScore(score) - findNavController(this).navigate(action) + private fun handleEventGameFinish(hasFinished: Boolean) { + if (hasFinished) { + val action = GameFragmentDirections.actionGameToScore(viewModel.score.value ?: 0) + findNavController(this).navigate(action) + viewModel.onGameFinishComplete() + } } /** - * Moves to the next word in the list + * */ - private fun nextWord() { - //Select and remove a word from the list - if (wordList.isEmpty()) { - gameFinished() - } else { - word = wordList.removeAt(0) + private fun handleEventBuzz(type: GameViewModel.BuzzType) { + if (type != GameViewModel.BuzzType.NO_BUZZ) { + buzz(type.pattern) + viewModel.onBuzzComplete() } - updateWordText() - updateScoreText() - } - - /** Methods for buttons presses **/ - - private fun onSkip() { - score-- - nextWord() - } - - private fun onCorrect() { - score++ - nextWord() - } - - /** Methods for updating the UI **/ - - private fun updateWordText() { - binding.wordText.text = word - } - private fun updateScoreText() { - binding.scoreText.text = score.toString() + @Suppress("DEPRECATION") + private fun buzz(pattern: LongArray) { + activity?.getSystemService() + ?.let { vibrator -> + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + vibrator.vibrate(VibrationEffect.createWaveform(pattern, -1)) + } else { + vibrator.vibrate(pattern, -1) + } + } } } diff --git a/app/src/main/java/com/example/android/guesstheword/screens/game/GameViewModel.kt b/app/src/main/java/com/example/android/guesstheword/screens/game/GameViewModel.kt new file mode 100644 index 000000000..2e7e7189b --- /dev/null +++ b/app/src/main/java/com/example/android/guesstheword/screens/game/GameViewModel.kt @@ -0,0 +1,160 @@ +package com.example.android.guesstheword.screens.game + +import android.os.CountDownTimer +import android.text.format.DateUtils +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Transformations +import androidx.lifecycle.ViewModel + +private val CORRECT_BUZZ_PATTERN = longArrayOf(100, 100, 100, 100, 100, 100) +private val PANIC_BUZZ_PATTERN = longArrayOf(0, 200) +private val GAME_OVER_BUZZ_PATTERN = longArrayOf(0, 2000) +private val NO_BUZZ_PATTERN = longArrayOf(0) + +class GameViewModel : ViewModel() { + + companion object { + // These represent different important times + // This is when the game is over + private const val DONE = 0L + + // This is the number of milliseconds in a second + private const val ONE_SECOND = 1000L + + // This is the total time of the game + private const val COUNTDOWN_TIME = 60000L + + // This is when we must start to buzz a panic pattern + private const val COUNTDOWN_PANIC_SECONDS = 10L + } + + // These are the three different types of buzzing in the game. Buzz pattern is the number of + // milliseconds each interval of buzzing and non-buzzing takes. + enum class BuzzType(val pattern: LongArray) { + CORRECT(CORRECT_BUZZ_PATTERN), + COUNTDOWN_PANIC(PANIC_BUZZ_PATTERN), + GAME_OVER(GAME_OVER_BUZZ_PATTERN), + NO_BUZZ(NO_BUZZ_PATTERN) + } + + // The current word + private val _word = MutableLiveData("") + val word: LiveData + get() = _word + + // The current score + private val _score = MutableLiveData(0) + val score: LiveData + get() = _score + + private val _eventGameFinish = MutableLiveData(false) + val eventGameFinish: LiveData + get() = _eventGameFinish + + private val _eventBuzz = MutableLiveData(BuzzType.NO_BUZZ) + val eventBuzz: LiveData + get() = _eventBuzz + + // The list of words - the front of the list is the next word to guess + private lateinit var wordList: MutableList + + private val timer: CountDownTimer + + private val _currentTime = MutableLiveData(0) + val currentTime: LiveData + get() = _currentTime + + val elapsedTime: LiveData + get() = Transformations.map(currentTime) { time -> DateUtils.formatElapsedTime(time) } + + init { + resetList() + nextWord() + timer = object : CountDownTimer(COUNTDOWN_TIME, ONE_SECOND) { + override fun onTick(millisUntilFinished: Long) = onTimerTick(millisUntilFinished) + override fun onFinish() = finishGame() + } + timer.start() + } + + private fun onTimerTick(millisUntilFinished: Long) { + val secondsToFinished = millisUntilFinished / ONE_SECOND + _currentTime.value = secondsToFinished + if (secondsToFinished <= COUNTDOWN_PANIC_SECONDS) { + _eventBuzz.value = BuzzType.COUNTDOWN_PANIC + } + } + + private fun finishGame() { + _currentTime.value = DONE + _eventGameFinish.value = true + _eventBuzz.value = BuzzType.GAME_OVER + } + + /** + * Resets the list of words and randomizes the order + */ + private fun resetList() { + wordList = mutableListOf( + "queen", + "hospital", + "basketball", + "cat", + "change", + "snail", + "soup", + "calendar", + "sad", + "desk", + "guitar", + "home", + "railway", + "zebra", + "jelly", + "car", + "crow", + "trade", + "bag", + "roll", + "bubble" + ) + wordList.shuffle() + } + + /** + * Moves to the next word in the list + */ + private fun nextWord() { + if (wordList.isEmpty()) { + resetList() + } + //Select and remove a word from the list + _word.value = wordList.removeAt(0) + } + + /** Methods for buttons presses **/ + fun onSkip() { + _score.value = score.value?.minus(1) + nextWord() + } + + fun onCorrect() { + _score.value = score.value?.plus(1) + _eventBuzz.value = BuzzType.CORRECT + nextWord() + } + + fun onGameFinishComplete() { + _eventGameFinish.value = false + } + + fun onBuzzComplete() { + _eventBuzz.value = BuzzType.NO_BUZZ + } + + override fun onCleared() { + super.onCleared() + timer.cancel() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/android/guesstheword/screens/score/ScoreFragment.kt b/app/src/main/java/com/example/android/guesstheword/screens/score/ScoreFragment.kt index 63bcb6191..867b89f10 100644 --- a/app/src/main/java/com/example/android/guesstheword/screens/score/ScoreFragment.kt +++ b/app/src/main/java/com/example/android/guesstheword/screens/score/ScoreFragment.kt @@ -22,6 +22,8 @@ import android.view.View import android.view.ViewGroup import androidx.databinding.DataBindingUtil import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import com.example.android.guesstheword.R @@ -48,8 +50,18 @@ class ScoreFragment : Fragment() { // Get args using by navArgs property delegate val scoreFragmentArgs by navArgs() - binding.scoreText.text = scoreFragmentArgs.score.toString() - binding.playAgainButton.setOnClickListener { onPlayAgain() } + val viewModelFactory = ScoreViewModelFactory(scoreFragmentArgs.score) + val viewModel = ViewModelProvider(this, viewModelFactory) + .get(ScoreViewModel::class.java) + binding.viewmodel = viewModel + binding.lifecycleOwner = viewLifecycleOwner + + viewModel.eventPlayAgain.observe(viewLifecycleOwner, Observer { playAgain -> + if (playAgain) { + onPlayAgain() + viewModel.onPlayAgainComplete() + } + }) return binding.root } diff --git a/app/src/main/java/com/example/android/guesstheword/screens/score/ScoreViewModel.kt b/app/src/main/java/com/example/android/guesstheword/screens/score/ScoreViewModel.kt new file mode 100644 index 000000000..4b3346da0 --- /dev/null +++ b/app/src/main/java/com/example/android/guesstheword/screens/score/ScoreViewModel.kt @@ -0,0 +1,24 @@ +package com.example.android.guesstheword.screens.score + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel + +class ScoreViewModel(finalScore: Int) : ViewModel() { + + private val _score = MutableLiveData(finalScore) + val score: LiveData + get() = _score + + private val _eventPlayAgain = MutableLiveData(false) + val eventPlayAgain: LiveData + get() = _eventPlayAgain + + fun onPlayAgain() { + _eventPlayAgain.value = true + } + + fun onPlayAgainComplete() { + _eventPlayAgain.value = false + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/android/guesstheword/screens/score/ScoreViewModelFactory.kt b/app/src/main/java/com/example/android/guesstheword/screens/score/ScoreViewModelFactory.kt new file mode 100644 index 000000000..b50510e86 --- /dev/null +++ b/app/src/main/java/com/example/android/guesstheword/screens/score/ScoreViewModelFactory.kt @@ -0,0 +1,14 @@ +package com.example.android.guesstheword.screens.score + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import java.lang.IllegalArgumentException + +class ScoreViewModelFactory(private val finalScore: Int) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(ScoreViewModel::class.java)) { + return ScoreViewModel(finalScore) as T + } + throw IllegalArgumentException("Unknown viewModel class: $modelClass") + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/game_fragment.xml b/app/src/main/res/layout/game_fragment.xml index 8fbd82f0c..732b7f4f5 100644 --- a/app/src/main/res/layout/game_fragment.xml +++ b/app/src/main/res/layout/game_fragment.xml @@ -18,6 +18,12 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> + + + + + tools:text="Current Score: 2" />