Small UX fix.
Trim on search. keyboard action done focus. maxline on search. collapsing header on search. remove keyboard hide on search scroll.
This commit is contained in:
		
							parent
							
								
									99f7546621
								
							
						
					
					
						commit
						82738a8f03
					
				
					 9 changed files with 303 additions and 113 deletions
				
			
		| 
						 | 
				
			
			@ -0,0 +1,57 @@
 | 
			
		|||
package com.pixelized.rplexicon.ui.composable
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.DrawableRes
 | 
			
		||||
import androidx.compose.foundation.layout.BoxScope
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.Stable
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.draw.alpha
 | 
			
		||||
import androidx.compose.ui.graphics.painter.Painter
 | 
			
		||||
import com.bumptech.glide.request.RequestListener
 | 
			
		||||
import com.pixelized.rplexicon.utilitary.rememberLoadingTransition
 | 
			
		||||
import com.skydoves.landscapist.ImageOptions
 | 
			
		||||
import com.skydoves.landscapist.InternalLandscapistApi
 | 
			
		||||
import com.skydoves.landscapist.components.ImageComponent
 | 
			
		||||
import com.skydoves.landscapist.components.rememberImageComponent
 | 
			
		||||
import com.skydoves.landscapist.glide.GlideImage
 | 
			
		||||
import com.skydoves.landscapist.glide.GlideImageState
 | 
			
		||||
