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 |
|||
|
|||
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.Temperature |
|||
import kotlinx.coroutines.flow.first |
|||
import kotlinx.coroutines.flow.map |
|||
|
|||
suspend fun mac() = obtainDataStore().data.map { it[address] }.first() |
|||
expect suspend fun getHeartRate(start: Long,end: Long): List<HeartRate> |
|||
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 |
|||
|
|||
|
|||
import androidx.compose.ui.unit.sp |
|||
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() { |
|||
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