I'm having some issues with ModalBottomSheetLayout trying to make it fully expanded. I tried some answers from other posts (like Make ModalBottomSheetLayout always Expanded) with no result.
This is the Composable function I've created:
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun ExpandedSheetDialog(
sheetContent: #Composable (() -> Unit),
screenContent: #Composable (() -> Unit),
modalState: ModalBottomSheetState
) {
ModalBottomSheetLayout(
sheetState = modalState,
sheetShape = RoundedCornerShape(topEnd = 10.dp, topStart = 10.dp),
sheetContent = { sheetContent.invoke() },
content = { screenContent.invoke() }
)
}
And this is the Preview:
#OptIn(ExperimentalMaterialApi::class)
#Preview(showBackground = true)
#Composable
fun ExpandedSheetDialogPreview() {
val scope: CoroutineScope = rememberCoroutineScope()
val modalSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Expanded)
ExpandedSheetDialog(
sheetContent = {
Button(
onClick = {
scope.launch {
modalSheetState.animateTo(ModalBottomSheetValue.Hidden)
}
},
text = "Hide"
)
},
screenContent = {
Column(
modifier = Modifier.padding(16.dp).fillMaxHeight(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Button(
onClick = {
scope.launch {
modalSheetState.animateTo(ModalBottomSheetValue.Expanded)
}
},
text = "Show Extended"
)
}
},
modalState = modalSheetState
)
}
I tried in several emulators, all of them with the same result, the "BottomSheetDialog" appears but is not fully expanded. What am I doing wrong?
Related
I have the following screen built in Compose -
#Composable
fun DashboardScreen(heroesViewModel: HeroesViewModel = get()) {
val searchState by heroesViewModel.searchState.collectAsState()
val uiState by heroesViewModel.uiState.collectAsState()
val focusRequester = remember { FocusRequester() }
Column(modifier = Modifier.fillMaxSize()) {
SearchBar(
searchState = searchState,
onQueryChanged = { text ->
heroesViewModel.submitEvent(HeroesViewModel.UiEvent.SearchQueryChanged(text))
},
onSearchFocusChange = { focused ->
heroesViewModel.submitEvent(HeroesViewModel.UiEvent.SearchBarFocusChanged(focused))
},
onClearQueryClicked = {
heroesViewModel.submitEvent(HeroesViewModel.UiEvent.ClearQueryClicked)
},
onBack = {},
focusRequester
)
LazyColumn {
items(uiState.modelsListResponse ?: listOf()) { model ->
if (model is HeroListSeparatorModel)
HeroesListSeparatorItem(model)
else if (model is HeroesListModel)
HeroesListItem(model) {
heroesViewModel.submitEvent(HeroesViewModel.UiEvent.ListItemClicked(model))
}
}
}
}
}
And here is my SearchBar -
#Composable
fun SearchBar(
searchState: SearchState,
onQueryChanged: (String) -> Unit,
onSearchFocusChange: (Boolean) -> Unit,
onClearQueryClicked: () -> Unit,
onBack: () -> Unit,
focusRequester : FocusRequester,
modifier: Modifier = Modifier
) {
val focusManager = LocalFocusManager.current
val keyboardController = LocalSoftwareKeyboardController.current
val focused = searchState.focused
Row(
modifier = modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
AnimatedVisibility(visible = focused) {
BackButton(focusManager, keyboardController, onBack)
}
SearchTextField(
searchState,
onQueryChanged,
onSearchFocusChange,
onClearQueryClicked,
focusRequester
)
}
}
#Composable
fun SearchTextField(
searchState: SearchState,
onQueryChanged: (String) -> Unit,
onSearchFocusChanged: (Boolean) -> Unit,
onClearQueryClicked: () -> Unit,
focusRequester : FocusRequester,
modifier: Modifier = Modifier
) {
val focused = searchState.focused
var query = searchState.query
val searching = searchState.searching
Surface(
modifier = modifier
.then(
Modifier
.height(56.dp)
.padding(
top = 8.dp, bottom = 8.dp,
start = if (focused.not()) 16.dp else 0.dp,
end = 16.dp
)
),
color = Color(0xffF5F5F5),
shape = RoundedCornerShape(percent = 50)
) {
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Box(
contentAlignment = Alignment.CenterStart,
modifier = modifier
) {
if (query.isEmpty()) {
SearchHint(modifier = modifier.padding(start = 24.dp, end = 8.dp))
}
Row(verticalAlignment = Alignment.CenterVertically) {
BasicTextField(
value = query,
onValueChange = {
query = it
onQueryChanged(it)
},
modifier = Modifier
.fillMaxSize()
.weight(1f)
.onFocusChanged { focusState ->
onSearchFocusChanged(focusState.isFocused)
}
.focusRequester(focusRequester)
.padding(top = 9.dp, bottom = 8.dp, start = 24.dp, end = 8.dp),
singleLine = true
)
when {
searching -> {
CircularProgressIndicator(
modifier = Modifier
.padding(horizontal = 16.dp)
.width(25.dp)
.size(24.dp)
)
}
query.isNotEmpty() -> {
IconButton(onClick = onClearQueryClicked) {
Icon(
imageVector = Icons.Filled.Close,
contentDescription = null
)
}
}
}
}
}
}
}
}
#Composable
fun SearchHint(
modifier: Modifier = Modifier,
hint: String = "Enter a hero name"
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxSize()
.then(modifier)
) {
Text(
text = hint,
color = Color(0xff757575)
)
}
}
class SearchState(
query: String,
focused: Boolean,
searching: Boolean,
) {
var query by mutableStateOf(query)
var focused by mutableStateOf(focused)
var searching by mutableStateOf(searching)
}
What I want to achieve is the ability to know when the user has tapped or click anywhere outside of my SearchBar Composable. I want to send an event to the ViewModel so that he recomposes the screen, removing the keyboard and removing the cursor I have on my SearchBar (basically just resetting the focus).
I have tried using the focusRequester like I did in my SearchBar but without success as nothing happened, tried using the clickable {} block which is not what I need (I need the tap and not the click) and tried using Modifier.pointerInput with detectTapGestures and it did not work, not at the root LazyColumn and not at the ListItem level.
What I am looking for should be something really easy.
Found the answer - I ended up using isScrollInProgress variable from LazyListState class that provides a boolean indicating if the list is currently in scrolling. When the value was true I removed the focus from where I needed to remove it and it worked :)
Attached the solution -
val listState = rememberLazyListState()
...
LazyColumn(state = listState) {
items(uiState.modelsListResponse ?: listOf()) { model ->
heroesViewModel.submitEvent(HeroesViewModel.UiEvent.ListIsScrolling(listState.isScrollInProgress))
if (model is HeroListSeparatorModel)
HeroesListSeparatorItem(model)
else if (model is HeroesListModel)
HeroesListItem(model) {
heroesViewModel.submitEvent(HeroesViewModel.UiEvent.ListItemClicked(model))
}
}
}
Use PointerInputModifier which provides access to the underlying MotionEvents originally dispatched to Compose.
Text(modifier = Modifier
.pointerInteropFilter { motionEvent ->
when (motionEvent.action) {
MotionEvent.ACTION_DOWN -> {
// When the user touches the composable
}
MotionEvent.ACTION_UP -> {
// When the user removes touch from the composable
}
}
true
},
text = "Click Me!"
)
Inside pointerInteropFilter block MotionEvent.ACTION_DOWN is triggered when user touches on the composable.
Let say i have a two screen;
#Composable
fun Screen1(
toScreen2:() -> Unit
) {
Box(
modifier = Modifier.fillMaxSize().border(4.dp, Color.Gray),
contentAlignment = Alignment.Center
) {
Text(text = "Screen 1")
Button(onClick = { toScreen2() }) {
Text(text = "To Screen 2")
}
}
}
#Composable
fun Screen2(
toScreen1:() -> Unit
) {
Box(
modifier = Modifier.fillMaxSize().border(4.dp, Color.Gray),
contentAlignment = Alignment.Center
) {
Text(text = "Screen 2")
Button(onClick = { toScreen1() }) {
Text(text = "To Screen 1")
}
}
}
and top bar that I can control the visibility;
#Composable
fun MyTopBar(
currentRoute: String?
) {
val isVisible = currentRoute == "Screen1"
AnimatedVisibility(
visible = isVisible,
enter = fadeIn(),
exit = fadeOut()
) {
TopAppBar(backgroundColor = Color.Red){}
}
}
Sccafold
val scaffoldState = rememberScaffoldState()
val navController = rememberNavController()
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
Scaffold(
modifier = Modifier
.fillMaxSize(),
scaffoldState = scaffoldState,
topBar = { MyTopBar(currentRoute) },
backgroundColor = Color.White
){
NavHost(
navController = navController,
startDestination = "Screen1"
){
composable("Screen1"){
Screen1(toScreen2 = { navController.navigate("Screen2") })
}
composable("Screen2"){
Screen2(toScreen1 = { navController.navigate("Screen1") })
}
}
}
How can i prevent this delayed effect;
i want to instant efect that simultaneously with the disappearance speed of the bottom bar. You can see that the border of the screen lags behind the topbar disappearance speed.
Remove the AnimatedVisibility from the MyTopBar.
You have animations for the TopBar visibility. So it won't be instantaneous.
I have a custom search bar will not allow me to place a cursor and type text into it. Before adding the viewmodel and state, I was able to have the TextField work and allow typing text into it.
I've tried using state variables within the composable instead of separating the logic into the view model and unfortunatly recieved the same result. I have a feeling it's something simple that I'm missing but can't quite find it.
Custom Search Bar:
#Composable
fun SearchBar(
modifier: Modifier,
viewModel: ToolSetListViewModel = hiltViewModel()
){
Surface (
modifier = modifier
.fillMaxWidth()
.height(74.dp)
.padding(20.dp, 15.dp, 20.dp, 0.dp),
elevation = 10.dp,
color = MaterialTheme.colors.primary,
shape = RoundedCornerShape(25)
){
TextField(
modifier = modifier
.fillMaxWidth(),
value = viewModel.searchText,
onValueChange = {
viewModel.onEvent(ToolSetListEvent.OnSearchToolSet(it))
},
placeholder = {
Text(
modifier = modifier
.alpha(ContentAlpha.medium),
text = "Search...",
color = White
)
},
textStyle = TextStyle(
fontSize = MaterialTheme.typography.subtitle1.fontSize,
),
singleLine = true,
colors = TextFieldDefaults.textFieldColors(
backgroundColor = Color.Transparent,
cursorColor = White
)
)
}
Screen:
#Composable
fun ToolSetListScreen(
onNavigate: (UiEvent.Navigate) -> Unit,
viewModel: ToolSetListViewModel = hiltViewModel()
) {
val toolSets = viewModel.toolSets.collectAsState(initial = emptyList())
LaunchedEffect(key1 = true) {
viewModel.uiEvent.collect { event ->
when(event) {
is UiEvent.Navigate -> onNavigate(event)
}
}
}
Scaffold (
floatingActionButton = {
FloatingActionButton(onClick = {
viewModel.onEvent(ToolSetListEvent.OnAddToolSetClick)
}) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = "Add")
}
}
) {
SearchBar(
modifier = Modifier,
)
LazyColumn(
modifier = Modifier.fillMaxSize()
.padding(0.dp, 20.dp)
) {
items(toolSets.value) { toolset ->
ToolSetItem(
toolSet = toolset,
onEvent = viewModel::onEvent,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.clickable {
viewModel.onEvent(ToolSetListEvent.OnToolSetClick(toolset))
}
)
}
}
}
}
ViewModel:
#HiltViewModel
class ToolSetListViewModel #Inject constructor(
private val repository: ToolSetRepository
): ViewModel() {
val toolSets = repository.getAllToolSets()
private val _uiEvent = Channel<UiEvent>()
val uiEvent = _uiEvent.receiveAsFlow()
var searchText by mutableStateOf("")
private set
fun onEvent(event: ToolSetListEvent) {
when(event) {
is ToolSetListEvent.OnToolSetClick -> {
sendUiEvent(UiEvent.Navigate(Routes.ADD_EDIT_TOOL_SET + "?PONumber=${event.toolSet.PONumber}"))
}
is ToolSetListEvent.OnDeleteToolSetClick -> {
viewModelScope.launch {
repository.deleteToolSet(event.toolset)
}
}
is ToolSetListEvent.OnAddToolSetClick -> {
sendUiEvent(UiEvent.Navigate(Routes.ADD_EDIT_TOOL_SET))
}
is ToolSetListEvent.OnSearchToolSet -> {
viewModelScope.launch {
if (event.searchText.isNotBlank()) {
searchText = event.searchText
repository.getToolSetByPO(event.searchText)
}
}
}
}
}
private fun sendUiEvent(event: UiEvent) {
viewModelScope.launch {
_uiEvent.send(event)
}
}
MainActivity:
#AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
PunchesManagerTheme {
val navController = rememberNavController()
Scaffold (
content = {
Navigation(navController)
},
bottomBar = { BottomNavigationBar(navController = navController) },
)
}
}
}
}
#Composable
fun BottomNavigationBar(navController: NavHostController) {
BottomNavigation {
val backStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = backStackEntry?.destination?.route
NavBarItems.bottomNavItem.forEach { navItem ->
BottomNavigationItem(selected = currentRoute == navItem.route, onClick = {
navController.navigate(navItem.route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = false
}
launchSingleTop = true
restoreState = true
}
},
icon = {
Icon(imageVector = navItem.icon,
contentDescription = navItem.name)
},
label = {
Text(text = navItem.name)
},
)
}
}
}
Any suggestions on what the issue may be?
I think I found the fix!
I adjusted the layout of my scaffoled in my ToolSetListScreen:
Scaffold (
content = {
SearchBar(
modifier = Modifier,
)
LazyColumn(
modifier = Modifier.fillMaxSize()
.padding(0.dp, 75.dp)
) {
items(toolSets.value) { toolset ->
ToolSetItem(
toolSet = toolset,
onEvent = viewModel::onEvent,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.clickable {
viewModel.onEvent(ToolSetListEvent.OnToolSetClick(toolset))
}
)
}
}
},
floatingActionButton = {
FloatingActionButton(onClick = {
viewModel.onEvent(ToolSetListEvent.OnAddToolSetClick)
}) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = "Add")
}
}
)
}
and this seemed to work.
I try to learn jetpack compose these days, and I have a simple project in jetpack compose, bottom sheet work in my project, but when I use bottom navigation, it is not work, I search in many website and stackoverflow especially, but I did not find any solution, I do not know what I missed? is there any idea?
#Composable
fun BSDataScreen() {
val modalBottomSheetState = rememberModalBottomSheetState(initialValue =
ModalBottomSheetValue.Hidden)
val scope = rememberCoroutineScope()
ModalBottomSheetLayout(
sheetContent = {
SheetScreen()
},
sheetState = modalBottomSheetState,
sheetShape = RoundedCornerShape(topStart = 15.dp, topEnd = 15.dp),
sheetBackgroundColor = Color.White,
) {
Scaffold(
backgroundColor = Color.White,
) {
DataScreen(
scope = scope, state = modalBottomSheetState)}}}
#Composable
fun DataScreen(
scope: CoroutineScope,
state: ModalBottomSheetState
) {
val listOfData = listOf(
Data( painterResource(R.drawable.image1)),
Data(painterResource(R.drawable.image2)),
Data(painterResource(R.drawable.image3),)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxSize()
.background(Color.White)
) {
LazyColumn(
modifier = Modifier
) {
items(listOfData.size) { index ->
DataListItem(listOfData[index]) data: Data->
scope.launch {
state.show()
}
}}}}
#Composable
fun DataListItem(data: Data, onLongClick: (Data) -> Unit) {
val context = LocalContext.current
Column(
modifier = Modifier.padding(5.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(5.dp)
.combinedClickable(
onLongClick= {
onLongClick(data)
},)
) {
Image(
painter = data.painter,
contentDescription = null,)}}}
BottomNav:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MainScreen()
}
}
}
#Composable
fun MainScreen() {
val navController = rememberNavController()
Scaffold(
bottomBar = { BottomNavigationBar(navController) }
) {
Navigation(navController = navController)
}
}
#Composable
fun Navigation(navController: NavHostController) {
val modalBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
val scope = rememberCoroutineScope()
NavHost(navController, startDestination = NavigationItem.Data.route) {
composable(NavigationItem.Data.route) {
DataScreen(
scope = scope, state = modalBottomSheetState
)
}
composable(NavigationItem.Data2.route) {
Data2Screen()
}
composable(NavigationItem.Data3.route) {
Data3Screen()
}
composable(NavigationItem.Data4.route) {
Data4Screen()
}
composable(NavigationItem.Data5.route) {
Data5Screen()
}
}
}
#Composable
fun BottomNavigationBar(navController: NavController
) {
val items = listOf(
NavigationItem.Data,
NavigationItem.Data2,
NavigationItem.Data3,
NavigationItem.Data4,
NavigationItem.Data5
)
BottomNavigation(
backgroundColor = colorResource(id = R.color.white),
contentColor = colorResource(id = R.color.black)
) {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
items.forEach { item ->
BottomNavigationItem(
icon = {
Icon(
painterResource(id = item.icon),
contentDescription = null
)
},
selectedContentColor = colorResource(id = R.color.red),
unselectedContentColor = colorResource(id = R.color.blue),
alwaysShowLabel = true,
selected = currentRoute == item.route,
onClick = {
navController.navigate(item.route) {
navController.graph.startDestinationRoute?.let { route ->
popUpTo(route) {
saveState = true
}
}
launchSingleTop = true
restoreState = true
}})}}}
I think your problem is your bottom sheets appear under bottom bar. The same thing I solved is very simple.
#Composable
fun SettingView() {
val navController = rememberNavController()
val scaffoldState = rememberScaffoldState()
Scaffold(
bottomBar = {
BottomBar(navController = navController)
},
content = {
Box(modifier = Modifier.padding(it)) {
BottomNavGraph(
navController = navController,
scaffoldState = scaffoldState
)
}
}
)
}
content screens wrap with Box and padding to scaffold's PaddingValues.
Just wrap the whole screen with ModalBottomSheetLayout.
You can put the following code:
Scaffold(
bottomBar = { BottomNavigationBar(navController) }
) {
Navigation(navController = navController)
}
inside
ModalBottomSheetLayout(...){
Scaffold(
bottomBar = { BottomNavigationBar(navController) }
) {
Navigation(navController = navController)
}
}
Try using the Navigation Material library provided by Accompanist.
https://google.github.io/accompanist/navigation-material/
I am try to learning jetpack compose those days, so I want to learning bottom sheet in jetpack compose, I do it just for one text, but I want to use it for multiple text, I have one example here, but I am not sure how to implement to my project, is there any solution for more than two text button to apply bottom sheet?
#Composable
fun BottomSheetMyScreen() {
val modalBottomSheetState = rememberModalBottomSheetState(initialValue =
ModalBottomSheetValue.Hidden)
val scope = rememberCoroutineScope()
ModalBottomSheetLayout(
sheetContent = {
BottomSheetFirstScreen()
},
sheetState = modalBottomSheetState,
sheetShape = RoundedCornerShape(topStart = 5.dp, topEnd = 5.dp),
sheetBackgroundColor = Color.Red,
) {
Scaffold(
backgroundColor = Color.Red
) {
MyScreen(
scope = scope, state = modalBottomSheetState
)}}}
#Composable
fun MyScreen(
scope: CoroutineScope, state: ModalBottomSheetState,
) {
MainRow(
name = "name1",
onClick = { scope.launch {
state.show()
}}
)
MainRow(
name = "name2",
onClick = { scope.launch {
state.show()
} }
)}}
#Composable
fun MainRow(
name: String,
onClick: () -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(10.dp)
.clickable(onClick = onClick),
verticalAlignment = Alignment.CenterVertically
) {
Column(
modifier = Modifier
.width(150.dp)
) {
Text(
text = name,
)}}}
I edit Your Code, you can use this code:
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun BottomSheetMyScreen() {
val modalBottomSheetState = rememberModalBottomSheetState(
initialValue =
ModalBottomSheetValue.Hidden
)
val scope = rememberCoroutineScope()
val sheetContentState = remember {
mutableStateOf(0)
}
ModalBottomSheetLayout(
sheetContent = {
when (sheetContentState.value) {
0 -> {
BottomSheetFirstScreen()
}
1 -> {
BottomSheetSecondScreen()
}
}
},
sheetState = modalBottomSheetState,
sheetShape = RoundedCornerShape(topStart = 5.dp, topEnd = 5.dp),
sheetBackgroundColor = Color.Red,
) {
Scaffold(
backgroundColor = Color.Red
) {
MyScreen(
scope = scope, state = modalBottomSheetState, sheetContentState = sheetContentState
)
}
}
}
Main Row
#Composable
fun MainRow(
name: String,
onClick: () -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(10.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = name,
modifier = Modifier.clickable { onClick.invoke() }
)
}
}
And MyScreen
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun MyScreen(
scope: CoroutineScope,
state: ModalBottomSheetState,
sheetContentState: MutableState<Int>
) {
Column(modifier = Modifier.fillMaxWidth()) {
MainRow(
name = "name 1",
onClick = {
scope.launch {
sheetContentState.value = 0
state.show()
}
}
)
MainRow(
name = "name 2",
onClick = {
scope.launch {
sheetContentState.value = 1
state.show()
}
}
)
}
}