How to employ BackdropScaffoldState.offset correctly to offset front contentlayer of compose BackdropScaffold

598 views Asked by At

My current android applications main screen contains a androidx.compose.material.BackdropScaffold. The backdrop looks great and functions exactly as it says on the tin.

however I have an issue with the frontLayerContent which contains a list of items.

I allow the user to interact with the frontLayerContent list while the backdrop is revealed, the issue is with the backdrop in the revealed state the user cannot scroll down to see the last item in the frontLayerContent list.

The solution to this issue is to use backdropState.offset in the modifier of the frontLayerContent which I obtain as follows:-

val backdropState = rememberBackdropScaffoldState(initialValue = BackdropValue.Concealed)
var offset by (backdropState.offset as MutableState)

and set as follows:-

onBackdropReveal = {
                if (!backdropState.isAnimationRunning) {
                    scope.launch {
                        if (backdropState.isConcealed) {
                            offset = backdropState.offset.value
                            backdropState.reveal()
                        } else {
                            offset = backdropState.offset.value
                            backdropState.conceal()
                        }
                    }
                }
            }

and make the offset value available to other composables with:-

frontLayerContent = {
            CompositionLocalProvider(LocalBackdropStateOffset provides offset) {
                frontLayerContent()
            }
        }

Then in my frontLayerContent I retrieve the offset value as use as follows:-

val context = LocalContext.current
val offset = LocalBackdropStateOffset.current

Scaffold(
    topBar = { Spacer(modifier = Modifier.height(0.dp)) },
    modifier = Modifier.fillMaxSize(),
) { paddingValues ->


    Column(
        modifier = Modifier
            .fillMaxSize()
            .offset { IntOffset(x = 0, y = -offset.toInt()) },
        verticalArrangement = Arrangement.Top
    ) {
        LazyVerticalGrid(
            modifier = Modifier.padding(10.dp),
            columns = GridCells.Adaptive(125.dp),
            contentPadding = paddingValues,
            verticalArrangement = Arrangement.spacedBy(8.dp),
            horizontalArrangement = Arrangement.spacedBy(8.dp),
            content = {
                items(state.size) { index ->
          

this solution is close to what is required, however it looks as though I have made a mistake somewhere as my frontLayerContent list is always vertically offset even when the backdrop is concealed. In fact revealing or concealing the backdrop does not change the amount of offset of my frontLayerContent list.

how do I fix this issue?

how can I correctly set the frontLayerContent list vertical offset depending on whether the backdrop is concealed or revealed?

UPDATE

I only need to "fix" when the backdrop is revealed. therefore i need a conditional configuration of my frontLayerContent Modifier.offset(y = -offset)

how are you supposed to use the backdropState.offset value when correcting the frontLayerContent offset? as the compose coordinate systems origin is the top left hand corner (x = 0, y = 0) and y dimension increases down the screen and x dimension increases Left to Right. when the backdrop is revealed the offset is a value (on my pixel 5) of approx 600.dp and concealed value of approx 300.dp. why is the concealed offset not 0.dp? is this taking into account the screens TopAppBar?

UPDATE (2) Heres a GIF I made earlier

UPDATE (3)

This basic sample shows the problem i am having. where have i made my mistake that stops me being able to scroll down to see the complete last item when the backdrop is revealed?

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.BackdropScaffold
import androidx.compose.material.BackdropValue
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.rememberBackdropScaffoldState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.elsevier.daisy.ui.theme.MyTheme
import kotlinx.coroutines.launch

class MainActivity : ComponentActivity() {
    
    @OptIn(ExperimentalMaterial3Api::class)
    @ExperimentalMaterialApi
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val scaffoldState = rememberBackdropScaffoldState(initialValue = BackdropValue.Concealed)
            val scope = rememberCoroutineScope()

            MyTheme {

                BackdropScaffold(
                    scaffoldState = scaffoldState,
                    frontLayerScrimColor = Color.Unspecified,
                    frontLayerElevation = 5.dp,
                    gesturesEnabled = false,
                    appBar = {
                        TopAppBar(
                            title = { Text("Backdrop") },
                            navigationIcon = {
                                if (scaffoldState.isConcealed) {
                                    IconButton(
                                        onClick = {
                                            scope.launch { scaffoldState.reveal() }
                                        }
                                    ) {
                                        Icon(
                                            Icons.Default.Menu,
                                            contentDescription = "Menu"
                                        )
                                    }
                                } else {
                                    IconButton(
                                        onClick = {
                                            scope.launch { scaffoldState.conceal() }
                                        }
                                    ) {
                                        Icon(
                                            Icons.Default.Close,
                                            contentDescription = "Close"
                                        )
                                    }
                                }
                            }
                        )
                    },
                    backLayerContent = {
                        Column {
                            Text(
                                text = "Menu Item 1", modifier = Modifier.padding(8.dp), color = Color.White, style = TextStyle(
                                    fontWeight = FontWeight.Bold,
                                    fontSize = 14.sp,
                                    color = Color.Black
                                )
                            )
                            Text(
                                text = "Menu Item 1", modifier = Modifier.padding(8.dp), color = Color.White, style = TextStyle(
                                    fontWeight = FontWeight.Bold,
                                    fontSize = 14.sp,
                                    color = Color.Black
                                )
                            )

                        }
                    },
                    frontLayerContent = {

                        LazyColumn(modifier = Modifier.fillMaxSize()) {
                            // Add 5 items
                            items(55) { index ->
                                Text(
                                    text = "Item: ${index + 1}",
                                    textAlign = TextAlign.Center,
                                    style = MaterialTheme.typography.headlineLarge,
                                    modifier = Modifier.padding(5.dp)
                                )
                            }
                        }


                    },
                    peekHeight = 60.dp,


                    ) {

                }
            }
        }
    }
}
1

There are 1 answers

6
Abdelilah El Aissaoui On

I would suggest adding a padding to the LazyColumn/LazyVerticalGrid itself rather than playing with the offset.

Given your example, I would simply add a conditional padding to the LazyColumn:

val padding = if (
    scaffoldState.isRevealed &&
    !scaffoldState.isAnimationRunning
) {
    64.dp
} else {
    0.dp
}

LazyColumn(
    modifier = Modifier
        .fillMaxSize()
        .padding(bottom = padding)
) {
  // Your items here
}

I've used 64.dp here but use whatever you feel is necessary according to your design.

If instead of using that magic number you want to calculate the height of the backLayerContent you can by using the onGloballyPositioned modifier and save the height in a state.

Add this before your BackdropScaffold

var backLayerContentHeight by remember { mutableStateOf(0.dp) }

Get the backLayerContent height:

backLayerContent = {
    val localDensity = LocalDensity.current
    Column(
        modifier = Modifier.onGloballyPositioned {
            val heightInPx = it.size.height
            val heightInDp = with(localDensity) { heightInPx.toDp() }
            backLayerContentHeight = heightInDp
        }
    ) {
       ...
    }
},

Then you can use this value for the padding:

val padding = if (
    scaffoldState.isRevealed &&
    !scaffoldState.isAnimationRunning
) {
    backLayerContentHeight
} else {
    0.dp
}

Alternative to BackdropScaffold

You may want to create a custom implementation to get a similar behavior without workarounds.

This custom component should help:

@Composable
fun CustomBackdropScaffold(
    modifier: Modifier = Modifier,
    appBar: @Composable () -> Unit,
    backLayerContent: @Composable () -> Unit,
    frontLayerContent: @Composable () -> Unit,
    isRevealed: Boolean,
) {
    Surface(modifier = modifier) {
        Column {
            appBar()

            AnimatedVisibility(
                visible = isRevealed,
                enter = expandVertically(
                    expandFrom = Alignment.Top,
                ),
                exit = shrinkVertically(
                    shrinkTowards = Alignment.Top,
                ),
            ) {
                backLayerContent()
            }

            frontLayerContent()
        }
    }
}

Here as an example of how to use it:

var isRevealed by remember { mutableStateOf(false) }

CustomBackdropScaffold(
    appBar = {
        TopAppBar(
            title = { Text("Backdrop") },
            navigationIcon = {
                if (!isRevealed) {
                    IconButton(
                        onClick = {
                            isRevealed = true
                        }
                    ) {
                        Icon(
                            Icons.Default.Menu,
                            contentDescription = "Menu"
                        )
                    }
                } else {
                    IconButton(
                        onClick = {
                            isRevealed = false
                        }
                    ) {
                        Icon(
                            Icons.Default.Close,
                            contentDescription = "Close"
                        )
                    }
                }
            }
        )
    },
    backLayerContent = {
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .background(MaterialTheme.colorScheme.primary)
        ) {
            Text(
                text = "Menu Item 1",
                modifier = Modifier.padding(8.dp),
                color = Color.White,
                style = TextStyle(
                    fontWeight = FontWeight.Bold,
                    fontSize = 14.sp,
                    color = Color.Black
                )
            )
            Text(
                text = "Menu Item 1",
                modifier = Modifier.padding(8.dp),
                color = Color.White,
                style = TextStyle(
                    fontWeight = FontWeight.Bold,
                    fontSize = 14.sp,
                    color = Color.Black
                )
            )
        }
    },
    frontLayerContent = {
        LazyColumn(
            modifier = Modifier
                .fillMaxSize()
        ) {
            // Add 5 items
            items(55, key = { it }) { index ->
                Text(
                    text = "Item: ${index + 1}",
                    textAlign = TextAlign.Center,
                    style = MaterialTheme.typography.headlineLarge,
                    modifier = Modifier.padding(5.dp)
                )
            }
        }
    },
    isRevealed = isRevealed
)