manager: Add visibility state management for floating action buttons

- Get change button style from Apatch
This commit is contained in:
ShirkNeko
2025-06-15 18:32:17 +08:00
parent 730d58f18b
commit 75e56038ec
4 changed files with 145 additions and 34 deletions

View File

@@ -0,0 +1,52 @@
package com.sukisu.ultra.ui.component
import android.annotation.SuppressLint
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.*
@SuppressLint("AutoboxingStateCreation")
@Composable
fun rememberFabVisibilityState(listState: LazyListState): State<Boolean> {
var previousScrollOffset by remember { mutableStateOf(0) }
var previousIndex by remember { mutableStateOf(0) }
val fabVisible = remember { mutableStateOf(true) }
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset }
.collect { (index, offset) ->
if (previousIndex == 0 && previousScrollOffset == 0) {
fabVisible.value = true
} else {
val isScrollingDown = when {
index > previousIndex -> false
index < previousIndex -> true
else -> offset < previousScrollOffset
}
fabVisible.value = isScrollingDown
}
previousIndex = index
previousScrollOffset = offset
}
}
return fabVisible
}
@Composable
fun AnimatedFab(
visible: Boolean,
content: @Composable () -> Unit
) {
AnimatedVisibility(
visible = visible,
enter = fadeIn(),
exit = fadeOut()
) {
content()
}
}

View File

