I am creating Heterogenous list using jetpack compose. I have an issue with recomposition when one of the item is clicked.
My Idea is to update the list with new item when an item clicked, I am receiving update list but list is not recomposed.
Please find below code:
//Initial function that prepared screen where content of list is wrapped in Scafold
#Composable fun PrepareOverViewScreen(overViewList: List<OverViewListItem>) {
val infoList = remember {mutableStateOf(overViewList) }
Scaffold(topBar = { TopBar("OverView") },
content = { DisplayOverViewScreen(overViewList = overViewList) },
backgroundColor = Color(0xFFf2f2f2)
)
}
//Displays list using LazyColumn when in itemType OverViewHeaderItem I am trying to recompose list with latest data , but not succesful.
#Composable fun DisplayOverViewScreen(
modifier: Modifier = Modifier, overViewList: List<OverViewListItem>
) {
val infoList = remember {mutableStateOf(overViewList) }
LazyColumn(modifier = modifier) {
items(infoList.value) { data ->
when (data) {
is OverViewHeaderItem -> {
HeaderItem(data,infoList.value,onClick = { newValue ->
println("New Value is $newValue")
infoList.value = newValue.toMutableList()
})
}
is OverViewChildItem -> {
ChildListBasedOnListType(data)
}
}
}
}
}
//In this function the onClick is executed when clicked on Image()
#Composable fun HeaderItem(overViewHeaderItem: OverViewHeaderItem,overViewList: List<OverViewListItem>,onClick: (List<OverViewListItem>) -> Unit) {
var angle by remember {
mutableStateOf(0f)
}
Row(modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.background(color = Color(0xffd7d7d7)),
horizontalArrangement = Arrangement.Start, verticalAlignment = Alignment.CenterVertically) {
Text(
text = stringResource(R.string.history_yesterday), color = Color.Black,
modifier = Modifier.padding(start = 16.dp)
)
// Spacer(Modifier.weight(1f))
Image(
painter = painterResource(id = R.drawable.ic_expand_more),
modifier = Modifier
.padding(end = 5.dp)
.rotate(angle)
.clickable {
angle = (angle + 180) % 360f
// canDisplayChild = ! canDisplayChild
overViewList.map {
when (it) {
is OverViewChildItem -> {
if (it.listType == ItemType.HISTORY_YESTERDAY_CHILD) {
it.canShowHistoryChild = true
}
}
else -> {}
}
}
onClick(overViewList)
},
contentDescription = "Expandable Image"
)
}
}
Please help me resolve this issue.
#Composable fun ChildListBasedOnListType(overViewChildItem: OverViewChildItem){
when(overViewChildItem.listType){
ItemType.HISTORY_YESTERDAY_CHILD -> {
if(overViewChildItem.canShowHistoryChild){
ChildListItem(overViewChildItem)
Divider(color = Color(0xffd7d7d7), thickness = 1.dp)
}
}
else -> {
ChildListItem(overViewChildItem)
Divider(color = Color(0xffd7d7d7), thickness = 1.dp)
}
}
}
#Composable fun ChildListItem(overViewChildItem: OverViewChildItem) {
ConstraintLayout(
constraintSet = getConstraints(),
modifier = Modifier
.fillMaxSize()
.height(60.dp)
.background(color = Color(0xffffffff))
) {
Text(
text = overViewChildItem.article.articleName, Modifier
.layoutId("articleLabel")
.padding(start = 16.dp)
.fillMaxWidth()
.wrapContentHeight()
)
Text(
text = overViewChildItem.article.description, Modifier
.layoutId("desc")
.padding(start = 16.dp)
.fillMaxWidth()
.wrapContentHeight()
)
Image(
painter = painterResource(id = R.drawable.ic_baseline_chevron_right_24),
modifier = Modifier.layoutId("detail"), contentDescription = "Detail Image"
)
}
}
#Composable fun getConstraints(): ConstraintSet {
return ConstraintSet {
val articleLabel = createRefFor("articleLabel")
val articleDesc = createRefFor("desc")
val detailImage = createRefFor("detail")
createVerticalChain(articleLabel, articleDesc, chainStyle = ChainStyle.Packed)
constrain(articleLabel) {
top.linkTo(parent.top)
start.linkTo(parent.start)
end.linkTo(parent.end)
}
constrain(articleDesc) {
top.linkTo(articleLabel.bottom)
start.linkTo(parent.start)
end.linkTo(parent.end)
}
constrain(detailImage){
top.linkTo(parent.top)
end.linkTo(parent.end,5.dp)
bottom.linkTo(parent.bottom)
}
}
}
Related
What I want
Top fixed item with status bar padding and adaptive radius
Bottom fixed item with navigation bar padding
Adaptive center item, have enough room => Not scrollable, if not => scrollable
Current status
For full demo click this link
Only problem here is when there is no extra space, and we are dragging the scrollable list.
I think it's a bug, because everything is fine except the scrollable column.
window.height - sheetContent.height >= statusBarHeight
Everything is fine.
window.height - sheetContent.height < statusBarHeight
Dragging top fixed item or bottom fixed item, scroll still acting well.
Dragging the scrollable list, sheet pops back to top when the sheetState.offset is approaching statusBarHeight
Test youself
You can test it with these 3 functions, for me, I'm using Pixel 2 Emulator, itemCount at 18,19 could tell the difference.
#Composable
fun CEModalBottomSheet(
sheetState: ModalBottomSheetState,
onCloseDialogClicked: () -> Unit,
title: String = "BottomSheet Title",
toolbarElevation: Dp = 0.dp,
sheetContent: #Composable ColumnScope.() -> Unit,
) {
val density = LocalDensity.current
val sheetOffset = sheetState.offset
val statusBar = WindowInsets.statusBars.asPaddingValues()
val shapeRadius by animateDpAsState(
if (sheetOffset.value < with(density) { statusBar.calculateTopPadding().toPx() }) {
0.dp
} else 12.dp
)
val dynamicStatusBarPadding by remember {
derivedStateOf {
val statusBarHeightPx2 = with(density) { statusBar.calculateTopPadding().toPx() }
val offsetValuePx = sheetOffset.value
if (offsetValuePx >= statusBarHeightPx2) {
0.dp
} else {
with(density) { (statusBarHeightPx2 - offsetValuePx).toDp() }
}
}
}
ModalBottomSheetLayout(
sheetState = sheetState,
sheetShape = RoundedCornerShape(topStart = shapeRadius, topEnd = shapeRadius),
content = {},
sheetContent = {
Column(modifier = Modifier.fillMaxWidth()) {
TopTitleItemForDialog(
title = title,
elevation = toolbarElevation,
topPadding = dynamicStatusBarPadding,
onClick = onCloseDialogClicked
)
sheetContent()
}
})
}
#Composable
fun TopTitleItemForDialog(
title: String,
elevation: Dp = 0.dp,
topPadding: Dp = 0.dp,
onClick: () -> Unit
) {
Surface(
modifier = Modifier.fillMaxWidth(),
color = Color.LightGray,
elevation = elevation
) {
Row(
modifier = Modifier.padding(top = topPadding),
verticalAlignment = Alignment.CenterVertically
) {
Spacer(modifier = Modifier.size(16.dp))
Text(
text = title,
maxLines = 1,
modifier = Modifier.weight(1f)
)
IconButton(onClick = onClick) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = stringResource(R.string.cancel),
tint = Color.Gray,
modifier = Modifier.size(24.dp)
)
}
}
}
}
class SheetPaddingTestActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.statusBarColor = android.graphics.Color.TRANSPARENT
window.navigationBarColor = android.graphics.Color.TRANSPARENT
WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {
Box(
Modifier
.fillMaxSize()
.background(Color.Green), contentAlignment = Alignment.Center
) {
var itemCount by remember { mutableStateOf(20) }
val state = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden) { false }
val scope = rememberCoroutineScope()
Text("显示弹窗", modifier = Modifier.clickable {
scope.launch { state.animateTo(ModalBottomSheetValue.Expanded) }
})
CEModalBottomSheet(sheetState = state,
onCloseDialogClicked = {
scope.launch {
state.hide()
}
}, sheetContent = {
Column(
Modifier
.verticalScroll(rememberScrollState())
.weight(1f, fill = false)
.padding(horizontal = 16.dp),
) {
repeat(itemCount) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(30.dp)
.background(Color.Blue.copy(alpha = 1f - it * 0.04f))
)
}
}
CompositionLocalProvider(
LocalContentColor.provides(Color.White)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colors.primary)
.padding(vertical = 12.dp)
.navigationBarsPadding(),
verticalAlignment = Alignment.CenterVertically
) {
IconButton(modifier = Modifier.weight(1f),
onClick = {
itemCount = max(itemCount - 1, 0)
}) {
Icon(Icons.Default.KeyboardArrowLeft, "")
}
Text(
modifier = Modifier.weight(1f), text = "$itemCount",
textAlign = TextAlign.Center
)
IconButton(modifier = Modifier.weight(1f),
onClick = {
itemCount++
}) {
Icon(Icons.Default.KeyboardArrowRight, "")
}
}
}
}
)
}
}
}
}
I am very new to jetpack compose please help,
I have use Surface() but my views are overlapping one another,
I want separate view as first one should be TopHeader() and another one should be BillForm
My code is:-
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApp {
//TopHeader()
MainContent()
}
}
}
}
#Composable
fun MyApp(content: #Composable () -> Unit) {
JetTipAppTheme {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
content()
}
}
}
TopHeader function to display the pink color view in the given image
#Composable
fun TopHeader(totalPerPerson: Double = 134.0) {
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(15.dp)
.height(150.dp)
.clip(shape = CircleShape.copy(all = CornerSize(12.dp))),
//.clip(shape = RoundedCornerShape(corner = CornerSize(12.dp))),
color = Color(0xFFE9D7F7)
) {
Column(
modifier = Modifier.padding(12.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
val total = "%.2f".format(totalPerPerson)
Text(
text = "Total per person",
style = MaterialTheme.typography.h5
)
Text(
text = "$$total",
style = MaterialTheme.typography.h4,
fontWeight = FontWeight.ExtraBold
)
}
}
}
This is MainContent function:-
#Composable
fun MainContent() {
BillForm(){ billAmt ->
Log.d("AMT","MainContent: $billAmt")
}
}
BillForm composable function
#Composable
fun BillForm(modifier: Modifier = Modifier,
onValChange: (String) -> Unit = {}
){
val totalBillState = remember{
mutableStateOf("")
}
val validState = remember(totalBillState.value) {
totalBillState.value.trim().isNotEmpty()
}
val keyboardController = LocalSoftwareKeyboardController.current
val sliderPositionState = remember {
mutableStateOf(0f) //slider will take position from zero
}
TopHeader()
Surface(
modifier = Modifier
.padding(2.dp)
.fillMaxWidth(),
shape = RoundedCornerShape(corner = CornerSize(8.dp)),
border = BorderStroke(width = 1.dp, color = Color.LightGray)
) {
Column(
modifier = Modifier.padding(6.dp),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.Start
) {
InputField(
valueState = totalBillState,
labelId = "Enter Bill",
enabled = true,
isSingleLined = true,
onAction = KeyboardActions{
if (!validState)return#KeyboardActions
onValChange(totalBillState.value.trim())
keyboardController?.hide()
}
)
//if(validState){
Row(
modifier = Modifier.padding(3.dp),
horizontalArrangement = Arrangement.Start
) {
Text(text = "Split",
modifier = Modifier.align(
alignment = Alignment.CenterVertically
))
//Spacer in between text and buttons
Spacer(modifier = Modifier.width(120.dp))
//Row for Buttons
Row(modifier = Modifier.padding(horizontal = 3.dp),
horizontalArrangement = Arrangement.End
) {
RoundIconButton( imageVector = Icons.Default.Remove,
onClick = { })
Text(text = "2",
modifier = Modifier
.align(Alignment.CenterVertically)
.padding(start = 9.dp, end = 9.dp)
)
RoundIconButton( imageVector = Icons.Default.Add,
onClick = { })
}
}
//Tip Row
Row(modifier = Modifier
.padding(horizontal = 3.dp, vertical = 12.dp)
) {
Text(text = "Tip",
modifier = Modifier.align(alignment = Alignment.CenterVertically))
Spacer(modifier = Modifier.width(200.dp))
Text(text = "$33.00",
modifier = Modifier.align(alignment = Alignment.CenterVertically))
}
Column(verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "33%")
Spacer(modifier = Modifier.height(14.dp))
//Slider
Slider(value = sliderPositionState.value,
onValueChange = { newVal ->
sliderPositionState.value = newVal //<- this will change the position of the slider
Log.d("Slider","BillForm: $newVal")
},
modifier = Modifier.padding(start = 16.dp, end = 16.dp),
steps = 5,
onValueChangeFinished = {
}
)
}
// }else{
// Box() {
//
// }
// }
}
}
}
TopHeader composable view:-
BillForm view:-
What I want is like this given image:-
Use a Column(){} Composable for that purpose.
You can follow these basics that can help you to organize your composables and understand how its work.
Based on what was answered in this question (Open ModalSheetLayout on TextField focus instead of Keyboard) and the exchange of comments I did with #Abhimanyu, I was able to get the ModalBottomSheetLayout to appear when I click on one of the TextFields, however I encountered two more problems. I can't center what's in the content, nor can I center the content that's inside the sheet content. Can anyone help me understand why?
Here is a print of what is happening and my code:
#ExperimentalMaterialApi
#Preview
#Composable
fun ProfileScreen() {
var profileModalBottomSheetType by remember { mutableStateOf(ProfileModalBottomSheetType.NONE) }
val modalBottomSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
val coroutineScope = rememberCoroutineScope()
if (modalBottomSheetState.currentValue != ModalBottomSheetValue.Hidden) {
DisposableEffect(Unit) {
onDispose {
profileModalBottomSheetType = ProfileModalBottomSheetType.NONE
}
}
}
ModalBottomSheetLayout(
sheetState = modalBottomSheetState,
sheetShape = RoundedCornerShape(topStart = 13.dp, topEnd = 13.dp),
sheetContent = {
Box(
modifier = Modifier
.padding(top = 10.dp)
.height(10.dp)
.width(100.dp)
.background(
color = Color.LightGray,
shape = RoundedCornerShape(4.dp)
)
)
when (profileModalBottomSheetType) {
ProfileModalBottomSheetType.SELECT_RATE -> {
SelectRateModalBottomSheet(listOf("Exact Rate", "Range"))
}
else -> {}
}
}
) {
LazyColumn(
modifier = Modifier
.width(320.dp)
) {
item {
HeightSpacer(40.dp)
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(id = R.drawable.ic_clearjobs_logo_2x),
contentDescription = null
)
}
HeightSpacer(47.dp)
Column(
modifier = Modifier
.width(320.dp)
.padding(start = 20.dp, end = 20.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Profile Light Title",
style = TextStyle32Light,
textAlign = TextAlign.Center
)
Text(
text = "Profile Bold Title",
style = TextStyle32Bold,
textAlign = TextAlign.Center
)
}
HeightSpacer(47.dp)
Column(
modifier = Modifier
.background(
shape = RoundedCornerShape(
topStart = 13.dp,
topEnd = 13.dp
), color = Color.White
)
.padding(bottom = 140.dp)
.width(320.dp)
) {
Text(
text = stringResource(id = R.string.your_profile),
style = TextStyle28Bold,
modifier = Modifier.padding(
top = 40.dp,
start = 20.dp,
bottom = 30.dp
)
)
Text(
text = stringResource(id = R.string.salary_range),
style = TextStyle28Bold,
modifier = Modifier.padding(
top = 40.dp,
start = 20.dp,
bottom = 30.dp
)
)
Box {
LightBlueBorderTextField(
title = "Rate",
initialState = "Exact Rate",
textFieldTextStyle = TextStyle16BlackOpacity50Normal,
enabled = false
) { innerTextField ->
Row(verticalAlignment = Alignment.CenterVertically) {
Box(
modifier = Modifier
.weight(1f)
.padding(start = Dimen10)
) {
innerTextField()
}
}
}
Box(
modifier = Modifier
.matchParentSize()
.alpha(0f)
.clickable(
onClick = {
profileModalBottomSheetType =
ProfileModalBottomSheetType.SELECT_RATE
toggleModalBottomSheetState(
coroutineScope = coroutineScope,
modalBottomSheetState = modalBottomSheetState,
)
}
)
)
}
}
}
}
}
}
#Composable
fun SelectRateModalBottomSheet(options: List<String>) {
LazyColumn(
modifier = Modifier.padding(
start = 20.dp,
end = 20.dp,
top = 15.dp,
bottom = 15.dp
)
) {
items(options.size) { optionIndex ->
val option = options[optionIndex]
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(start = 10.dp, end = 10.dp, bottom = 15.dp)
) {
Box(Modifier.weight(1f)) {
Text(text = option, style = TextStyle16BlackOpacity50Normal)
}
RadioButton(
selected = false,
onClick = { /*TODO*/ },
modifier = Modifier
.width(20.dp)
.height(20.dp)
)
}
if (optionIndex != options.lastIndex) {
Divider(color = Color.DarkGray, thickness = 1.dp)
HeightSpacer(dimen = 15.dp)
}
}
}
}
#Composable
fun HeightSpacer(dimen: Dp) {
Spacer(modifier = Modifier.height(dimen))
}
#Preview
#Composable
fun LightBlueBorderTextField(
title: String = "",
initialState: String = "",
textFieldTextStyle: TextStyle = TextStyle18Normal,
enabled: Boolean = true,
decorationBox: #Composable (innerTextField: #Composable () -> Unit) -> Unit = { innerTextField ->
Box(
Modifier.padding(start = Dimen10),
contentAlignment = Alignment.CenterStart
) {
innerTextField()
}
}
) {
Column {
val state = remember { mutableStateOf(TextFieldValue(initialState)) }
if (title.isNotEmpty()) {
Text(
text = title,
style = TextStyle16BlackBold,
modifier = Modifier.padding(
top = Dimen40,
start = Dimen30,
bottom = Dimen10
)
)
} else {
HeightSpacer(Dimen40)
}
CustomTextField(
state = state,
modifier = Modifier
.height(Dimen45)
.padding(start = Dimen20, end = Dimen20)
.border(
width = Dimen1,
color = LightBlue,
shape = RoundedCornerShape(Dimen13)
)
.background(Color.White, RoundedCornerShape(Dimen13))
.fillMaxWidth(),
textStyle = textFieldTextStyle,
decorationBox = decorationBox,
enabled = enabled
)
}
}
#Composable
fun CustomTextField(
state: MutableState<TextFieldValue>,
modifier: Modifier,
textStyle: TextStyle = TextStyle18Normal,
decorationBox: #Composable (innerTextField: #Composable () -> Unit) -> Unit,
enabled: Boolean = true
) {
BasicTextField(
modifier = modifier,
value = state.value,
onValueChange = { value -> state.value = value },
singleLine = true,
textStyle = textStyle,
decorationBox = decorationBox,
enabled = enabled
)
}
#ExperimentalMaterialApi
fun toggleModalBottomSheetState(
coroutineScope: CoroutineScope,
modalBottomSheetState: ModalBottomSheetState,
action: (() -> Unit)? = null,
) {
coroutineScope.launch {
if (!modalBottomSheetState.isAnimationRunning) {
if (modalBottomSheetState.isVisible) {
modalBottomSheetState.hide()
} else {
modalBottomSheetState.show()
}
}
action?.invoke()
}
}
internal enum class ProfileModalBottomSheetType {
NONE,
SELECT_JOB_KEYWORDS,
SELECT_WORK_LOCATIONS,
SELECT_TAGS,
SELECT_RATE,
SELECT_SALARY_PERIOD
}
I solved this issue by putting all the ModalBottomSheet content inside a Box with Modifier.fillMaxSize() and the contentAlignment = Alignment.Center. See my code below:
ModalBottomSheetLayout(
sheetState = modalBottomSheetState,
sheetShape = RoundedCornerShape(topStart = Dimen13, topEnd = Dimen13),
sheetContent = {
JobDetailsModalBottomSheet(modalBottomSheetType, jobClicked) {
modalBottomSheetType = JobOpeningsScreenModalBottomSheetType.NONE
toggleModalBottomSheetState(
coroutineScope = coroutineScope,
modalBottomSheetState = modalBottomSheetState,
)
}
}) {
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
// any content
}
}
I'm not really sure why this doesn't work without Box, but after a few tries, this was the solution I found and it worked.
I have a LazyVerticalGrid composable that shows my items. I want to show an error message with a button in the center of LazyVerticalGrid when an error has occured. Here is my error item and my LazyVerticalGrid:
ErrorItem
#Composable
fun ErrorItem(
message: String,
modifier: Modifier = Modifier,
onRetryClick: () -> Unit,
) {
Column(
modifier = modifier
.padding(dimensionResource(id = R.dimen.dimen_8)),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = message,
maxLines = 1,
style = MaterialTheme.typography.subtitle2,
color = MaterialTheme.colors.error,
modifier = Modifier.padding(vertical = dimensionResource(id = R.dimen.dimen_16))
)
Button(
onClick = onRetryClick,
shape = CircleShape,
colors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.error,
contentColor = MaterialTheme.colors.onError
)
) {
Text(text = stringResource(id = R.string.try_again))
}
}
}
#Composable
fun CompaniesScreen(
modifier: Modifier = Modifier
) {
val viewModel: CompaniesScreenViewModel = hiltViewModel()
val companies: LazyPagingItems<Company> = viewModel.companies.collectAsLazyPagingItems()
val isLoading = companies.loadState.refresh is LoadState.Loading
val isError = companies.loadState.refresh is LoadState.Error
val swipeRefreshState = rememberSwipeRefreshState(isRefreshing = isLoading)
val listState = rememberLazyGridState()
Scaffold(modifier = modifier, topBar = { LogoToolbar() }) { padding ->
SwipeRefresh(indicator = { state, trigger ->
SwipeRefreshIndicator(
state = state,
refreshTriggerDistance = trigger,
scale = true,
backgroundColor = MaterialTheme.colors.primary,
contentColor = MaterialTheme.colors.onPrimary
)
}, state = swipeRefreshState, onRefresh = companies::refresh) {
LazyVerticalGrid(
state = listState,
modifier = Modifier
.fillMaxSize()
.padding(
horizontal = dimensionResource(id = R.dimen.dimen_20),
vertical = dimensionResource(
id = R.dimen.dimen_24
)
),
columns = GridCells.Fixed(2),
horizontalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.dimen_20)),
verticalArrangement = Arrangement.spacedBy(
dimensionResource(
id = R.dimen.dimen_24
)
)
) {
if (!isLoading && !isError) {
items(companies.itemCount) { index ->
CompanyItem(companyName = companies[index]?.name!!)
}
}
companies.apply {
when {
loadState.refresh is LoadState.Loading -> {
items(16) {
CompanyItem(companyName = "", isLoading = true)
}
}
loadState.refresh is LoadState.Error -> {
val e = companies.loadState.refresh as LoadState.Error
item(span = {GridItemSpan(2)}) {
ErrorItem(
modifier = Modifier.fillMaxSize(), // fillParentMaxSize() is not working
message = e.error.localizedMessage
?: stringResource(id = R.string.something_went_wrong),
onRetryClick = companies::retry
)
}
}
}
}
}
}
}
}
I have tried to use fillParentMaxSizewith my ErrorItem but it is not working. Here is the result :
If you place your LazyVerticalGrid and ErrorItem inside a Box with contentAlignment = Alignment.Center ErrorItem will show center of your Box when isError is true
Box(
modifier= Modifier.fillMaxSize(),
contentAlignment = Alignment.Center){
LazyVerticalGrid(
// Rest of the properties
)
if (isError){
ErrorItem(""){
}
}
}
I am trying to show a section within LazyColumn which has a list of Rows that are shown horizontally using LazyRow. I would like to have a button which displays show/hide so that I can show a minimal list in this section instead of full list. I would like to animate the expand/collapse part and currently expanding on button click is working as expected but when collapsing, the LazyCloumn scrolls up which seems to push this section out of screen (as shown in the video below). Is there any way we can collapse so that the button at least gets snapped to the top and the remaining section is removed? This way, user can still expand the list if required rather than scrolling up to find the button.
I have tried the following but none of them seem to work:
Using AnimatedVisibility
Using animate*AsState low level API's
Also tried to just remove the contents from list allowing LazyColumn to re-order based on the list content
val RandomColor
get() = Color(Random.nextInt(256), Random.nextInt(256), Random.nextInt(256))
typealias ClickHandler = (Boolean) -> Unit
#Composable
fun DemoLayout(demoDataList: List<DemoData>, isExpanded: Boolean, clickHandler: ClickHandler) {
LazyColumn {
demoDataList.forEachIndexed { index, it ->
when (it) {
is DemoData.Header -> item(key = "cell_$index") { HeaderComposable(header = it) }
is DemoData.BigCard -> item(key = "hero_$index") { BigCardComposable(bigCard = it) }
is DemoData.Card -> item(key = "banner_$index") { CardComposable(card = it) }
is DemoData.ExpandableSection -> {
items(count = 2, key = { indexInner: Int -> "categories_first_half_$index$indexInner" }) { index ->
Section(
sectionInfo = it.sectionInfo[index]
)
}
//Comment below and try another approach
item(key = "first_approach_$index") {
FirstApproach(
expandableSection = DemoData.ExpandableSection(
it.sectionInfo.subList(
3,
5
)
)
)
}
//Second approach
/*if (isExpanded)
items(count = 3, key = { indexInner -> "categories_second_half_$index$indexInner" }) { index ->
Section(
sectionInfo = it.sectionInfo[index + 2]
)
}
item(key = "button_$index") {
ShowHideButton(isExpanded, clickHandler)
}*/
}
}
}
}
}
#Composable
fun FirstApproach(expandableSection: DemoData.ExpandableSection) {
var expanded by remember { mutableStateOf(false) }
val density = LocalDensity.current
Column {
AnimatedVisibility(
visible = expanded,
enter = slideInVertically() +
expandVertically(
// Expand from the top.
expandFrom = Alignment.Top,
animationSpec = tween(durationMillis = 350, easing = FastOutLinearInEasing)
) + fadeIn(
// Fade in with the initial alpha of 0.3f.
initialAlpha = 0.3f
),
exit = slideOutVertically(
animationSpec = tween(durationMillis = 350, easing = FastOutLinearInEasing)
) + shrinkVertically(
shrinkTowards = Alignment.Bottom,
animationSpec = tween(durationMillis = 350, easing = FastOutLinearInEasing)
) + fadeOut(
animationSpec = tween(durationMillis = 350, easing = FastOutLinearInEasing),
targetAlpha = 0f
)
) {
Column {
for (i in 0 until expandableSection.sectionInfo.size) {
HeaderComposable(header = expandableSection.sectionInfo[i].header)
InfoCardsComposable(expandableSection.sectionInfo[i].infoCard)
DetailsCardComposable(expandableSection.sectionInfo[i].detailCard)
}
}
}
Button(
modifier = Modifier
.padding(top = 16.dp, start = 16.dp, end = 16.dp)
.fillMaxWidth(),
onClick = {
expanded = !expanded
}) {
Text(text = if (expanded) "Hide" else "Show")
}
}
}
#Composable
fun HeaderComposable(header: DemoData.Header) {
Row(
modifier = Modifier
.padding(top = 16.dp)
.fillMaxWidth()
.height(64.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(text = header.title, modifier = Modifier.padding(horizontal = 16.dp))
}
}
#Composable
fun CardComposable(card: DemoData.Card) {
Card(
modifier = Modifier
.padding(top = 16.dp)
.size(164.dp),
backgroundColor = RandomColor
) {
Text(text = card.cardText, modifier = Modifier.padding(horizontal = 16.dp))
}
}
#Composable
fun BigCardComposable(bigCard: DemoData.BigCard) {
Card(
modifier = Modifier
.padding(top = 16.dp)
.size(172.dp),
backgroundColor = RandomColor
) {
Text(text = bigCard.bigCardText, modifier = Modifier.padding(horizontal = 16.dp))
}
}
#Composable
fun Section(sectionInfo: SectionInfo) {
Column(
modifier = Modifier.animateContentSize()
) {
HeaderComposable(header = sectionInfo.header)
InfoCardsComposable(sectionInfo.infoCard)
DetailsCardComposable(sectionInfo.detailCard)
}
}
#Composable
private fun ShowHideButton(isExpanded: Boolean, clickHandler: ClickHandler) {
Button(
modifier = Modifier
.padding(top = 16.dp, start = 16.dp, end = 16.dp)
.fillMaxWidth(),
onClick = {
clickHandler.invoke(
!isExpanded
)
}) {
Text(text = if (isExpanded) "Hide" else "Show")
}
}
#Composable
fun DetailsCardComposable(detailCardsList: List<DetailCard>) {
LazyRow(
modifier = Modifier.padding(top = 16.dp)
) {
items(detailCardsList) {
DetailCardComposable(detailCard = it)
}
}
}
#Composable
fun InfoCardsComposable(infoCardsList: List<InfoCard>) {
LazyRow(
modifier = Modifier.padding(top = 16.dp)
) {
items(infoCardsList) {
InfoCardComposable(infoCard = it)
}
}
}
#Composable
fun InfoCardComposable(infoCard: InfoCard) {
Card(
modifier = Modifier
.size(136.dp),
backgroundColor = RandomColor
) {
Text(text = infoCard.infoText, modifier = Modifier.padding(horizontal = 16.dp))
}
}
#Composable
fun DetailCardComposable(detailCard: DetailCard) {
Card(
modifier = Modifier
.size(156.dp),
backgroundColor = RandomColor
) {
Text(text = detailCard.detailText, modifier = Modifier.padding(horizontal = 16.dp))
}
}
Complete code to try out is available here: https://github.com/DirajHS/ComposeAnimation/tree/master
I would like to know if this is the expected behavior or am I doing something wrong?
Any suggestions on snapping the button to the top during collapse would be much appreciated.