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"?>
<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" />
</component>
<component name="ProjectType">

View file

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

View file

@ -1,5 +1,7 @@
package com.wbrawner.trainterval.activetimer
import android.animation.ArgbEvaluator
import android.animation.ValueAnimator
import android.content.Context
import android.media.AudioManager
import android.media.SoundPool
@ -16,6 +18,7 @@ import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Observer
import androidx.lifecycle.viewModelScope
import androidx.navigation.fragment.findNavController
import com.robinhood.ticker.TickerUtils
import com.google.android.gms.wearable.*
import com.wbrawner.trainterval.Logger
import com.wbrawner.trainterval.R
@ -89,6 +92,9 @@ class ActiveTimerFragment : Fragment(), MessageClient.OnMessageReceivedListener
it.setSupportActionBar(toolbar)
it.supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
timeRemaining.setCharacterLists(TickerUtils.provideNumberList() + ":")
timerSets.setCharacterLists(TickerUtils.provideNumberList())
timerRounds.setCharacterLists(TickerUtils.provideNumberList())
coroutineScope = CoroutineScope(Dispatchers.Main)
coroutineScope!!.launch {
activeTimerViewModel.timerState.observe(viewLifecycleOwner, Observer { state ->
@ -131,7 +137,19 @@ class ActiveTimerFragment : Fragment(), MessageClient.OnMessageReceivedListener
}
(activity as? AppCompatActivity)?.supportActionBar?.title = state.timerName
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(
requireContext().getDrawable(
if (state.isRunning) R.drawable.ic_pause

View file

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

View file

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

View file

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