feat: search
This commit is contained in:
@@ -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"
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user