pref: 吧页面顶栏动画

This commit is contained in:
HuanCheng65 2023-10-06 18:13:19 +08:00
parent 8b358a080a
commit 2c7194cda7
No known key found for this signature in database
GPG Key ID: 5EC9DD60A32C7360
4 changed files with 527 additions and 345 deletions

View File

@ -312,7 +312,7 @@ class ForumActivity : BaseActivity(), View.OnClickListener, OnRefreshedListener,
)
else
ForumFragment.newInstance(forumName, false, getSortType()),
getString(R.string.tab_forum_1)
getString(R.string.tab_forum_latest)
)
addFragment(
ForumFragment.newInstance(forumName, true, getSortType()),

View File

@ -6,13 +6,11 @@ import android.graphics.Typeface
import android.net.Uri
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.rememberScrollableState
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.layout.Arrangement
@ -22,28 +20,33 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeContent
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.FloatingActionButton
import androidx.compose.material.FractionalThreshold
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.LinearProgressIndicator
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Tab
import androidx.compose.material.TabRow
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Add
@ -56,8 +59,11 @@ import androidx.compose.material.icons.rounded.VerticalAlignTop
import androidx.compose.material.rememberScaffoldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
@ -71,16 +77,18 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.util.fastForEach
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import com.google.accompanist.placeholder.PlaceholderHighlight
@ -99,7 +107,6 @@ import com.huanchengfly.tieba.post.dataStore
import com.huanchengfly.tieba.post.getInt
import com.huanchengfly.tieba.post.goToActivity
import com.huanchengfly.tieba.post.models.database.History
import com.huanchengfly.tieba.post.pxToDp
import com.huanchengfly.tieba.post.toastShort
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
import com.huanchengfly.tieba.post.ui.page.LocalNavigator
@ -118,12 +125,16 @@ import com.huanchengfly.tieba.post.ui.widgets.compose.LazyLoad
import com.huanchengfly.tieba.post.ui.widgets.compose.MenuScope
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.ScrollableTabRow
import com.huanchengfly.tieba.post.ui.widgets.compose.Sizes
import com.huanchengfly.tieba.post.ui.widgets.compose.SwipeableState
import com.huanchengfly.tieba.post.ui.widgets.compose.Toolbar
import com.huanchengfly.tieba.post.ui.widgets.compose.picker.ListSinglePicker
import com.huanchengfly.tieba.post.ui.widgets.compose.rememberDialogState
import com.huanchengfly.tieba.post.ui.widgets.compose.rememberMenuState
import com.huanchengfly.tieba.post.ui.widgets.compose.rememberSwipeableState
import com.huanchengfly.tieba.post.ui.widgets.compose.states.StateScreen
import com.huanchengfly.tieba.post.ui.widgets.compose.swipeable
import com.huanchengfly.tieba.post.utils.AccountUtil.LocalAccount
import com.huanchengfly.tieba.post.utils.HistoryUtil
import com.huanchengfly.tieba.post.utils.StringUtil.getShortNumString
@ -134,15 +145,19 @@ import com.ramcosta.composedestinations.annotation.DeepLink
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.launch
import kotlin.math.absoluteValue
import kotlin.math.max
import kotlin.math.min
private val LoadDistance = 70.dp
fun getSortType(
context: Context,
forumName: String
forumName: String,
): Int {
val defaultSortType = context.appPreferences.defaultSortType?.toIntOrNull() ?: 0
return context.dataStore.getInt("${forumName}_sort_type", defaultSortType)
@ -151,7 +166,7 @@ fun getSortType(
suspend fun setSortType(
context: Context,
forumName: String,
sortType: Int
sortType: Int,
) {
context.dataStore.edit {
it[intPreferencesKey("${forumName}_sort_type")] = sortType
@ -352,7 +367,7 @@ private suspend fun sendToDesktop(
)
}
@OptIn(ExperimentalFoundationApi::class)
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class)
@Destination(
deepLinks = [
DeepLink(uriPattern = "tblite://forum/{forumName}")
@ -437,36 +452,29 @@ fun ForumPage(
val account = LocalAccount.current
val pagerState = rememberPagerState { 2 }
val currentPage by remember {
derivedStateOf {
pagerState.currentPage
}
}
val coroutineScope = rememberCoroutineScope()
val lazyListStates = persistentListOf(rememberLazyListState(), rememberLazyListState())
val density = LocalDensity.current
val playDistance = with(density) { 12.dp.toPx() }
val isShowTopBarArea by viewModel.uiState.collectPartialAsState(
prop1 = ForumUiState::showForumHeader,
initial = true
var heightOffset by remember { mutableFloatStateOf(0f) }
var headerHeight by remember {
mutableFloatStateOf(
with(density) {
(Sizes.Large + 16.dp * 2).toPx()
}
)
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(
available: Offset,
source: NestedScrollSource
): Offset {
lazyListStates.getOrNull(pagerState.currentPage)?.let { lazyListState ->
if (available.y > 0 && lazyListState.firstVisibleItemIndex == 0) {
// 一番上の要素が表示されたので表示
viewModel.send(ForumUiIntent.ToggleShowHeader(true))
} else {
if (available.y.absoluteValue > playDistance && available.y < 0) {
viewModel.send(ForumUiIntent.ToggleShowHeader(false))
}
}
}
return Offset.Zero
}
val isShowTopBarArea by remember {
derivedStateOf {
heightOffset.absoluteValue < headerHeight
}
}
@ -524,12 +532,55 @@ fun ForumPage(
LoadingPlaceholder(forumName)
}
) {
val loadDistance = with(LocalDensity.current) { LoadDistance.toPx() }
var isFakeLoading by remember { mutableStateOf(false) }
val swipeableState = rememberSwipeableState(false) {
if (it && !isFakeLoading) {
coroutineScope.launch {
emitGlobalEvent(
ForumThreadListUiEvent.Refresh(
currentPage == 1,
getSortType(
context,
forumName
)
)
)
}
isFakeLoading = true
}
false
}
val showTip by remember {
derivedStateOf { swipeableState.offset.value > -loadDistance / 2 }
}
LaunchedEffect(isFakeLoading) {
if (isFakeLoading) {
delay(1000)
isFakeLoading = false
}
}
Box(modifier = Modifier) {
MyScaffold(
scaffoldState = scaffoldState,
backgroundColor = Color.Transparent,
modifier = Modifier
.fillMaxSize()
.nestedScroll(nestedScrollConnection),
.nestedScroll(swipeableState.LoadPreDownPostUpNestedScrollConnection)
.swipeable(
state = swipeableState,
anchors = mapOf(
-loadDistance to false,
loadDistance to true,
),
thresholds = { _, _ -> FractionalThreshold(0.75f) },
orientation = Orientation.Vertical,
),
topBar = {
ForumToolbar(
forumName = forumName,
@ -598,12 +649,12 @@ fun ForumPage(
coroutineScope.launch {
emitGlobalEventSuspend(
ForumThreadListUiEvent.BackToTop(
pagerState.currentPage == 1
currentPage == 1
)
)
emitGlobalEventSuspend(
ForumThreadListUiEvent.Refresh(
pagerState.currentPage == 1,
currentPage == 1,
getSortType(
context,
forumName
@ -617,7 +668,7 @@ fun ForumPage(
coroutineScope.launch {
emitGlobalEvent(
ForumThreadListUiEvent.BackToTop(
pagerState.currentPage == 1
currentPage == 1
)
)
}
@ -644,22 +695,88 @@ fun ForumPage(
}
}
) { contentPadding ->
Column(modifier = Modifier.padding(contentPadding)) {
AnimatedVisibility(
visible = isShowTopBarArea,
enter = expandVertically(
expandFrom = Alignment.Top
),
exit = shrinkVertically()
val headerNestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(
available: Offset,
source: NestedScrollSource,
): Offset {
if (available.y < 0) {
val prevHeightOffset = heightOffset
heightOffset = max(heightOffset + available.y, -headerHeight)
if (prevHeightOffset != heightOffset) {
return available.copy(x = 0f)
}
}
return Offset.Zero
}
override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource,
): Offset {
if (available.y > 0f) {
// Adjust the height offset in case the consumed delta Y is less than what was
// recorded as available delta Y in the pre-scroll.
val prevHeightOffset = heightOffset
heightOffset = min(heightOffset + available.y, 0f)
if (prevHeightOffset != heightOffset) {
return available.copy(x = 0f)
}
}
return Offset.Zero
}
}
}
Box(
modifier = Modifier
.padding(contentPadding)
.nestedScroll(headerNestedScrollConnection)
) {
forumInfo?.let {
Column(
modifier = Modifier.offset {
IntOffset(
x = 0,
y = (swipeableState.offset.value + loadDistance).toInt()
)
}
) {
val containerHeight by remember {
derivedStateOf {
with(density) {
(headerHeight + heightOffset).toDp()
}
}
}
Box(
modifier = Modifier
.height(containerHeight)
) {
Box(
modifier = Modifier
.wrapContentHeight(
align = Alignment.Bottom,
unbounded = true
)
.onSizeChanged {
headerHeight = it.height.toFloat()
}
) {
forumInfo?.let { holder ->
ForumHeader(
forumInfoImmutableHolder = it,
forumInfoImmutableHolder = holder,
onOpenForumInfo = {
navigator.navigate(ForumDetailPageDestination(forumId = it.get { this.id }))
navigator.navigate(
ForumDetailPageDestination(
forumId = holder.get { this.id })
)
},
onBtnClick = {
val (forum) = it
val (forum) = holder
when {
forum.is_like != 1 -> viewModel.send(
ForumUiIntent.Like(
@ -683,32 +800,19 @@ fun ForumPage(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
// enable event when scroll image.
.scrollable(
orientation = Orientation.Vertical,
state = rememberScrollableState { it }
)
)
}
}
}
val textMeasurer = rememberTextMeasurer()
val tabText = stringResource(id = R.string.tab_forum_1)
val tabTextStyle = MaterialTheme.typography.button.copy(
fontWeight = FontWeight.Bold,
fontSize = 13.sp,
letterSpacing = 0.sp
)
val tabWidth = remember {
val width = textMeasurer.measure(
AnnotatedString(tabText),
style = tabTextStyle
).size.width.pxToDp()
(width + 16 * 2) * 2
}
TabRow(
selectedTabIndex = pagerState.currentPage,
ScrollableTabRow(
selectedTabIndex = currentPage,
indicator = { tabPositions ->
PagerTabIndicator(
pagerState = pagerState,
@ -718,8 +822,9 @@ fun ForumPage(
divider = {},
backgroundColor = Color.Transparent,
contentColor = ExtendedTheme.colors.primary,
edgePadding = 0.dp,
modifier = Modifier
.width(tabWidth.dp)
.wrapContentWidth(align = Alignment.Start)
.align(Alignment.Start)
) {
val menuState = rememberMenuState()
@ -759,7 +864,7 @@ fun ForumPage(
coroutineScope.launch {
emitGlobalEvent(
ForumThreadListUiEvent.Refresh(
pagerState.currentPage == 1,
currentPage == 1,
value
)
)
@ -772,12 +877,12 @@ fun ForumPage(
}
) {
val rotate by animateFloatAsState(targetValue = if (menuState.expanded) 180f else 0f)
val alpha by animateFloatAsState(targetValue = if (pagerState.currentPage == 0) 1f else 0f)
val alpha by animateFloatAsState(targetValue = if (currentPage == 0) 1f else 0f)
Tab(
selected = pagerState.currentPage == 0,
selected = currentPage == 0,
onClick = {
if (pagerState.currentPage != 0) {
if (currentPage != 0) {
coroutineScope.launch {
pagerState.animateScrollToPage(0)
}
@ -796,7 +901,7 @@ fun ForumPage(
.padding(start = 16.dp)
) {
Text(
text = stringResource(id = R.string.tab_forum_1),
text = stringResource(id = R.string.tab_forum_latest),
style = tabTextStyle
)
Icon(
@ -811,7 +916,7 @@ fun ForumPage(
}
}
Tab(
selected = pagerState.currentPage == 1,
selected = currentPage == 1,
onClick = {
coroutineScope.launch {
pagerState.animateScrollToPage(1)
@ -846,13 +951,35 @@ fun ForumPage(
forumId = forumInfo!!.get { id },
forumName = forumInfo!!.get { name },
isGood = it == 1,
lazyListState = remember { lazyListStates[it] }
)
}
}
}
}
}
AnimatedVisibility(
visible = showTip,
enter = fadeIn(),
exit = fadeOut(),
modifier = Modifier.align(Alignment.TopCenter)
) {
Box(
modifier = Modifier
.align(Alignment.TopCenter)
.windowInsetsPadding(WindowInsets.safeContent)
) {
Text(
text = if (isFakeLoading || swipeableState.targetValue)
stringResource(id = R.string.release_to_refresh)
else stringResource(id = R.string.pull_down_to_refresh),
modifier = Modifier.align(Alignment.Center),
style = MaterialTheme.typography.caption,
)
}
}
}
}
}
}
@ -891,7 +1018,10 @@ fun LoadingPlaceholder(
.padding(16.dp)
)
Row(modifier = Modifier.height(48.dp)) {
repeat(2) {
persistentListOf(
stringResource(id = R.string.tab_forum_latest),
stringResource(id = R.string.tab_forum_good),
).fastForEach {
Box(
modifier = Modifier
.padding(horizontal = 16.dp)
@ -899,7 +1029,7 @@ fun LoadingPlaceholder(
contentAlignment = Alignment.Center
) {
Text(
text = stringResource(id = R.string.tab_forum_1),
text = it,
modifier = Modifier.placeholder(
visible = true,
highlight = PlaceholderHighlight.fade(),
@ -1015,3 +1145,52 @@ private fun RowScope.StatCardItem(
)
}
}
@ExperimentalMaterialApi
private val <T> SwipeableState<T>.LoadPreDownPostUpNestedScrollConnection: NestedScrollConnection
get() = object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val delta = available.toFloat()
return if (delta < 0 && source == NestedScrollSource.Drag) {
performDrag(delta).toOffset()
} else {
Offset.Zero
}
}
override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource,
): Offset {
return if (source == NestedScrollSource.Drag) {
performDrag(available.toFloat()).toOffset()
} else {
Offset.Zero
}
}
override suspend fun onPreFling(available: Velocity): Velocity {
val toFling = Offset(available.x, available.y).toFloat()
return if (toFling > 0) {
performFling(velocity = toFling)
// since we go to the anchor with tween settling, consume all for the best UX
// available
Velocity.Zero
} else {
Velocity.Zero
}
}
override suspend fun onPostFling(
consumed: Velocity,
available: Velocity,
): Velocity {
performFling(velocity = Offset(available.x, available.y).toFloat())
return Velocity.Zero
}
private fun Float.toOffset(): Offset = Offset(0f, this)
private fun Offset.toFloat(): Float = this.y
}

View File

@ -24,7 +24,6 @@ import androidx.compose.material.MaterialTheme
import androidx.compose.material.SnackbarResult
import androidx.compose.material.Text
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -276,12 +275,14 @@ fun ForumThreadListPage(
forumId: Long,
forumName: String,
isGood: Boolean = false,
lazyListState: LazyListState = rememberLazyListState(),
viewModel: ForumThreadListViewModel = if (isGood) pageViewModel<GoodThreadListViewModel>() else pageViewModel<LatestThreadListViewModel>()
) {
val context = LocalContext.current
val navigator = LocalNavigator.current
val snackbarHostState = LocalSnackbarHostState.current
val lazyListState = rememberLazyListState()
LazyLoad(loaded = viewModel.initialized) {
viewModel.send(getFirstLoadIntent(context, forumName, isGood))
viewModel.initialized = true
@ -356,7 +357,7 @@ fun ForumThreadListPage(
refreshing = isRefreshing,
onRefresh = { viewModel.send(getRefreshIntent(context, forumName, isGood)) }
)
Box(modifier = Modifier.pullRefresh(pullRefreshState)) {
Box {
LoadMoreLayout(
isLoading = isLoadingMore,
onLoadMore = {

View File

@ -134,8 +134,8 @@
<string name="button_signed">已签到</string>
<string name="button_like">关注</string>
<string name="toast_like_success">关注成功,你是第 %s 个关注本吧的</string>
<string name="tab_forum_1">查看贴子</string>
<string name="tab_forum_good">吧内精华</string>
<string name="tab_forum_latest">最新</string>
<string name="tab_forum_good">精华</string>
<string name="tip_good"></string>
<string name="title_my_message">我的消息</string>
<string name="title_messages">消息</string>
@ -745,4 +745,6 @@
<string name="title_forum_intro">本吧简介</string>
<string name="title_forum_rule">本吧吧规</string>
<string name="desc_forum_rule">吧规</string>
<string name="pull_down_to_refresh">继续下拉以刷新</string>
<string name="release_to_refresh">松手刷新</string>
</resources>