feat: search

This commit is contained in:
AnranYus
2024-06-15 02:52:23 +08:00
parent 6b71b31dfc
commit 5baa502363
3 changed files with 142 additions and 54 deletions
@@ -32,36 +32,29 @@ import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridS
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Star
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
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
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.lsp.view.bean.Post import com.lsp.view.bean.Post
import com.lsp.view.ui.compose.theme.LspViewTheme import com.lsp.view.ui.compose.theme.LspViewTheme
@@ -75,6 +68,7 @@ import coil.compose.SubcomposeAsyncImage
import coil.compose.SubcomposeAsyncImageContent import coil.compose.SubcomposeAsyncImageContent
import com.lsp.view.R import com.lsp.view.R
import com.lsp.view.service.DownloadService import com.lsp.view.service.DownloadService
import com.lsp.view.ui.compose.widget.SearchBar
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
@@ -100,15 +94,14 @@ class PostActivity : ComponentActivity() {
setContent { setContent {
LspViewTheme { LspViewTheme {
Surface( Surface(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background
color = MaterialTheme.colorScheme.background
) { ) {
App() App()
} }
} }
} }
viewModel.downloadAction.observe(this){ viewModel.downloadAction.observe(this) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val result = val result =
downloadBinder.downloadImage(viewModel.uiState.value.selectPost?.fileUrl ?: "") downloadBinder.downloadImage(viewModel.uiState.value.selectPost?.fileUrl ?: "")
@@ -116,15 +109,11 @@ class PostActivity : ComponentActivity() {
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
if (result.isSuccess) { if (result.isSuccess) {
Toast.makeText( Toast.makeText(
this@PostActivity, this@PostActivity, "Download successful", Toast.LENGTH_SHORT
"Download successful",
Toast.LENGTH_SHORT
).show() ).show()
} else { } else {
Toast.makeText( Toast.makeText(
this@PostActivity, this@PostActivity, "Download fail", Toast.LENGTH_SHORT
"Download fail",
Toast.LENGTH_SHORT
).show() ).show()
} }
} }
@@ -166,54 +155,61 @@ fun App(viewModel: PostViewModel = viewModel()) {
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun PostListScreen( fun PostListScreen(
navController: NavController, navController: NavController, onNavigateToDetail: (Post) -> Unit, viewModel: PostViewModel
onNavigateToDetail: (Post) -> Unit,
viewModel: PostViewModel
) { ) {
val postList by viewModel.postData.collectAsState() val postList by viewModel.postData.collectAsState()
val listState = rememberLazyStaggeredGridState() val listState = rememberLazyStaggeredGridState()
LaunchedEffect(listState) { LaunchedEffect(listState) {
snapshotFlow { listState.layoutInfo } snapshotFlow { listState.layoutInfo }.map { layoutInfo ->
.map { layoutInfo ->
layoutInfo.visibleItemsInfo.lastOrNull()?.index == postList.lastIndex layoutInfo.visibleItemsInfo.lastOrNull()?.index == postList.lastIndex
} }.distinctUntilChanged().collect { reachedEnd ->
.distinctUntilChanged()
.collect { reachedEnd ->
//避免初次加载数据时,list未绘制导致被判断为到达底部 //避免初次加载数据时,list未绘制导致被判断为到达底部
if (reachedEnd && listState.firstVisibleItemIndex != 0) { if (reachedEnd && listState.firstVisibleItemIndex != 0) {
viewModel.fetchPost() 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 @Composable
fun PostItem(post: Post, clickable: (Post) -> Unit) { fun PostItem(post: Post, clickable: (Post) -> Unit) {
SubcomposeAsyncImage( SubcomposeAsyncImage(model = post.sampleUrl,
model = post.sampleUrl,
contentScale = ContentScale.FillWidth, contentScale = ContentScale.FillWidth,
contentDescription = null, contentDescription = null,
loading = { loading = {
@@ -236,8 +232,7 @@ fun PostItem(post: Post, clickable: (Post) -> Unit) {
.clip(RoundedCornerShape(5.dp)) .clip(RoundedCornerShape(5.dp))
.fillMaxWidth() .fillMaxWidth()
.wrapContentHeight() .wrapContentHeight()
.clickable { clickable.invoke(post) } .clickable { clickable.invoke(post) })
)
} }
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -251,7 +246,11 @@ fun DetailScreen(navController: NavController, viewModel: PostViewModel) {
scale *= zoomChange scale *= zoomChange
rotation += rotationChange rotation += rotationChange
} }
Box(modifier = Modifier.background(Color.Black).fillMaxSize()){ Box(
modifier = Modifier
.background(Color.Black)
.fillMaxSize()
) {
AsyncImage( AsyncImage(
model = uiState.selectPost?.sampleUrl, model = uiState.selectPost?.sampleUrl,
contentDescription = null, contentDescription = null,
@@ -280,8 +279,11 @@ fun DetailScreen(navController: NavController, viewModel: PostViewModel) {
} }
}, },
actions = { actions = {
IconButton(onClick = { viewModel.actionDownload()}) { IconButton(onClick = { viewModel.actionDownload() }) {
Icon(painter = painterResource(id = R.drawable.ic_twotone_arrow_downward_24), contentDescription = "Download") Icon(
painter = painterResource(id = R.drawable.ic_twotone_arrow_downward_24),
contentDescription = "Download"
)
} }
}, },
) )
@@ -32,21 +32,24 @@ class PostViewModel:ViewModel() {
downloadAction.postValue(Unit) downloadAction.postValue(Unit)
} }
fun fetchPost(){ fun fetchPost(searchTarget: String = _uiState.value.searchTarget,refresh:Boolean = false){
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
_uiState.value.refresh = true _uiState.value.searchTarget = searchTarget
try { try {
val data = repository.fetchPostData( val data = repository.fetchPostData(
_uiState.value.searchTarget, searchTarget,
_uiState.value.safeModel, _uiState.value.safeModel,
_uiState.value.page _uiState.value.page
) )
_uiState.value.page ++ _uiState.value.page ++
postData.value += data postData.value = if (refresh){
data
}else{
postData.value + data
}
} catch (e: NetworkErrorException) { } catch (e: NetworkErrorException) {
//todo network error //todo network error
} }
_uiState.value.refresh = false
} }
} }
@@ -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)
)
}
}