Autofocus, Inset handling
This commit is contained in:
@@ -170,6 +170,7 @@
|
|||||||
android:name=".ui.MainActivity"
|
android:name=".ui.MainActivity"
|
||||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||||
android:theme="@style/NoActionBarAppTheme"
|
android:theme="@style/NoActionBarAppTheme"
|
||||||
|
android:windowSoftInputMode="adjustResize"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import androidx.activity.viewModels
|
|||||||
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
|
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
|
||||||
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
|
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.google.accompanist.adaptive.calculateDisplayFeatures
|
import com.google.accompanist.adaptive.calculateDisplayFeatures
|
||||||
import xyz.quaver.pupil.ui.composable.MainApp
|
import xyz.quaver.pupil.ui.composable.MainApp
|
||||||
@@ -39,6 +40,8 @@ class MainActivity : BaseActivity() {
|
|||||||
|
|
||||||
val viewModel: MainViewModel by viewModels()
|
val viewModel: MainViewModel by viewModels()
|
||||||
|
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
AppTheme {
|
AppTheme {
|
||||||
val windowSize = calculateWindowSizeClass(this)
|
val windowSize = calculateWindowSizeClass(this)
|
||||||
|
|||||||
@@ -5,7 +5,11 @@ import androidx.compose.foundation.background
|
|||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.ime
|
||||||
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
import androidx.compose.material3.DrawerValue
|
import androidx.compose.material3.DrawerValue
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.ModalNavigationDrawer
|
import androidx.compose.material3.ModalNavigationDrawer
|
||||||
@@ -105,13 +109,15 @@ private fun MainNavigationWrapper(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (navigationType == NavigationType.PERMANENT_NAVIGATION_DRAWER) {
|
if (navigationType == NavigationType.PERMANENT_NAVIGATION_DRAWER) {
|
||||||
PermanentNavigationDrawer(drawerContent = {
|
PermanentNavigationDrawer(
|
||||||
PermanentNavigationDrawerContent(
|
drawerContent = {
|
||||||
selectedDestination = uiState.currentDestination,
|
PermanentNavigationDrawerContent(
|
||||||
navigationContentPosition = navigationContentPosition,
|
selectedDestination = uiState.currentDestination,
|
||||||
navigateToDestination = navigateToDestination
|
navigationContentPosition = navigationContentPosition,
|
||||||
)
|
navigateToDestination = navigateToDestination
|
||||||
}) {
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
MainContent(
|
MainContent(
|
||||||
navigationType = navigationType,
|
navigationType = navigationType,
|
||||||
contentType = contentType,
|
contentType = contentType,
|
||||||
@@ -180,6 +186,12 @@ fun MainContent(
|
|||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
|
.run {
|
||||||
|
if (navigationType == NavigationType.BOTTOM_NAVIGATION) {
|
||||||
|
this.consumeWindowInsets(WindowInsets.ime)
|
||||||
|
.consumeWindowInsets(WindowInsets.navigationBars)
|
||||||
|
} else this
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
MainScreen(
|
MainScreen(
|
||||||
contentType = contentType,
|
contentType = contentType,
|
||||||
|
|||||||
@@ -4,17 +4,20 @@ import androidx.compose.foundation.background
|
|||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.ime
|
||||||
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.sizeIn
|
import androidx.compose.foundation.layout.sizeIn
|
||||||
|
import androidx.compose.foundation.layout.union
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.MenuOpen
|
import androidx.compose.material.icons.automirrored.filled.MenuOpen
|
||||||
import androidx.compose.material.icons.filled.Menu
|
import androidx.compose.material.icons.filled.Menu
|
||||||
import androidx.compose.material.icons.filled.MenuOpen
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -34,7 +37,6 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.layout.Layout
|
import androidx.compose.ui.layout.Layout
|
||||||
import androidx.compose.ui.layout.Measurable
|
import androidx.compose.ui.layout.Measurable
|
||||||
import androidx.compose.ui.layout.MeasurePolicy
|
import androidx.compose.ui.layout.MeasurePolicy
|
||||||
import androidx.compose.ui.layout.layout
|
|
||||||
import androidx.compose.ui.layout.layoutId
|
import androidx.compose.ui.layout.layoutId
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@@ -237,7 +239,7 @@ fun BottomNavigationBar(
|
|||||||
selectedDestination: MainDestination,
|
selectedDestination: MainDestination,
|
||||||
navigateToDestination: (MainDestination) -> Unit
|
navigateToDestination: (MainDestination) -> Unit
|
||||||
) {
|
) {
|
||||||
NavigationBar(modifier = Modifier.fillMaxWidth()) {
|
NavigationBar(modifier = Modifier.fillMaxWidth(), windowInsets = WindowInsets.ime.union(WindowInsets.navigationBars)) {
|
||||||
mainDestinations.forEach { destination ->
|
mainDestinations.forEach { destination ->
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
selected = selectedDestination.route == destination.route,
|
selected = selectedDestination.route == destination.route,
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
package xyz.quaver.pupil.ui.composable
|
package xyz.quaver.pupil.ui.composable
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.compose.animation.AnimatedContent
|
import androidx.compose.animation.AnimatedContent
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
|
import androidx.compose.animation.core.Spring
|
||||||
|
import androidx.compose.animation.core.spring
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.focusable
|
import androidx.compose.foundation.gestures.animateScrollBy
|
||||||
import androidx.compose.foundation.horizontalScroll
|
import androidx.compose.foundation.horizontalScroll
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -19,6 +20,7 @@ import androidx.compose.foundation.layout.width
|
|||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
@@ -39,24 +41,33 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.runtime.toMutableStateList
|
import androidx.compose.runtime.toMutableStateList
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.input.key.Key
|
import androidx.compose.ui.input.key.Key
|
||||||
import androidx.compose.ui.input.key.key
|
import androidx.compose.ui.input.key.key
|
||||||
import androidx.compose.ui.input.key.onKeyEvent
|
import androidx.compose.ui.input.key.onKeyEvent
|
||||||
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
|
import androidx.compose.ui.layout.positionInRoot
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextRange
|
import androidx.compose.ui.text.TextRange
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.text.toLowerCase
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.networking.SearchQuery
|
import xyz.quaver.pupil.networking.SearchQuery
|
||||||
import xyz.quaver.pupil.networking.validNamespace
|
import xyz.quaver.pupil.networking.validNamespace
|
||||||
@@ -66,7 +77,6 @@ import xyz.quaver.pupil.ui.theme.Gray300
|
|||||||
import xyz.quaver.pupil.ui.theme.Pink600
|
import xyz.quaver.pupil.ui.theme.Pink600
|
||||||
import xyz.quaver.pupil.ui.theme.Red300
|
import xyz.quaver.pupil.ui.theme.Red300
|
||||||
import xyz.quaver.pupil.ui.theme.Yellow400
|
import xyz.quaver.pupil.ui.theme.Yellow400
|
||||||
import kotlin.math.exp
|
|
||||||
|
|
||||||
private fun SearchQuery.toEditableStateInternal(): EditableSearchQueryState = when (this) {
|
private fun SearchQuery.toEditableStateInternal(): EditableSearchQueryState = when (this) {
|
||||||
is SearchQuery.Tag -> EditableSearchQueryState.Tag(namespace, tag)
|
is SearchQuery.Tag -> EditableSearchQueryState.Tag(namespace, tag)
|
||||||
@@ -102,10 +112,12 @@ fun coalesceTags(oldTag: EditableSearchQueryState.Tag?, newTag: EditableSearchQu
|
|||||||
sealed interface EditableSearchQueryState {
|
sealed interface EditableSearchQueryState {
|
||||||
class Tag(
|
class Tag(
|
||||||
namespace: String? = null,
|
namespace: String? = null,
|
||||||
tag: String = ""
|
tag: String = "",
|
||||||
|
expanded: Boolean = false
|
||||||
): EditableSearchQueryState {
|
): EditableSearchQueryState {
|
||||||
val namespace = mutableStateOf(namespace)
|
val namespace = mutableStateOf(namespace)
|
||||||
val tag = mutableStateOf(tag)
|
val tag = mutableStateOf(tag)
|
||||||
|
val expanded = mutableStateOf(expanded)
|
||||||
}
|
}
|
||||||
|
|
||||||
class And(
|
class And(
|
||||||
@@ -138,6 +150,8 @@ sealed interface EditableSearchQueryState {
|
|||||||
fun EditableTagChip(
|
fun EditableTagChip(
|
||||||
state: EditableSearchQueryState.Tag,
|
state: EditableSearchQueryState.Tag,
|
||||||
isFavorite: Boolean = false,
|
isFavorite: Boolean = false,
|
||||||
|
autoFocus: Boolean = true,
|
||||||
|
requestScrollTo: (Float) -> Unit,
|
||||||
leftIcon: @Composable RowScope.(SearchQuery.Tag) -> Unit = { tag -> TagChipIcon(tag) },
|
leftIcon: @Composable RowScope.(SearchQuery.Tag) -> Unit = { tag -> TagChipIcon(tag) },
|
||||||
rightIcon: @Composable RowScope.(SearchQuery.Tag) -> Unit = { _ -> Spacer(Modifier.width(16.dp)) },
|
rightIcon: @Composable RowScope.(SearchQuery.Tag) -> Unit = { _ -> Spacer(Modifier.width(16.dp)) },
|
||||||
content: @Composable RowScope.(SearchQuery.Tag) -> Unit = { tag ->
|
content: @Composable RowScope.(SearchQuery.Tag) -> Unit = { tag ->
|
||||||
@@ -149,10 +163,20 @@ fun EditableTagChip(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
var namespace by state.namespace
|
var namespace by state.namespace
|
||||||
var tag by state.tag
|
var tag by state.tag
|
||||||
|
var expanded by state.expanded
|
||||||
|
var wasFocused by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
var expanded by remember { mutableStateOf(false) }
|
var positionY by remember { mutableFloatStateOf(0f) }
|
||||||
|
|
||||||
|
LaunchedEffect(expanded) {
|
||||||
|
if (!expanded) {
|
||||||
|
wasFocused = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val surfaceColor by animateColorAsState(
|
val surfaceColor by animateColorAsState(
|
||||||
when {
|
when {
|
||||||
@@ -175,6 +199,9 @@ fun EditableTagChip(
|
|||||||
)
|
)
|
||||||
|
|
||||||
Surface(
|
Surface(
|
||||||
|
modifier = Modifier.onGloballyPositioned {
|
||||||
|
positionY = it.positionInRoot().y
|
||||||
|
},
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
color = surfaceColor
|
color = surfaceColor
|
||||||
) {
|
) {
|
||||||
@@ -217,20 +244,34 @@ fun EditableTagChip(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
var selection by remember { mutableStateOf(TextRange.Zero) }
|
var selection by remember { mutableStateOf(TextRange(tag.length)) }
|
||||||
var composition by remember { mutableStateOf<TextRange?>(null) }
|
var composition by remember { mutableStateOf<TextRange?>(null) }
|
||||||
|
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
val textFieldValue = remember(tag, selection, composition) {
|
val textFieldValue = remember(tag, selection, composition) {
|
||||||
TextFieldValue(tag, selection, composition)
|
TextFieldValue(tag, selection, composition)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(expanded) {
|
||||||
|
if (autoFocus && expanded) {
|
||||||
|
focusRequester.requestFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = textFieldValue,
|
value = textFieldValue,
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
autoCorrect = false,
|
||||||
|
capitalization = KeyboardCapitalization.None,
|
||||||
|
imeAction = ImeAction.Done
|
||||||
|
),
|
||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
TagChipIcon(SearchQuery.Tag(namespace, tag))
|
TagChipIcon(SearchQuery.Tag(namespace, tag))
|
||||||
},
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
.onKeyEvent { event ->
|
.onKeyEvent { event ->
|
||||||
if (event.key == Key.Backspace && tag.isEmpty()) {
|
if (event.key == Key.Backspace && tag.isEmpty()) {
|
||||||
val newTag = namespace?.dropLast(1) ?: ""
|
val newTag = namespace?.dropLast(1) ?: ""
|
||||||
@@ -240,6 +281,18 @@ fun EditableTagChip(
|
|||||||
composition = null
|
composition = null
|
||||||
true
|
true
|
||||||
} else false
|
} else false
|
||||||
|
}
|
||||||
|
.focusRequester(focusRequester)
|
||||||
|
.onFocusChanged { event ->
|
||||||
|
if (event.isFocused) {
|
||||||
|
wasFocused = true
|
||||||
|
coroutineScope.launch {
|
||||||
|
delay(300)
|
||||||
|
requestScrollTo(positionY)
|
||||||
|
}
|
||||||
|
} else if (wasFocused) {
|
||||||
|
expanded = false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
keyboardActions = KeyboardActions(
|
keyboardActions = KeyboardActions(
|
||||||
onDone = {
|
onDone = {
|
||||||
@@ -247,9 +300,10 @@ fun EditableTagChip(
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
onValueChange = { newTextValue ->
|
onValueChange = { newTextValue ->
|
||||||
val newTag = newTextValue.text.lowercase()
|
val newTag = newTextValue.text
|
||||||
tag = if (namespace == null && newTag.trim() in validNamespace) {
|
val possibleNamespace = newTag.dropLast(1).lowercase().trim()
|
||||||
namespace = newTag.trim()
|
tag = if (namespace == null && newTag.endsWith(':') && possibleNamespace in validNamespace) {
|
||||||
|
namespace = possibleNamespace
|
||||||
""
|
""
|
||||||
} else newTag
|
} else newTag
|
||||||
selection = newTextValue.selection
|
selection = newTextValue.selection
|
||||||
@@ -310,10 +364,10 @@ fun NewQueryChip(
|
|||||||
opened = false
|
opened = false
|
||||||
}
|
}
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
if (currentQuery !is EditableSearchQueryState.Tag) {
|
if (currentQuery !is EditableSearchQueryState.Tag && currentQuery !is EditableSearchQueryState.And) {
|
||||||
NewQueryRow(modifier = Modifier.fillMaxWidth(), text = stringResource(R.string.search_add_query_item_tag)) {
|
NewQueryRow(modifier = Modifier.fillMaxWidth(), text = stringResource(R.string.search_add_query_item_tag)) {
|
||||||
opened = false
|
opened = false
|
||||||
onNewQuery(EditableSearchQueryState.Tag())
|
onNewQuery(EditableSearchQueryState.Tag(expanded = true))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (currentQuery !is EditableSearchQueryState.And) {
|
if (currentQuery !is EditableSearchQueryState.And) {
|
||||||
@@ -351,11 +405,14 @@ fun NewQueryChip(
|
|||||||
fun QueryEditorQueryView(
|
fun QueryEditorQueryView(
|
||||||
state: EditableSearchQueryState,
|
state: EditableSearchQueryState,
|
||||||
onQueryRemove: (EditableSearchQueryState) -> Unit,
|
onQueryRemove: (EditableSearchQueryState) -> Unit,
|
||||||
|
requestScrollTo: (Float) -> Unit,
|
||||||
|
requestScrollBy: (Float) -> Unit,
|
||||||
) {
|
) {
|
||||||
when (state) {
|
when (state) {
|
||||||
is EditableSearchQueryState.Tag -> {
|
is EditableSearchQueryState.Tag -> {
|
||||||
EditableTagChip(
|
EditableTagChip(
|
||||||
state,
|
state,
|
||||||
|
requestScrollTo = requestScrollTo,
|
||||||
rightIcon = {
|
rightIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -404,7 +461,9 @@ fun QueryEditorQueryView(
|
|||||||
if (index != 0) { Text("+", modifier = Modifier.padding(horizontal = 8.dp)) }
|
if (index != 0) { Text("+", modifier = Modifier.padding(horizontal = 8.dp)) }
|
||||||
QueryEditorQueryView(
|
QueryEditorQueryView(
|
||||||
subQueryState,
|
subQueryState,
|
||||||
onQueryRemove = { state.queries.remove(it) }
|
onQueryRemove = { state.queries.remove(it) },
|
||||||
|
requestScrollTo = requestScrollTo,
|
||||||
|
requestScrollBy = requestScrollBy
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
NewQueryChip(state) { newQueryState ->
|
NewQueryChip(state) { newQueryState ->
|
||||||
@@ -429,6 +488,24 @@ fun QueryEditorQueryView(
|
|||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
) {
|
) {
|
||||||
|
val newSearchQuery = remember { EditableSearchQueryState.Tag() }
|
||||||
|
|
||||||
|
var newQueryNamespace by newSearchQuery.namespace
|
||||||
|
var newQueryTag by newSearchQuery.tag
|
||||||
|
var newQueryExpanded by newSearchQuery.expanded
|
||||||
|
|
||||||
|
val offset = with(LocalDensity.current) { 40.dp.toPx() }
|
||||||
|
|
||||||
|
LaunchedEffect(newQueryExpanded) {
|
||||||
|
if (!newQueryExpanded && (newQueryNamespace != null || newQueryTag.isNotBlank())) {
|
||||||
|
state.queries.add(EditableSearchQueryState.Tag(newQueryNamespace, newQueryTag))
|
||||||
|
newQueryNamespace = null
|
||||||
|
newQueryTag = ""
|
||||||
|
newQueryExpanded = true
|
||||||
|
requestScrollBy(offset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
@@ -446,9 +523,27 @@ fun QueryEditorQueryView(
|
|||||||
state.queries.forEach { subQuery ->
|
state.queries.forEach { subQuery ->
|
||||||
QueryEditorQueryView(
|
QueryEditorQueryView(
|
||||||
subQuery,
|
subQuery,
|
||||||
onQueryRemove = { state.queries.remove(it) }
|
onQueryRemove = { state.queries.remove(it) },
|
||||||
|
requestScrollTo = requestScrollTo,
|
||||||
|
requestScrollBy = requestScrollBy
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
EditableTagChip(
|
||||||
|
newSearchQuery,
|
||||||
|
requestScrollTo = requestScrollTo,
|
||||||
|
rightIcon = {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(8.dp)
|
||||||
|
.size(16.dp)
|
||||||
|
.clickable {
|
||||||
|
onQueryRemove(state)
|
||||||
|
},
|
||||||
|
imageVector = Icons.Default.RemoveCircleOutline,
|
||||||
|
contentDescription = stringResource(R.string.search_remove_query_item_description)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
NewQueryChip(state) { newQueryState ->
|
NewQueryChip(state) { newQueryState ->
|
||||||
state.queries.add(newQueryState)
|
state.queries.add(newQueryState)
|
||||||
}
|
}
|
||||||
@@ -491,7 +586,9 @@ fun QueryEditorQueryView(
|
|||||||
if (subQueryStateSnapshot != null) {
|
if (subQueryStateSnapshot != null) {
|
||||||
QueryEditorQueryView(
|
QueryEditorQueryView(
|
||||||
subQueryStateSnapshot,
|
subQueryStateSnapshot,
|
||||||
onQueryRemove = { subQueryState = null }
|
onQueryRemove = { subQueryState = null },
|
||||||
|
requestScrollTo = requestScrollTo,
|
||||||
|
requestScrollBy = requestScrollBy,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -518,9 +615,19 @@ fun QueryEditor(
|
|||||||
) {
|
) {
|
||||||
var rootQuery by state.query
|
var rootQuery by state.query
|
||||||
|
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
var topY by remember { mutableFloatStateOf(0f) }
|
||||||
|
|
||||||
|
val scrollOffset = with (LocalDensity.current) { 16.dp.toPx() }
|
||||||
|
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.verticalScroll(rememberScrollState())
|
.onGloballyPositioned {
|
||||||
|
topY = it.positionInRoot().y
|
||||||
|
}
|
||||||
|
.verticalScroll(scrollState)
|
||||||
.padding(8.dp),
|
.padding(8.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
) {
|
) {
|
||||||
@@ -528,7 +635,19 @@ fun QueryEditor(
|
|||||||
if (rootQuerySnapshot != null) {
|
if (rootQuerySnapshot != null) {
|
||||||
QueryEditorQueryView(
|
QueryEditorQueryView(
|
||||||
state = rootQuerySnapshot,
|
state = rootQuerySnapshot,
|
||||||
onQueryRemove = { rootQuery = null }
|
onQueryRemove = { rootQuery = null },
|
||||||
|
requestScrollTo = { target ->
|
||||||
|
val topYSnapshot = topY
|
||||||
|
|
||||||
|
coroutineScope.launch {
|
||||||
|
scrollState.animateScrollBy(target - topYSnapshot - scrollOffset, spring(stiffness = Spring.StiffnessLow))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
requestScrollBy = { value ->
|
||||||
|
coroutineScope.launch {
|
||||||
|
scrollState.animateScrollBy(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user