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.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)
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user