@@ -5,9 +5,13 @@ import android.content.Intent
import android.util.Log import android.util.Log
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.*
import androidx.compose.animation.core.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
import androidx.compose.material3.* import androidx.compose.material3.*
@@ -38,6 +42,7 @@ import java.io.FileInputStream
import java.io.InputStreamReader import java.io.InputStreamReader
import java.net.* import java.net.*
import android.app.Activity import android.app.Activity
import androidx.compose.ui.res.painterResource
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
/** /**
@@ -57,6 +62,9 @@ fun KpmScreen(
val snackBarHost = remember { SnackbarHostState() } val snackBarHost = remember { SnackbarHostState() }
val confirmDialog = rememberConfirmDialog() val confirmDialog = rememberConfirmDialog()
val listState = rememberLazyListState()
val fabVisible by rememberFabVisibilityState(listState)
val moduleConfirmContentMap = viewModel.moduleList.associate { module -> val moduleConfirmContentMap = viewModel.moduleList.associate { module ->
val moduleFileName = module.id val moduleFileName = module.id
module.id to stringResource(R.string.confirm_uninstall_content, moduleFileName) module.id to stringResource(R.string.confirm_uninstall_content, moduleFileName)
@@ -283,28 +291,29 @@ fun KpmScreen(
) )
}, },
floatingActionButton = { floatingActionButton = {
ExtendedFloatingActionButton( AnimatedFab(visible = fabVisible) {
onClick = { FloatingActionButton(
selectPatchLauncher.launch( contentColor = MaterialTheme.colorScheme.onPrimary,
Intent(Intent.ACTION_GET_CONTENT).apply { containerColor = MaterialTheme.colorScheme.primary,
type = "application/octet-stream" onClick = {
} selectPatchLauncher.launch(
) Intent(Intent.ACTION_GET_CONTENT).apply {
}, type = "application/octet-stream"
icon = { }
Icon( )
imageVector = Icons.Filled.Add, },
contentDescription = stringResource(R.string.kpm_install), content = {
) Icon(
}, painter = painterResource(id = R.drawable.package_import),
text = { contentDescription = null
Text( )
text = stringResource(R.string.kpm_install), }
) )
}, }
expanded = true,
)
}, },
contentWindowInsets = WindowInsets.safeDrawing.only(
WindowInsetsSides.Top + WindowInsetsSides.Horizontal
),
snackbarHost = { SnackbarHost(snackBarHost) } snackbarHost = { SnackbarHost(snackBarHost) }
) { padding -> ) { padding ->
Column(modifier = Modifier.padding(padding)) { Column(modifier = Modifier.padding(padding)) {
@@ -378,6 +387,7 @@ fun KpmScreen(
} }
} else { } else {
LazyColumn( LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 16.dp), contentPadding = PaddingValues(horizontal = 16.dp, vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)

View File

@@ -1,5 +1,6 @@
package com.sukisu.ultra.ui.screen package com.sukisu.ultra.ui.screen
import android.annotation.SuppressLint
import android.app.Activity.* import android.app.Activity.*
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@@ -15,10 +16,12 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.selection.toggleable import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
@@ -36,6 +39,7 @@ import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.* import androidx.compose.ui.platform.*
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@@ -81,6 +85,8 @@ import com.dergoogler.mmrl.platform.Platform
import androidx.core.net.toUri import androidx.core.net.toUri
import com.dergoogler.mmrl.platform.model.ModuleConfig import com.dergoogler.mmrl.platform.model.ModuleConfig
import com.dergoogler.mmrl.platform.model.ModuleConfig.Companion.asModuleConfig import com.dergoogler.mmrl.platform.model.ModuleConfig.Companion.asModuleConfig
import com.sukisu.ultra.ui.component.AnimatedFab
import com.sukisu.ultra.ui.component.rememberFabVisibilityState
import com.sukisu.ultra.ui.theme.getCardElevation import com.sukisu.ultra.ui.theme.getCardElevation
// 菜单项数据类 // 菜单项数据类
@@ -94,6 +100,7 @@ data class ModuleBottomSheetMenuItem(
* @author ShirkNeko * @author ShirkNeko
* @date 2025/5/31. * @date 2025/5/31.
*/ */
@SuppressLint("ResourceType", "AutoboxingStateCreation")
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Destination<RootGraph> @Destination<RootGraph>
@Composable @Composable
@@ -110,6 +117,8 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
skipPartiallyExpanded = true skipPartiallyExpanded = true
) )
var showBottomSheet by remember { mutableStateOf(false) } var showBottomSheet by remember { mutableStateOf(false) }
val listState = rememberLazyListState()
val fabVisible by rememberFabVisibilityState(listState)
val selectZipLauncher = rememberLauncherForActivityResult( val selectZipLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult() contract = ActivityResultContracts.StartActivityForResult()
@@ -272,9 +281,10 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
) )
}, },
floatingActionButton = { floatingActionButton = {
if (!hideInstallButton) { AnimatedFab(visible = !hideInstallButton && fabVisible) {
val moduleInstall = stringResource(id = R.string.module_install) FloatingActionButton(
ExtendedFloatingActionButton( contentColor = MaterialTheme.colorScheme.onPrimary,
containerColor = MaterialTheme.colorScheme.primary,
onClick = { onClick = {
selectZipLauncher.launch( selectZipLauncher.launch(
Intent(Intent.ACTION_GET_CONTENT).apply { Intent(Intent.ACTION_GET_CONTENT).apply {
@@ -283,18 +293,12 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
} }
) )
}, },
icon = { content = {
Icon( Icon(
imageVector = Icons.Filled.Add, painter = painterResource(id = R.drawable.package_import),
contentDescription = moduleInstall, contentDescription = null
) )
}, }
text = {
Text(
text = moduleInstall,
)
},
expanded = true,
) )
} }
}, },
@@ -334,6 +338,7 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
ModuleList( ModuleList(
navigator = navigator, navigator = navigator,
viewModel = viewModel, viewModel = viewModel,
listState = listState,
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
boxModifier = Modifier.padding(innerPadding), boxModifier = Modifier.padding(innerPadding),
onInstallModule = { onInstallModule = {
@@ -605,6 +610,7 @@ private fun ModuleBottomSheetMenuItemView(menuItem: ModuleBottomSheetMenuItem) {
private fun ModuleList( private fun ModuleList(
navigator: DestinationsNavigator, navigator: DestinationsNavigator,
viewModel: ModuleViewModel, viewModel: ModuleViewModel,
listState: LazyListState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
boxModifier: Modifier = Modifier, boxModifier: Modifier = Modifier,
onInstallModule: (Uri) -> Unit, onInstallModule: (Uri) -> Unit,
@@ -765,6 +771,7 @@ private fun ModuleList(
isRefreshing = viewModel.isRefreshing isRefreshing = viewModel.isRefreshing
) { ) {
LazyColumn( LazyColumn(
state = listState,
modifier = modifier, modifier = modifier,
verticalArrangement = Arrangement.spacedBy(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp),
contentPadding = remember { contentPadding = remember {

View File

@@ -0,0 +1,42 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,21l-8,-4.5v-9l8,-4.5l8,4.5v4.5"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:strokeColor="#ffff"
android:strokeLineCap="round"/>
<path
android:pathData="M12,12l8,-4.5"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:strokeColor="#ffff"
android:strokeLineCap="round"/>
<path
android:pathData="M12,12v9"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:strokeColor="#ffff"
android:strokeLineCap="round"/>
<path
android:pathData="M12,12l-8,-4.5"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:strokeColor="#ffff"
android:strokeLineCap="round"/>
<path
android:pathData="M22,18h-7"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:strokeColor="#ffff"
android:strokeLineCap="round"/>
<path
android:pathData="M18,15l-3,3l3,3"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:strokeColor="#ffff"
android:strokeLineCap="round"/>
</vector>