Add animation to the timer screen

This commit is contained in:
William Brawner 2020-09-22 15:45:54 -07:00
parent 6fab0ff4f9
commit 26c6977fd6
8 changed files with 48 additions and 287 deletions

View file

@ -1,139 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value>
<package name="java.util" alias="false" withSubpackages="false" />
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
<package name="io.ktor" alias="false" withSubpackages="true" />
</value>
</option>
<option name="PACKAGES_IMPORT_LAYOUT">
<value>
<package name="" alias="false" withSubpackages="true" />
<package name="java" alias="false" withSubpackages="true" />
<package name="javax" alias="false" withSubpackages="true" />
<package name="kotlin" alias="false" withSubpackages="true" />
<package name="" alias="true" withSubpackages="true" />
</value>
</option>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
<option name="WRAP_ON_TYPING" value="1" />
</codeStyleSettings>
</code_scheme>
</component>

View file

@ -1,123 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
<option name="WRAP_ON_TYPING" value="1" />
</codeStyleSettings>
</code_scheme>
</component>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8 (5)" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" /> <output url="file://$PROJECT_DIR$/build/classes" />
</component> </component>
<component name="ProjectType"> <component name="ProjectType">

View file

@ -102,6 +102,8 @@ dependencies {
implementation "org.koin:koin-android:$koin_version" implementation "org.koin:koin-android:$koin_version"
testImplementation "org.koin:koin-test:$koin_version" testImplementation "org.koin:koin-test:$koin_version"
implementation 'com.robinhood.ticker:ticker:2.0.2'
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'

View file

@ -1,5 +1,7 @@
package com.wbrawner.trainterval.activetimer package com.wbrawner.trainterval.activetimer
import android.animation.ArgbEvaluator
import android.animation.ValueAnimator
import android.content.Context import android.content.Context
import android.media.AudioManager import android.media.AudioManager
import android.media.SoundPool import android.media.SoundPool
@ -16,6 +18,7 @@ import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.robinhood.ticker.TickerUtils
import com.google.android.gms.wearable.* import com.google.android.gms.wearable.*
import com.wbrawner.trainterval.Logger import com.wbrawner.trainterval.Logger
import com.wbrawner.trainterval.R import com.wbrawner.trainterval.R
@ -89,6 +92,9 @@ class ActiveTimerFragment : Fragment(), MessageClient.OnMessageReceivedListener
it.setSupportActionBar(toolbar) it.setSupportActionBar(toolbar)
it.supportActionBar?.setDisplayHomeAsUpEnabled(true) it.supportActionBar?.setDisplayHomeAsUpEnabled(true)
} }
timeRemaining.setCharacterLists(TickerUtils.provideNumberList() + ":")
timerSets.setCharacterLists(TickerUtils.provideNumberList())
timerRounds.setCharacterLists(TickerUtils.provideNumberList())
coroutineScope = CoroutineScope(Dispatchers.Main) coroutineScope = CoroutineScope(Dispatchers.Main)
coroutineScope!!.launch { coroutineScope!!.launch {
activeTimerViewModel.timerState.observe(viewLifecycleOwner, Observer { state -> activeTimerViewModel.timerState.observe(viewLifecycleOwner, Observer { state ->
@ -131,7 +137,19 @@ class ActiveTimerFragment : Fragment(), MessageClient.OnMessageReceivedListener
} }
(activity as? AppCompatActivity)?.supportActionBar?.title = state.timerName (activity as? AppCompatActivity)?.supportActionBar?.title = state.timerName
val backgroundColor = resources.getColor(state.phase.colorRes, context?.theme) val backgroundColor = resources.getColor(state.phase.colorRes, context?.theme)
timerBackground.setBackgroundColor(backgroundColor) Log.d("ActiveTimerFragment", "State: $state")
state.previousPhase?.let {
val previousBackgroundColor = resources.getColor(it.colorRes, context?.theme)
val colorAnimation =
ValueAnimator.ofObject(ArgbEvaluator(), previousBackgroundColor, backgroundColor)
colorAnimation.duration = 250
colorAnimation.addUpdateListener { animator ->
timerBackground.setBackgroundColor(
animator.animatedValue as Int
)
}
colorAnimation.start()
} ?: timerBackground.setBackgroundColor(backgroundColor)
playPauseButton.setImageDrawable( playPauseButton.setImageDrawable(
requireContext().getDrawable( requireContext().getDrawable(
if (state.isRunning) R.drawable.ic_pause if (state.isRunning) R.drawable.ic_pause

View file

@ -39,7 +39,7 @@ class ActiveTimerViewModel : ViewModel() {
currentRound = timer.cycles currentRound = timer.cycles
timeRemaining = timer.warmUpDuration timeRemaining = timer.warmUpDuration
currentPhase = Phase.WARM_UP currentPhase = Phase.WARM_UP
updateTimer() updateTimer(null)
} }
} }
@ -47,34 +47,27 @@ class ActiveTimerViewModel : ViewModel() {
if (timerJob != null) { if (timerJob != null) {
timerJob?.cancel() timerJob?.cancel()
timerJob = null timerJob = null
timerState.postValue( updateTimer(null)
TimerRunningState(
timer,
timeRemaining,
currentSet,
currentRound,
currentPhase,
timerJob != null
)
)
} else { } else {
viewModelScope.launch { viewModelScope.launch {
startTimer() startTimer(null)
} }
} }
} }
private fun startTimer() { private fun startTimer(previousPhase: Phase?) {
viewModelScope.launch { viewModelScope.launch {
timerJob = launch { timerJob = launch {
updateTimer() updateTimer(previousPhase)
while (coroutineContext.isActive && timerJob != null) { while (coroutineContext.isActive && timerJob != null) {
delay(1_000) delay(1_000)
timeRemaining -= 1_000 timeRemaining -= 1_000
// We need to recalculate the previous phase on each iteration
val previousPhaseOngoing = currentPhase
if (timeRemaining <= 0) { if (timeRemaining <= 0) {
goForward() goForward()
} }
updateTimer() updateTimer(if (previousPhaseOngoing != currentPhase) previousPhaseOngoing else null)
} }
} }
} }
@ -82,22 +75,24 @@ class ActiveTimerViewModel : ViewModel() {
fun skipAhead() { fun skipAhead() {
timerJob?.cancel() timerJob?.cancel()
var previousPhase: Phase? = null
when (currentPhase) { when (currentPhase) {
Phase.COOL_DOWN -> { Phase.COOL_DOWN -> {
timeRemaining = 0 timeRemaining = 0
} }
else -> { else -> {
previousPhase = currentPhase
goForward() goForward()
} }
} }
if (timerJob != null) { if (timerJob != null) {
startTimer() startTimer(previousPhase)
} else { } else {
updateTimer() updateTimer(previousPhase)
} }
} }
private fun updateTimer() { private fun updateTimer(previousPhase: Phase?) {
timerState.postValue( timerState.postValue(
TimerRunningState( TimerRunningState(
timer, timer,
@ -105,6 +100,7 @@ class ActiveTimerViewModel : ViewModel() {
currentSet, currentSet,
currentRound, currentRound,
currentPhase, currentPhase,
previousPhase,
timerJob != null timerJob != null
) )
) )
@ -154,6 +150,7 @@ class ActiveTimerViewModel : ViewModel() {
fun goBack() { fun goBack() {
timerJob?.cancel() timerJob?.cancel()
var previousPhase: Phase = currentPhase
when (currentPhase) { when (currentPhase) {
Phase.WARM_UP -> { Phase.WARM_UP -> {
timeRemaining = timer.warmUpDuration timeRemaining = timer.warmUpDuration
@ -191,9 +188,9 @@ class ActiveTimerViewModel : ViewModel() {
} }
} }
if (timerJob != null) { if (timerJob != null) {
startTimer() startTimer(previousPhase)
} else { } else {
updateTimer() updateTimer(previousPhase)
} }
} }
} }

View file

@ -55,7 +55,7 @@
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6" android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
android:textColor="@color/colorOnSurface" /> android:textColor="@color/colorOnSurface" />
<TextView <com.robinhood.ticker.TickerView
android:id="@+id/timerSets" android:id="@+id/timerSets"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -82,7 +82,7 @@
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6" android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
android:textColor="@color/colorOnSurface" /> android:textColor="@color/colorOnSurface" />
<TextView <com.robinhood.ticker.TickerView
android:id="@+id/timerRounds" android:id="@+id/timerRounds"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -109,7 +109,7 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
tools:text="Warm-Up" /> tools:text="Warm-Up" />
<TextView <com.robinhood.ticker.TickerView
android:id="@+id/timeRemaining" android:id="@+id/timeRemaining"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View file

@ -15,6 +15,7 @@ sealed class IntervalTimerState : Serializable {
val currentRound: Int, val currentRound: Int,
val soundId: Int?, val soundId: Int?,
val phase: Phase, val phase: Phase,
val previousPhase: Phase?,
val isRunning: Boolean, val isRunning: Boolean,
val vibrate: Boolean val vibrate: Boolean
) : IntervalTimerState() { ) : IntervalTimerState() {
@ -24,10 +25,12 @@ sealed class IntervalTimerState : Serializable {
currentSet: Int, currentSet: Int,
currentRound: Int, currentRound: Int,
phase: Phase, phase: Phase,
previousPhase: Phase?,
timerRunning: Boolean timerRunning: Boolean
) : this( ) : this(
timerName = timer.name, timerName = timer.name,
phase = phase, phase = phase,
previousPhase = previousPhase,
timeRemaining = timeRemaining.toIntervalDuration().toString(), timeRemaining = timeRemaining.toIntervalDuration().toString(),
currentSet = currentSet, currentSet = currentSet,
currentRound = currentRound, currentRound = currentRound,
@ -57,6 +60,7 @@ const val KEY_CURRENT_SET = "com.wbrawner.trainterval.currentSet"
const val KEY_CURRENT_ROUND = "com.wbrawner.trainterval.currentRound" const val KEY_CURRENT_ROUND = "com.wbrawner.trainterval.currentRound"
const val KEY_SOUND_ID = "com.wbrawner.trainterval.soundId" const val KEY_SOUND_ID = "com.wbrawner.trainterval.soundId"
const val KEY_PHASE = "com.wbrawner.trainterval.phase" const val KEY_PHASE = "com.wbrawner.trainterval.phase"
const val KEY_PREVIOUS_PHASE = "com.wbrawner.trainterval.previousPhase"
const val KEY_RUNNING = "com.wbrawner.trainterval.timerRunning" const val KEY_RUNNING = "com.wbrawner.trainterval.timerRunning"
const val KEY_VIBRATE = "com.wbrawner.trainterval.vibrate" const val KEY_VIBRATE = "com.wbrawner.trainterval.vibrate"
@ -83,6 +87,7 @@ fun DataMap.toIntervalTimerState(): IntervalTimerState? = when (getString(KEY_ST
getInt(KEY_CURRENT_ROUND), getInt(KEY_CURRENT_ROUND),
getInt(KEY_SOUND_ID), getInt(KEY_SOUND_ID),
Phase.valueOf(getString(KEY_PHASE)), Phase.valueOf(getString(KEY_PHASE)),
getString(KEY_PREVIOUS_PHASE)?.let { Phase.valueOf(it) },
getBoolean(KEY_RUNNING), getBoolean(KEY_RUNNING),
getBoolean(KEY_VIBRATE) getBoolean(KEY_VIBRATE)
) )
@ -100,6 +105,7 @@ fun IntervalTimerState.TimerRunningState.toDataMap(): DataMap {
putInt(KEY_SOUND_ID, it) putInt(KEY_SOUND_ID, it)
} }
putString(KEY_PHASE, state.phase.name) putString(KEY_PHASE, state.phase.name)
putString(KEY_PREVIOUS_PHASE, state.previousPhase?.name)
putBoolean(KEY_RUNNING, state.isRunning) putBoolean(KEY_RUNNING, state.isRunning)
putBoolean(KEY_VIBRATE, state.vibrate) putBoolean(KEY_VIBRATE, state.vibrate)
} }