Click a close button on a bottomsheet to hide it in compose - android-jetpack-compose

I have the following composable bottomsheet.
I want to be able to close the bottomsheet either by dragging, clicking the background, and clicking the close button.
#Composable
fun CDSModelBottomSheet(toolBar: #Composable () -> Unit, content: #Composable () -> Unit) {
val modelBottomSheetState = rememberModalBottomSheetState(
initialValue = ModalBottomSheetValue.Expanded
)
ModalBottomSheetLayout(
sheetState = modelBottomSheetState,
sheetShape = RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp),
sheetContent = {
Column {
toolBar()
content()
}
}
) {}
}
#Composable
#Preview
fun PreviewCDSBottomSheet() {
CDSModelBottomSheet(
toolBar = { Toolbar(
title = "Select Account",
trailingIcon = {
IconButton(
modifier = Modifier.size(24.dp),
onClick = {
/* close bottom sheet */
}
) {
Icon(
imageVector = Icons.Filled.Close,
contentDescription = stringResource(R.string.close_bottom_sheet),
tint = Color.Black,
)
}
})},
content = {
LoginMode()
}
)
}
In the trailingIcon I have an onClick event. But not sure how to trigger the bottomsheet to close. Unless I have to pass in the rememberModelBottomSheetState which I don't want to do.
This is the preview

Create a lambda to hide ModalBottomSheet as
val coroutineScope = rememberCoroutineScope()
val hideModalBottomSheet: () -> Unit = { coroutineScope.launch { sheetState.hide()} }
And pass this lambda as parameter to your content by updating toolbar as
toolbar: #Composable (() -> Unit) -> Unit
Full function
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun CDSModelBottomSheet(
toolBar: #Composable (() -> Unit) -> Unit,
content: #Composable () -> Unit
) {
val modelBottomSheetState = rememberModalBottomSheetState(
initialValue = ModalBottomSheetValue.Expanded
)
val coroutineScope = rememberCoroutineScope()
val hideModalBottomSheet: () -> Unit =
{ coroutineScope.launch { modelBottomSheetState.hide() } }
ModalBottomSheetLayout(
sheetState = modelBottomSheetState,
sheetShape = RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp),
sheetContent = {
Column {
toolBar(hideModalBottomSheet)
content()
}
}
) {}
}
And use it as
CDSModelBottomSheet(
toolBar = { hide: () -> Unit ->
Toolbar(
title = "Select Account",
trailingIcon = {
IconButton(
modifier = Modifier.size(24.dp),
onClick = hide
) {
Icon(
imageVector = Icons.Filled.Close,
contentDescription = "Close",
tint = Color.Black,
)
}
}
)
},
content = {
LoginMode()
}
)

Related

Dynamic Item width in Column (Jetpack Compose)

I have some LazyColumn (or Column) in Composable fun:
#Composable
fun MyColumn(items: List<Item>) {
val width = 0.dp // max for all amounts
LazyColumn {
items(items) {
Row(modifier = Modifier.fillMaxWidth()) {
Text(text = it.title, modifier = Modifier.weight(1f))
Text(text = it.amount, modifier = Modifier.background(Color.Red).width(width))
}
}
}
}
class Item(val title: String, val amount: String)
How can I measure the width param? It must be equals the longest value for all amounts in items.
Finally I got some solution (I don't know is it correct or no, but it works):
I use SubcomposeLayout to measure width of whole separate column with amounts. Code of the measuring function:
#Composable
fun WithCalculateWidth(modifier: Modifier = Modifier,
contentForCalculate: #Composable () -> Unit,
dependentContent: #Composable (Dp) -> Unit) {
SubcomposeLayout(modifier = modifier) { constraints ->
val measuredWidth = subcompose("viewToMeasure", contentForCalculate)[0].measure(Constraints()).width.toDp()
val contentPlaceable = subcompose("content") { dependentContent(measuredWidth) }[0].measure(constraints)
layout(contentPlaceable.width, contentPlaceable.height) {
contentPlaceable.place(0, 0)
}
}
}
I use calculated width in my List:
#Composable
fun ListWithMeasuredAmounts(items: List<Item>) {
val allAmounts: #Composable () -> Unit = {
Column {
items.forEach {
AmountItem(amount = it.amount)
}
}
}
WithCalculateWidth(contentForCalculate = { allAmounts() }) { amountWidth ->
LazyColumn {
items(items) {
Row(modifier = Modifier.fillMaxWidth()) {
Text(text = it.title, modifier = Modifier.weight(1f))
AmountItem(amount = it.amount, modifier = Modifier.width(amountWidth))
}
}
}
}
}
#Composable
fun AmountItem(amount: String, modifier: Modifier = Modifier) {
Text(text = amount, modifier = modifier.background(Color.Red))
}

How to create a JSpinner-like widget in Jetpack Compose

I would like to create a widget in Jetpack Compose (Desktop) with similar functionality as the JSpinner in Swing, i.e. an editable text field and two buttons that increase/decrease the value in the text field. Also, I would like
the value to be validated and to be saved when the spinner loses its focus
the buttons not to be skipped in the navigation, so that the user can navigate directly between multiple spinner text fields
After a lot of trial and error I have figured out the following working version, but I wonder if there is a simpler or more elegant way to do this:
#Composable
fun TextFieldSpinner(
label: #Composable (() -> Unit)?,
lastText: String,
validateText: (String) -> Boolean,
commitText: (String) -> Unit,
onIncrement: () -> Unit,
onDecrement: () -> Unit
) {
val (isEditing, setEditing) = remember { mutableStateOf(false) }
// intermediateTextFieldValue is only used locally to store the temporary state of the TextField while editing
val (intermediateTextFieldValue, setIntermediateTextFieldValue) = remember { mutableStateOf(TextFieldValue(lastText))}
var isError by remember { mutableStateOf(!validateText(lastText)) }
val resetText = { setIntermediateTextFieldValue(TextFieldValue(lastText)) }
if (!isEditing && !isError && lastText != intermediateTextFieldValue.text) {
resetText()
}
val onCommit = {
if (validateText(intermediateTextFieldValue.text)) {
isError = false
commitText(intermediateTextFieldValue.text)
} else {
isError = true
}
}
val onLeaveTextField = {
setEditing(false)
onCommit()
isError = false
}
val onNewFocusState = { newFocusState: Boolean ->
setEditing(newFocusState)
if (!newFocusState)
onCommit()
}
}
TextFieldSpinnerUI(
label = label,
value = intermediateTextFieldValue,
onValueChange = { newTextFieldValue:TextFieldValue ->
setEditing(true)
setIntermediateTextFieldValue(newTextFieldValue)
// eager committing without showing error state
if (validateText(newTextFieldValue.text)) {
isError = false
commitText(newTextFieldValue.text)
}
},
modifier = modifier,
isError = isError,
onIncrement = {
onLeaveTextField()
onIncrement()
},
onDecrement = {
onLeaveTextField()
onDecrement()
},
onFocusChanged = { state ->
onNewFocusState(state.isFocused)
}
)
}
#OptIn(ExperimentalComposeUiApi::class)
#Composable
private fun TextFieldSpinnerUI(
label: #Composable (() -> Unit)?,
value: TextFieldValue,
onValueChange: (TextFieldValue) -> Unit,
onIncrement: () -> Unit,
onDecrement: () -> Unit,
isError: Boolean,
onFocusChanged: (FocusState) -> Unit
) {
Row() {
DecreaseButton(onDecrement)
TextField(
label = label,
value = value,
singleLine = true,
isError = isError,
onValueChange = onValueChange,
modifier = Modifier.onFocusChanged(onFocusChanged)
)
IncreaseButton(onIncrement)
}
}
#Composable
private fun DecreaseButton(
onClick: () -> Unit
) {
IconButton(
onClick = onClick,
modifier = Modifier.focusProperties {this.canFocus = false }
) {
Icon(
imageVector = Icons.Rounded.Remove
)
}
}
#Composable
private fun IncreaseButton(
onClick: () -> Unit
) {
IconButton(
onClick = onClick,
modifier = Modifier.focusProperties { this.canFocus = false }
) {
Icon(
imageVector = Icons.Rounded.Add
)
}
}
In particular, it seems to be hard to have the text field at the same time
to be editable
to be validated, saved and then recomposed with the new value
to have another widget such as the buttons change its value

Is there a better way to wrap bottom sheet height for ModalBottomSheetLayout/BottomSheetScaffold

My solution works, but sometimes when switching between boxes using openSheet() height of content cannot be calculated in time and it's jumps when opens.
Working solution using ModalBottomSheetLayout:
val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
val scope = rememberCoroutineScope()
val density = LocalDensity.current
var topAppBarHeight = Dp.Unspecified
var sumHeight by remember { mutableStateOf(Dp.Unspecified) }
var currentSheet by remember { mutableStateOf(0) }
val openSheet: (sheet: Int) -> Unit = {
currentSheet = it
sumHeight = topAppBarHeight + (currentSheet * 50).dp
scope.launch { sheetState.show() }
}
val closeSheet: () -> Unit = {
scope.launch { sheetState.hide() }
sumHeight = Dp.Unspecified
}
ModalBottomSheetLayout(
modifier = Modifier.fillMaxSize(),
sheetState = sheetState,
sheetContent = {
Scaffold(
modifier = Modifier.height(sumHeight),
topBar = {
TopAppBar(
modifier = Modifier
.clickable(onClick = closeSheet)
.onGloballyPositioned { lc ->
topAppBarHeight = with(density) { lc.size.height.toDp() }
},
title = { Text("TopAppBarSheetContent") }
)
},
content = {
Box(
modifier = Modifier
.height((currentSheet * 50).dp)
.fillMaxWidth()
.background(Color.Blue)
)
},
)
},
content = {
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
TopAppBar(title = { Text("TopAppBarContent") })
},
content = { innerPadding ->
LazyColumn(
modifier = Modifier.padding(innerPadding),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(50) { item ->
Box(
modifier = Modifier
.clip(CircleShape)
.fillMaxWidth()
.height(75.dp)
.background(Color.Red)
.clickable { openSheet(item) }
)
}
}
}
)
}
)
Maybe there is a better way to solve this problem?

States in Buttons from LazyVerticalGrid

I have a data class which provide information of a CarParking Place. This one:
data class ParkingSpace(
val id: Int,
val parkNumber: Int,
val parkState: String
)
fun getParkingSpace() = (1..4).map {
when (it) {
1 -> {
ParkingSpace(
id = it,
parkNumber = 150,
parkState = "Free $it"
)
}
2 -> {
ParkingSpace(
id = it,
parkNumber = 152,
parkState = "Free $it"
)
}
3 -> {
ParkingSpace(
id = it,
parkNumber = 153,
parkState = "Free $it"
)
}
4 -> {
ParkingSpace(
id = it,
parkNumber = 154,
parkState = "Free $it"
)
}
else -> {
ParkingSpace(
id = it,
parkNumber = 150,
parkState = "Free $it"
)
}
}
}
This information is populated in a LazyVerticalGrid through buttons
My goal is:
When you press the button, the Text content of the Button will change into an user name (now is just fixed data) - like saying this park place is reserved to one User
There are two separated buttons, to cancel Reservation. I'm trying to show a different content in the Text's button like "Free". I created this CancelButton for practicing interacting with the content of the buttons.
This is my main Activity with all composables.
class MainActivity : ComponentActivity() {
#ExperimentalCoilApi
#ExperimentalFoundationApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ZebraCarPark_ComposeTheme {
ParkingListLayout()
}
}
}
}
#ExperimentalFoundationApi
#ExperimentalCoilApi
#Composable
fun ParkingListLayout() {
Column() {
Box(
modifier = Modifier
.background(Color.LightGray)
.fillMaxWidth()
.padding(horizontal = 0.dp, vertical = 15.dp)
) {
LazyVerticalGrid(
contentPadding = PaddingValues(2.dp),
cells = GridCells.Fixed(4)
) {
items(getParkingSpace()) { parkSpc ->
ParkingPlace(parkSpc)
}
}
}
Divider(
color = Color.Black,
thickness = 2.dp
)
Box(
modifier = Modifier
.background(Color.Green)
) {
Column(
modifier = Modifier
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceEvenly
) {
Button(onClick = {}) {
Text(text = "Cancel Park Place 1")
}
Button(onClick = {}) {
Text(text = "Cancel Park Place 2")
}
}
}
}
}
#Composable
fun ParkingPlace(parkingSpace: ParkingSpace) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Text(
modifier = Modifier.padding(5.dp),
text = "${parkingSpace.parkNumber}",
)
Button(
onClick = { },
colors = ButtonDefaults.buttonColors(Color.Green)
) {
Text(
text = parkingSpace.parkState,
maxLines = 1,
softWrap = false,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.padding(5.dp)
)
}
}
}
I was able to make some progress using State Hoisting, so pressing a button will change the content of the button with a new value and all of this stuff. The problem is when the cancel button enter in action. I'm not able to handle correctly the states of the content of the Text's button. I prefer to remove all that code to show you this cleaner.
Project result:

How can I take reference of Keyboard View in Jetpack Compose?

Dialog moves up when keyboard appears. I want a Composable do the same , but i cant find how to do this.
Ok, I've found the answer.
First, in manifest:
<activity
android:windowSoftInputMode="adjustResize">
After that use "Insets", that are explained in this page:
https://google.github.io/accompanist/insets/
in the following example I use a fab that makes a textfield appear that is positioned above the keyboard
PD: Notice in FloatingActionButton I use Modifier.systemBarsPadding(), it is made so that it is not hidden
class MainActivity : ComponentActivity() {
#ExperimentalAnimatedInsets
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {
MyApplicationTheme {
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
val focusRequester = FocusRequester()
Greeting(focusRequester)
}
}
}
}
}
#ExperimentalAnimatedInsets
#Composable
fun Greeting(focusRequester: FocusRequester) {
var creatorVisibility by remember{ mutableStateOf(false)}
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
Scaffold(floatingActionButton = {
Fab(onClick = { creatorVisibility = true }, creatorVisibility = creatorVisibility)}
) {
ConstraintLayout(constraintSet = constraints, modifier = Modifier.fillMaxSize(1f)) {
TextField(
value = TextFieldValue(),
onValueChange = { /*TODO*/ },
modifier = Modifier.layoutId("mainTf")
)
if (creatorVisibility){
Box(
modifier = Modifier
.fillMaxSize()
.clickable {
creatorVisibility = false
}
.background(color = Color(0f, 0f, 0f, 0.5f))
,contentAlignment = Alignment.BottomCenter
) {
SideEffect(effect = { focusRequester.requestFocus() })
TextField(colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.White),
value = TextFieldValue(),
onValueChange = { /*TODO*/ },
modifier = Modifier
.layoutId("textField")
.imePadding()
.focusRequester(focusRequester = focusRequester)
)
}
}
}
}
}
}
#Composable
fun Fab(onClick : () -> Unit, creatorVisibility:Boolean){
if (!creatorVisibility){
FloatingActionButton(
onClick = { onClick() },
modifier = Modifier.layoutId("fab").systemBarsPadding()
) {
Text(text = "Crear tarea", modifier = Modifier.padding(start = 10.dp, end = 10.dp))
}
}
}
val constraints = ConstraintSet {
val textField = createRefFor("textField")
val mainTf = createRefFor("mainTf")
val fab = createRefFor("fab")
constrain(textField){
start.linkTo(parent.start)
end.linkTo(parent.end)
bottom.linkTo(parent.bottom)
}
constrain(mainTf){
start.linkTo(parent.start)
end.linkTo(parent.end)
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
}
constrain(fab){
end.linkTo(parent.end)
bottom.linkTo(parent.bottom)
}
}

Resources