chat android tv adaptation
This commit is contained in:
+2
-43
@@ -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)
|
||||
|
||||
+190
-54
@@ -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 -> {
|
||||
|
||||
Reference in New Issue
Block a user