Fix style issues after leaving timer and rotation breaking timers

This commit is contained in:
William Brawner 2020-07-04 18:15:10 -07:00
parent cb284b800d
commit f21754a0be
12 changed files with 267 additions and 173 deletions

View file

@ -9,7 +9,8 @@ data class IntervalDuration(
val seconds: Long = 0
) {
override fun toString(): String {
return "%02d:%02d:%02d".format(hours, minutes, seconds)
return if (hours > 0) "%02d:%02d:%02d".format(hours, minutes, seconds)
else "%02d:%02d".format(minutes, seconds)
}
}
@ -19,9 +20,10 @@ fun IntervalDuration.toMillis(): Long {
TimeUnit.SECONDS.toMillis(seconds)
}
private const val SECONDS_IN_HOUR = 3600
private const val SECONDS_IN_MINUTE = 60
fun Long.toIntervalDuration(): IntervalDuration {
val SECONDS_IN_HOUR = 3600
val SECONDS_IN_MINUTE = 60
if (this < 1000) {
return IntervalDuration(0, 0, 0)

View file

@ -2,7 +2,6 @@ package com.wbrawner.trainterval
import android.app.Application
import androidx.room.Room
import com.wbrawner.trainterval.activetimer.ActiveTimerViewModel
import com.wbrawner.trainterval.timerform.TimerFormViewModel
import com.wbrawner.trainterval.timerlist.TimerListViewModel
import org.koin.android.ext.koin.androidContext
@ -46,10 +45,6 @@ val traintervalModule = module {
TimerFormViewModel(get(parameters = { parametersOf("TimerFormStore") }), get())
}
factory {
ActiveTimerViewModel(get(parameters = { parametersOf("ActiveTimerStore") }), get())
}
factory<Logger> { params ->
AndroidLogger(params.component1())
}

View file

@ -7,25 +7,31 @@ import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Observer
import androidx.navigation.fragment.findNavController
import com.wbrawner.trainterval.Logger
import com.wbrawner.trainterval.R
import com.wbrawner.trainterval.model.IntervalTimerDao
import kotlinx.android.synthetic.main.fragment_active_timer.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import org.koin.core.parameter.parametersOf
class ActiveTimerFragment : Fragment() {
private var coroutineScope: CoroutineScope? = null
private val activeTimerViewModel: ActiveTimerViewModel by inject()
private val activeTimerViewModel: ActiveTimerViewModel by activityViewModels()
private val logger: Logger by inject(parameters = { parametersOf("ActiveTimerStore") })
private val timerDao: IntervalTimerDao by inject()
private var timerId: Long = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
timerId = requireArguments().getLong("timerId")
timerId = requireArguments().getLong(EXTRA_TIMER_ID)
setHasOptionsMenu(true)
}
@ -56,35 +62,46 @@ class ActiveTimerFragment : Fragment() {
is IntervalTimerActiveState.ExitState -> findNavController().navigateUp()
}
})
activeTimerViewModel.init(timerId)
activeTimerViewModel.init(logger, timerDao, timerId)
}
skipPreviousButton.setOnClickListener {
coroutineScope!!.launch {
activeTimerViewModel.goBack()
}
activeTimerViewModel.goBack()
}
playPauseButton.setOnClickListener {
coroutineScope!!.launch {
activeTimerViewModel.toggleTimer()
}
activeTimerViewModel.toggleTimer()
}
skipNextButton.setOnClickListener {
coroutineScope!!.launch {
activeTimerViewModel.skipAhead()
}
activeTimerViewModel.skipAhead()
}
}
private fun renderLoading() {
progressBar.visibility = View.VISIBLE
timerLayout.visibility = View.GONE
timerLayout.referencedIds.forEach {
view?.findViewById<View>(it)?.visibility = View.GONE
}
}
private fun renderTimer(state: IntervalTimerActiveState.TimerRunningState) {
progressBar.visibility = View.GONE
timerLayout.visibility = View.VISIBLE
timerLayout.referencedIds.forEach {
view?.findViewById<View>(it)?.visibility = View.VISIBLE
}
(activity as? AppCompatActivity)?.supportActionBar?.title = state.timerName
val backgroundColor = resources.getColor(state.timerBackground, context?.theme)
timerBackground.setBackgroundColor(backgroundColor)
playPauseButton.setImageDrawable(requireContext().getDrawable(state.playPauseIcon))
timeRemaining.text = state.timeRemaining
timerSets.text = getString(
R.string.timer_sets_formatted,
state.currentSet,
state.totalSets
)
timerRounds.text = getString(
R.string.timer_rounds_formatted,
state.currentRound,
state.totalRounds
)
}
override fun onDestroyView() {

View file

@ -1,8 +1,10 @@
package com.wbrawner.trainterval.activetimer
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wbrawner.trainterval.Logger
import com.wbrawner.trainterval.R
import com.wbrawner.trainterval.activetimer.IntervalTimerActiveState.LoadingState
@ -11,93 +13,83 @@ import com.wbrawner.trainterval.model.IntervalTimer
import com.wbrawner.trainterval.model.IntervalTimerDao
import com.wbrawner.trainterval.model.Phase
import com.wbrawner.trainterval.toIntervalDuration
import kotlinx.coroutines.*
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
class ActiveTimerViewModel(
private val logger: Logger,
private val timerDao: IntervalTimerDao
) : ViewModel() {
class ActiveTimerViewModel : ViewModel() {
val timerState: MutableLiveData<IntervalTimerActiveState> = MutableLiveData(LoadingState)
private var timerJob: Job? = null
private lateinit var timer: IntervalTimer
private lateinit var logger: Logger
private var timerComplete = false
private var timerRunning = false
private var currentPhase = Phase.WARM_UP
private var currentSet = 1
private var currentRound = 1
private var timeRemaining: Long = 0
suspend fun init(timerId: Long) {
logger.d(message = "Initializing with Timer id $timerId")
timer = timerDao.getById(timerId)
timeRemaining = timer.warmUpDuration
timerState.postValue(
TimerRunningState(
timerRunning,
timeRemaining.toIntervalDuration().toString(),
currentSet,
timer.sets,
currentRound,
timer.cycles
)
)
}
suspend fun toggleTimer() {
if (timerRunning) {
timerJob?.cancel()
timerRunning = false
suspend fun init(
logger: Logger,
timerDao: IntervalTimerDao,
timerId: Long
) {
this.logger = logger
if (timerJob == null || timer.id != timerId) {
logger.d(message = "Initializing with Timer id $timerId")
timer = timerDao.getById(timerId)
timeRemaining = timer.warmUpDuration
timerState.postValue(
TimerRunningState(
timerRunning,
timeRemaining.toIntervalDuration().toString(),
timer,
timeRemaining,
currentSet,
timer.sets,
currentRound,
timer.cycles
currentPhase,
timerJob != null
)
)
} else {
startTimer()
}
}
private suspend fun startTimer() {
coroutineScope {
timerJob = launch {
timerRunning = true
timerState.postValue(
TimerRunningState(
timerRunning,
timeRemaining.toIntervalDuration().toString(),
currentSet,
timer.sets,
currentRound,
timer.cycles
)
fun toggleTimer() {
if (timerJob != null) {
timerJob?.cancel()
timerJob = null
timerState.postValue(
TimerRunningState(
timer,
timeRemaining,
currentSet,
currentRound,
currentPhase,
timerJob != null
)
while (coroutineContext.isActive && timerRunning) {
)
} else {
viewModelScope.launch {
startTimer()
}
}
}
private fun startTimer() {
viewModelScope.launch {
timerJob = launch {
updateTimer()
while (coroutineContext.isActive && timerJob != null) {
delay(1_000)
timeRemaining -= 1_000
if (timeRemaining <= 0) {
goForward()
}
timerState.postValue(
TimerRunningState(
timerRunning,
timeRemaining.toIntervalDuration().toString(),
currentSet,
timer.sets,
currentRound,
timer.cycles
)
)
updateTimer()
}
}
}
}
suspend fun skipAhead() {
fun skipAhead() {
timerJob?.cancel()
when (currentPhase) {
Phase.COOL_DOWN -> {
@ -107,11 +99,26 @@ class ActiveTimerViewModel(
goForward()
}
}
if (timerRunning) {
if (timerJob != null) {
startTimer()
} else {
updateTimer()
}
}
private fun updateTimer() {
timerState.postValue(
TimerRunningState(
timer,
timeRemaining,
currentSet,
currentRound,
currentPhase,
timerJob != null
)
)
}
private fun goForward() {
timerComplete = currentPhase == Phase.COOL_DOWN
when (currentPhase) {
@ -147,12 +154,14 @@ class ActiveTimerViewModel(
timeRemaining = timer.lowIntensityDuration
}
Phase.COOL_DOWN -> {
timerRunning = false
timeRemaining = 0
timerJob?.cancel()
timerJob = null
}
}
}
suspend fun goBack() {
fun goBack() {
timerJob?.cancel()
when (currentPhase) {
Phase.WARM_UP -> {
@ -169,11 +178,11 @@ class ActiveTimerViewModel(
timeRemaining = timer.restDuration
}
else -> {
currentSet--
currentPhase = Phase.HIGH_INTENSITY
timeRemaining = timer.highIntensityDuration
}
}
timeRemaining = timer.highIntensityDuration
}
Phase.HIGH_INTENSITY -> {
currentPhase = Phase.LOW_INTENSITY
@ -190,8 +199,10 @@ class ActiveTimerViewModel(
timeRemaining = timer.highIntensityDuration
}
}
if (timerRunning) {
if (timerJob != null) {
startTimer()
} else {
updateTimer()
}
}
}
@ -202,14 +213,33 @@ class ActiveTimerViewModel(
sealed class IntervalTimerActiveState {
object LoadingState : IntervalTimerActiveState()
class TimerRunningState(
timerRunning: Boolean,
val timerName: String,
val timeRemaining: String,
val currentSet: Int,
val totalSets: Int,
val currentRound: Int,
val totalRounds: Int,
val timerComplete: Boolean = false,
@DrawableRes val playPauseIcon: Int = if (timerRunning) R.drawable.ic_pause else R.drawable.ic_play_arrow
) : IntervalTimerActiveState()
@ColorRes val timerBackground: Int,
@DrawableRes val playPauseIcon: Int
) : IntervalTimerActiveState() {
constructor(
timer: IntervalTimer,
timeRemaining: Long,
currentSet: Int,
currentRound: Int,
phase: Phase,
timerRunning: Boolean
) : this(
timerName = timer.name,
timeRemaining = timeRemaining.toIntervalDuration().toString(),
currentSet = currentSet,
currentRound = currentRound,
totalSets = timer.sets,
totalRounds = timer.cycles,
timerBackground = phase.colorRes,
playPauseIcon = if (timerRunning) R.drawable.ic_pause else R.drawable.ic_play_arrow
)
}
object ExitState : IntervalTimerActiveState()
}

View file

@ -1,6 +1,8 @@
package com.wbrawner.trainterval.model
import androidx.annotation.ColorRes
import androidx.room.*
import com.wbrawner.trainterval.R
import kotlinx.coroutines.flow.Flow
import java.util.concurrent.TimeUnit
@ -18,12 +20,12 @@ data class IntervalTimer(
val cycles: Int = 1
)
enum class Phase {
WARM_UP,
LOW_INTENSITY,
HIGH_INTENSITY,
REST,
COOL_DOWN,
enum class Phase(@ColorRes val colorRes: Int) {
WARM_UP(R.color.colorSurface),
LOW_INTENSITY(R.color.colorSurfaceLowIntensity),
HIGH_INTENSITY(R.color.colorSurfaceHighIntensity),
REST(R.color.colorSurfaceRest),
COOL_DOWN(R.color.colorSurfaceCoolDown),
}
@Dao

View file

@ -1,21 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph" />
</androidx.constraintlayout.widget.ConstraintLayout>
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/nav_graph"
tools:context=".MainActivity" />

View file

@ -1,9 +1,12 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/timerBackground"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true"
android:fitsSystemWindows="true"
android:keepScreenOn="true"
android:orientation="vertical"
tools:context="com.wbrawner.trainterval.activetimer.ActiveTimerFragment">
@ -12,82 +15,125 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#00000000"
android:clipChildren="false"
android:clipToPadding="false"
android:elevation="0dp"
app:elevation="0dp">
android:padding="16dp"
app:elevation="0dp"
app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:background="@drawable/background_rounded_corners"
app:layout_scrollFlags="scroll|enterAlways"
app:title="@string/app_name" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.constraintlayout.widget.ConstraintLayout
<androidx.constraintlayout.widget.Group
android:id="@+id/timerLayout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:visibility="gone"
tools:visibility="visible">
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="playPauseButton,timerSets,timerRounds,timeRemaining,skipNextButton,skipPreviousButton" />
<TextView
android:id="@+id/timeRemaining"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline2"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="00:00" />
<TextView
android:id="@+id/timerSets"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/timerRounds"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintStart_toStartOf="parent"
tools:text="Set: 4/5" />
<ImageButton
android:id="@+id/skipPreviousButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:backgroundTint="@android:color/transparent"
android:contentDescription="@string/skip_previous"
android:src="@drawable/ic_skip_previous"
app:layout_constraintEnd_toStartOf="@+id/playPauseButton"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/timeRemaining" />
<TextView
android:id="@+id/timerRounds"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/timerSets"
tools:text="Round: 4/5" />
<ImageButton
android:id="@+id/playPauseButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:backgroundTint="@android:color/transparent"
android:contentDescription="@string/start_timer"
app:layout_constraintEnd_toStartOf="@+id/skipNextButton"
app:layout_constraintStart_toEndOf="@+id/skipPreviousButton"
app:layout_constraintTop_toBottomOf="@+id/timeRemaining"
tools:src="@drawable/ic_play_arrow" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/timerInfo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="top"
app:constraint_referenced_ids="timerSets,timerRounds" />
<ImageButton
android:id="@+id/skipNextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:backgroundTint="@android:color/transparent"
android:contentDescription="@string/skip_next"
android:src="@drawable/ic_skip_next"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/playPauseButton"
app:layout_constraintTop_toBottomOf="@+id/timeRemaining" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/timeRemaining"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="monospace"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline2"
android:textColor="@color/colorOnSurface"
app:layout_constraintBottom_toTopOf="@+id/playPauseButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/appBarLayout"
app:layout_constraintVertical_chainStyle="packed"
tools:text="00:00" />
<ImageButton
android:id="@+id/skipPreviousButton"
android:layout_width="64dp"
android:layout_height="64dp"
android:backgroundTint="@android:color/transparent"
android:contentDescription="@string/skip_previous"
android:scaleType="fitCenter"
android:src="@drawable/ic_skip_previous"
app:layout_constraintBottom_toTopOf="@+id/timerInfo"
app:layout_constraintEnd_toStartOf="@+id/playPauseButton"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/timeRemaining"
app:layout_constraintVertical_bias="0.0" />
<ImageButton
android:id="@+id/playPauseButton"
android:layout_width="64dp"
android:layout_height="64dp"
android:backgroundTint="@android:color/transparent"
android:contentDescription="@string/start_timer"
android:scaleType="fitCenter"
app:layout_constraintBottom_toTopOf="@+id/timerInfo"
app:layout_constraintEnd_toStartOf="@+id/skipNextButton"
app:layout_constraintStart_toEndOf="@+id/skipPreviousButton"
app:layout_constraintTop_toBottomOf="@+id/timeRemaining"
tools:src="@drawable/ic_play_arrow" />
<ImageButton
android:id="@+id/skipNextButton"
android:layout_width="64dp"
android:layout_height="64dp"
android:backgroundTint="@android:color/transparent"
android:contentDescription="@string/skip_next"
android:scaleType="fitCenter"
android:src="@drawable/ic_skip_next"
app:layout_constraintBottom_toTopOf="@+id/timerInfo"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/playPauseButton"
app:layout_constraintTop_toBottomOf="@+id/timeRemaining"
app:layout_constraintVertical_bias="0.0" />
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_weight="1"
android:visibility="gone" />
</LinearLayout>
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -5,6 +5,7 @@
android:layout_height="match_parent"
android:animateLayoutChanges="true"
android:fillViewport="true"
android:fitsSystemWindows="true"
tools:context="com.wbrawner.trainterval.timerform.TimerFormFragment">
<androidx.core.widget.NestedScrollView

View file

@ -6,6 +6,7 @@
android:animateLayoutChanges="true"
android:clipChildren="false"
android:clipToPadding="false"
android:fitsSystemWindows="true"
android:orientation="vertical"
tools:context="com.wbrawner.trainterval.timerlist.TimerListFragment">

View file

@ -1,9 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#6200EE</color>
<color name="colorPrimaryDark">#3700B3</color>
<color name="colorAccent">#03DAC5</color>
<color name="colorPrimary">#FFEB3B</color>
<color name="colorPrimaryDark">#F9A825</color>
<color name="colorAccent">#F57C00</color>
<color name="colorOnSurface">#111111</color>
<color name="colorSurface">#FFFFFF</color>
<color name="colorSurfaceStroke">#F1F1F1</color>
<color name="colorSurfaceWarmUp">@color/colorSurface</color>
<color name="colorSurfaceLowIntensity">#F06292</color>
<color name="colorSurfaceHighIntensity">#81C784</color>
<color name="colorSurfaceRest">#FFF176</color>
<color name="colorSurfaceCoolDown">#64B5F6</color>
</resources>

View file

@ -18,4 +18,6 @@
<string name="rest_duration">Rest Duration</string>
<string name="title_item_list">Items</string>
<string name="title_item_detail">Item Detail</string>
<string name="timer_sets_formatted">Set: %1$d/%2$d</string>
<string name="timer_rounds_formatted">Round: %1$d/%2$d</string>
</resources>

View file

@ -6,9 +6,8 @@
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="android:windowLightStatusBar">true</item>
<item name="android:statusBarColor">?android:windowBackground</item>
<item name="statusBarBackground">?android:windowBackground</item>
<item name="android:windowTranslucentStatus">true</item>
<item name="android:windowTranslucentNavigation">true</item>
</style>
<style name="AppTheme.NoActionBar">