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.fillMaxWidth
 | 
				
			||||||
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.text.KeyboardActions
 | 
				
			||||||
 | 
					import androidx.compose.foundation.text.KeyboardOptions
 | 
				
			||||||
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
 | 
				
			||||||
| 
						 | 
					@ -22,8 +24,11 @@ import androidx.compose.runtime.State
 | 
				
			||||||
import androidx.compose.runtime.mutableStateOf
 | 
					import androidx.compose.runtime.mutableStateOf
 | 
				
			||||||
import androidx.compose.runtime.remember
 | 
					import androidx.compose.runtime.remember
 | 
				
			||||||
import androidx.compose.ui.Modifier
 | 
					import androidx.compose.ui.Modifier
 | 
				
			||||||
 | 
					import androidx.compose.ui.platform.LocalFocusManager
 | 
				
			||||||
import androidx.compose.ui.res.painterResource
 | 
					import androidx.compose.ui.res.painterResource
 | 
				
			||||||
import androidx.compose.ui.res.stringResource
 | 
					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.Preview
 | 
				
			||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
 | 
					import androidx.compose.ui.tooling.preview.PreviewParameter
 | 
				
			||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
 | 
					import androidx.compose.ui.tooling.preview.PreviewParameterProvider
 | 
				
			||||||
| 
						 | 
					@ -52,6 +57,8 @@ fun TextField(
 | 
				
			||||||
    modifier: Modifier = Modifier,
 | 
					    modifier: Modifier = Modifier,
 | 
				
			||||||
    field: TextFieldUio,
 | 
					    field: TextFieldUio,
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
 | 
					    val focus = LocalFocusManager.current
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    OutlinedTextField(
 | 
					    OutlinedTextField(
 | 
				
			||||||
        modifier = modifier,
 | 
					        modifier = modifier,
 | 
				
			||||||
        shape = MaterialTheme.lexicon.shapes.textField,
 | 
					        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,
 | 
					        value = field.value.value,
 | 
				
			||||||
        onValueChange = field.onValueChange,
 | 
					        onValueChange = field.onValueChange,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -38,7 +38,6 @@ import androidx.compose.runtime.remember
 | 
				
			||||||
import androidx.compose.ui.Alignment
 | 
					import androidx.compose.ui.Alignment
 | 
				
			||||||
import androidx.compose.ui.Modifier
 | 
					import androidx.compose.ui.Modifier
 | 
				
			||||||
import androidx.compose.ui.composed
 | 
					import androidx.compose.ui.composed
 | 
				
			||||||
import androidx.compose.ui.draw.alpha
 | 
					 | 
				
			||||||
import androidx.compose.ui.geometry.Offset
 | 
					import androidx.compose.ui.geometry.Offset
 | 
				
			||||||
import androidx.compose.ui.graphics.Brush
 | 
					import androidx.compose.ui.graphics.Brush
 | 
				
			||||||
import androidx.compose.ui.graphics.Color
 | 
					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.layout.ContentScale
 | 
				
			||||||
import androidx.compose.ui.platform.LocalConfiguration
 | 
					import androidx.compose.ui.platform.LocalConfiguration
 | 
				
			||||||
import androidx.compose.ui.platform.LocalDensity
 | 
					import androidx.compose.ui.platform.LocalDensity
 | 
				
			||||||
 | 
					import androidx.compose.ui.platform.LocalView
 | 
				
			||||||
import androidx.compose.ui.res.painterResource
 | 
					import androidx.compose.ui.res.painterResource
 | 
				
			||||||
import androidx.compose.ui.res.stringResource
 | 
					import androidx.compose.ui.res.stringResource
 | 
				
			||||||
import androidx.compose.ui.text.AnnotatedString
 | 
					import androidx.compose.ui.text.AnnotatedString
 | 
				
			||||||
| 
						 | 
					@ -59,16 +59,14 @@ import androidx.compose.ui.unit.dp
 | 
				
			||||||
import androidx.hilt.navigation.compose.hiltViewModel
 | 
					import androidx.hilt.navigation.compose.hiltViewModel
 | 
				
			||||||
import com.pixelized.rplexicon.R
 | 
					import com.pixelized.rplexicon.R
 | 
				
			||||||
import com.pixelized.rplexicon.model.Lexicon
 | 
					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.navigation.LocalScreenNavHost
 | 
				
			||||||
import com.pixelized.rplexicon.ui.theme.LexiconTheme
 | 
					import com.pixelized.rplexicon.ui.theme.LexiconTheme
 | 
				
			||||||
import com.pixelized.rplexicon.utilitary.composable.stringResource
 | 
					import com.pixelized.rplexicon.utilitary.composable.stringResource
 | 
				
			||||||
import com.pixelized.rplexicon.utilitary.extentions.annotatedSpan
 | 
					import com.pixelized.rplexicon.utilitary.extentions.annotatedSpan
 | 
				
			||||||
import com.pixelized.rplexicon.utilitary.extentions.annotatedString
 | 
					import com.pixelized.rplexicon.utilitary.extentions.annotatedString
 | 
				
			||||||
import com.pixelized.rplexicon.utilitary.extentions.highlightRegex
 | 
					import com.pixelized.rplexicon.utilitary.extentions.highlightRegex
 | 
				
			||||||
import com.pixelized.rplexicon.utilitary.rememberLoadingTransition
 | 
					 | 
				
			||||||
import com.skydoves.landscapist.ImageOptions
 | 
					import com.skydoves.landscapist.ImageOptions
 | 
				
			||||||
import com.skydoves.landscapist.glide.GlideImage
 | 
					 | 
				
			||||||
import com.skydoves.landscapist.glide.GlideImageState
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Stable
 | 
					@Stable
 | 
				
			||||||
data class CharacterDetailUio(
 | 
					data class CharacterDetailUio(
 | 
				
			||||||
| 
						 | 
					@ -200,16 +198,8 @@ private fun CharacterDetailScreenContent(
 | 
				
			||||||
                        .aspectRatio(ratio = 1f)
 | 
					                        .aspectRatio(ratio = 1f)
 | 
				
			||||||
                        .scrollOffset(scrollState = state) { -it / 2 },
 | 
					                        .scrollOffset(scrollState = state) { -it / 2 },
 | 
				
			||||||
                ) {
 | 
					                ) {
 | 
				
			||||||
                    val transition = rememberLoadingTransition { uri }
 | 
					                    AsyncImage(
 | 
				
			||||||
                    GlideImage(
 | 
					                        modifier = Modifier.matchParentSize(),
 | 
				
			||||||
                        modifier = Modifier
 | 
					 | 
				
			||||||
                            .matchParentSize()
 | 
					 | 
				
			||||||
                            .alpha(alpha = transition.alpha),
 | 
					 | 
				
			||||||
                        onImageStateChanged = {
 | 
					 | 
				
			||||||
                            if (it is GlideImageState.Success) {
 | 
					 | 
				
			||||||
                                transition.target = 1f
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                        },
 | 
					 | 
				
			||||||
                        imageOptions = ImageOptions(
 | 
					                        imageOptions = ImageOptions(
 | 
				
			||||||
                            alignment = Alignment.TopCenter,
 | 
					                            alignment = Alignment.TopCenter,
 | 
				
			||||||
                            contentScale = ContentScale.Crop,
 | 
					                            contentScale = ContentScale.Crop,
 | 
				
			||||||
| 
						 | 
					@ -299,8 +289,7 @@ private fun CharacterDetailScreenContent(
 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                if (annotatedItem.portrait.isNotEmpty()) {
 | 
					                if (annotatedItem.portrait.isNotEmpty()) {
 | 
				
			||||||
                    val configuration = LocalConfiguration.current
 | 
					                    val maxSize = rememberPortraitWidth()
 | 
				
			||||||
                    val maxSize = remember { (configuration.screenWidthDp.dp - 16.dp * 2) }
 | 
					 | 
				
			||||||
                    Text(
 | 
					                    Text(
 | 
				
			||||||
                        modifier = Modifier.padding(start = 16.dp, top = 24.dp, end = 16.dp),
 | 
					                        modifier = Modifier.padding(start = 16.dp, top = 24.dp, end = 16.dp),
 | 
				
			||||||
                        style = typography.titleMedium,
 | 
					                        style = typography.titleMedium,
 | 
				
			||||||
| 
						 | 
					@ -308,19 +297,16 @@ private fun CharacterDetailScreenContent(
 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
                    LazyRow(
 | 
					                    LazyRow(
 | 
				
			||||||
                        contentPadding = PaddingValues(horizontal = 16.dp),
 | 
					                        contentPadding = PaddingValues(horizontal = 16.dp),
 | 
				
			||||||
                        horizontalArrangement = Arrangement.spacedBy(8.dp)
 | 
					                        horizontalArrangement = Arrangement.spacedBy(8.dp),
 | 
				
			||||||
                    ) {
 | 
					                    ) {
 | 
				
			||||||
                        items(items = annotatedItem.portrait) {
 | 
					                        items(items = annotatedItem.portrait) {
 | 
				
			||||||
                            val transition = rememberLoadingTransition { it }
 | 
					                            AsyncImage(
 | 
				
			||||||
                            GlideImage(
 | 
					                                modifier = Modifier.sizeIn(
 | 
				
			||||||
                                modifier = Modifier
 | 
					                                    minWidth = maxSize / 2,
 | 
				
			||||||
                                    .sizeIn(maxWidth = maxSize, maxHeight = maxSize)
 | 
					                                    maxWidth = maxSize,
 | 
				
			||||||
                                    .alpha(alpha = transition.alpha),
 | 
					                                    minHeight = maxSize,
 | 
				
			||||||
                                onImageStateChanged = {
 | 
					                                    maxHeight = maxSize,
 | 
				
			||||||
                                    if (it is GlideImageState.Success) {
 | 
					                                ),
 | 
				
			||||||
                                        transition.target = 1f
 | 
					 | 
				
			||||||
                                    }
 | 
					 | 
				
			||||||
                                },
 | 
					 | 
				
			||||||
                                imageOptions = ImageOptions(
 | 
					                                imageOptions = ImageOptions(
 | 
				
			||||||
                                    contentScale = ContentScale.FillHeight
 | 
					                                    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
 | 
					@Stable
 | 
				
			||||||
private fun Modifier.scrollOffset(
 | 
					private fun Modifier.scrollOffset(
 | 
				
			||||||
    scrollState: ScrollState,
 | 
					    scrollState: ScrollState,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -99,12 +99,13 @@ class AnnotatedSearchItemUio(
 | 
				
			||||||
private fun SearchItemUio.annotate(): AnnotatedSearchItemUio {
 | 
					private fun SearchItemUio.annotate(): AnnotatedSearchItemUio {
 | 
				
			||||||
    val colorScheme = MaterialTheme.colorScheme
 | 
					    val colorScheme = MaterialTheme.colorScheme
 | 
				
			||||||
    val highlight = remember { SpanStyle(color = colorScheme.primary) }
 | 
					    val highlight = remember { SpanStyle(color = colorScheme.primary) }
 | 
				
			||||||
    val highlightRegex = remember(search) { search.highlightRegex }
 | 
					    val trimmedSearch = remember(search) { search.trim() }
 | 
				
			||||||
    val finderRegex = remember(search) { search.finderRegex }
 | 
					    val highlightRegex = remember(search) { trimmedSearch.highlightRegex }
 | 
				
			||||||
 | 
					    val finderRegex = remember(search) { trimmedSearch.finderRegex }
 | 
				
			||||||
    val gender = stringResource(id = gender, short = true)
 | 
					    val gender = stringResource(id = gender, short = true)
 | 
				
			||||||
    val race = stringResource(id = race)
 | 
					    val race = stringResource(id = race)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return remember(search, race, highlightRace, gender, highlightGender) {
 | 
					    return remember(trimmedSearch, race, highlightRace, gender, highlightGender) {
 | 
				
			||||||
        AnnotatedSearchItemUio(
 | 
					        AnnotatedSearchItemUio(
 | 
				
			||||||
            id = id,
 | 
					            id = id,
 | 
				
			||||||
            name = AnnotatedString(
 | 
					            name = AnnotatedString(
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -41,7 +41,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
 | 
				
			||||||
import com.pixelized.rplexicon.NO_WINDOW_INSETS
 | 
					import com.pixelized.rplexicon.NO_WINDOW_INSETS
 | 
				
			||||||
import com.pixelized.rplexicon.R
 | 
					import com.pixelized.rplexicon.R
 | 
				
			||||||
import com.pixelized.rplexicon.model.Lexicon
 | 
					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.DropDownField
 | 
				
			||||||
import com.pixelized.rplexicon.ui.composable.form.DropDownFieldUio
 | 
					import com.pixelized.rplexicon.ui.composable.form.DropDownFieldUio
 | 
				
			||||||
import com.pixelized.rplexicon.ui.composable.form.TextField
 | 
					import com.pixelized.rplexicon.ui.composable.form.TextField
 | 
				
			||||||
| 
						 | 
					@ -88,9 +88,6 @@ fun SearchScreen(
 | 
				
			||||||
                screen.popBackStack()
 | 
					                screen.popBackStack()
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        ScrollingKeyboardHandler(
 | 
					 | 
				
			||||||
            lazyListState = lazyState,
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -125,16 +122,45 @@ private fun SearchScreenContent(
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
    ) { paddingValues ->
 | 
					    ) { paddingValues ->
 | 
				
			||||||
 | 
					        CollapsingHeader(
 | 
				
			||||||
 | 
					            modifier = Modifier.padding(paddingValues = paddingValues),
 | 
				
			||||||
 | 
					            header = {
 | 
				
			||||||
 | 
					                SearchBox(
 | 
				
			||||||
 | 
					                    modifier = Modifier.padding(horizontal = 16.dp),
 | 
				
			||||||
 | 
					                    form = form,
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            content = {
 | 
				
			||||||
                LazyColumn(
 | 
					                LazyColumn(
 | 
				
			||||||
            modifier = Modifier
 | 
					                    modifier = Modifier.fillMaxSize(),
 | 
				
			||||||
                .fillMaxSize()
 | 
					 | 
				
			||||||
                .padding(paddingValues = paddingValues),
 | 
					 | 
				
			||||||
                    state = lazyColumnState,
 | 
					                    state = lazyColumnState,
 | 
				
			||||||
                    contentPadding = PaddingValues(vertical = 8.dp),
 | 
					                    contentPadding = PaddingValues(vertical = 8.dp),
 | 
				
			||||||
                ) {
 | 
					                ) {
 | 
				
			||||||
            item {
 | 
					                    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(
 | 
					    Column(
 | 
				
			||||||
                    modifier = Modifier.padding(horizontal = 16.dp),
 | 
					        modifier = modifier,
 | 
				
			||||||
        verticalArrangement = Arrangement.spacedBy(2.dp),
 | 
					        verticalArrangement = Arrangement.spacedBy(2.dp),
 | 
				
			||||||
    ) {
 | 
					    ) {
 | 
				
			||||||
        TextField(
 | 
					        TextField(
 | 
				
			||||||
| 
						 | 
					@ -156,21 +182,6 @@ private fun SearchScreenContent(
 | 
				
			||||||
        Divider(modifier = Modifier.padding(top = 16.dp, bottom = 8.dp))
 | 
					        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
 | 
					@Composable
 | 
				
			||||||
@Preview(uiMode = UI_MODE_NIGHT_NO)
 | 
					@Preview(uiMode = UI_MODE_NIGHT_NO)
 | 
				
			||||||
| 
						 | 
					@ -196,18 +207,41 @@ private fun SearchScreenContentPreview() {
 | 
				
			||||||
                items = remember {
 | 
					                items = remember {
 | 
				
			||||||
                    mutableStateOf(
 | 
					                    mutableStateOf(
 | 
				
			||||||
                        listOf(
 | 
					                        listOf(
 | 
				
			||||||
                            SearchItemUio(
 | 
					                            SearchItemUio.preview(
 | 
				
			||||||
                                id = 0,
 | 
					                                id = 0,
 | 
				
			||||||
                                name = "Brulkhai",
 | 
					                                name = "Brulkhai",
 | 
				
			||||||
                                diminutive = "Bru",
 | 
					                                diminutive = "Bru",
 | 
				
			||||||
                                gender = Lexicon.Gender.FEMALE,
 | 
					                                gender = Lexicon.Gender.FEMALE,
 | 
				
			||||||
                                race = Lexicon.Race.HALF_ORC,
 | 
					                                race = Lexicon.Race.HALF_ORC,
 | 
				
			||||||
                                description = null,
 | 
					                            ),
 | 
				
			||||||
                                history = null,
 | 
					                            SearchItemUio.preview(
 | 
				
			||||||
                                search = "",
 | 
					                                id = 0,
 | 
				
			||||||
                                highlightGender = false,
 | 
					                                name = "Léandre",
 | 
				
			||||||
                                highlightRace = false,
 | 
					                                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 ->
 | 
					        data.filter { item ->
 | 
				
			||||||
            val gender = _gender.value?.let { it == item.gender }
 | 
					            val gender = _gender.value?.let { it == item.gender }
 | 
				
			||||||
            val race = _race.value?.let { it == item.race }
 | 
					            val race = _race.value?.let { it == item.race }
 | 
				
			||||||
            val search = _search.value.takeIf { it.isNotEmpty() }?.let {
 | 
					            val search = _search.value.takeIf { it.isNotEmpty() }?.trim()?.let { search ->
 | 
				
			||||||
                val name = item.name.contains(_search.value, true)
 | 
					                val name = item.name.contains(search, true)
 | 
				
			||||||
                val diminutive = item.diminutive?.contains(_search.value, true) == true
 | 
					                val diminutive = item.diminutive?.contains(search, true) == true
 | 
				
			||||||
                val description = item.description?.contains(_search.value, true) == true
 | 
					                val description = item.description?.contains(search, true) == true
 | 
				
			||||||
                val history = item.history?.contains(_search.value, true) == true
 | 
					                val history = item.history?.contains(search, true) == true
 | 
				
			||||||
                name || diminutive || description || history
 | 
					                name || diminutive || description || history
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            (gender == null || gender) && (race == null || race) && (search == null || search)
 | 
					            (gender == null || gender) && (race == null || race) && (search == null || search)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -23,18 +23,28 @@ class GlideLoadingTransition(
 | 
				
			||||||
@Composable
 | 
					@Composable
 | 
				
			||||||
fun rememberLoadingTransition(model: () -> Any?): GlideLoadingTransition {
 | 
					fun rememberLoadingTransition(model: () -> Any?): GlideLoadingTransition {
 | 
				
			||||||
    val isInEditMode = LocalView.current.isInEditMode
 | 
					    val isInEditMode = LocalView.current.isInEditMode
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return if (isInEditMode) {
 | 
				
			||||||
 | 
					        remember {
 | 
				
			||||||
 | 
					            GlideLoadingTransition(
 | 
				
			||||||
 | 
					                target = mutableStateOf(1f),
 | 
				
			||||||
 | 
					                alpha = mutableStateOf(1f),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
        val key = model()
 | 
					        val key = model()
 | 
				
			||||||
        val target = remember(key) {
 | 
					        val target = remember(key) {
 | 
				
			||||||
        mutableStateOf(if (isInEditMode) 1f else 0f)
 | 
					            mutableStateOf(0f)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        val alpha = animateFloatAsState(
 | 
					        val alpha = animateFloatAsState(
 | 
				
			||||||
            targetValue = target.value,
 | 
					            targetValue = target.value,
 | 
				
			||||||
        label = "RememberLoadingTransition"
 | 
					            label = "LoadingAlphaTransition"
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
    return remember(key) {
 | 
					        remember(key) {
 | 
				
			||||||
            GlideLoadingTransition(
 | 
					            GlideLoadingTransition(
 | 
				
			||||||
                target = target,
 | 
					                target = target,
 | 
				
			||||||
                alpha = alpha,
 | 
					                alpha = alpha,
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue