From 5baa502363ae586270f090af33051b2d3cc766f8 Mon Sep 17 00:00:00 2001 From: AnranYus Date: Sat, 15 Jun 2024 02:52:23 +0800 Subject: [PATCH] feat: search --- .../com/lsp/view/ui/compose/PostActivity.kt | 100 +++++++++--------- .../com/lsp/view/ui/compose/PostViewModel.kt | 13 ++- .../com/lsp/view/ui/compose/widget/Widget.kt | 83 +++++++++++++++ 3 files changed, 142 insertions(+), 54 deletions(-) create mode 100644 app/src/main/java/com/lsp/view/ui/compose/widget/Widget.kt diff --git a/app/src/main/java/com/lsp/view/ui/compose/PostActivity.kt b/app/src/main/java/com/lsp/view/ui/compose/PostActivity.kt index 7a46a13..619d867 100644 --- a/app/src/main/java/com/lsp/view/ui/compose/PostActivity.kt +++ b/app/src/main/java/com/lsp/view/ui/compose/PostActivity.kt @@ -32,36 +32,29 @@ import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridS import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack -import androidx.compose.material.icons.filled.Star import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.lsp.view.bean.Post import com.lsp.view.ui.compose.theme.LspViewTheme @@ -75,6 +68,7 @@ import coil.compose.SubcomposeAsyncImage import coil.compose.SubcomposeAsyncImageContent import com.lsp.view.R import com.lsp.view.service.DownloadService +import com.lsp.view.ui.compose.widget.SearchBar import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.distinctUntilChanged @@ -100,15 +94,14 @@ class PostActivity : ComponentActivity() { setContent { LspViewTheme { Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background + modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { App() } } } - viewModel.downloadAction.observe(this){ + viewModel.downloadAction.observe(this) { CoroutineScope(Dispatchers.IO).launch { val result = downloadBinder.downloadImage(viewModel.uiState.value.selectPost?.fileUrl ?: "") @@ -116,15 +109,11 @@ class PostActivity : ComponentActivity() { launch(Dispatchers.Main) { if (result.isSuccess) { Toast.makeText( - this@PostActivity, - "Download successful", - Toast.LENGTH_SHORT + this@PostActivity, "Download successful", Toast.LENGTH_SHORT ).show() } else { Toast.makeText( - this@PostActivity, - "Download fail", - Toast.LENGTH_SHORT + this@PostActivity, "Download fail", Toast.LENGTH_SHORT ).show() } } @@ -166,54 +155,61 @@ fun App(viewModel: PostViewModel = viewModel()) { @OptIn(ExperimentalFoundationApi::class) @Composable fun PostListScreen( - navController: NavController, - onNavigateToDetail: (Post) -> Unit, - viewModel: PostViewModel + navController: NavController, onNavigateToDetail: (Post) -> Unit, viewModel: PostViewModel ) { val postList by viewModel.postData.collectAsState() val listState = rememberLazyStaggeredGridState() LaunchedEffect(listState) { - snapshotFlow { listState.layoutInfo } - .map { layoutInfo -> + snapshotFlow { listState.layoutInfo }.map { layoutInfo -> layoutInfo.visibleItemsInfo.lastOrNull()?.index == postList.lastIndex - } - .distinctUntilChanged() - .collect { reachedEnd -> + }.distinctUntilChanged().collect { reachedEnd -> //避免初次加载数据时,list未绘制导致被判断为到达底部 if (reachedEnd && listState.firstVisibleItemIndex != 0) { viewModel.fetchPost() } } } - LazyVerticalStaggeredGrid( - state = listState, - columns = StaggeredGridCells.Adaptive(200.dp), - verticalItemSpacing = 4.dp, - horizontalArrangement = Arrangement.spacedBy(4.dp), - content = { - items(items = postList, key = { - it.postId//提供key以保持列表顺序稳定 - }) { - Row(Modifier.animateItemPlacement(tween(durationMillis = 250))) { - PostItem( - it, - clickable = { - onNavigateToDetail.invoke(it) - } - ) - } + val uiState by viewModel.uiState.collectAsState() + Box(modifier = Modifier.background(MaterialTheme.colorScheme.secondaryContainer)) { + Column { + Row(modifier = Modifier.padding(vertical = 4.dp)) { + SearchBar(uiState.searchTarget) { + viewModel.fetchPost(it, refresh = true) + } } + Row { + LazyVerticalStaggeredGrid( + state = listState, + columns = StaggeredGridCells.Adaptive(200.dp), + verticalItemSpacing = 4.dp, + horizontalArrangement = Arrangement.spacedBy(4.dp), + content = { + items(items = postList, key = { + it.postId//提供key以保持列表顺序稳定 + }) { + Row(Modifier.animateItemPlacement(tween(durationMillis = 250))) { + PostItem(it, clickable = { + onNavigateToDetail.invoke(it) + }) + } + + } + }, + modifier = Modifier.background(Color.Transparent) + ) + } + } - ) + } + } @Composable fun PostItem(post: Post, clickable: (Post) -> Unit) { - SubcomposeAsyncImage( - model = post.sampleUrl, + SubcomposeAsyncImage(model = post.sampleUrl, contentScale = ContentScale.FillWidth, contentDescription = null, loading = { @@ -236,8 +232,7 @@ fun PostItem(post: Post, clickable: (Post) -> Unit) { .clip(RoundedCornerShape(5.dp)) .fillMaxWidth() .wrapContentHeight() - .clickable { clickable.invoke(post) } - ) + .clickable { clickable.invoke(post) }) } @OptIn(ExperimentalMaterial3Api::class) @@ -251,7 +246,11 @@ fun DetailScreen(navController: NavController, viewModel: PostViewModel) { scale *= zoomChange rotation += rotationChange } - Box(modifier = Modifier.background(Color.Black).fillMaxSize()){ + Box( + modifier = Modifier + .background(Color.Black) + .fillMaxSize() + ) { AsyncImage( model = uiState.selectPost?.sampleUrl, contentDescription = null, @@ -280,8 +279,11 @@ fun DetailScreen(navController: NavController, viewModel: PostViewModel) { } }, actions = { - IconButton(onClick = { viewModel.actionDownload()}) { - Icon(painter = painterResource(id = R.drawable.ic_twotone_arrow_downward_24), contentDescription = "Download") + IconButton(onClick = { viewModel.actionDownload() }) { + Icon( + painter = painterResource(id = R.drawable.ic_twotone_arrow_downward_24), + contentDescription = "Download" + ) } }, ) diff --git a/app/src/main/java/com/lsp/view/ui/compose/PostViewModel.kt b/app/src/main/java/com/lsp/view/ui/compose/PostViewModel.kt index 4f5f546..2f06776 100644 --- a/app/src/main/java/com/lsp/view/ui/compose/PostViewModel.kt +++ b/app/src/main/java/com/lsp/view/ui/compose/PostViewModel.kt @@ -32,21 +32,24 @@ class PostViewModel:ViewModel() { downloadAction.postValue(Unit) } - fun fetchPost(){ + fun fetchPost(searchTarget: String = _uiState.value.searchTarget,refresh:Boolean = false){ viewModelScope.launch(Dispatchers.IO) { - _uiState.value.refresh = true + _uiState.value.searchTarget = searchTarget try { val data = repository.fetchPostData( - _uiState.value.searchTarget, + searchTarget, _uiState.value.safeModel, _uiState.value.page ) _uiState.value.page ++ - postData.value += data + postData.value = if (refresh){ + data + }else{ + postData.value + data + } } catch (e: NetworkErrorException) { //todo network error } - _uiState.value.refresh = false } } diff --git a/app/src/main/java/com/lsp/view/ui/compose/widget/Widget.kt b/app/src/main/java/com/lsp/view/ui/compose/widget/Widget.kt new file mode 100644 index 0000000..46c1170 --- /dev/null +++ b/app/src/main/java/com/lsp/view/ui/compose/widget/Widget.kt @@ -0,0 +1,83 @@ +package com.lsp.view.ui.compose.widget + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Menu +import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + + +@Composable +fun SearchBar(searchTarget: String = "", searchEvent: (String) -> Unit) { + Row( + modifier = Modifier + .padding(horizontal = 24.dp) + .fillMaxWidth() + .height(56.dp) + .background(Color.White, shape = RoundedCornerShape(56.dp)), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Default.Menu, + contentDescription = null, + modifier = Modifier + .padding(15.dp) + .height(48.dp) + .width(48.dp) + ) + + var input: String by remember { mutableStateOf(searchTarget) } + val keyboardController = LocalSoftwareKeyboardController.current + val forceManager = LocalFocusManager.current + BasicTextField( + value = input, + onValueChange = { + input = it + }, + singleLine = true, + textStyle = TextStyle(fontSize = 20.sp), + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), + keyboardActions = KeyboardActions { + keyboardController?.hide() + forceManager.clearFocus() + searchEvent.invoke(input) + }, + modifier = Modifier + .weight(1f) + .fillMaxWidth(), + ) + + Icon( + imageVector = Icons.Default.Search, + contentDescription = null, + modifier = Modifier + .padding(15.dp) + .height(48.dp) + .width(48.dp) + ) + + } +} \ No newline at end of file