20 changed files with 523 additions and 55 deletions
After Width: | Height: | Size: 741 B |
After Width: | Height: | Size: 720 B |
After Width: | Height: | Size: 550 B |
After Width: | Height: | Size: 773 B |
After Width: | Height: | Size: 725 B |
@ -0,0 +1,6 @@ |
|||||
|
package com.whitefish.ring.bean.ui |
||||
|
|
||||
|
data class Oximetry( |
||||
|
val time: Long, |
||||
|
val value: Float, |
||||
|
) |
@ -0,0 +1,6 @@ |
|||||
|
package com.whitefish.ring.bean.ui |
||||
|
|
||||
|
data class Temperature ( |
||||
|
val time: Long, |
||||
|
val value: Float, |
||||
|
) |
@ -0,0 +1,15 @@ |
|||||
|
package com.whitefish.ring.bean.ui |
||||
|
|
||||
|
data class TimeComponents( |
||||
|
val hours: Int, |
||||
|
val minutes: Int, |
||||
|
val seconds: Int |
||||
|
) |
||||
|
|
||||
|
fun Int.convertSecondsToTime(): TimeComponents { |
||||
|
val hours = this / 3600 |
||||
|
val minutes = (this % 3600) / 60 |
||||
|
val seconds = this % 60 |
||||
|
|
||||
|
return TimeComponents(hours, minutes, seconds) |
||||
|
} |
@ -1,10 +1,14 @@ |
|||||
package com.whitefish.ring.data |
package com.whitefish.ring.data |
||||
|
|
||||
import com.whitefish.ring.bean.ui.HeartRate |
import com.whitefish.ring.bean.ui.HeartRate |
||||
|
import com.whitefish.ring.bean.ui.Oximetry |
||||
import com.whitefish.ring.bean.ui.SleepState |
import com.whitefish.ring.bean.ui.SleepState |
||||
|
import com.whitefish.ring.bean.ui.Temperature |
||||
import kotlinx.coroutines.flow.first |
import kotlinx.coroutines.flow.first |
||||
import kotlinx.coroutines.flow.map |
import kotlinx.coroutines.flow.map |
||||
|
|
||||
suspend fun mac() = obtainDataStore().data.map { it[address] }.first() |
suspend fun mac() = obtainDataStore().data.map { it[address] }.first() |
||||
expect suspend fun getHeartRate(start: Long,end: Long): List<HeartRate> |
expect suspend fun getHeartRate(start: Long,end: Long): List<HeartRate> |
||||
expect suspend fun getSleep(start: Long, end: Long): SleepState |
expect suspend fun getSleep(start: Long, end: Long): SleepState |
||||
|
expect suspend fun getOximetry(start: Long, end: Long): List<Oximetry> |
||||
|
expect suspend fun getTemperature(start: Long, end: Long): List<Temperature> |
@ -0,0 +1,122 @@ |
|||||
|
package com.whitefish.ring.ui.chart |
||||
|
|
||||
|
import androidx.compose.foundation.Image |
||||
|
import androidx.compose.foundation.background |
||||
|
import androidx.compose.foundation.layout.Arrangement |
||||
|
import androidx.compose.foundation.layout.Box |
||||
|
import androidx.compose.foundation.layout.Column |
||||
|
import androidx.compose.foundation.layout.ColumnScope |
||||
|
import androidx.compose.foundation.layout.Row |
||||
|
import androidx.compose.foundation.layout.Spacer |
||||
|
import androidx.compose.foundation.layout.fillMaxHeight |
||||
|
import androidx.compose.foundation.layout.fillMaxSize |
||||
|
import androidx.compose.foundation.layout.fillMaxWidth |
||||
|
import androidx.compose.foundation.layout.height |
||||
|
import androidx.compose.foundation.layout.padding |
||||
|
import androidx.compose.foundation.layout.size |
||||
|
import androidx.compose.foundation.layout.width |
||||
|
import androidx.compose.foundation.layout.wrapContentHeight |
||||
|
import androidx.compose.foundation.layout.wrapContentSize |
||||
|
import androidx.compose.foundation.lazy.LazyColumn |
||||
|
import androidx.compose.foundation.lazy.items |
||||
|
import androidx.compose.foundation.shape.RoundedCornerShape |
||||
|
import androidx.compose.material3.Card |
||||
|
import androidx.compose.material3.Text |
||||
|
import androidx.compose.runtime.Composable |
||||
|
import androidx.compose.ui.Alignment |
||||
|
import androidx.compose.ui.Modifier |
||||
|
import androidx.compose.ui.draw.clip |
||||
|
import androidx.compose.ui.graphics.Color |
||||
|
import androidx.compose.ui.graphics.painter.Painter |
||||
|
import androidx.compose.ui.text.AnnotatedString |
||||
|
import androidx.compose.ui.unit.dp |
||||
|
import androidx.compose.ui.unit.sp |
||||
|
import com.whitefish.app.ui.chart.sleep.SleepChart |
||||
|
import com.whitefish.ring.bean.ui.SleepState |
||||
|
import com.whitefish.ring.utils.buildStyledText |
||||
|
import org.jetbrains.compose.resources.DrawableResource |
||||
|
import org.jetbrains.compose.resources.painterResource |
||||
|
import org.jetbrains.compose.ui.tooling.preview.Preview |
||||
|
import ring.shared.generated.resources.Res |
||||
|
|
||||
|
|
||||
|
data class RecoveryStateCardItem( |
||||
|
val icon: DrawableResource, |
||||
|
val title: String, |
||||
|
val tip: String, |
||||
|
val value: AnnotatedString, |
||||
|
val type: RecoveryStateCardType |
||||
|
) |
||||
|
|
||||
|
sealed class RecoveryStateCardType() { |
||||
|
class Sleep(val state: SleepState) : RecoveryStateCardType() |
||||
|
class HeartRate(val values: List<Int>) : RecoveryStateCardType() |
||||
|
class Oximetry(val values: List<Float>) : RecoveryStateCardType() |
||||
|
class Pressure(val values: List<Int>) : RecoveryStateCardType() |
||||
|
class Temperature(val values: List<Float>) : RecoveryStateCardType() |
||||
|
} |
||||
|
|
||||
|
@Composable |
||||
|
fun RecoveryStateCard(states: List<RecoveryStateCardItem>) { |
||||
|
Card( |
||||
|
shape = RoundedCornerShape(40.dp), |
||||
|
modifier = Modifier.fillMaxWidth().wrapContentHeight() |
||||
|
) { |
||||
|
Spacer(modifier = Modifier.height(6.dp)) |
||||
|
Box(modifier = Modifier.width(92.dp).height(6.dp).clip(RoundedCornerShape(15.dp)).background(Color(0xff352764)).align( |
||||
|
Alignment.CenterHorizontally)) |
||||
|
Column(modifier = Modifier.padding(top = 28.dp)) { |
||||
|
states.forEach { |
||||
|
StateItem(painterResource(it.icon), it.title, it.tip, it.value) { modifier -> |
||||
|
when (it.type) { |
||||
|
is RecoveryStateCardType.Sleep -> { |
||||
|
SleepChart( |
||||
|
it.type.state.segments,modifier = modifier |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
is RecoveryStateCardType.HeartRate -> { |
||||
|
ComposeMultiplatformBasicLineChart(it.type.values,modifier = modifier) |
||||
|
} |
||||
|
is RecoveryStateCardType.Oximetry -> { |
||||
|
ComposeMultiplatformBasicLineChart(it.type.values,modifier = modifier) |
||||
|
} |
||||
|
is RecoveryStateCardType.Pressure -> TODO() |
||||
|
is RecoveryStateCardType.Temperature -> { |
||||
|
ComposeMultiplatformBasicLineChart(it.type.values,modifier = modifier) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
Spacer(modifier = Modifier.height(38.dp)) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Composable |
||||
|
fun StateItem( |
||||
|
icon: Painter, |
||||
|
title: String, |
||||
|
tip: String, |
||||
|
value: AnnotatedString, |
||||
|
chart: @Composable (Modifier) -> Unit |
||||
|
) { |
||||
|
Row( |
||||
|
verticalAlignment = Alignment.CenterVertically, |
||||
|
modifier = Modifier.fillMaxWidth().padding(start = 20.dp).height(72.dp) |
||||
|
) { |
||||
|
Image(painter = icon, contentDescription = null, modifier = Modifier.size(40.dp)) |
||||
|
|
||||
|
Spacer(modifier = Modifier.width(21.dp)) |
||||
|
Column { |
||||
|
Text(title, fontSize = 18.sp) |
||||
|
Spacer(modifier = Modifier.height(7.dp)) |
||||
|
Text(tip, fontSize = 12.sp, color = Color(0xff61B7C8), modifier = Modifier.width(76.dp)) |
||||
|
} |
||||
|
|
||||
|
Spacer(modifier = Modifier.width(20.dp)) |
||||
|
chart(Modifier.fillMaxHeight().width(72.dp)) |
||||
|
Spacer(modifier = Modifier.width(28.dp)) |
||||
|
Text(value) |
||||
|
} |
||||
|
} |
@ -1,9 +1,102 @@ |
|||||
package com.whitefish.ring.ui.home.recovery |
package com.whitefish.ring.ui.home.recovery |
||||
|
|
||||
|
|
||||
|
import androidx.compose.ui.unit.sp |
||||
import androidx.lifecycle.ViewModel |
import androidx.lifecycle.ViewModel |
||||
|
import androidx.lifecycle.viewModelScope |
||||
|
import com.whitefish.ring.bean.ui.convertSecondsToTime |
||||
|
import com.whitefish.ring.data.getHeartRate |
||||
|
import com.whitefish.ring.data.getOximetry |
||||
|
import com.whitefish.ring.data.getSleep |
||||
|
import com.whitefish.ring.data.getTemperature |
||||
|
import com.whitefish.ring.obtainDeviceManager |
||||
|
import com.whitefish.ring.ui.chart.RecoveryStateCardItem |
||||
|
import com.whitefish.ring.ui.chart.RecoveryStateCardType |
||||
|
import com.whitefish.ring.utils.buildStyledText |
||||
|
import com.whitefish.ring.utils.getPastDayStartMillis |
||||
|
import com.whitefish.ring.utils.getTodayEndMillis |
||||
|
import com.whitefish.ring.utils.today |
||||
|
import kotlinx.coroutines.flow.MutableStateFlow |
||||
|
import kotlinx.coroutines.flow.asStateFlow |
||||
|
import kotlinx.coroutines.flow.collectLatest |
||||
|
import kotlinx.coroutines.launch |
||||
|
import ring.shared.generated.resources.Res |
||||
|
import ring.shared.generated.resources.ic_state_heart_rate |
||||
|
import ring.shared.generated.resources.ic_state_oxygen |
||||
|
import ring.shared.generated.resources.ic_state_sleep |
||||
|
import ring.shared.generated.resources.ic_state_temperature |
||||
|
|
||||
|
|
||||
class RecoveryViewModel : ViewModel() { |
class RecoveryViewModel : ViewModel() { |
||||
|
private val _uiState = MutableStateFlow(RecoveryUiState(emptyList())) |
||||
|
val uiState = _uiState.asStateFlow() |
||||
|
|
||||
|
init { |
||||
|
viewModelScope.launch { |
||||
|
obtainDeviceManager().bleReadyStateFlow.collectLatest { |
||||
|
if (it) { |
||||
|
val sleepState = getSleep(getPastDayStartMillis(1), getTodayEndMillis()) |
||||
|
|
||||
|
val heartRate = getHeartRate(sleepState.start, sleepState.end) |
||||
|
val oximetry = getOximetry(sleepState.start, sleepState.end) |
||||
|
val temperature = getTemperature(sleepState.start, sleepState.end) |
||||
|
|
||||
|
_uiState.value = RecoveryUiState( |
||||
|
listOf( |
||||
|
RecoveryStateCardItem( |
||||
|
icon = Res.drawable.ic_state_sleep, |
||||
|
title = "睡眠", |
||||
|
tip = "睡眠质量较好", |
||||
|
value = buildStyledText { |
||||
|
val time = sleepState.totalSeconds.convertSecondsToTime() |
||||
|
append(time.hours.toString(),20.sp) |
||||
|
append("小时",11.sp) |
||||
|
append(time.minutes.toString(),20.sp) |
||||
|
append("分钟",11.sp) |
||||
|
}, |
||||
|
type = RecoveryStateCardType.Sleep(sleepState) |
||||
|
), |
||||
|
RecoveryStateCardItem( |
||||
|
icon = Res.drawable.ic_state_heart_rate, |
||||
|
title = "心率", |
||||
|
tip = "睡眠质量较好", |
||||
|
value = buildStyledText { |
||||
|
append("${heartRate.lastOrNull()?.value}", 20.sp) |
||||
|
append("次/分钟", 11.sp) |
||||
|
}, |
||||
|
type = RecoveryStateCardType.HeartRate(heartRate.map { it.value }) |
||||
|
), |
||||
|
RecoveryStateCardItem( |
||||
|
icon = Res.drawable.ic_state_oxygen, |
||||
|
title = "血氧", |
||||
|
tip = "血氧水平正常", |
||||
|
value = buildStyledText { |
||||
|
append("${oximetry.lastOrNull()?.value}", 20.sp) |
||||
|
append("%", 11.sp) |
||||
|
}, |
||||
|
type = RecoveryStateCardType.Oximetry(oximetry.map { it.value } |
||||
|
) |
||||
|
), |
||||
|
RecoveryStateCardItem( |
||||
|
icon = Res.drawable.ic_state_temperature, |
||||
|
title = "体温", |
||||
|
tip = "体温水平正常", |
||||
|
value = buildStyledText { |
||||
|
append("${temperature.lastOrNull()?.value}", 20.sp) |
||||
|
append("°C", 11.sp) |
||||
|
}, |
||||
|
type = RecoveryStateCardType.Temperature(temperature.map { it.value }) |
||||
|
) |
||||
|
) |
||||
|
|
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
} |
} |
||||
|
|
||||
|
data class RecoveryUiState( |
||||
|
val stateCards: List<RecoveryStateCardItem> |
||||
|
) |
@ -0,0 +1,129 @@ |
|||||
|
package com.whitefish.ring.utils |
||||
|
|
||||
|
import androidx.compose.ui.graphics.Color |
||||
|
import androidx.compose.ui.text.AnnotatedString |
||||
|
import androidx.compose.ui.text.SpanStyle |
||||
|
import androidx.compose.ui.text.font.FontStyle |
||||
|
import androidx.compose.ui.text.font.FontWeight |
||||
|
import androidx.compose.ui.text.withStyle |
||||
|
import androidx.compose.ui.unit.TextUnit |
||||
|
import androidx.compose.ui.unit.sp |
||||
|
|
||||
|
/** |
||||
|
* 多样式文本构建器 |
||||
|
* 用于方便地构建包含不同样式文本段落的AnnotatedString |
||||
|
*/ |
||||
|
class StyledTextBuilder { |
||||
|
private val builder = AnnotatedString.Builder() |
||||
|
|
||||
|
/** |
||||
|
* 添加普通文本 |
||||
|
*/ |
||||
|
fun append(text: String): StyledTextBuilder { |
||||
|
builder.append(text) |
||||
|
return this |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 添加指定大小的文本 |
||||
|
*/ |
||||
|
fun append( |
||||
|
text: String, |
||||
|
fontSize: TextUnit |
||||
|
): StyledTextBuilder { |
||||
|
builder.withStyle( |
||||
|
style = SpanStyle(fontSize = fontSize) |
||||
|
) { |
||||
|
append(text) |
||||
|
} |
||||
|
return this |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 添加指定大小和颜色的文本 |
||||
|
*/ |
||||
|
fun append( |
||||
|
text: String, |
||||
|
fontSize: TextUnit, |
||||
|
color: Color |
||||
|
): StyledTextBuilder { |
||||
|
builder.withStyle( |
||||
|
style = SpanStyle( |
||||
|
fontSize = fontSize, |
||||
|
color = color |
||||
|
) |
||||
|
) { |
||||
|
append(text) |
||||
|
} |
||||
|
return this |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 添加完全自定义样式的文本 |
||||
|
*/ |
||||
|
fun append( |
||||
|
text: String, |
||||
|
fontSize: TextUnit = TextUnit.Unspecified, |
||||
|
color: Color = Color.Unspecified, |
||||
|
fontWeight: FontWeight? = null, |
||||
|
fontStyle: FontStyle? = null |
||||
|
): StyledTextBuilder { |
||||
|
builder.withStyle( |
||||
|
style = SpanStyle( |
||||
|
fontSize = fontSize, |
||||
|
color = color, |
||||
|
fontWeight = fontWeight, |
||||
|
fontStyle = fontStyle |
||||
|
) |
||||
|
) { |
||||
|
append(text) |
||||
|
} |
||||
|
return this |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 添加大号文本 |
||||
|
*/ |
||||
|
fun appendLarge(text: String, color: Color = Color.Unspecified): StyledTextBuilder { |
||||
|
return append(text, 24.sp, color) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 添加中号文本 |
||||
|
*/ |
||||
|
fun appendMedium(text: String, color: Color = Color.Unspecified): StyledTextBuilder { |
||||
|
return append(text, 18.sp, color) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 添加小号文本 |
||||
|
*/ |
||||
|
fun appendSmall(text: String, color: Color = Color.Unspecified): StyledTextBuilder { |
||||
|
return append(text, 14.sp, color) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 添加粗体文本 |
||||
|
*/ |
||||
|
fun appendBold(text: String, fontSize: TextUnit = TextUnit.Unspecified): StyledTextBuilder { |
||||
|
return append(text, fontSize, Color.Unspecified, FontWeight.Bold) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 添加斜体文本 |
||||
|
*/ |
||||
|
fun appendItalic(text: String, fontSize: TextUnit = TextUnit.Unspecified): StyledTextBuilder { |
||||
|
return append(text, fontSize, Color.Unspecified, null, FontStyle.Italic) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 构建最终的AnnotatedString |
||||
|
*/ |
||||
|
fun build(): AnnotatedString { |
||||
|
return builder.toAnnotatedString() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
fun buildStyledText(block: StyledTextBuilder.() -> Unit): AnnotatedString { |
||||
|
return StyledTextBuilder().apply(block).build() |
||||
|
} |
Loading…
Reference in new issue