Add animation to the timer screen
This commit is contained in:
parent
6fab0ff4f9
commit
26c6977fd6
8 changed files with 48 additions and 287 deletions
|
@ -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>
|
|
@ -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>
|
|
@ -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">
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue