Add drawer menu.
This commit is contained in:
		
							parent
							
								
									01d635ca1a
								
							
						
					
					
						commit
						bb9e1b36f7
					
				
					 3 changed files with 204 additions and 61 deletions
				
			
		| 
						 | 
				
			
			@ -2,19 +2,28 @@ package com.pixelized.biblib.ui.composable.screen
 | 
			
		|||
 | 
			
		||||
import androidx.compose.animation.*
 | 
			
		||||
import androidx.compose.animation.core.tween
 | 
			
		||||
import androidx.compose.foundation.clickable
 | 
			
		||||
import androidx.compose.foundation.layout.*
 | 
			
		||||
import androidx.compose.foundation.lazy.rememberLazyListState
 | 
			
		||||
import androidx.compose.foundation.rememberScrollState
 | 
			
		||||
import androidx.compose.foundation.shape.CutCornerShape
 | 
			
		||||
import androidx.compose.material.*
 | 
			
		||||
import androidx.compose.material.icons.Icons
 | 
			
		||||
import androidx.compose.material.icons.filled.*
 | 
			
		||||
import androidx.compose.material.icons.sharp.ArrowBack
 | 
			
		||||
import androidx.compose.material.icons.sharp.LocalLibrary
 | 
			
		||||
import androidx.compose.material.icons.sharp.Menu
 | 
			
		||||
import androidx.compose.runtime.*
 | 
			
		||||
import androidx.compose.runtime.livedata.observeAsState
 | 
			
		||||
import androidx.compose.ui.Alignment
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.graphics.vector.ImageVector
 | 
			
		||||
import androidx.compose.ui.res.stringResource
 | 
			
		||||
import androidx.compose.ui.tooling.preview.Preview
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import androidx.lifecycle.viewmodel.compose.viewModel
 | 
			
		||||
import androidx.paging.compose.LazyPagingItems
 | 
			
		||||
import androidx.paging.compose.collectAsLazyPagingItems
 | 
			
		||||
import com.pixelized.biblib.BuildConfig
 | 
			
		||||
import com.pixelized.biblib.R
 | 
			
		||||
import com.pixelized.biblib.ui.composable.pages.DetailPageComposable
 | 
			
		||||
import com.pixelized.biblib.ui.composable.pages.HomePageComposable
 | 
			
		||||
| 
						 | 
				
			
			@ -26,6 +35,8 @@ import com.pixelized.biblib.ui.viewmodel.book.IBooksViewModel
 | 
			
		|||
import com.pixelized.biblib.ui.viewmodel.navigation.INavigationViewModel
 | 
			
		||||
import com.pixelized.biblib.ui.viewmodel.navigation.INavigationViewModel.Navigable.Page
 | 
			
		||||
import com.pixelized.biblib.ui.viewmodel.navigation.NavigationViewModel
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
import java.util.*
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@OptIn(ExperimentalAnimationApi::class)
 | 
			
		||||
| 
						 | 
				
			
			@ -34,96 +45,225 @@ fun MainScreenComposable(
 | 
			
		|||
    navigationViewModel: INavigationViewModel = viewModel<NavigationViewModel>(),
 | 
			
		||||
    booksViewModel: IBooksViewModel = viewModel<BooksViewModel>(),
 | 
			
		||||
) {
 | 
			
		||||
    val lazyBooks: LazyPagingItems<BookThumbnailUio> =
 | 
			
		||||
        booksViewModel.books.collectAsLazyPagingItems()
 | 
			
		||||
    val lazyListState = rememberLazyListState()
 | 
			
		||||
    val scrollableState = rememberScrollState()
 | 
			
		||||
    // navigation
 | 
			
		||||
    val page by navigationViewModel.page.observeAsState()
 | 
			
		||||
    // coroutine
 | 
			
		||||
    val coroutineScope = rememberCoroutineScope()
 | 
			
		||||
    // scaffold & toolbar
 | 
			
		||||
    val canNavigateBack = page !is Page.HomePage
 | 
			
		||||
    val scaffoldState = rememberScaffoldState()
 | 
			
		||||
 | 
			
		||||
    LaunchedEffect(key1 = "MainScreen", block = {
 | 
			
		||||
        navigationViewModel.navigateTo(Page.HomePage, true)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    Scaffold(
 | 
			
		||||
        topBar = { ToolbarComposable(navigationViewModel) },
 | 
			
		||||
        drawerContent = {}
 | 
			
		||||
    ) {
 | 
			
		||||
        AnimatedVisibility(
 | 
			
		||||
            visible = page is Page.HomePage,
 | 
			
		||||
            initiallyVisible = true,
 | 
			
		||||
            enter = slideInHorizontally(
 | 
			
		||||
                animationSpec = tween(Animation.MEDIUM_DURATION),
 | 
			
		||||
                initialOffsetX = { width -> -width }
 | 
			
		||||
            ),
 | 
			
		||||
            exit = slideOutHorizontally(
 | 
			
		||||
                animationSpec = tween(Animation.MEDIUM_DURATION),
 | 
			
		||||
                targetOffsetX = { width -> -width }
 | 
			
		||||
            ),
 | 
			
		||||
        ) {
 | 
			
		||||
            HomePageComposable(
 | 
			
		||||
                navigationViewModel,
 | 
			
		||||
                lazyListState,
 | 
			
		||||
                scrollableState,
 | 
			
		||||
                lazyBooks
 | 
			
		||||
        scaffoldState = scaffoldState,
 | 
			
		||||
        topBar = {
 | 
			
		||||
            ToolbarComposable(
 | 
			
		||||
                canNavigateBack,
 | 
			
		||||
                onBackPress = {
 | 
			
		||||
                    navigationViewModel.navigateBack()
 | 
			
		||||
                },
 | 
			
		||||
                onDrawerPress = {
 | 
			
		||||
                    coroutineScope.launch { scaffoldState.drawerState.open() }
 | 
			
		||||
                }
 | 
			
		||||
            )
 | 
			
		||||
        },
 | 
			
		||||
        drawerContent = { DrawerContentComposable() }
 | 
			
		||||
    ) {
 | 
			
		||||
        Box {
 | 
			
		||||
            val lazyBooks: LazyPagingItems<BookThumbnailUio> =
 | 
			
		||||
                booksViewModel.books.collectAsLazyPagingItems()
 | 
			
		||||
            val lazyListState = rememberLazyListState()
 | 
			
		||||
            val scrollableState = rememberScrollState()
 | 
			
		||||
 | 
			
		||||
            AnimatedVisibility(
 | 
			
		||||
                visible = page is Page.HomePage,
 | 
			
		||||
                initiallyVisible = true,
 | 
			
		||||
                enter = slideInHorizontally(
 | 
			
		||||
                    animationSpec = tween(Animation.MEDIUM_DURATION),
 | 
			
		||||
                    initialOffsetX = { width -> -width }
 | 
			
		||||
                ),
 | 
			
		||||
                exit = slideOutHorizontally(
 | 
			
		||||
                    animationSpec = tween(Animation.MEDIUM_DURATION),
 | 
			
		||||
                    targetOffsetX = { width -> -width }
 | 
			
		||||
                ),
 | 
			
		||||
            ) {
 | 
			
		||||
                HomePageComposable(
 | 
			
		||||
                    navigationViewModel,
 | 
			
		||||
                    lazyListState,
 | 
			
		||||
                    scrollableState,
 | 
			
		||||
                    lazyBooks
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        AnimatedVisibility(
 | 
			
		||||
            visible = page is Page.Detail,
 | 
			
		||||
            initiallyVisible = false,
 | 
			
		||||
            enter = slideInHorizontally(
 | 
			
		||||
                animationSpec = tween(Animation.MEDIUM_DURATION),
 | 
			
		||||
                initialOffsetX = { width -> width },
 | 
			
		||||
            ),
 | 
			
		||||
            exit = slideOutHorizontally(
 | 
			
		||||
                animationSpec = tween(Animation.MEDIUM_DURATION),
 | 
			
		||||
                targetOffsetX = { width -> width },
 | 
			
		||||
            ),
 | 
			
		||||
        ) {
 | 
			
		||||
            // Small trick to display the detail page during animation exit.
 | 
			
		||||
            var currentPage by remember { mutableStateOf<Page.Detail?>(null) }
 | 
			
		||||
            currentPage = page as? Page.Detail ?: currentPage
 | 
			
		||||
            currentPage?.let { DetailPageComposable(booksViewModel, it.bookId) }
 | 
			
		||||
        Box {
 | 
			
		||||
            AnimatedVisibility(
 | 
			
		||||
                visible = page is Page.Detail,
 | 
			
		||||
                initiallyVisible = false,
 | 
			
		||||
                enter = slideInHorizontally(
 | 
			
		||||
                    animationSpec = tween(Animation.MEDIUM_DURATION),
 | 
			
		||||
                    initialOffsetX = { width -> width },
 | 
			
		||||
                ),
 | 
			
		||||
                exit = slideOutHorizontally(
 | 
			
		||||
                    animationSpec = tween(Animation.MEDIUM_DURATION),
 | 
			
		||||
                    targetOffsetX = { width -> width },
 | 
			
		||||
                ),
 | 
			
		||||
            ) {
 | 
			
		||||
                // Small trick to display the detail page during animation exit.
 | 
			
		||||
                var currentPage by remember { mutableStateOf<Page.Detail?>(null) }
 | 
			
		||||
                currentPage = page as? Page.Detail ?: currentPage
 | 
			
		||||
                currentPage?.let { DetailPageComposable(booksViewModel, it.bookId) }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun ToolbarComposable(navigationViewModel: INavigationViewModel) {
 | 
			
		||||
fun ToolbarComposable(
 | 
			
		||||
    canNavigateBack: Boolean,
 | 
			
		||||
    onBackPress: () -> Unit,
 | 
			
		||||
    onDrawerPress: () -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    TopAppBar(
 | 
			
		||||
        title = { Text(stringResource(id = R.string.app_name)) },
 | 
			
		||||
        navigationIcon = { NavigationIcon(navigationViewModel) }
 | 
			
		||||
        title = {
 | 
			
		||||
            Text(stringResource(id = R.string.app_name))
 | 
			
		||||
        },
 | 
			
		||||
        navigationIcon = {
 | 
			
		||||
            NavigationIcon(
 | 
			
		||||
                canNavigateBack,
 | 
			
		||||
                onBackPress,
 | 
			
		||||
                onDrawerPress,
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun NavigationIcon(navigationViewModel: INavigationViewModel) {
 | 
			
		||||
    val page: Page? by navigationViewModel.page.observeAsState()
 | 
			
		||||
 | 
			
		||||
    Crossfade(targetState = page) {
 | 
			
		||||
        when (it) {
 | 
			
		||||
            is Page.HomePage -> IconButton(onClick = {}) {
 | 
			
		||||
                Icon(
 | 
			
		||||
                    imageVector = Icons.Sharp.LocalLibrary,
 | 
			
		||||
                    contentDescription = ""
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            else -> IconButton(onClick = {
 | 
			
		||||
                navigationViewModel.navigateBack()
 | 
			
		||||
            }) {
 | 
			
		||||
fun NavigationIcon(
 | 
			
		||||
    canNavigateBack: Boolean,
 | 
			
		||||
    onBackPress: () -> Unit,
 | 
			
		||||
    onDrawerPress: () -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    Crossfade(targetState = canNavigateBack) {
 | 
			
		||||
        if (it) {
 | 
			
		||||
            IconButton(onClick = onBackPress) {
 | 
			
		||||
                Icon(
 | 
			
		||||
                    imageVector = Icons.Sharp.ArrowBack,
 | 
			
		||||
                    contentDescription = "back"
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            IconButton(onClick = onDrawerPress) {
 | 
			
		||||
                Icon(
 | 
			
		||||
                    imageVector = Icons.Sharp.Menu,
 | 
			
		||||
                    contentDescription = "drawer"
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun DrawerContentComposable() {
 | 
			
		||||
    val typography = MaterialTheme.typography
 | 
			
		||||
    Column(
 | 
			
		||||
        modifier = Modifier
 | 
			
		||||
            .fillMaxWidth()
 | 
			
		||||
            .fillMaxHeight()
 | 
			
		||||
    ) {
 | 
			
		||||
        Card(
 | 
			
		||||
            shape = CutCornerShape(bottomEnd = 16.dp),
 | 
			
		||||
            elevation = 8.dp
 | 
			
		||||
        ) {
 | 
			
		||||
            Column(
 | 
			
		||||
                modifier = Modifier
 | 
			
		||||
                    .fillMaxWidth()
 | 
			
		||||
                    .padding(16.dp)
 | 
			
		||||
            ) {
 | 
			
		||||
                Text(
 | 
			
		||||
                    style = typography.body1,
 | 
			
		||||
                    text = "R. Daneel Olivaw"
 | 
			
		||||
                )
 | 
			
		||||
                Text(
 | 
			
		||||
                    style = typography.caption,
 | 
			
		||||
                    text = "r.daneel.olivaw@biblib.com"
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        Spacer(modifier = Modifier.height(8.dp))
 | 
			
		||||
        DrawerLink(
 | 
			
		||||
            text = "Nouveautés",
 | 
			
		||||
            imageVector = Icons.Default.NewReleases,
 | 
			
		||||
            onClick = {},
 | 
			
		||||
        )
 | 
			
		||||
        DrawerLink(
 | 
			
		||||
            text = "Livres",
 | 
			
		||||
            imageVector = Icons.Default.AutoStories,
 | 
			
		||||
            onClick = {},
 | 
			
		||||
        )
 | 
			
		||||
        DrawerLink(
 | 
			
		||||
            text = "Séries",
 | 
			
		||||
            imageVector = Icons.Default.AutoAwesomeMotion,
 | 
			
		||||
            onClick = {},
 | 
			
		||||
        )
 | 
			
		||||
        DrawerLink(
 | 
			
		||||
            text = "Auteurs",
 | 
			
		||||
            imageVector = Icons.Default.SupervisorAccount,
 | 
			
		||||
            onClick = {},
 | 
			
		||||
        )
 | 
			
		||||
        DrawerLink(
 | 
			
		||||
            text = "Étiquettes",
 | 
			
		||||
            imageVector = Icons.Default.LocalOffer,
 | 
			
		||||
            onClick = {},
 | 
			
		||||
        )
 | 
			
		||||
        Spacer(modifier = Modifier.weight(1f))
 | 
			
		||||
        Text(
 | 
			
		||||
            modifier = Modifier
 | 
			
		||||
                .align(Alignment.End)
 | 
			
		||||
                .padding(16.dp),
 | 
			
		||||
            style = typography.caption,
 | 
			
		||||
            text = stringResource(
 | 
			
		||||
                R.string.app_version,
 | 
			
		||||
                BuildConfig.BUILD_TYPE.toUpperCase(Locale.getDefault()),
 | 
			
		||||
                BuildConfig.VERSION_NAME,
 | 
			
		||||
                BuildConfig.VERSION_CODE
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun DrawerLink(text: String, imageVector: ImageVector, onClick: () -> Unit) {
 | 
			
		||||
    Row(
 | 
			
		||||
        verticalAlignment = Alignment.CenterVertically,
 | 
			
		||||
        modifier = Modifier
 | 
			
		||||
            .fillMaxWidth()
 | 
			
		||||
            .height(52.dp)
 | 
			
		||||
            .clickable { onClick() }) {
 | 
			
		||||
        Icon(
 | 
			
		||||
            modifier = Modifier.padding(horizontal = 16.dp),
 | 
			
		||||
            imageVector = imageVector,
 | 
			
		||||
            contentDescription = ""
 | 
			
		||||
        )
 | 
			
		||||
        Text(
 | 
			
		||||
            text = text,
 | 
			
		||||
        )
 | 
			
		||||
        Spacer(modifier = Modifier.weight(1f))
 | 
			
		||||
        Icon(
 | 
			
		||||
            modifier = Modifier.padding(end = 8.dp),
 | 
			
		||||
            imageVector = Icons.Default.NavigateNext,
 | 
			
		||||
            contentDescription = ""
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Preview
 | 
			
		||||
@Composable
 | 
			
		||||
fun ToolbarComposableDarkPreview() {
 | 
			
		||||
    BibLibTheme(darkTheme = false) {
 | 
			
		||||
        ToolbarComposable(navigationViewModel = INavigationViewModel.Mock())
 | 
			
		||||
        ToolbarComposable(false, {}, {})
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -131,7 +271,7 @@ fun ToolbarComposableDarkPreview() {
 | 
			
		|||
@Composable
 | 
			
		||||
fun ToolbarComposableLightPreview() {
 | 
			
		||||
    BibLibTheme(darkTheme = true) {
 | 
			
		||||
        ToolbarComposable(navigationViewModel = INavigationViewModel.Mock())
 | 
			
		||||
        ToolbarComposable(true, {}, {})
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,6 @@ package com.pixelized.biblib.ui.viewmodel.navigation
 | 
			
		|||
 | 
			
		||||
import androidx.lifecycle.LiveData
 | 
			
		||||
import androidx.lifecycle.MutableLiveData
 | 
			
		||||
import com.pixelized.biblib.ui.data.BookUio
 | 
			
		||||
 | 
			
		||||
interface INavigationViewModel {
 | 
			
		||||
    val screen: LiveData<Navigable.Screen>
 | 
			
		||||
| 
						 | 
				
			
			@ -10,6 +9,7 @@ interface INavigationViewModel {
 | 
			
		|||
 | 
			
		||||
    fun navigateTo(navigable: Navigable, addToBackStack: Boolean = false): Boolean
 | 
			
		||||
    fun navigateBack(): Boolean
 | 
			
		||||
    fun canNavigateBack(): Boolean
 | 
			
		||||
 | 
			
		||||
    sealed class Navigable {
 | 
			
		||||
        sealed class Screen : Navigable() {
 | 
			
		||||
| 
						 | 
				
			
			@ -33,5 +33,6 @@ interface INavigationViewModel {
 | 
			
		|||
 | 
			
		||||
        override fun navigateTo(navigable: Navigable, addToBackStack: Boolean): Boolean = false
 | 
			
		||||
        override fun navigateBack(): Boolean = false
 | 
			
		||||
        override fun canNavigateBack(): Boolean = false
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -43,6 +43,8 @@ class NavigationViewModel : ViewModel(), INavigationViewModel {
 | 
			
		|||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun canNavigateBack(): Boolean = stack.any { it != null }
 | 
			
		||||
 | 
			
		||||
    private fun popBackStack(): Navigable? {
 | 
			
		||||
        return if (stack.isEmpty()) {
 | 
			
		||||
            null
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue