mirror of
https://github.com/topjohnwu/Magisk.git
synced 2026-03-12 21:23:02 -07:00
Implement floating pill-shaped navigation bar
Replace the standard miuix NavigationBar with a custom floating pill-shaped bar built from scratch. The miuix NavigationBar draws its own background internally which defeats external clipping, so the floating bar uses a Row with custom FloatingNavItem composables instead. The bar has RoundedCornerShape(28dp), shadow elevation, 24dp horizontal margin, and sits above the system gesture bar. Add bottom content padding to all 5 main tab screens to account for the overlay. Made-with: Cursor
This commit is contained in:
@@ -1,52 +1,63 @@
|
||||
package com.topjohnwu.magisk.ui
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
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.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
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.draw.shadow
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.arch.AsyncLoadViewModel
|
||||
import com.topjohnwu.magisk.arch.VMFactory
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.model.module.LocalModule
|
||||
import com.topjohnwu.magisk.ui.deny.DenyListScreen
|
||||
import com.topjohnwu.magisk.ui.deny.DenyListViewModel
|
||||
import com.topjohnwu.magisk.ui.flash.FlashScreen
|
||||
import com.topjohnwu.magisk.ui.flash.FlashViewModel
|
||||
import com.topjohnwu.magisk.ui.home.HomeScreen
|
||||
import com.topjohnwu.magisk.ui.home.HomeViewModel
|
||||
import com.topjohnwu.magisk.ui.install.InstallScreen
|
||||
import com.topjohnwu.magisk.ui.install.InstallViewModel
|
||||
import com.topjohnwu.magisk.ui.log.LogScreen
|
||||
import com.topjohnwu.magisk.ui.log.LogViewModel
|
||||
import com.topjohnwu.magisk.ui.module.ActionScreen
|
||||
import com.topjohnwu.magisk.ui.module.ActionViewModel
|
||||
import com.topjohnwu.magisk.ui.module.ModuleScreen
|
||||
import com.topjohnwu.magisk.ui.module.ModuleViewModel
|
||||
import com.topjohnwu.magisk.ui.navigation.CollectNavEvents
|
||||
import com.topjohnwu.magisk.ui.navigation.LocalNavigator
|
||||
import com.topjohnwu.magisk.ui.navigation.Navigator
|
||||
import com.topjohnwu.magisk.ui.navigation.ObserveViewEvents
|
||||
import com.topjohnwu.magisk.ui.navigation.Route
|
||||
import com.topjohnwu.magisk.ui.settings.SettingsScreen
|
||||
import com.topjohnwu.magisk.ui.settings.SettingsViewModel
|
||||
import com.topjohnwu.magisk.ui.superuser.SuperuserScreen
|
||||
import com.topjohnwu.magisk.ui.superuser.SuperuserViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import top.yukonga.miuix.kmp.basic.Icon
|
||||
import top.yukonga.miuix.kmp.basic.NavigationBar
|
||||
import top.yukonga.miuix.kmp.basic.NavigationBarItem
|
||||
import top.yukonga.miuix.kmp.basic.NavigationItem
|
||||
import top.yukonga.miuix.kmp.basic.Text
|
||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
||||
import com.topjohnwu.magisk.core.R as CoreR
|
||||
|
||||
enum class Tab(val titleRes: Int, val iconRes: Int) {
|
||||
@@ -61,19 +72,11 @@ enum class Tab(val titleRes: Int, val iconRes: Int) {
|
||||
fun MainScreen(initialTab: Int = 0) {
|
||||
val navigator = LocalNavigator.current
|
||||
val pagerState = rememberPagerState(initialPage = initialTab, pageCount = { Tab.entries.size })
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val items = Tab.entries.map { tab ->
|
||||
NavigationItem(
|
||||
label = stringResource(tab.titleRes),
|
||||
icon = ImageVector.vectorResource(tab.iconRes),
|
||||
)
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier.weight(1f),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
beyondViewportPageCount = Tab.entries.size - 1,
|
||||
userScrollEnabled = true,
|
||||
) { page ->
|
||||
@@ -109,25 +112,95 @@ fun MainScreen(initialTab: Int = 0) {
|
||||
}
|
||||
}
|
||||
|
||||
NavigationBar {
|
||||
items.forEachIndexed { index, item ->
|
||||
val tab = Tab.entries[index]
|
||||
val enabled = when (tab) {
|
||||
Tab.SUPERUSER -> Info.showSuperUser
|
||||
Tab.MODULES -> Info.env.isActive && LocalModule.loaded()
|
||||
else -> true
|
||||
}
|
||||
NavigationBarItem(
|
||||
modifier = Modifier.weight(1f),
|
||||
icon = item.icon,
|
||||
label = item.label,
|
||||
selected = pagerState.currentPage == index,
|
||||
enabled = enabled,
|
||||
onClick = {
|
||||
scope.launch { pagerState.animateScrollToPage(index) }
|
||||
}
|
||||
)
|
||||
FloatingNavigationBar(
|
||||
pagerState = pagerState,
|
||||
modifier = Modifier.align(Alignment.BottomCenter)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FloatingNavigationBar(
|
||||
pagerState: PagerState,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val shape = RoundedCornerShape(28.dp)
|
||||
val navBarInset = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.padding(bottom = navBarInset + 12.dp, start = 24.dp, end = 24.dp)
|
||||
.shadow(elevation = 6.dp, shape = shape)
|
||||
.clip(shape)
|
||||
.background(MiuixTheme.colorScheme.surfaceContainer)
|
||||
.fillMaxWidth()
|
||||
.height(64.dp)
|
||||
.padding(horizontal = 4.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Tab.entries.forEachIndexed { index, tab ->
|
||||
val selected = pagerState.currentPage == index
|
||||
val enabled = when (tab) {
|
||||
Tab.SUPERUSER -> Info.showSuperUser
|
||||
Tab.MODULES -> Info.env.isActive && LocalModule.loaded()
|
||||
else -> true
|
||||
}
|
||||
FloatingNavItem(
|
||||
icon = ImageVector.vectorResource(tab.iconRes),
|
||||
label = stringResource(tab.titleRes),
|
||||
selected = selected,
|
||||
enabled = enabled,
|
||||
onClick = { scope.launch { pagerState.animateScrollToPage(index) } },
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FloatingNavItem(
|
||||
icon: ImageVector,
|
||||
label: String,
|
||||
selected: Boolean,
|
||||
enabled: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val contentColor by animateColorAsState(
|
||||
targetValue = when {
|
||||
!enabled -> MiuixTheme.colorScheme.disabledOnSecondaryVariant
|
||||
selected -> MiuixTheme.colorScheme.primary
|
||||
else -> MiuixTheme.colorScheme.onSurfaceVariantActions
|
||||
},
|
||||
animationSpec = tween(200),
|
||||
label = "navItemColor"
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.clickable(
|
||||
enabled = enabled,
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
role = Role.Tab,
|
||||
onClick = onClick,
|
||||
),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = label,
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = contentColor,
|
||||
)
|
||||
Spacer(Modifier.height(2.dp))
|
||||
Text(
|
||||
text = label,
|
||||
fontSize = 11.sp,
|
||||
color = contentColor,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ fun HomeScreen(viewModel: HomeViewModel) {
|
||||
.padding(padding)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(bottom = 16.dp),
|
||||
.padding(bottom = 88.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
if (uiState.isNoticeVisible) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.topjohnwu.magisk.ui.log
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -115,6 +116,7 @@ private fun SuLogTab(logs: List<SuLog>, onClear: () -> Unit) {
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(horizontal = 12.dp),
|
||||
contentPadding = PaddingValues(bottom = 88.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
item { Spacer(Modifier.height(4.dp)) }
|
||||
@@ -217,6 +219,7 @@ private fun MagiskLogTab(log: String, onSave: () -> Unit, onClear: () -> Unit) {
|
||||
.horizontalScroll(rememberScrollState())
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(12.dp)
|
||||
.padding(bottom = 76.dp)
|
||||
) {
|
||||
Text(
|
||||
text = log,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.topjohnwu.magisk.ui.module
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -61,6 +62,7 @@ fun ModuleScreen(viewModel: ModuleViewModel) {
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.padding(horizontal = 12.dp),
|
||||
contentPadding = PaddingValues(bottom = 88.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
item { Spacer(Modifier.height(4.dp)) }
|
||||
|
||||
@@ -63,7 +63,7 @@ fun SettingsScreen(viewModel: SettingsViewModel) {
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(padding)
|
||||
.padding(horizontal = 12.dp)
|
||||
.padding(bottom = 16.dp)
|
||||
.padding(bottom = 88.dp)
|
||||
) {
|
||||
CustomizationSection(viewModel)
|
||||
Spacer(Modifier.height(12.dp))
|
||||
|
||||
@@ -5,6 +5,7 @@ import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -85,6 +86,7 @@ fun SuperuserScreen(viewModel: SuperuserViewModel) {
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.padding(horizontal = 12.dp),
|
||||
contentPadding = PaddingValues(bottom = 88.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
item { Spacer(Modifier.height(4.dp)) }
|
||||
|
||||
Reference in New Issue
Block a user