import com.skydoves.landscapist.glide.GlideRequestType
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun AsyncImage(
 | 
			
		||||
    imageModel: () -> Any?,
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    glideRequestType: GlideRequestType = GlideRequestType.DRAWABLE,
 | 
			
		||||
    requestListener: (() -> RequestListener<Any>)? = null,
 | 
			
		||||
    component: ImageComponent = rememberImageComponent {},
 | 
			
		||||
    imageOptions: ImageOptions = ImageOptions(),
 | 
			
		||||
    onImageStateChanged: (GlideImageState) -> Unit = {},
 | 
			
		||||
    @DrawableRes previewPlaceholder: Int = 0,
 | 
			
		||||
    loading: @Composable (BoxScope.(imageState: GlideImageState.Loading) -> Unit)? = null,
 | 
			
		||||
    success: @Composable (BoxScope.(imageState: GlideImageState.Success, painter: Painter) -> Unit)? = null,
 | 
			
		||||
    failure: @Composable (BoxScope.(imageState: GlideImageState.Failure) -> Unit)? = null,
 | 
			
		||||
) {
 | 
			
		||||
    val transition = rememberLoadingTransition(imageModel)
 | 
			
		||||
 | 
			
		||||
    GlideImage(
 | 
			
		||||
        imageModel = imageModel,
 | 
			
		||||
        modifier = modifier.alpha(alpha = transition.alpha),
 | 
			
		||||
        glideRequestType = glideRequestType,
 | 
			
		||||
        requestListener = requestListener,
 | 
			
		||||
        component = component,
 | 
			
		||||
        imageOptions = imageOptions,
 | 
			
		||||
        onImageStateChanged = {
 | 
			
		||||
            when (it) {
 | 
			
		||||
                is GlideImageState.Success -> transition.target = 1f
 | 
			
		||||
                is GlideImageState.Failure -> transition.target = 1f
 | 
			
		||||
                is GlideImageState.Loading -> transition.target = 0f
 | 
			
		||||
                is GlideImageState.None -> transition.target = 0f
 | 
			
		||||
            }
 | 
			
		||||
            onImageStateChanged(it)
 | 
			
		||||
        },
 | 
			
		||||
        previewPlaceholder = previewPlaceholder,
 | 
			
		||||
        loading = loading,
 | 
			
		||||
        success = success,
 | 
			
		||||
        failure = failure,
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,92 @@
 | 
			
		|||
package com.pixelized.rplexicon.ui.composable
 | 
			
		||||
 | 
			
		||||
import androidx.compose.animation.core.animateIntAsState
 | 
			
		||||
import androidx.compose.foundation.layout.Box
 | 
			
		||||
import androidx.compose.foundation.lazy.LazyListState
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.mutableStateOf
 | 
			
		||||
import androidx.compose.runtime.remember
 | 
			
		||||
import androidx.compose.runtime.saveable.rememberSaveable
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.geometry.Offset
 | 
			
		||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
 | 
			
		||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
 | 
			
		||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
 | 
			
		||||
import androidx.compose.ui.layout.Layout
 | 
			
		||||
import androidx.compose.ui.layout.Measurable
 | 
			
		||||
import androidx.compose.ui.layout.layoutId
 | 
			
		||||
import androidx.compose.ui.unit.Velocity
 | 
			
		||||
 | 
			
		||||
private val List<Measurable>.grid: Measurable get() = first { it.layoutId == GRID_ID }
 | 
			
		||||
private val List<Measurable>.header: Measurable get() = first { it.layoutId == HEADER_ID }
 | 
			
		||||
 | 
			
		||||
private const val GRID_ID = "GRID_ID"
 | 
			
		||||
private const val HEADER_ID = "HEADER_ID"
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun CollapsingHeader(
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    header: @Composable () -> Unit,
 | 
			
		||||
    content: @Composable () -> Unit
 | 
			
		||||
) {
 | 
			
		||||
    val headerHeight = rememberSaveable { mutableStateOf(0) }
 | 
			
		||||
    val headerScroll = rememberSaveable { mutableStateOf(0) }
 | 
			
		||||
 | 
			
		||||
    val animatedHeaderScroll = animateIntAsState(
 | 
			
		||||
        targetValue = headerScroll.value,
 | 
			
		||||
        label = "HeaderSnapAnimation",
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    val nestedScrollConnection = remember {
 | 
			
		||||
        object : NestedScrollConnection {
 | 
			
		||||
            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
 | 
			
		||||
                val scroll = (headerScroll.value + available.y.toInt()).coerceIn(
 | 
			
		||||
                    minimumValue = -headerHeight.value,
 | 
			
		||||
                    maximumValue = 0,
 | 
			
		||||
                )
 | 
			
		||||
                return if (headerScroll.value != scroll) {
 | 
			
		||||
                    headerScroll.value = scroll
 | 
			
		||||
                    available
 | 
			
		||||
                } else {
 | 
			
		||||
                    Offset.Zero
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
 | 
			
		||||
                headerScroll.value = if (headerScroll.value < -headerHeight.value / 2) {
 | 
			
		||||
                    -headerHeight.value
 | 
			
		||||
                } else {
 | 
			
		||||
                    0
 | 
			
		||||
                }
 | 
			
		||||
                return super.onPostFling(consumed, available)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Layout(
 | 
			
		||||
        modifier = modifier.nestedScroll(nestedScrollConnection),
 | 
			
		||||
        content = {
 | 
			
		||||
            Box(
 | 
			
		||||
                modifier = Modifier.layoutId(HEADER_ID),
 | 
			
		||||
                content = { header() },
 | 
			
		||||
            )
 | 
			
		||||
            Box(
 | 
			
		||||
                modifier = Modifier.layoutId(GRID_ID),
 | 
			
		||||
                content = { content() },
 | 
			
		||||
            )
 | 
			
		||||
        },
 | 
			
		||||
        measurePolicy = { measurables, constraints ->
 | 
			
		||||
            val headerPlaceable = measurables.header.measure(constraints)
 | 
			
		||||
            val gridPlaceable = measurables.grid.measure(constraints)
 | 
			
		||||
 | 
			
		||||
            if (headerHeight.value == 0) {
 | 
			
		||||
                headerHeight.value = headerPlaceable.height
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            layout(constraints.maxWidth, constraints.maxHeight) {
 | 
			
		||||
                headerPlaceable.place(0, animatedHeaderScroll.value)
 | 
			
		||||
                gridPlaceable.place(0, headerPlaceable.height + animatedHeaderScroll.value)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,18 +0,0 @@
 | 
			
		|||
package com.pixelized.rplexicon.ui.composable
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.lazy.LazyListState
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.ui.ExperimentalComposeUiApi
 | 
			
		||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
 | 
			
		||||
 | 
			
		||||
@OptIn(ExperimentalComposeUiApi::class)
 | 
			
		||||
@Composable
 | 
			
		||||
fun  ScrollingKeyboardHandler(
 | 
			
		||||
    lazyListState: LazyListState
 | 
			
		||||
) {
 | 
			
		||||
    val keyboard = LocalSoftwareKeyboardController.current
 | 
			
		||||
 | 
			
		||||
    if (lazyListState.isScrollInProgress) {
 | 
			
		||||
        keyboard?.hide()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -9,6 +9,8 @@ import androidx.compose.animation.fadeOut
 | 
			
		|||
import androidx.compose.foundation.layout.fillMaxWidth
 | 
			
		||||
import androidx.compose.foundation.layout.padding
 | 
			
		||||
import androidx.compose.foundation.layout.size
 | 
			
		||||
import androidx.compose.foundation.text.KeyboardActions
 | 
			
		||||
import androidx.compose.foundation.text.KeyboardOptions
 | 
			
		||||
import androidx.compose.material3.Icon
 | 
			
		||||
import androidx.compose.material3.IconButton
 | 
			
		||||
import androidx.compose.material3.MaterialTheme
 | 
			
		||||
| 
						 | 
				
			
			@ -22,8 +24,11 @@ import androidx.compose.runtime.State
 | 
			
		|||
import androidx.compose.runtime.mutableStateOf
 | 
			
		||||
import androidx.compose.runtime.remember
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.platform.LocalFocusManager
 | 
			
		||||
import androidx.compose.ui.res.painterResource
 | 
			
		||||
import androidx.compose.ui.res.stringResource
 | 
			
		||||
import androidx.compose.ui.text.input.ImeAction
 | 
			
		||||
import androidx.compose.ui.text.input.KeyboardType
 | 
			
		||||
import androidx.compose.ui.tooling.preview.Preview
 | 
			
		||||
import androidx.compose.ui.tooling.preview.PreviewParameter
 | 
			
		||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
 | 
			
		||||
| 
						 | 
				
			
			@ -52,6 +57,8 @@ fun TextField(
 | 
			
		|||
    modifier: Modifier = Modifier,
 | 
			
		||||
    field: TextFieldUio,
 | 
			
		||||
) {
 | 
			
		||||
    val focus = LocalFocusManager.current
 | 
			
		||||
 | 
			
		||||
    OutlinedTextField(
 | 
			
		||||
        modifier = modifier,
 | 
			
		||||
        shape = MaterialTheme.lexicon.shapes.textField,
 | 
			
		||||
| 
						 | 
				
			
			@ -77,6 +84,14 @@ fun TextField(
 | 
			
		|||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        keyboardOptions = KeyboardOptions(
 | 
			
		||||
            keyboardType = KeyboardType.Text,
 | 
			
		||||
            imeAction = ImeAction.Done,
 | 
			
		||||
        ),
 | 
			
		||||
        keyboardActions = KeyboardActions(
 | 
			
		||||
            onDone = { focus.clearFocus(true) },
 | 
			
		||||
        ),
 | 
			
		||||
        maxLines = 1,
 | 
			
		||||
        value = field.value.value,
 | 
			
		||||
        onValueChange = field.onValueChange,
 | 
			
		||||
    )
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -38,7 +38,6 @@ import androidx.compose.runtime.remember
 | 
			
		|||
import androidx.compose.ui.Alignment
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.composed
 | 
			
		||||
import androidx.compose.ui.draw.alpha
 | 
			
		||||
import androidx.compose.ui.geometry.Offset
 | 
			
		||||
import androidx.compose.ui.graphics.Brush
 | 
			
		||||
import androidx.compose.ui.graphics.Color
 | 
			
		||||
| 
						 | 
				
			
			@ -48,6 +47,7 @@ import androidx.compose.ui.graphics.Shadow
 | 
			
		|||
import androidx.compose.ui.layout.ContentScale
 | 
			
		||||
import androidx.compose.ui.platform.LocalConfiguration
 | 
			
		||||
import androidx.compose.ui.platform.LocalDensity
 | 
			
		||||
import androidx.compose.ui.platform.LocalView
 | 
			
		||||
import androidx.compose.ui.res.painterResource
 | 
			
		||||
import androidx.compose.ui.res.stringResource
 | 
			
		||||
import androidx.compose.ui.text.AnnotatedString
 | 
			
		||||
| 
						 | 
				
			
			@ -59,16 +59,14 @@ import androidx.compose.ui.unit.dp
 | 
			
		|||
import androidx.hilt.navigation.compose.hiltViewModel
 | 
			
		||||
import com.pixelized.rplexicon.R
 | 
			
		||||
import com.pixelized.rplexicon.model.Lexicon
 | 
			
		||||
import com.pixelized.rplexicon.ui.composable.AsyncImage
 | 
			
		||||
import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost
 | 
			
		||||
import com.pixelized.rplexicon.ui.theme.LexiconTheme
 | 
			
		||||
import com.pixelized.rplexicon.utilitary.composable.stringResource
 | 
			
		||||
import com.pixelized.rplexicon.utilitary.extentions.annotatedSpan
 | 
			
		||||
import com.pixelized.rplexicon.utilitary.extentions.annotatedString
 | 
			
		||||
import com.pixelized.rplexicon.utilitary.extentions.highlightRegex
 | 
			
		||||
import com.pixelized.rplexicon.utilitary.rememberLoadingTransition
 | 
			
		||||
import com.skydoves.landscapist.ImageOptions
 | 
			
		||||
import com.skydoves.landscapist.glide.GlideImage
 | 
			
		||||
import com.skydoves.landscapist.glide.GlideImageState
 | 
			
		||||
 | 
			
		||||
@Stable
 | 
			
		||||
data class CharacterDetailUio(
 | 
			
		||||
| 
						 | 
				
			
			@ -200,16 +198,8 @@ private fun CharacterDetailScreenContent(
 | 
			
		|||
                        .aspectRatio(ratio = 1f)
 | 
			
		||||
                        .scrollOffset(scrollState = state) { -it / 2 },
 | 
			
		||||
                ) {
 | 
			
		||||
                    val transition = rememberLoadingTransition { uri }
 | 
			
		||||
                    GlideImage(
 | 
			
		||||
                        modifier = Modifier
 | 
			
		||||
                            .matchParentSize()
 | 
			
		||||
                            .alpha(alpha = transition.alpha),
 | 
			
		||||
                        onImageStateChanged = {
 | 
			
		||||
                            if (it is GlideImageState.Success) {
 | 
			
		||||
                                transition.target = 1f
 | 
			
		||||
                            }
 | 
			
		||||
                        },
 | 
			
		||||
                    AsyncImage(
 | 
			
		||||
                        modifier = Modifier.matchParentSize(),
 | 
			
		||||
                        imageOptions = ImageOptions(
 | 
			
		||||
                            alignment = Alignment.TopCenter,
 | 
			
		||||
                            contentScale = ContentScale.Crop,
 | 
			
		||||
| 
						 | 
				
			
			@ -299,8 +289,7 @@ private fun CharacterDetailScreenContent(
 | 
			
		|||
                    )
 | 
			
		||||
                }
 | 
			
		||||
                if (annotatedItem.portrait.isNotEmpty()) {
 | 
			
		||||
                    val configuration = LocalConfiguration.current
 | 
			
		||||
                    val maxSize = remember { (configuration.screenWidthDp.dp - 16.dp * 2) }
 | 
			
		||||
                    val maxSize = rememberPortraitWidth()
 | 
			
		||||
                    Text(
 | 
			
		||||
                        modifier = Modifier.padding(start = 16.dp, top = 24.dp, end = 16.dp),
 | 
			
		||||
                        style = typography.titleMedium,
 | 
			
		||||
| 
						 | 
				
			
			@ -308,19 +297,16 @@ private fun CharacterDetailScreenContent(
 | 
			
		|||
                    )
 | 
			
		||||
                    LazyRow(
 | 
			
		||||
                        contentPadding = PaddingValues(horizontal = 16.dp),
 | 
			
		||||
                        horizontalArrangement = Arrangement.spacedBy(8.dp)
 | 
			
		||||
                        horizontalArrangement = Arrangement.spacedBy(8.dp),
 | 
			
		||||
                    ) {
 | 
			
		||||
                        items(items = annotatedItem.portrait) {
 | 
			
		||||
                            val transition = rememberLoadingTransition { it }
 | 
			
		||||
                            GlideImage(
 | 
			
		||||
                                modifier = Modifier
 | 
			
		||||
                                    .sizeIn(maxWidth = maxSize, maxHeight = maxSize)
 | 
			
		||||
                                    .alpha(alpha = transition.alpha),
 | 
			
		||||
                                onImageStateChanged = {
 | 
			
		||||
                                    if (it is GlideImageState.Success) {
 | 
			
		||||
                                        transition.target = 1f
 | 
			
		||||
                                    }
 | 
			
		||||
                                },
 | 
			
		||||
                            AsyncImage(
 | 
			
		||||
                                modifier = Modifier.sizeIn(
 | 
			
		||||
                                    minWidth = maxSize / 2,
 | 
			
		||||
                                    maxWidth = maxSize,
 | 
			
		||||
                                    minHeight = maxSize,
 | 
			
		||||
                                    maxHeight = maxSize,
 | 
			
		||||
                                ),
 | 
			
		||||
                                imageOptions = ImageOptions(
 | 
			
		||||
                                    contentScale = ContentScale.FillHeight
 | 
			
		||||
                                ),
 | 
			
		||||
| 
						 | 
				
			
			@ -348,6 +334,19 @@ private fun rememberBackgroundGradient(): Brush {
 | 
			
		|||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
private fun rememberPortraitWidth(): Dp {
 | 
			
		||||
    val configuration = LocalConfiguration.current
 | 
			
		||||
    val view = LocalView.current
 | 
			
		||||
    return remember(configuration, view) {
 | 
			
		||||
        if (view.isInEditMode) {
 | 
			
		||||
            300.dp
 | 
			
		||||
        } else {
 | 
			
		||||
            (configuration.screenWidthDp.dp - 16.dp * 2)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Stable
 | 
			
		||||
private fun Modifier.scrollOffset(
 | 
			
		||||
    scrollState: ScrollState,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -99,12 +99,13 @@ class AnnotatedSearchItemUio(
 | 
			
		|||
private fun SearchItemUio.annotate(): AnnotatedSearchItemUio {
 | 
			
		||||
    val colorScheme = MaterialTheme.colorScheme
 | 
			
		||||
    val highlight = remember { SpanStyle(color = colorScheme.primary) }
 | 
			
		||||
    val highlightRegex = remember(search) { search.highlightRegex }
 | 
			
		||||
    val finderRegex = remember(search) { search.finderRegex }
 | 
			
		||||
    val trimmedSearch = remember(search) { search.trim() }
 | 
			
		||||
    val highlightRegex = remember(search) { trimmedSearch.highlightRegex }
 | 
			
		||||
    val finderRegex = remember(search) { trimmedSearch.finderRegex }
 | 
			
		||||
    val gender = stringResource(id = gender, short = true)
 | 
			
		||||
    val race = stringResource(id = race)
 | 
			
		||||
 | 
			
		||||
    return remember(search, race, highlightRace, gender, highlightGender) {
 | 
			
		||||
    return remember(trimmedSearch, race, highlightRace, gender, highlightGender) {
 | 
			
		||||
        AnnotatedSearchItemUio(
 | 
			
		||||
            id = id,
 | 
			
		||||
            name = AnnotatedString(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -41,7 +41,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
 | 
			
		|||
import com.pixelized.rplexicon.NO_WINDOW_INSETS
 | 
			
		||||
import com.pixelized.rplexicon.R
 | 
			
		||||
import com.pixelized.rplexicon.model.Lexicon
 | 
			
		||||
import com.pixelized.rplexicon.ui.composable.ScrollingKeyboardHandler
 | 
			
		||||
import com.pixelized.rplexicon.ui.composable.CollapsingHeader
 | 
			
		||||
import com.pixelized.rplexicon.ui.composable.form.DropDownField
 | 
			
		||||
import com.pixelized.rplexicon.ui.composable.form.DropDownFieldUio
 | 
			
		||||
import com.pixelized.rplexicon.ui.composable.form.TextField
 | 
			
		||||
| 
						 | 
				
			
			@ -88,9 +88,6 @@ fun SearchScreen(
 | 
			
		|||
                screen.popBackStack()
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
        ScrollingKeyboardHandler(
 | 
			
		||||
            lazyListState = lazyState,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -125,50 +122,64 @@ private fun SearchScreenContent(
 | 
			
		|||
            )
 | 
			
		||||
        },
 | 
			
		||||
    ) { paddingValues ->
 | 
			
		||||
        LazyColumn(
 | 
			
		||||
            modifier = Modifier
 | 
			
		||||
                .fillMaxSize()
 | 
			
		||||
                .padding(paddingValues = paddingValues),
 | 
			
		||||
            state = lazyColumnState,
 | 
			
		||||
            contentPadding = PaddingValues(vertical = 8.dp),
 | 
			
		||||
        ) {
 | 
			
		||||
            item {
 | 
			
		||||
                Column(
 | 
			
		||||
        CollapsingHeader(
 | 
			
		||||
            modifier = Modifier.padding(paddingValues = paddingValues),
 | 
			
		||||
            header = {
 | 
			
		||||
                SearchBox(
 | 
			
		||||
                    modifier = Modifier.padding(horizontal = 16.dp),
 | 
			
		||||
                    verticalArrangement = Arrangement.spacedBy(2.dp),
 | 
			
		||||
                    form = form,
 | 
			
		||||
                )
 | 
			
		||||
            },
 | 
			
		||||
            content = {
 | 
			
		||||
                LazyColumn(
 | 
			
		||||
                    modifier = Modifier.fillMaxSize(),
 | 
			
		||||
                    state = lazyColumnState,
 | 
			
		||||
                    contentPadding = PaddingValues(vertical = 8.dp),
 | 
			
		||||
                ) {
 | 
			
		||||
                    TextField(
 | 
			
		||||
                        modifier = Modifier.fillMaxWidth(),
 | 
			
		||||
                        field = form.search,
 | 
			
		||||
                    )
 | 
			
		||||
                    Row(
 | 
			
		||||
                        horizontalArrangement = Arrangement.spacedBy(8.dp),
 | 
			
		||||
                    items(
 | 
			
		||||
                        items = items.value,
 | 
			
		||||
                        key = { it.id },
 | 
			
		||||
                        contentType = { "Search" },
 | 
			
		||||
                    ) {
 | 
			
		||||
                        DropDownField(
 | 
			
		||||
                            modifier = Modifier.weight(1f),
 | 
			
		||||
                            field = form.gender,
 | 
			
		||||
                        )
 | 
			
		||||
                        DropDownField(
 | 
			
		||||
                            modifier = Modifier.weight(1f),
 | 
			
		||||
                            field = form.race,
 | 
			
		||||
                        SearchItem(
 | 
			
		||||
                            modifier = Modifier
 | 
			
		||||
                                .clickable { onItem(it) }
 | 
			
		||||
                                .heightIn(min = MaterialTheme.lexicon.dimens.item),
 | 
			
		||||
                            item = it,
 | 
			
		||||
                        )
 | 
			
		||||
                    }
 | 
			
		||||
                    Divider(modifier = Modifier.padding(top = 16.dp, bottom = 8.dp))
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            items(
 | 
			
		||||
                items = items.value,
 | 
			
		||||
                key = { it.id },
 | 
			
		||||
                contentType = { "Search" },
 | 
			
		||||
            ) {
 | 
			
		||||
                SearchItem(
 | 
			
		||||
                    modifier = Modifier
 | 
			
		||||
                        .clickable { onItem(it) }
 | 
			
		||||
                        .heightIn(min = MaterialTheme.lexicon.dimens.item),
 | 
			
		||||
                    item = it,
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
private fun SearchBox(
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    form: SearchFormUio,
 | 
			
		||||
) {
 | 
			
		||||
    Column(
 | 
			
		||||
        modifier = modifier,
 | 
			
		||||
        verticalArrangement = Arrangement.spacedBy(2.dp),
 | 
			
		||||
    ) {
 | 
			
		||||
        TextField(
 | 
			
		||||
            modifier = Modifier.fillMaxWidth(),
 | 
			
		||||
            field = form.search,
 | 
			
		||||
        )
 | 
			
		||||
        Row(
 | 
			
		||||
            horizontalArrangement = Arrangement.spacedBy(8.dp),
 | 
			
		||||
        ) {
 | 
			
		||||
            DropDownField(
 | 
			
		||||
                modifier = Modifier.weight(1f),
 | 
			
		||||
                field = form.gender,
 | 
			
		||||
            )
 | 
			
		||||
            DropDownField(
 | 
			
		||||
                modifier = Modifier.weight(1f),
 | 
			
		||||
                field = form.race,
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
        Divider(modifier = Modifier.padding(top = 16.dp, bottom = 8.dp))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -196,18 +207,41 @@ private fun SearchScreenContentPreview() {
 | 
			
		|||
                items = remember {
 | 
			
		||||
                    mutableStateOf(
 | 
			
		||||
                        listOf(
 | 
			
		||||
                            SearchItemUio(
 | 
			
		||||
                            SearchItemUio.preview(
 | 
			
		||||
                                id = 0,
 | 
			
		||||
                                name = "Brulkhai",
 | 
			
		||||
                                diminutive = "Bru",
 | 
			
		||||
                                gender = Lexicon.Gender.FEMALE,
 | 
			
		||||
                                race = Lexicon.Race.HALF_ORC,
 | 
			
		||||
                                description = null,
 | 
			
		||||
                                history = null,
 | 
			
		||||
                                search = "",
 | 
			
		||||
                                highlightGender = false,
 | 
			
		||||
                                highlightRace = false,
 | 
			
		||||
                            )
 | 
			
		||||
                            ),
 | 
			
		||||
                            SearchItemUio.preview(
 | 
			
		||||
                                id = 0,
 | 
			
		||||
                                name = "Léandre",
 | 
			
		||||
                                diminutive = null,
 | 
			
		||||
                                gender = Lexicon.Gender.MALE,
 | 
			
		||||
                                race = Lexicon.Race.HUMAN,
 | 
			
		||||
                            ),
 | 
			
		||||
                            SearchItemUio.preview(
 | 
			
		||||
                                id = 0,
 | 
			
		||||
                                name = "Nélia",
 | 
			
		||||
                                diminutive = null,
 | 
			
		||||
                                gender = Lexicon.Gender.FEMALE,
 | 
			
		||||
                                race = Lexicon.Race.ELF,
 | 
			
		||||
                            ),
 | 
			
		||||
                            SearchItemUio.preview(
 | 
			
		||||
                                id = 0,
 | 
			
		||||
                                name = "Tigrane",
 | 
			
		||||
                                diminutive = null,
 | 
			
		||||
                                gender = Lexicon.Gender.MALE,
 | 
			
		||||
                                race = Lexicon.Race.TIEFLING,
 | 
			
		||||
                            ),
 | 
			
		||||
                            SearchItemUio.preview(
 | 
			
		||||
                                id = 0,
 | 
			
		||||
                                name = "Unathana",
 | 
			
		||||
                                diminutive = "Una",
 | 
			
		||||
                                gender = Lexicon.Gender.FEMALE,
 | 
			
		||||
                                race = Lexicon.Race.HALF_ELF,
 | 
			
		||||
                            ),
 | 
			
		||||
                        )
 | 
			
		||||
                    )
 | 
			
		||||
                },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -52,11 +52,11 @@ class SearchViewModel @Inject constructor(
 | 
			
		|||
        data.filter { item ->
 | 
			
		||||
            val gender = _gender.value?.let { it == item.gender }
 | 
			
		||||
            val race = _race.value?.let { it == item.race }
 | 
			
		||||
            val search = _search.value.takeIf { it.isNotEmpty() }?.let {
 | 
			
		||||
                val name = item.name.contains(_search.value, true)
 | 
			
		||||
                val diminutive = item.diminutive?.contains(_search.value, true) == true
 | 
			
		||||
                val description = item.description?.contains(_search.value, true) == true
 | 
			
		||||
                val history = item.history?.contains(_search.value, true) == true
 | 
			
		||||
            val search = _search.value.takeIf { it.isNotEmpty() }?.trim()?.let { search ->
 | 
			
		||||
                val name = item.name.contains(search, true)
 | 
			
		||||
                val diminutive = item.diminutive?.contains(search, true) == true
 | 
			
		||||
                val description = item.description?.contains(search, true) == true
 | 
			
		||||
                val history = item.history?.contains(search, true) == true
 | 
			
		||||
                name || diminutive || description || history
 | 
			
		||||
            }
 | 
			
		||||
            (gender == null || gender) && (race == null || race) && (search == null || search)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,18 +23,28 @@ class GlideLoadingTransition(
 | 
			
		|||
@Composable
 | 
			
		||||
fun rememberLoadingTransition(model: () -> Any?): GlideLoadingTransition {
 | 
			
		||||
    val isInEditMode = LocalView.current.isInEditMode
 | 
			
		||||
    val key = model()
 | 
			
		||||
    val target = remember(key) {
 | 
			
		||||
        mutableStateOf(if (isInEditMode) 1f else 0f)
 | 
			
		||||
    }
 | 
			
		||||
    val alpha = animateFloatAsState(
 | 
			
		||||
        targetValue = target.value,
 | 
			
		||||
        label = "RememberLoadingTransition"
 | 
			
		||||
    )
 | 
			
		||||
    return remember(key) {
 | 
			
		||||
        GlideLoadingTransition(
 | 
			
		||||
            target = target,
 | 
			
		||||
            alpha = alpha,
 | 
			
		||||
 | 
			
		||||
    return if (isInEditMode) {
 | 
			
		||||
        remember {
 | 
			
		||||
            GlideLoadingTransition(
 | 
			
		||||
                target = mutableStateOf(1f),
 | 
			
		||||
                alpha = mutableStateOf(1f),
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        val key = model()
 | 
			
		||||
        val target = remember(key) {
 | 
			
		||||
            mutableStateOf(0f)
 | 
			
		||||
        }
 | 
			
		||||
        val alpha = animateFloatAsState(
 | 
			
		||||
            targetValue = target.value,
 | 
			
		||||
            label = "LoadingAlphaTransition"
 | 
			
		||||
        )
 | 
			
		||||
        remember(key) {
 | 
			
		||||
            GlideLoadingTransition(
 | 
			
		||||
                target = target,
 | 
			
		||||
                alpha = alpha,
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue