refactor(Search): 封装搜索框、结果列表等组件

This commit is contained in:
HuanCheng65 2024-01-28 18:09:08 +08:00
parent d99f723cbc
commit 0207f830f7
No known key found for this signature in database
GPG Key ID: 5EC9DD60A32C7360
3 changed files with 296 additions and 232 deletions

View File

@ -1,6 +1,5 @@
package com.huanchengfly.tieba.post.ui.page.search
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
@ -14,7 +13,6 @@ import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@ -27,20 +25,16 @@ import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.ContentAlpha
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.ProvideTextStyle
import androidx.compose.material.Surface
import androidx.compose.material.Tab
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.ArrowBack
import androidx.compose.material.icons.rounded.Clear
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.rounded.ExpandLess
import androidx.compose.material.icons.rounded.ExpandMore
import androidx.compose.material.icons.rounded.Search
@ -59,13 +53,11 @@ 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.focus.onFocusEvent
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@ -87,13 +79,13 @@ import com.huanchengfly.tieba.post.ui.page.search.thread.SearchThreadPage
import com.huanchengfly.tieba.post.ui.page.search.thread.SearchThreadSortType
import com.huanchengfly.tieba.post.ui.page.search.thread.SearchThreadUiEvent
import com.huanchengfly.tieba.post.ui.page.search.user.SearchUserPage
import com.huanchengfly.tieba.post.ui.widgets.compose.BaseTextField
import com.huanchengfly.tieba.post.ui.widgets.compose.Button
import com.huanchengfly.tieba.post.ui.widgets.compose.Container
import com.huanchengfly.tieba.post.ui.widgets.compose.LazyLoadHorizontalPager
import com.huanchengfly.tieba.post.ui.widgets.compose.MyBackHandler
import com.huanchengfly.tieba.post.ui.widgets.compose.MyScaffold
import com.huanchengfly.tieba.post.ui.widgets.compose.PagerTabIndicator
import com.huanchengfly.tieba.post.ui.widgets.compose.SearchBox
import com.huanchengfly.tieba.post.ui.widgets.compose.TabClickMenu
import com.huanchengfly.tieba.post.ui.widgets.compose.TabRow
import com.huanchengfly.tieba.post.ui.widgets.compose.TopAppBarContainer
@ -269,8 +261,10 @@ fun SearchPage(
}
}
}
) {
Box(modifier = Modifier.fillMaxSize()) {
) { paddingValues ->
Box(modifier = Modifier
.fillMaxSize()
.padding(paddingValues)) {
if (!isKeywordEmpty) {
ProvideNavigator(navigator = navigator) {
LazyLoadHorizontalPager(
@ -562,20 +556,18 @@ private fun SearchTopBar(
onKeywordSubmit: (String) -> Unit = {},
onBack: () -> Unit = {},
) {
val isKeywordNotEmpty = remember(keyword) { keyword.isNotEmpty() }
var isFocused by remember { mutableStateOf(false) }
Surface(
SearchBox(
keyword = keyword,
onKeywordChange = onKeywordChange,
modifier = Modifier.fillMaxSize(),
shape = RoundedCornerShape(6.dp),
color = ExtendedTheme.colors.topBarSurface,
contentColor = ExtendedTheme.colors.onTopBarSurface,
elevation = 0.dp
) {
Row(
modifier = Modifier.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
onKeywordSubmit = onKeywordSubmit,
placeholder = {
Text(
text = stringResource(id = R.string.hint_search),
color = ExtendedTheme.colors.onTopBarSurface.copy(alpha = ContentAlpha.medium)
)
},
prependIcon = {
Box(
modifier = Modifier
.clip(RoundedCornerShape(100))
@ -588,76 +580,13 @@ private fun SearchTopBar(
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = Icons.Rounded.ArrowBack,
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = stringResource(id = R.string.button_back)
)
}
BaseTextField(
value = keyword,
onValueChange = {
onKeywordChange(it)
},
singleLine = true,
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Search,
),
keyboardActions = KeyboardActions(
onSearch = {
onKeywordSubmit(keyword)
}
),
placeholder = {
Text(
text = stringResource(id = R.string.hint_search),
color = ExtendedTheme.colors.onTopBarSurface.copy(alpha = ContentAlpha.medium)
)
},
modifier = Modifier
.fillMaxHeight()
.weight(1f)
.onFocusEvent { isFocused = it.isFocused }
)
AnimatedVisibility(visible = isKeywordNotEmpty && isFocused) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Box(
modifier = Modifier
.clip(RoundedCornerShape(100))
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false, 24.dp),
role = Role.Button
) { onKeywordChange("") },
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = Icons.Rounded.Clear,
contentDescription = stringResource(id = R.string.button_clear)
)
}
}
}
AnimatedVisibility(visible = isKeywordNotEmpty) {
Box(
modifier = Modifier
.clip(RoundedCornerShape(100))
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false, 24.dp),
role = Role.Button
) { onKeywordSubmit(keyword) },
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = Icons.Rounded.Search,
contentDescription = stringResource(id = R.string.button_search)
)
}
}
}
}
},
shape = RoundedCornerShape(6.dp)
)
}
@Preview("SearchBox")

View File

@ -1,15 +1,9 @@
package com.huanchengfly.tieba.post.ui.page.search.thread
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.LocalContentColor
import androidx.compose.material.Text
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
@ -20,8 +14,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import com.huanchengfly.tieba.post.api.models.SearchThreadBean
import com.huanchengfly.tieba.post.arch.collectPartialAsState
import com.huanchengfly.tieba.post.arch.onGlobalEvent
import com.huanchengfly.tieba.post.arch.pageViewModel
@ -32,24 +24,12 @@ import com.huanchengfly.tieba.post.ui.page.destinations.ForumPageDestination
import com.huanchengfly.tieba.post.ui.page.destinations.ThreadPageDestination
import com.huanchengfly.tieba.post.ui.page.destinations.UserProfilePageDestination
import com.huanchengfly.tieba.post.ui.page.search.SearchUiEvent
import com.huanchengfly.tieba.post.ui.widgets.compose.Avatar
import com.huanchengfly.tieba.post.ui.widgets.compose.Card
import com.huanchengfly.tieba.post.ui.widgets.compose.ErrorScreen
import com.huanchengfly.tieba.post.ui.widgets.compose.ForumInfoChip
import com.huanchengfly.tieba.post.ui.widgets.compose.LazyLoad
import com.huanchengfly.tieba.post.ui.widgets.compose.LoadMoreLayout
import com.huanchengfly.tieba.post.ui.widgets.compose.LocalShouldLoad
import com.huanchengfly.tieba.post.ui.widgets.compose.MyLazyColumn
import com.huanchengfly.tieba.post.ui.widgets.compose.Sizes
import com.huanchengfly.tieba.post.ui.widgets.compose.ThreadAgreeBtn
import com.huanchengfly.tieba.post.ui.widgets.compose.ThreadContent
import com.huanchengfly.tieba.post.ui.widgets.compose.ThreadReplyBtn
import com.huanchengfly.tieba.post.ui.widgets.compose.ThreadShareBtn
import com.huanchengfly.tieba.post.ui.widgets.compose.UserHeader
import com.huanchengfly.tieba.post.ui.widgets.compose.SearchThreadList
import com.huanchengfly.tieba.post.ui.widgets.compose.states.StateScreen
import com.huanchengfly.tieba.post.utils.DateTimeUtils
import com.huanchengfly.tieba.post.utils.StringUtil
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@OptIn(ExperimentalMaterialApi::class)
@ -59,7 +39,6 @@ fun SearchThreadPage(
initialSortType: Int = SearchThreadSortType.SORT_TYPE_NEWEST,
viewModel: SearchThreadViewModel = pageViewModel(),
) {
val context = LocalContext.current
val navigator = LocalNavigator.current
LazyLoad(loaded = viewModel.initialized) {
viewModel.send(SearchThreadUiIntent.Refresh(keyword, initialSortType))
@ -187,121 +166,3 @@ fun SearchThreadPage(
}
}
}
@Composable
private fun SearchThreadList(
data: ImmutableList<SearchThreadBean.ThreadInfoBean>,
lazyListState: LazyListState,
onItemClick: (SearchThreadBean.ThreadInfoBean) -> Unit,
onItemUserClick: (SearchThreadBean.UserInfoBean) -> Unit,
onItemForumClick: (SearchThreadBean.ForumInfo) -> Unit,
modifier: Modifier = Modifier,
) {
MyLazyColumn(
state = lazyListState,
modifier = modifier
) {
items(data) {
SearchThreadItem(
item = it,
onClick = onItemClick,
onUserClick = onItemUserClick,
onForumClick = onItemForumClick,
)
}
}
}
@Composable
private fun SearchThreadUserHeader(
user: SearchThreadBean.UserInfoBean,
time: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
UserHeader(
avatar = {
Avatar(
data = StringUtil.getAvatarUrl(user.portrait),
size = Sizes.Small,
contentDescription = null
)
},
name = {
Text(
text = StringUtil.getUsernameAnnotatedString(
LocalContext.current,
user.userName.orEmpty(),
user.showNickname,
color = LocalContentColor.current
)
)
},
desc = {
Text(
text = DateTimeUtils.getRelativeTimeString(LocalContext.current, time)
)
},
onClick = onClick,
modifier = modifier
)
}
@Composable
private fun SearchThreadItem(
item: SearchThreadBean.ThreadInfoBean,
onClick: (SearchThreadBean.ThreadInfoBean) -> Unit,
onUserClick: (SearchThreadBean.UserInfoBean) -> Unit,
onForumClick: (SearchThreadBean.ForumInfo) -> Unit,
modifier: Modifier = Modifier,
) {
Card(
modifier = modifier,
header = {
SearchThreadUserHeader(
user = item.user,
time = item.time,
onClick = { onUserClick(item.user) }
)
},
content = {
ThreadContent(
title = item.title,
abstractText = item.content,
showTitle = item.title.isNotBlank(),
showAbstract = item.content.isNotBlank(),
)
if (item.forumName.isNotEmpty()) {
ForumInfoChip(
imageUriProvider = { item.forumInfo.avatar },
nameProvider = { item.forumName }
) {
onForumClick(item.forumInfo)
}
}
},
action = {
Row(modifier = Modifier.fillMaxWidth()) {
ThreadReplyBtn(
replyNum = item.postNum.toInt(),
onClick = {},
modifier = Modifier.weight(1f)
)
ThreadAgreeBtn(
hasAgree = false,
agreeNum = item.likeNum.toInt(),
onClick = {},
modifier = Modifier.weight(1f)
)
ThreadShareBtn(
shareNum = item.shareNum.toLong(),
onClick = {},
modifier = Modifier.weight(1f)
)
}
},
onClick = { onClick(item) },
)
}

