Navigation bars

This commit is contained in:
tom5079
2024-02-20 11:52:52 -08:00
parent 72b0fa78bb
commit b0fedd78fb
11 changed files with 433 additions and 63 deletions

View File

@@ -92,9 +92,12 @@ dependencies {
implementation 'androidx.compose.material:material-icons-extended'
implementation 'androidx.activity:activity-compose:1.8.2'
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0'
implementation 'androidx.lifecycle:lifecycle-runtime-compose:2.7.0'
implementation "com.google.accompanist:accompanist-adaptive:0.34.0"
implementation "androidx.navigation:navigation-compose:2.7.7"
kapt 'androidx.lifecycle:lifecycle-compiler:2.7.0'
implementation "androidx.paging:paging-compose:3.2.1"
implementation "io.ktor:ktor-client-core:2.3.8"

View File

@@ -24,8 +24,10 @@ import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.accompanist.adaptive.calculateDisplayFeatures
import xyz.quaver.pupil.ui.composable.PupilApp
import xyz.quaver.pupil.ui.composable.MainApp
import xyz.quaver.pupil.ui.theme.AppTheme
import xyz.quaver.pupil.ui.viewmodel.MainViewModel
@@ -42,10 +44,13 @@ class MainActivity : BaseActivity() {
val windowSize = calculateWindowSizeClass(this)
val displayFeatures = calculateDisplayFeatures(this)
PupilApp(
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
MainApp(
windowSize = windowSize,
displayFeatures = displayFeatures,
uiState = viewModel.uiState
uiState = uiState,
navigateToDestination = viewModel::navigateToDestination
)
}
}

View File

@@ -0,0 +1,10 @@
package xyz.quaver.pupil.ui.composable
import androidx.compose.runtime.Composable
@Composable
fun GalleryList(
) {
}

View File

@@ -1,6 +1,14 @@
package xyz.quaver.pupil.ui.composable
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.PermanentNavigationDrawer
import androidx.compose.material3.rememberDrawerState
import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass
@@ -8,16 +16,18 @@ import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.navigation.compose.rememberNavController
import androidx.compose.ui.Modifier
import androidx.window.layout.DisplayFeature
import androidx.window.layout.FoldingFeature
import kotlinx.coroutines.launch
import xyz.quaver.pupil.ui.viewmodel.MainUIState
@Composable
fun PupilApp(
fun MainApp(
windowSize: WindowSizeClass,
displayFeatures: List<DisplayFeature>,
uiState: MainUIState
uiState: MainUIState,
navigateToDestination: (MainDestination) -> Unit
) {
val navigationType: NavigationType
val contentType: ContentType
@@ -31,7 +41,7 @@ fun PupilApp(
when (windowSize.widthSizeClass) {
WindowWidthSizeClass.Compact -> {
navigationType = NavigationType.NAVIGATION_RAIL
navigationType = NavigationType.BOTTOM_NAVIGATION
contentType = ContentType.SINGLE_PANE
}
WindowWidthSizeClass.Medium -> {
@@ -51,7 +61,7 @@ fun PupilApp(
contentType = ContentType.DUAL_PANE
}
else -> {
navigationType = NavigationType.NAVIGATION_RAIL
navigationType = NavigationType.BOTTOM_NAVIGATION
contentType = ContentType.SINGLE_PANE
}
}
@@ -63,30 +73,113 @@ fun PupilApp(
else -> NavigationContentPosition.TOP
}
PupilNavigationWrapper(
MainNavigationWrapper(
navigationType,
contentType,
navigationContentPosition
displayFeatures,
navigationContentPosition,
uiState,
navigateToDestination
)
}
@Composable
private fun PupilNavigationWrapper(
private fun MainNavigationWrapper(
navigationType: NavigationType,
contentType: ContentType,
navigationContentPosition: NavigationContentPosition
displayFeatures: List<DisplayFeature>,
navigationContentPosition: NavigationContentPosition,
uiState: MainUIState,
navigateToDestination: (MainDestination) -> Unit
) {
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val coroutineScope = rememberCoroutineScope()
val openDrawer: () -> Unit = {
coroutineScope.launch {
drawerState.open()
}
}
if (navigationType == NavigationType.PERMANENT_NAVIGATION_DRAWER) {
PermanentNavigationDrawer(drawerContent = {
PermanentNavigationDrawerContent(
navigationContentPosition = navigationContentPosition
selectedDestination = uiState.currentDestination,
navigationContentPosition = navigationContentPosition,
navigateToDestination = navigateToDestination
)
}) {
// PupilMain()
MainContent(
navigationType = navigationType,
contentType = contentType,
displayFeatures = displayFeatures,
navigationContentPosition = navigationContentPosition,
uiState = uiState,
navigateToDestination = navigateToDestination,
onDrawerClicked = openDrawer
)
}
} else {
ModalNavigationDrawer(
drawerContent = {
ModalNavigationDrawerContent(
selectedDestination = uiState.currentDestination,
navigationContentPosition = navigationContentPosition,
navigateToDestination = navigateToDestination,
onDrawerClicked = {
coroutineScope.launch {
drawerState.close()
}
}
)
},
drawerState = drawerState
) {
MainContent(
navigationType = navigationType,
contentType = contentType,
displayFeatures = displayFeatures,
navigationContentPosition = navigationContentPosition,
uiState = uiState,
navigateToDestination = navigateToDestination,
onDrawerClicked = openDrawer
)
}
}
}
@Composable
fun MainContent(
navigationType: NavigationType,
contentType: ContentType,
displayFeatures: List<DisplayFeature>,
navigationContentPosition: NavigationContentPosition,
uiState: MainUIState,
navigateToDestination: (MainDestination) -> Unit,
onDrawerClicked: () -> Unit
) {
Row(modifier = Modifier.fillMaxSize()) {
AnimatedVisibility(visible = navigationType == NavigationType.NAVIGATION_RAIL) {
MainNavigationRail(
selectedDestination = uiState.currentDestination,
navigationContentPosition = navigationContentPosition,
navigateToDestination = navigateToDestination,
onDrawerClicked = onDrawerClicked
)
}
Column(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.inverseOnSurface)
) {
Box(modifier = Modifier.weight(1f))
AnimatedVisibility(visible = navigationType == NavigationType.BOTTOM_NAVIGATION) {
BottomNavigationBar(
selectedDestination = uiState.currentDestination,
navigateToDestination = navigateToDestination
)
}
}
}
}

View File

@@ -2,37 +2,46 @@ package xyz.quaver.pupil.ui.composable
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.History
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Star
import androidx.compose.ui.graphics.vector.ImageVector
import xyz.quaver.pupil.R
data class MainDestination(
val route: String,
val icon: ImageVector,
sealed interface MainDestination {
val route: String
val icon: ImageVector
val textId: Int
)
object Search: MainDestination {
override val route = "search"
override val icon = Icons.Default.Search
override val textId = R.string.main_destination_search
}
object History: MainDestination {
override val route = "history"
override val icon = Icons.Default.History
override val textId = R.string.main_destination_history
}
object Downloads: MainDestination {
override val route = "downloads"
override val icon = Icons.Default.Download
override val textId = R.string.main_destination_downloads
}
object Favorites: MainDestination {
override val route = "favorites"
override val icon = Icons.Default.Favorite
override val textId = R.string.main_destination_favorites
}
}
val mainDestinations = listOf(
MainDestination(
"search",
Icons.Default.Search,
R.string.main_destination_search
),
MainDestination(
"history",
Icons.Default.History,
R.string.main_destination_history
),
MainDestination(
"downloads",
Icons.Default.Download,
R.string.main_destination_downloads
),
MainDestination(
"favorites",
Icons.Default.Star,
R.string.main_destination_favorites
),
MainDestination.Search,
MainDestination.History,
MainDestination.Downloads,
MainDestination.Favorites
)

View File

@@ -4,56 +4,289 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
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.layout.size
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.MenuOpen
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.MenuOpen
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.NavigationDrawerItem
import androidx.compose.material3.NavigationDrawerItemDefaults
import androidx.compose.material3.NavigationRail
import androidx.compose.material3.NavigationRailItem
import androidx.compose.material3.PermanentDrawerSheet
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasurePolicy
import androidx.compose.ui.layout.layout
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.offset
import xyz.quaver.pupil.R
@Composable
fun PermanentNavigationDrawerContent(
navigationContentPosition: NavigationContentPosition
selectedDestination: MainDestination,
navigationContentPosition: NavigationContentPosition,
navigateToDestination: (MainDestination) -> Unit
) {
PermanentDrawerSheet(
modifier = Modifier.sizeIn(minWidth = 200.dp, maxWidth = 300.dp),
drawerContainerColor = MaterialTheme.colorScheme.inverseOnSurface
) {
Column(
Layout(
modifier = Modifier
.background(MaterialTheme.colorScheme.inverseOnSurface)
.padding(16.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
.padding(16.dp),
content = {
Row(
modifier = Modifier.layoutId(LayoutType.HEADER),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
modifier = Modifier.size(32.dp),
painter = painterResource(R.drawable.app_icon),
tint = Color.Unspecified,
contentDescription = "app icon"
)
Text(
modifier = Modifier.padding(16.dp),
text = "Pupil",
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold),
color = MaterialTheme.colorScheme.primary
)
}
Column(
modifier = Modifier
.layoutId(LayoutType.CONTENT)
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally
) {
mainDestinations.forEach { destination ->
NavigationDrawerItem(
label = {
Text(
text = stringResource(destination.textId),
modifier = Modifier.padding(16.dp)
)
},
icon = {
Icon(
imageVector = destination.icon,
contentDescription = stringResource(destination.textId)
)
},
selected = selectedDestination.route == destination.route,
colors = NavigationDrawerItemDefaults.colors(
unselectedContainerColor = Color.Transparent
),
onClick = { navigateToDestination(destination) }
)
}
}
},
measurePolicy = navigationMeasurePolicy(navigationContentPosition)
)
}
}
@Composable
fun ModalNavigationDrawerContent(
selectedDestination: MainDestination,
navigationContentPosition: NavigationContentPosition,
navigateToDestination: (MainDestination) -> Unit,
onDrawerClicked: () -> Unit
) {
ModalDrawerSheet {
Layout(
modifier = Modifier
.background(MaterialTheme.colorScheme.inverseOnSurface)
.padding(16.dp),
content = {
Row(
modifier = Modifier
.layoutId(LayoutType.HEADER)
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
modifier = Modifier.size(32.dp),
painter = painterResource(R.drawable.app_icon),
tint = Color.Unspecified,
contentDescription = "app icon"
)
Text(
modifier = Modifier.padding(16.dp),
text = "Pupil",
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold),
color = MaterialTheme.colorScheme.primary
)
}
IconButton(onClick = onDrawerClicked) {
Icon(
imageVector = Icons.AutoMirrored.Default.MenuOpen,
contentDescription = stringResource(R.string.main_open_navigation_drawer)
)
}
}
Column (
modifier = Modifier
.layoutId(LayoutType.CONTENT)
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally
) {
mainDestinations.forEach { destination ->
NavigationDrawerItem(
label = {
Text(
text = stringResource(destination.textId),
modifier = Modifier.padding(16.dp)
)
},
icon = {
Icon(
imageVector = destination.icon,
contentDescription = stringResource(destination.textId)
)
},
selected = selectedDestination.route == destination.route,
colors = NavigationDrawerItemDefaults.colors(
unselectedContainerColor = Color.Transparent
),
onClick = { navigateToDestination(destination) }
)
}
}
},
measurePolicy = navigationMeasurePolicy(navigationContentPosition)
)
}
}
@Composable
fun MainNavigationRail(
selectedDestination: MainDestination,
navigationContentPosition: NavigationContentPosition,
navigateToDestination: (MainDestination) -> Unit,
onDrawerClicked: () -> Unit
) {
NavigationRail (
modifier = Modifier.fillMaxHeight(),
containerColor = MaterialTheme.colorScheme.inverseOnSurface
) {
NavigationRailItem(
selected = false,
onClick = onDrawerClicked,
icon = {
Icon(
modifier = Modifier.size(32.dp),
painter = painterResource(R.drawable.app_icon),
tint = Color.Unspecified,
contentDescription = "app icon"
imageVector = Icons.Default.Menu,
contentDescription = stringResource(R.string.main_open_navigation_drawer)
)
Text(
modifier = Modifier.padding(16.dp),
text = "Pupil",
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold),
color = MaterialTheme.colorScheme.primary
}
)
Column(
modifier = Modifier.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
mainDestinations.forEach { destination ->
NavigationRailItem(
selected = selectedDestination.route == destination.route,
onClick = { navigateToDestination(destination) },
icon = {
Icon(
imageVector = destination.icon,
contentDescription = stringResource(destination.textId)
)
}
)
}
}
Column(
}
}
) {
Text("Help")
@Composable
fun BottomNavigationBar(
selectedDestination: MainDestination,
navigateToDestination: (MainDestination) -> Unit
) {
NavigationBar(modifier = Modifier.fillMaxWidth()) {
mainDestinations.forEach { destination ->
NavigationBarItem(
selected = selectedDestination.route == destination.route,
onClick = { navigateToDestination(destination) },
icon = {
Icon(
imageVector = destination.icon,
contentDescription = stringResource(destination.textId)
)
}
)
}
}
}
}
fun navigationMeasurePolicy(
navigationContentPosition: NavigationContentPosition,
): MeasurePolicy {
return MeasurePolicy { measurables, constraints ->
lateinit var headerMeasurable: Measurable
lateinit var contentMeasurable: Measurable
measurables.forEach {
when (it.layoutId) {
LayoutType.HEADER -> headerMeasurable = it
LayoutType.CONTENT -> contentMeasurable = it
else -> error("Unknown layoutId encountered!")
}
}
val headerPlaceable = headerMeasurable.measure(constraints)
val contentPlaceable = contentMeasurable.measure(
constraints.offset(vertical = -headerPlaceable.height)
)
layout(constraints.maxWidth, constraints.maxHeight) {
headerPlaceable.placeRelative(0, 0)
val nonContentVerticalSpace = constraints.maxHeight - contentPlaceable.height
val contentPlaceableY = when (navigationContentPosition) {
NavigationContentPosition.TOP -> 0
NavigationContentPosition.CENTER -> nonContentVerticalSpace / 2
}.coerceAtLeast(headerPlaceable.height)
contentPlaceable.placeRelative(0, contentPlaceableY)
}
}
}
enum class LayoutType {
HEADER, CONTENT
}

View File

@@ -1,5 +1,5 @@
package xyz.quaver.pupil.ui.composable
enum class NavigationType {
NAVIGATION_RAIL, PERMANENT_NAVIGATION_DRAWER
NAVIGATION_RAIL, PERMANENT_NAVIGATION_DRAWER, BOTTOM_NAVIGATION
}

View File

@@ -1,14 +1,25 @@
package xyz.quaver.pupil.ui.viewmodel
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import xyz.quaver.pupil.networking.SearchQuery
import xyz.quaver.pupil.ui.composable.MainRoutes
import xyz.quaver.pupil.ui.composable.MainDestination
import xyz.quaver.pupil.ui.composable.mainDestinations
class MainViewModel : ViewModel() {
val uiState: MainUIState = MainUIState()
private val _uiState = MutableStateFlow(MainUIState())
val uiState: StateFlow<MainUIState> = _uiState
fun navigateToDestination(destination: MainDestination) {
_uiState.value = MainUIState(
currentDestination = destination
)
}
}
data class MainUIState(
val route: MainRoutes = MainRoutes.SEARCH,
val query: SearchQuery? = null
val currentDestination: MainDestination = mainDestinations.first(),
val query: SearchQuery? = null,
val loading: Boolean = true
)

View File

@@ -33,6 +33,7 @@
<string name="default_query_dialog_filter_guro">グロフィルター</string>
<string name="default_query_dialog_language">"言語: "</string>
<string name="default_query_dialog_title">デフォルトキーワード設定</string>
<string name="main_open_navigation_drawer">メニューを開く</string>
<string name="main_drawer_group_contact_title">お問い合わせ先</string>
<string name="main_drawer_group_contact_homepage">ホームページ</string>
<string name="main_drawer_group_contact_help">ヘルプ</string>
@@ -159,4 +160,5 @@
<string name="unaccessible_download_folder">アンドロイド11以上では外部からのアプリ内部空間接近が不可能です。ダウンロードフォルダを変更しますか</string>
<string name="settings_networking">ネットワーク</string>
<string name="settings_recover_downloads">ダウンロードデータベースを再構築</string>
<string name="main_close_navigation_drawer">メニューを閉じる</string>
</resources>

View File

@@ -36,6 +36,7 @@
<string name="main_drawer_group_contact_github">Github</string>
<string name="main_drawer_group_contact_help">도움말</string>
<string name="main_drawer_group_contact_homepage">홈페이지</string>
<string name="main_open_navigation_drawer">메뉴 열기</string>
<string name="main_drawer_group_contact_title">문의</string>
<string name="reader_fab_fullscreen">전체 화면</string>
<string name="channel_download">다운로드</string>
@@ -159,4 +160,5 @@
<string name="unaccessible_download_folder">안드로이드 11 이상에서는 외부에서 현재 다운로드 폴더에 접근할 수 없습니다. 변경하시겠습니까?</string>
<string name="settings_networking">네트워크</string>
<string name="settings_recover_downloads">다운로드 데이터베이스 복구</string>
<string name="main_close_navigation_drawer">메뉴 닫기</string>
</resources>

View File

@@ -57,6 +57,8 @@
<string name="main_destination_history">History</string>
<string name="main_destination_downloads">Downloads</string>
<string name="main_destination_favorites">Favorites</string>
<string name="main_open_navigation_drawer">Open Navigation Drawer</string>
<string name="main_close_navigation_drawer">Close Navigation Drawer</string>
<string name="main_drawer_group_contact_title">Contact</string>
<string name="main_drawer_group_contact_help">Help</string>
<string name="main_drawer_group_contact_homepage">Visit homepage</string>