chat android tv adaptation

This commit is contained in:
2026-08-09 11:59:33 +03:00
parent db47d8c2ce
commit 523b164964
8 changed files with 302 additions and 164 deletions
@@ -5,15 +5,8 @@ import androiddev.apps.vbox.client.shared.generated.resources.not_found
import androiddev.apps.vbox.client.shared.generated.resources.recommendations
import androiddev.apps.vbox.client.shared.generated.resources.retry
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -24,7 +17,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@@ -32,17 +24,13 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
@@ -54,6 +42,7 @@ import org.jetbrains.compose.resources.stringResource
import ru.shadowsparky.ui.components.ErrorContent
import ru.shadowsparky.ui.components.LazyVerticalGrid
import ru.shadowsparky.ui.components.Loading
import ru.shadowsparky.ui.theme.focusBorder
import ru.shadowsparky.vbox.shared.domain.model.VideoItem
import ru.shadowsparky.vbox.shared.presentation.AppScaffold
import ru.shadowsparky.vbox.shared.presentation.search.AppBarWithSearch
@@ -179,41 +168,11 @@ private fun Poster(
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
var isFocused by remember { mutableStateOf(false) }
val scale by animateFloatAsState(
targetValue = if (isFocused) 1.0f else 0.92f,
animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
label = "scale"
)
val borderColor by animateColorAsState(
targetValue = if (isFocused) MaterialTheme.colorScheme.primary else Color.Transparent,
animationSpec = tween(200),
label = "borderColor"
)
val borderWidth by animateDpAsState(
targetValue = if (isFocused) 3.dp else 0.dp,
animationSpec = tween(200),
label = "borderWidth"
)
Box(
modifier = modifier
.aspectRatio(0.7f)
.graphicsLayer {
scaleX = scale
scaleY = scale
clip = true
shape = RoundedCornerShape(12.dp)
}
.onFocusChanged { isFocused = it.isFocused }
.border(
width = borderWidth,
color = borderColor,
shape = RoundedCornerShape(12.dp)
)
.focusBorder()
.clickable(onClick = onClick)
) {
Image(
@@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.window.ComposeViewport
@@ -62,24 +61,19 @@ fun mainInternal() {
startKoin<VBox>()
val lifecycle = LifecycleRegistry()
val composeRoot = document.getElementById("compose-root") as HTMLElement
var rootRef: VBoxRootComponent? = null
val root: VBoxRootComponent = withWebHistory { stateKeeper: StateKeeper, deepLinkUrl: String? ->
if (rootRef == null) {
rootRef = createRoot(lifecycle, stateKeeper, deepLinkUrl)
}
rootRef
createRoot(lifecycle, stateKeeper, deepLinkUrl)
}
hookPageVisibility(lifecycle)
lifecycle.doOnResume { hideElementById("loader") }
ComposeViewport(viewportContainer = composeRoot, content = {
val r = remember { root }
ComposeViewport(composeRoot) {
LaunchedEffect(null) {
r.onFinish.collect { r.navReplace(Route.Loading()) }
root.onFinish.collect { root.navReplace(Route.Loading()) }
}
Box(Modifier.fillMaxSize().padding(WindowInsets.safeDrawing.asPaddingValues())) {
Nav(r)
Nav(root)
}
})
}
}
private fun createRoot(
@@ -87,9 +81,7 @@ private fun createRoot(
stateKeeper: StateKeeper,
deepLinkUrl: String?
): VBoxRootComponent {
val ctx = DefaultComponentContext(
lifecycle = lifecycle, stateKeeper = stateKeeper
)
val ctx = DefaultComponentContext(lifecycle, stateKeeper)
val deep = deepLinkUrl?.let { UrlRoutes.parseUrl(it) }
val initRoute: Route = deep ?: Route.default
return koin.get<RootFactory>().create(context = ctx, initStack = initRoute)
@@ -7,6 +7,7 @@ import androiddev.feature.chat.chat_client.generated.resources.chat_ok
import androiddev.feature.chat.chat_client.generated.resources.chat_retry_button
import androiddev.feature.chat.chat_client.generated.resources.send
import androidx.compose.foundation.focusable
import androidx.compose.foundation.gestures.animateScrollBy
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -20,12 +21,14 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
@@ -37,20 +40,25 @@ import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.withFrameNanos
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextLinkStyles
@@ -61,6 +69,7 @@ import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withLink
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
import ru.shadowsparky.chat.domain.ChatRoles
@@ -69,6 +78,9 @@ import ru.shadowsparky.chat.domain.TextToken
import ru.shadowsparky.ui.components.AlertContent
import ru.shadowsparky.ui.components.LazyColumn
import ru.shadowsparky.ui.components.Loading
import ru.shadowsparky.ui.components.TextPreference
import ru.shadowsparky.ui.components.isTv
import ru.shadowsparky.ui.theme.focusBorder
@Composable
fun ChatContent(
@@ -132,7 +144,6 @@ private fun ChatContent(
val listState = rememberLazyListState()
var textInput by remember { mutableStateOf("") }
val textFieldFocusRequester = remember { FocusRequester() }
val listFocusRequester = remember { FocusRequester() }
LaunchedEffect(state.messages.size) {
if (state.messages.isNotEmpty()) {
@@ -145,18 +156,25 @@ private fun ChatContent(
Column(modifier = Modifier.fillMaxSize()) {
LazyColumn(
state = listState,
modifier = Modifier.focusRequester(listFocusRequester)
.weight(1f)
modifier = Modifier.weight(1f)
.fillMaxWidth(),
contentPadding = contentPadding,
verticalArrangement = Arrangement.spacedBy(8.dp),
reverseLayout = true
) {
items(items = state.messages, key = { it.id ?: it.hashCode() }) { message ->
itemsIndexed(
items = state.messages,
key = { _, item -> item.id ?: item.hashCode() }
) { index, message ->
MessageBubble(
message = message,
onParseMessage = onParseMessage,
onClickLink = onClickLink
onClickLink = onClickLink,
onKeyEvent = {
if (index == 0) {
if (it.key == Key.DirectionDown) textFieldFocusRequester.requestFocus()
}
}
)
}
item { Spacer(Modifier.height(4.dp)) }
@@ -178,7 +196,7 @@ private fun ChatContent(
OutlinedTextField(
value = textInput,
onValueChange = { textInput = it },
modifier = Modifier.focusRequester(textFieldFocusRequester).resetFocusOnUpEvent(listFocusRequester).weight(1f),
modifier = Modifier.focusRequester(textFieldFocusRequester).weight(1f),
shape = RoundedCornerShape(24.dp),
placeholder = { Text(stringResource(Res.string.chat_input_placeholder)) },
enabled = !state.isSending,
@@ -207,73 +225,191 @@ private fun ChatContent(
private fun MessageBubble(
message: Message,
onParseMessage: (String) -> List<TextToken>,
onClickLink: (String) -> Unit
onClickLink: (String) -> Unit,
onKeyEvent: (KeyEvent) -> Unit
) {
val isUser = message.role == ChatRoles.USER
val alignment = if (isUser) Alignment.CenterEnd else Alignment.CenterStart
val containerColor = if (isUser) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceVariant
val contentColor = if (isUser) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurfaceVariant
val tokens = remember(message.content) { onParseMessage(message.content) }
val linkTokens by derivedStateOf { tokens.filterIsInstance<TextToken.Link>() }
val primaryColor = MaterialTheme.colorScheme.primary
val focusBackgroundColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.15f)
val annotatedString = remember(tokens, primaryColor, focusBackgroundColor) {
buildAnnotatedString {
tokens.forEach { token ->
when (token) {
is TextToken.Plain -> append(token.text)
is TextToken.Link -> {
val linkAnnotation = LinkAnnotation.Clickable(
tag = "LINK",
styles = TextLinkStyles(
style = SpanStyle(
color = primaryColor,
fontWeight = FontWeight.Bold,
textDecoration = TextDecoration.Underline
),
focusedStyle = SpanStyle(
background = focusBackgroundColor,
color = primaryColor
)
),
linkInteractionListener = { onClickLink(token.text) }
)
withLink(linkAnnotation) { append(token.text) }
}
val focusedColor = MaterialTheme.colorScheme.error
val isTv = isTv()
val annotatedString = remember(tokens) {
if (isTv) {
buildAnnotatedString { append(message.content) }
} else {
buildText(tokens, primaryColor, focusedColor, onClickLink)
}
}
var dialog by remember { mutableStateOf(false) }
var boxModifier = Modifier.widthIn(max = 280.dp)
if (isTv) {
boxModifier = boxModifier.focusBorder(false).onKeyEvent {
if (it.type == KeyEventType.KeyDown) {
when (it.key) {
Key.Enter -> dialog = true
else -> onKeyEvent(it)
}
}
false
}
}
Box(
modifier = Modifier.fillMaxWidth().focusable(),
contentAlignment = alignment
modifier = Modifier.fillMaxWidth(),
contentAlignment = if (isUser) Alignment.CenterEnd else Alignment.CenterStart
) {
Card(
colors = CardDefaults.cardColors(
containerColor = containerColor,
contentColor = contentColor
containerColor = if (isUser) {
MaterialTheme.colorScheme.primaryContainer
} else {
MaterialTheme.colorScheme.surfaceVariant
},
contentColor = if (isUser) {
MaterialTheme.colorScheme.onPrimaryContainer
} else {
MaterialTheme.colorScheme.onSurfaceVariant
}
),
modifier = Modifier.widthIn(max = 280.dp)
) {
Column(modifier = Modifier.padding(12.dp)) {
SelectionContainer {
Text(
text = annotatedString,
style = MaterialTheme.typography.bodyLarge
)
modifier = boxModifier
) { ChatText(annotatedString) }
}
if (dialog && isTv) {
TextDialog(message.content, linkTokens, onClickLink) { dialog = false }
}
}
@Composable
private fun ChatText(annotatedString: AnnotatedString) {
SelectionContainer {
Column(modifier = Modifier.padding(8.dp)) {
Text(
text = annotatedString,
style = MaterialTheme.typography.bodyLarge
)
}
}
}
@Composable
private fun TextDialog(
text: String,
links: List<TextToken.Link>,
onClickLink: (String) -> Unit,
dismiss: () -> Unit
) {
var showLinks by remember { mutableStateOf(false) }
AlertContent(
onDismissRequest = dismiss,
content = {
Column {
val focusRequester = remember { FocusRequester() }
if (!showLinks) {
ScrollableMessage(focusRequester, text) {
if (links.isNotEmpty()) {
showLinks = true
} else {
dismiss()
}
}
} else {
LazyColumn {
itemsIndexed(links) { index, item ->
val mod = Modifier.focusBorder(false)
TextPreference(
text = item.text,
onClick = { onClickLink(item.text) },
modifier = if (index == 0) {
mod.focusRequester(focusRequester)
} else {
mod
}
)
}
}
}
LaunchedEffect(showLinks) {
withFrameNanos {}
focusRequester.requestFocus()
}
}
}
)
}
@Composable
private fun ScrollableMessage(
focusRequester: FocusRequester,
text: String,
onEnter: () -> Unit
) {
val scrollStep = 250f
val scrollState = rememberScrollState()
val coroutineScope = rememberCoroutineScope()
Box(
modifier = Modifier.focusRequester(focusRequester)
.focusable()
.onKeyEvent { keyEvent ->
if (keyEvent.type == KeyEventType.KeyDown) {
when (keyEvent.key) {
Key.DirectionDown -> {
coroutineScope.launch { scrollState.animateScrollBy(scrollStep) }
true
}
Key.DirectionUp -> {
coroutineScope.launch { scrollState.animateScrollBy(-scrollStep) }
true
}
Key.Enter -> {
onEnter()
true
}
else -> false
}
} else {
false
}
}
) {
Text(
text = text,
modifier = Modifier.fillMaxWidth()
.verticalScroll(scrollState)
)
}
}
private fun Modifier.resetFocusOnUpEvent(focusRequester: FocusRequester): Modifier {
return onKeyEvent {
if (it.type == KeyEventType.KeyUp) {
if (it.key == Key.DirectionUp) {
focusRequester.requestFocus()
private fun buildText(
tokens: List<TextToken>,
primaryColor: Color,
focusedColor: Color,
onClickLink: (String) -> Unit
): AnnotatedString = buildAnnotatedString {
tokens.forEach { token ->
when (token) {
is TextToken.Plain -> append(token.text)
is TextToken.Link -> {
val linkAnnotation = LinkAnnotation.Clickable(
tag = "LINK",
styles = TextLinkStyles(
style = SpanStyle(
color = primaryColor,
fontWeight = FontWeight.Bold,
textDecoration = TextDecoration.Underline
),
focusedStyle = SpanStyle(
color = focusedColor,
fontWeight = FontWeight.Bold,
textDecoration = TextDecoration.Underline
)
),
linkInteractionListener = { onClickLink(token.text) }
)
withLink(linkAnnotation) { append(token.text) }
}
}
true
}
}
@@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
@@ -18,6 +19,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialExpressiveTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarDefaults
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.NavigationRail
import androidx.compose.material3.NavigationRailItem
@@ -30,6 +32,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
@@ -37,7 +40,6 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import ru.shadowsparky.ui.nav.Route
import ru.shadowsparky.ui.theme.ComponentDefaults
import ru.shadowsparky.ui.theme.getColorScheme
@Immutable
@@ -138,8 +140,8 @@ private fun <T : Route> BottomBar(
.windowInsetsPadding(WindowInsets.navigationBars)
) {
NavigationBar(
containerColor = ComponentDefaults.navBarContentColor(),
modifier = ComponentDefaults.roundedClip().blurEffect()
containerColor = navBarContentColor(),
modifier = Modifier.clip(RoundedCornerShape(24.dp)).blurEffect()
) {
entries.forEach { entry ->
NavigationBarItem(
@@ -153,6 +155,14 @@ private fun <T : Route> BottomBar(
}
}
@Composable
private fun navBarContentColor(): Color {
return if (isBlurEnabled()) {
MaterialTheme.colorScheme.surfaceContainerHigh.copy(alpha = 0.5f)
} else {
NavigationBarDefaults.containerColor
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -11,10 +11,11 @@ import androidx.compose.material3.SearchBarState
import androidx.compose.material3.rememberSearchBarState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.Dp
import ru.shadowsparky.ui.blurEffect
import ru.shadowsparky.ui.theme.ComponentDefaults
import ru.shadowsparky.ui.isBlurEnabled
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -25,7 +26,7 @@ fun AppBarWithSearch(
navigationIcon: @Composable (() -> Unit)? = null,
actions: @Composable (RowScope.() -> Unit)? = null,
shape: Shape = SearchBarDefaults.inputFieldShape,
colors: AppBarWithSearchColors = ComponentDefaults.appBarWithSearchDefaults(),
colors: AppBarWithSearchColors = appBarWithSearchDefaults(),
tonalElevation: Dp = SearchBarDefaults.TonalElevation,
shadowElevation: Dp = SearchBarDefaults.ShadowElevation,
contentPadding: PaddingValues = SearchBarDefaults.AppBarContentPadding,
@@ -47,3 +48,15 @@ fun AppBarWithSearch(
scrollBehavior
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun appBarWithSearchDefaults(): AppBarWithSearchColors {
return if (isBlurEnabled()) {
SearchBarDefaults.appBarWithSearchColors(
appBarContainerColor = Color.Transparent
)
} else {
SearchBarDefaults.appBarWithSearchColors()
}
}
@@ -10,9 +10,10 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import ru.shadowsparky.ui.blurEffect
import ru.shadowsparky.ui.theme.ComponentDefaults
import ru.shadowsparky.ui.isBlurEnabled
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -23,7 +24,7 @@ fun TopAppBar(
actions: @Composable RowScope.() -> Unit = {},
expandedHeight: Dp = TopAppBarDefaults.TopAppBarExpandedHeight,
windowInsets: WindowInsets = TopAppBarDefaults.windowInsets,
colors: TopAppBarColors = ComponentDefaults.topAppBarColors(),
colors: TopAppBarColors = topAppBarColors(),
scrollBehavior: TopAppBarScrollBehavior? = null,
contentPadding: PaddingValues = TopAppBarDefaults.ContentPadding
) {
@@ -39,3 +40,16 @@ fun TopAppBar(
contentPadding = contentPadding
)
}
@Composable
private fun topAppBarColors(): TopAppBarColors {
return if (isBlurEnabled()) {
TopAppBarDefaults.topAppBarColors(
Color.Transparent,
Color.Transparent
)
} else {
TopAppBarDefaults.topAppBarColors()
}
}
@@ -1,56 +1,68 @@
package ru.shadowsparky.ui.theme
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.foundation.border
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.AppBarWithSearchColors
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBarDefaults
import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.TopAppBarColors
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.dp
import ru.shadowsparky.ui.isBlurEnabled
object ComponentDefaults {
@Composable
fun topAppBarColors(): TopAppBarColors {
return if (isBlurEnabled()) {
TopAppBarDefaults.topAppBarColors(
Color.Transparent,
Color.Transparent
internal fun focusBorder(
modifier: Modifier,
needScale: Boolean
): Modifier {
var isFocused by remember { mutableStateOf(false) }
val borderColor by animateColorAsState(
targetValue = if (isFocused) MaterialTheme.colorScheme.primary else Color.Transparent,
animationSpec = tween(200),
label = "borderColor"
)
val borderWidth by animateDpAsState(
targetValue = if (isFocused) 3.dp else 0.dp,
animationSpec = tween(200),
label = "borderWidth"
)
val scale by animateFloatAsState(
targetValue = if (isFocused) 1.0f else 0.92f,
animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
label = "scale"
)
val mod = if (needScale) {
modifier.graphicsLayer {
scaleX = scale
scaleY = scale
clip = true
shape = RoundedCornerShape(12.dp)
}
} else {
modifier
}
return mod.onFocusChanged { isFocused = it.isFocused }
.border(
width = borderWidth,
color = borderColor,
shape = RoundedCornerShape(12.dp)
)
} else {
TopAppBarDefaults.topAppBarColors()
}
}
@Composable
fun navBarContentColor(): Color {
return if (isBlurEnabled()) {
MaterialTheme.colorScheme.surfaceContainerHigh.copy(alpha = 0.5f)
} else {
NavigationBarDefaults.containerColor
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun appBarWithSearchDefaults(): AppBarWithSearchColors {
return if (isBlurEnabled()) {
SearchBarDefaults.appBarWithSearchColors(
appBarContainerColor = Color.Transparent
)
} else {
SearchBarDefaults.appBarWithSearchColors()
}
}
fun roundedClip(modifier: Modifier = Modifier): Modifier {
return modifier.clip(RoundedCornerShape(24.dp))
}
}
@Composable
fun Modifier.focusBorder(needScale: Boolean = true): Modifier {
return ComponentDefaults.focusBorder(this, needScale)
}
@@ -38,10 +38,10 @@ fun hookPageVisibility(lifecycle: LifecycleRegistry) {
fun onPageHideShow(hidden: Boolean) {
if (hidden) {
// уходим глубже: STARTED → CREATED
when (lifecycle.state) {
Lifecycle.State.RESUMED -> {
lifecycle.pause(); lifecycle.stop()
lifecycle.pause();
lifecycle.stop()
}
Lifecycle.State.STARTED -> {
@@ -51,14 +51,16 @@ fun hookPageVisibility(lifecycle: LifecycleRegistry) {
else -> Unit
}
} else {
// pageshow: поднимемся обратно
when (lifecycle.state) {
Lifecycle.State.INITIALIZED -> {
lifecycle.create(); lifecycle.start(); if (pageVisibilityState() == "visible") lifecycle.resume()
lifecycle.create()
lifecycle.start()
if (pageVisibilityState() == "visible") lifecycle.resume()
}
Lifecycle.State.CREATED -> {
lifecycle.start(); if (pageVisibilityState() == "visible") lifecycle.resume()
lifecycle.start()
if (pageVisibilityState() == "visible") lifecycle.resume()
}
Lifecycle.State.STARTED -> {