View File

@ -0,0 +1,274 @@
package com.huanchengfly.tieba.post.ui.widgets.compose
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.Icon
import androidx.compose.material.LocalContentColor
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Clear
import androidx.compose.material.icons.rounded.Search
import androidx.compose.material.ripple.rememberRipple
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.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusEvent
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.huanchengfly.tieba.post.R
import com.huanchengfly.tieba.post.api.models.SearchThreadBean
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
import com.huanchengfly.tieba.post.utils.DateTimeUtils
import com.huanchengfly.tieba.post.utils.StringUtil
import kotlinx.collections.immutable.ImmutableList
@Composable
fun SearchThreadList(
data: ImmutableList<SearchThreadBean.ThreadInfoBean>,
lazyListState: LazyListState,
onItemClick: (SearchThreadBean.ThreadInfoBean) -> Unit,
onItemUserClick: (SearchThreadBean.UserInfoBean) -> Unit,
onItemForumClick: (SearchThreadBean.ForumInfo) -> Unit,
modifier: Modifier = Modifier,
hideForum: Boolean = false,
header: LazyListScope.() -> Unit = {},
) {
MyLazyColumn(
state = lazyListState,
modifier = modifier
) {
header()
itemsIndexed(data) { index, item ->
if (index > 0) {
VerticalDivider(modifier = Modifier.padding(horizontal = 16.dp))
}
SearchThreadItem(
item = item,
onClick = onItemClick,
onUserClick = onItemUserClick,
onForumClick = onItemForumClick,
hideForum = hideForum,
)
}
}
}
@Composable
fun SearchThreadUserHeader(
user: SearchThreadBean.UserInfoBean,
time: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
UserHeader(
avatar = {
Avatar(
data = StringUtil.getAvatarUrl(user.portrait),
size = Sizes.Small,
contentDescription = null
)
},
name = {
Text(
text = StringUtil.getUsernameAnnotatedString(
LocalContext.current,
user.userName.orEmpty(),
user.showNickname,
color = LocalContentColor.current
)
)
},
desc = {
Text(
text = DateTimeUtils.getRelativeTimeString(LocalContext.current, time)
)
},
onClick = onClick,
modifier = modifier
)
}
@Composable
fun SearchThreadItem(
item: SearchThreadBean.ThreadInfoBean,
onClick: (SearchThreadBean.ThreadInfoBean) -> Unit,
onUserClick: (SearchThreadBean.UserInfoBean) -> Unit,
onForumClick: (SearchThreadBean.ForumInfo) -> Unit,
modifier: Modifier = Modifier,
hideForum: Boolean = false,
) {
Card(
modifier = modifier,
header = {
SearchThreadUserHeader(
user = item.user,
time = item.time,
onClick = { onUserClick(item.user) }
)
},
content = {
ThreadContent(
title = item.title,
abstractText = item.content,
showTitle = item.title.isNotBlank(),
showAbstract = item.content.isNotBlank(),
)
if (!hideForum && item.forumName.isNotEmpty()) {
ForumInfoChip(
imageUriProvider = { item.forumInfo.avatar },
nameProvider = { item.forumName }
) {
onForumClick(item.forumInfo)
}
}
},
action = {
Row(modifier = Modifier.fillMaxWidth()) {
ThreadReplyBtn(
replyNum = item.postNum.toInt(),
onClick = {},
modifier = Modifier.weight(1f)
)
ThreadAgreeBtn(
hasAgree = false,
agreeNum = item.likeNum.toInt(),
onClick = {},
modifier = Modifier.weight(1f)
)
ThreadShareBtn(
shareNum = item.shareNum.toLong(),
onClick = {},
modifier = Modifier.weight(1f)
)
}
},
onClick = { onClick(item) },
)
}
@Composable
fun SearchBox(
keyword: String,
onKeywordChange: (String) -> Unit,
modifier: Modifier = Modifier,
onKeywordSubmit: (String) -> Unit = {},
placeholder: @Composable () -> Unit = {},
prependIcon: @Composable () -> Unit = {},
appendIcon: @Composable () -> Unit = {},
focusRequester: FocusRequester = remember { FocusRequester() },
shape: Shape = RectangleShape,
color: Color = ExtendedTheme.colors.topBarSurface,
contentColor: Color = ExtendedTheme.colors.onTopBarSurface,
elevation: Dp = 0.dp,
) {
val isKeywordNotEmpty = remember(keyword) { keyword.isNotEmpty() }
var isFocused by remember { mutableStateOf(false) }
Surface(
modifier = modifier,
shape = shape,
color = color,
contentColor = contentColor,
elevation = 0.dp
) {
Row(
modifier = Modifier.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
prependIcon()
BaseTextField(
value = keyword,
onValueChange = {
onKeywordChange(it)
},
singleLine = true,
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Search,
),
keyboardActions = KeyboardActions(
onSearch = {
onKeywordSubmit(keyword)
}
),
placeholder = placeholder,
modifier = Modifier
.fillMaxHeight()
.weight(1f)
.focusRequester(focusRequester)
.onFocusEvent {
isFocused = it.isFocused
}
)
appendIcon()
AnimatedVisibility(visible = isKeywordNotEmpty && isFocused) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Box(
modifier = Modifier
.clip(RoundedCornerShape(100))
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false, 24.dp),
role = Role.Button
) { onKeywordChange("") },
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = Icons.Rounded.Clear,
contentDescription = stringResource(id = R.string.button_clear)
)
}
}
}
AnimatedVisibility(visible = isKeywordNotEmpty) {
Box(
modifier = Modifier
.clip(RoundedCornerShape(100))
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false, 24.dp),
role = Role.Button
) { onKeywordSubmit(keyword) },
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = Icons.Rounded.Search,
contentDescription = stringResource(id = R.string.button_search)
)
}
}
}
}
}