Custom Layout won't change its position relative to other elements - android

I have created a custom layout to create a hexagonal grid, however, when I place the grid with other elements, for example inside a Row or Column, the grid won't update its position and stays on the top left corner of the map.
Here is my code:
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.GenericShape
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout
import com.example.mygame.ui.theme.MyGameTheme
private val HexagonalShape = GenericShape { size, _ ->
moveTo(size.width / 2f, 0f)
lineTo(size.width, size.height / 4f)
lineTo(size.width, 3 * size.height / 4f)
lineTo(size.width / 2f, size.height)
lineTo(0f, 3 * size.height / 4f)
lineTo(0f, size.height / 4f)
}
#Composable
fun StandardTile(color: Color) {
Box(
modifier = Modifier
.padding(horizontal = 1.dp, vertical = 1.dp)
.wrapContentSize(Alignment.Center)
.size(50.dp)
.clip(HexagonalShape)
.background(color)
.clickable { /* TODO */ }
.border(
width = 1.dp,
brush = SolidColor(Color.Black),
shape = HexagonalShape
)
) { }
}
#Composable
fun Grid(
modifier: Modifier,
lineLength: Int,
content: #Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
val placeables = measurables.map { measurable ->
measurable.measure(constraints)
}
layout(constraints.maxWidth, constraints.maxHeight) {
var yPosition = 0
var xPosition = 0
val index = mutableListOf(0, 0)
placeables.forEach { placeable ->
placeable.placeRelative(x = xPosition, y = yPosition)
if (index[0] != lineLength - 1) {
index[0]++
xPosition += placeable.width
} else {
xPosition = if (index[1] % 2 == 0) placeable.width / 2 else 0
yPosition += 3 * placeable.height / 4
index[0] = 0
index[1]++
}
}
}
}
}
#Preview
#Composable
fun ShapePreview() {
MyGameTheme {
ConstraintLayout {
val (gridRef, leftMenu) = createRefs()
val margin = 0.10f
val leftConstraint = createGuidelineFromStart(fraction = margin)
val rightConstraint = createGuidelineFromEnd(fraction = margin)
Column(
modifier = Modifier
.constrainAs(gridRef) {
linkTo(start = rightConstraint, end = leftConstraint)
start.linkTo(rightConstraint)
}
.fillMaxWidth()) {
Grid(
modifier = Modifier,
lineLength = 8
) {
for (i in 1..64) {
StandardTile(Color.Blue)
}
}
}
val leftMenuScrollState = rememberScrollState()
Column(
modifier = Modifier
.constrainAs(leftMenu) {
linkTo(start = parent.start, end = leftConstraint)
start.linkTo(parent.start)
}
.background(Color.Black)
.fillMaxWidth(fraction = margin)
.fillMaxHeight()
.verticalScroll(leftMenuScrollState)
) {
Text("Help Please, (this is inside the left menu btw)")
}
}
}
}
As you can see that I am using a ConstraintLayout. The relative position of the grid won't change and I don't know why, and I am having the same issue when I am using a row or column.
So here is my question, how do I implement it so that the grid goes to its relative attributed place and not at the top left corner and what's the reason for the same?
There is a post here that might answer my question but I'm not sure how to implement it in my case.

Use width = Dimension.fillToConstraints in addition to specifying start and end constraint on grid.
Also, in your code leftContraint (constraint from parent's start) was used for end and rightConstraint (constraint from parent's end) for start. Just swapped those two.
Column(
modifier = Modifier
.constrainAs(gridRef) {
// corrected start and end constraints
linkTo(start = leftConstraint, end = rightConstraint)
width = Dimension.fillToConstraints
}
) {
Grid(
modifier = Modifier,
lineLength = 8
) {
for (i in 1..64) {
StandardTile(Color.Blue)
}
}
}

Related

Custom top app bar with children drawn outside of parent bounds - Compose

I am trying to create a custom top app bar in Compose and Material3 (using TopAppBarLayout as base), that has a row of buttons running along the bottom edge. I want the row to spill past the bottom boundary and appear over content body of the Scaffold. Within the layout inspector, it seems that the row of buttons is placed in the correct spot, however the content body is covering the bottom part of the row:
I've looked at the Scaffold source to see if the placement issue is there, but it looks correct to me (content body is placed first, followed by top bar):
// Placing to control drawing order to match default elevation of each placeable
bodyContentPlaceables.forEach {
it.place(0, 0)
}
topBarPlaceables.forEach {
it.place(0, 0)
}
Swapping those around had no effect in any case. I also tried adding Modifier.wrapContentHeight(unbounded = true) in various spots, but none of them did what I was after.
This code is essentially TopAppBarLayout with the button row added to it:
#Composable
private fun TopAppBarLayout(
modifier: Modifier,
heightPx: Float,
navigationIconContentColor: Color,
titleContentColor: Color,
actionIconContentColor: Color,
title: #Composable () -> Unit,
titleTextStyle: TextStyle,
titleAlpha: Float,
titleVerticalArrangement: Arrangement.Vertical,
titleHorizontalArrangement: Arrangement.Horizontal,
titleBottomPadding: Int,
hideTitleSemantics: Boolean,
navigationIcon: #Composable () -> Unit,
actions: #Composable () -> Unit,
buttons: #Composable () -> Unit
) {
Layout(
{
Box(
Modifier
.layoutId("navigationIcon")
.padding(start = TopAppBarHorizontalPadding)
) {
CompositionLocalProvider(
LocalContentColor provides navigationIconContentColor,
content = navigationIcon
)
}
Box(
Modifier
.layoutId("title")
.padding(horizontal = TopAppBarHorizontalPadding)
.then(if (hideTitleSemantics) Modifier.clearAndSetSemantics { } else Modifier)
) {
ProvideTextStyle(value = titleTextStyle) {
CompositionLocalProvider(
LocalContentColor provides titleContentColor.copy(alpha = titleAlpha),
content = title
)
}
}
Box(
Modifier
.layoutId("actionIcons")
.padding(end = TopAppBarHorizontalPadding)
) {
CompositionLocalProvider(
LocalContentColor provides actionIconContentColor,
content = actions
)
}
Box(
Modifier
.layoutId("buttons")
.wrapContentHeight(unbounded = true)
) {
CompositionLocalProvider(
LocalContentColor provides Color.Red,
content = buttons
)
}
},
modifier = modifier
) { measurables, constraints ->
val navigationIconPlaceable =
measurables.first { it.layoutId == "navigationIcon" }
.measure(constraints.copy(minWidth = 0))
val actionIconsPlaceable =
measurables.first { it.layoutId == "actionIcons" }
.measure(constraints.copy(minWidth = 0))
val maxTitleWidth = if (constraints.maxWidth == Constraints.Infinity) {
constraints.maxWidth
} else {
(constraints.maxWidth - navigationIconPlaceable.width - actionIconsPlaceable.width)
.coerceAtLeast(0)
}
val titlePlaceable =
measurables.first { it.layoutId == "title" }
.measure(constraints.copy(minWidth = 0, maxWidth = maxTitleWidth))
// Locate the title's baseline.
val titleBaseline =
if (titlePlaceable[LastBaseline] != AlignmentLine.Unspecified) {
titlePlaceable[LastBaseline]
} else {
0
}
val buttonRowPlaceable =
measurables.first { it.layoutId == "buttons" }
.measure(constraints.copy(minWidth = 0))
val layoutHeight = heightPx.roundToInt()
layout(constraints.maxWidth, layoutHeight) {
// Navigation icon
navigationIconPlaceable.placeRelative(
x = 0,
y = (layoutHeight - navigationIconPlaceable.height) / 2
)
// Title
titlePlaceable.placeRelative(
x = when (titleHorizontalArrangement) {
Arrangement.Center -> (constraints.maxWidth - titlePlaceable.width) / 2
Arrangement.End ->
constraints.maxWidth - titlePlaceable.width - actionIconsPlaceable.width
// Arrangement.Start.
// An TopAppBarTitleInset will make sure the title is offset in case the
// navigation icon is missing.
else -> max(TopAppBarTitleInset.roundToPx(), navigationIconPlaceable.width)
},
y = when (titleVerticalArrangement) {
Arrangement.Center -> (layoutHeight - titlePlaceable.height) / 2
// Apply bottom padding from the title's baseline only when the Arrangement is
// "Bottom".
Arrangement.Bottom ->
if (titleBottomPadding == 0) layoutHeight - titlePlaceable.height
else layoutHeight - titlePlaceable.height - max(
0,
titleBottomPadding - titlePlaceable.height + titleBaseline
)
// Arrangement.Top
else -> 0
}
)
// Action icons
actionIconsPlaceable.placeRelative(
x = constraints.maxWidth - actionIconsPlaceable.width,
y = (layoutHeight - actionIconsPlaceable.height) / 2
)
// Button row
buttonRowPlaceable.placeRelative(
x = 0,
y = layoutHeight - buttonRowPlaceable.height / 2,
)
}
}
}

Can't represent a size of 357913941 in Constraints in Jetpack Compose

I have a CustomGrid that is inside a Column. That column is inside a HorizontalPager and the HorizontalPager is inside another column where there are other elements. I want the Grid to grow in height as much as necessary, but whenever I add fillMaxHeight() or wrapContentSize() or another equivalent method, the application crashes with the error you can see in the title. Is there anything I can do to fix this error and have the Grid take up as much space as I need?
I leave you the prints of my Grid, which is custom, and the respective components.
Custom Grid code:
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.ParentDataModifier
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import kotlin.math.max
interface GridScope {
#Stable
fun Modifier.span(columns: Int = 1, rows: Int = 1) = this.then(
GridData(columns, rows)
)
companion object : GridScope
}
private class GridData(
val columnSpan: Int,
val rowSpan: Int,
) : ParentDataModifier {
override fun Density.modifyParentData(parentData: Any?): Any = this#GridData
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as GridData
if (columnSpan != other.columnSpan) return false
if (rowSpan != other.rowSpan) return false
return true
}
override fun hashCode(): Int {
var result = columnSpan
result = 31 * result + rowSpan
return result
}
}
private val Measurable.gridData: GridData?
get() = parentData as? GridData
private val Measurable.columnSpan: Int
get() = gridData?.columnSpan ?: 1
private val Measurable.rowSpan: Int
get() = gridData?.rowSpan ?: 1
data class GridInfo(
val numChildren: Int,
val columnSpan: Int,
val rowSpan: Int,
)
#Composable
fun Grid(
columns: Int,
modifier: Modifier = Modifier,
content: #Composable GridScope.() -> Unit,
) {
check(columns > 0) { "Columns must be greater than 0" }
Layout(
content = { GridScope.content() },
modifier = modifier,
) { measurables, constraints ->
// calculate how many rows we need
val standardGrid = GridData(1, 1)
val spans = measurables.map { measurable -> measurable.gridData ?: standardGrid }
val gridInfo = calculateGridInfo(spans, columns)
val rows = gridInfo.sumOf { it.rowSpan }
// build constraints
val baseConstraints = Constraints.fixed(
width = constraints.maxWidth / columns,
height = constraints.maxHeight / rows,
)
val cellConstraints = measurables.map { measurable ->
val columnSpan = measurable.columnSpan
val rowSpan = measurable.rowSpan
Constraints.fixed(
width = baseConstraints.maxWidth * columnSpan,
height = baseConstraints.maxHeight * rowSpan
)
}
// measure children
val placeables = measurables.mapIndexed { index, measurable ->
measurable.measure(cellConstraints[index])
}
// place children
layout(
width = constraints.maxWidth,
height = constraints.maxHeight,
) {
var x = 0
var y = 0
var childIndex = 0
gridInfo.forEach { info ->
repeat(info.numChildren) {
val placeable = placeables[childIndex++]
placeable.placeRelative(
x = x,
y = y,
)
x += placeable.width
}
x = 0
y += info.rowSpan * baseConstraints.maxHeight
}
}
}
}
private fun calculateGridInfo(
spans: List<GridData>,
columns: Int,
): List<GridInfo> {
var currentColumnSpan = 0
var currentRowSpan = 0
var numChildren = 0
return buildList {
spans.forEach { span ->
val columnSpan = span.columnSpan.coerceAtMost(columns)
val rowSpan = span.rowSpan
if (currentColumnSpan + columnSpan <= columns) {
currentColumnSpan += columnSpan
currentRowSpan = max(currentRowSpan, rowSpan)
++numChildren
} else {
add(
GridInfo(
numChildren = numChildren,
columnSpan = currentColumnSpan,
rowSpan = currentRowSpan
)
)
currentColumnSpan = columnSpan
currentRowSpan = rowSpan
numChildren = 1
}
}
add(
GridInfo(
numChildren = numChildren,
columnSpan = currentColumnSpan,
rowSpan = currentRowSpan,
)
)
}
}
Code where Grid will be inserted or other components, it is generated based on data that comes from API:
Box(
modifier = Modifier
.fillMaxSize()
.background(backgroundColor.value)
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
// We only consume nested flings in the main-axis, allowing cross-axis flings to propagate
// as normal
val consumeFlingNestedScrollConnection =
remember { ConsumeFlingNestedScrollConnection(consumeHorizontal = true) }
Column(
modifier = Modifier
.background(
backgroundColor.value,
RoundedCornerShape(topStart = Dimen30, topEnd = Dimen30)
)
.nestedScroll(connection = consumeFlingNestedScrollConnection)
.fillMaxWidth()
) {
HorizontalPager(
count = size,
state = pagerState,
itemSpacing = Dimen20,
modifier = Modifier.padding(top = Dimen33),
userScrollEnabled = false
) { page ->
Column(
modifier = Modifier
// We don't any nested flings to continue in the pager, so we add a
// connection which consumes them.
.nestedScroll(connection = consumeFlingNestedScrollConnection)
// Constraint the content width to be <= than the width of the pager.
.fillParentMaxWidth()
.wrapContentSize()
) {
// content (where grid could be, content is generated dinamically based on data that comes from api
}
}
}
}
}
How Grid is added to that layout:
Grid(
columns = 5,
modifier = Modifier
.padding(start = Dimen20, end = Dimen20, top = Dimen16)
.fillMaxWidth()
.wrapContentSize()
) {
// cards content
}
The crash points to the baseConstraints of the Grid code, but I can't figure out why and I can't solve the problem.
When you use Constraints.fixed() you need to have Constraints maxWidth and maxHeight that are not infinite.
When you apply Modifier.verticalScroll you transform your maxHeight to Constraints.Infinity which is 2147483647. I explained in detail in this answer about constraints with vertical scroll and Constraints.
And created a sample to show why it crashes
#Composable
private fun Grid(
modifier: Modifier = Modifier,
content: #Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
val placeables = measurables.map { measurable: Measurable ->
measurable.measure(
Constraints.fixed(
width = constraints.maxWidth,
height = constraints.maxHeight
)
)
}
layout(constraints.maxWidth, constraints.maxHeight) {
placeables.forEach {
it.placeRelative(0, 0)
}
}
}
}
demonstration why it crashes
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
HorizontalPager(count = 3) {
Column(
modifier = Modifier.wrapContentSize()
) {
Grid() {
BoxWithConstraints() {
Text("Constraints: ${this.constraints}")
}
}
}
}
}
What you should be doing is to constrain maxHeight to parent or screen size.
One, and simple way to do is, passing it via Modifier.onSizeChanged to your Layout, but be careful using Modifier.onSizeChanged which you might trigger recomposition continuously when mutableState is used to update or set size of another Composable.
var height by remember {
mutableStateOf(0)
}
Box(
modifier = Modifier
.fillMaxSize()
.onSizeChanged {
if (height == 0) {
height = it.height
}
}
){
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
HorizontalPager(count = 3) {
Column(
modifier = Modifier.wrapContentSize()
) {
Grid(height = height) {
BoxWithConstraints {
Text("Constraints: ${this.constraints}")
}
}
}
}
}
}
And use parent height instead of infinite height due to Modifier.verticalScroll
#Composable
private fun Grid(
modifier: Modifier = Modifier,
height:Int,
content: #Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
val placeables = measurables.map { measurable: Measurable ->
measurable.measure(
Constraints.fixed(
width = constraints.maxWidth,
height = constraints.maxHeight.coerceAtMost(height)
)
)
}
layout(constraints.maxWidth, constraints.maxHeight) {
placeables.forEach {
it.placeRelative(0, 0)
}
}
}
}
Changing your Compsable based on sample above you can achieve what you wish to create

How to build grid images that change position and size and display number when image count is above threshold?

This is a share your knowledge, Q&A-style question about using Layout, different Constraints and placing based on size and other Composables' positions to achieve a grid that changes Composable size, position and adds a number if number of Composables are greater than 4.
To layout Composables based on their count Layout is required and need to select Constraints, a detailed answer about Constraints is available in this link, based on number of items and place them accordingly.
For Constraints selection when there are 2 items we need to pick half width and full height to have result in question. When there are 4 items we need to pick half width and half height.
When item count is 3 we need to use 2 constraints, 1 for measuring first 2 items, another one measuring the one that covers whole width
#Composable
private fun ImageDrawLayout(
modifier: Modifier = Modifier,
itemCount: Int,
divider: Dp,
content: #Composable () -> Unit
) {
val spacePx = LocalDensity.current.run { (divider).roundToPx() }
val measurePolicy = remember(itemCount, spacePx) {
MeasurePolicy { measurables, constraints ->
val newConstraints = when (itemCount) {
1 -> constraints
2 -> Constraints.fixed(
width = constraints.maxWidth / 2 - spacePx / 2,
height = constraints.maxHeight
)
else -> Constraints.fixed(
width = constraints.maxWidth / 2 - spacePx / 2,
height = constraints.maxHeight / 2 - spacePx / 2
)
}
val gridMeasurables = if (itemCount < 5) {
measurables
} else {
measurables.take(3) + measurables.first { it.layoutId == "Text" }
}
val placeables: List<Placeable> = if (measurables.size != 3) {
gridMeasurables.map { measurable: Measurable ->
measurable.measure(constraints = newConstraints)
}
} else {
gridMeasurables
.take(2)
.map { measurable: Measurable ->
measurable.measure(constraints = newConstraints)
} +
gridMeasurables
.last()
.measure(
constraints = Constraints.fixed(
constraints.maxWidth,
constraints.maxHeight / 2 - spacePx
)
)
}
layout(constraints.maxWidth, constraints.maxHeight) {
when (itemCount) {
1 -> {
placeables.forEach { placeable: Placeable ->
placeable.placeRelative(0, 0)
}
}
2 -> {
var xPos = 0
placeables.forEach { placeable: Placeable ->
placeable.placeRelative(xPos, 0)
xPos += placeable.width + spacePx
}
}
else -> {
var xPos = 0
var yPos = 0
placeables.forEachIndexed { index: Int, placeable: Placeable ->
placeable.placeRelative(xPos, yPos)
if (index % 2 == 0) {
xPos += placeable.width + spacePx
} else {
xPos = 0
}
if (index % 2 == 1) {
yPos += placeable.height + spacePx
}
}
}
}
}
}
}
Layout(
modifier = modifier,
content = content,
measurePolicy = measurePolicy
)
}
Another thing to note here is we need to find find Composable that contains Text. It's possible to find it from index since it's 4th item but i used Modifier.layoutId() for demonstration. This Modifier helps finding Composables when you don't know in which order they are placed inside a Composaable.
val gridMeasurables = if (size < 5) {
measurables
} else {
measurables.take(3) + measurables.first { it.layoutId == "Text" }
}
And place items based on item count and we add space only after first item on each row.
Usage
#Composable
fun GridImageLayout(
modifier: Modifier = Modifier,
thumbnails: List<Int>,
divider: Dp = 2.dp,
onClick: ((List<Int>) -> Unit)? = null
) {
if (thumbnails.isNotEmpty()) {
ImageDrawLayout(
modifier = modifier
.clickable {
onClick?.invoke(thumbnails)
},
divider = divider,
itemCount = thumbnails.size
) {
thumbnails.forEach {
Image(
modifier = Modifier.layoutId("Icon"),
painter = painterResource(id = it),
contentDescription = "Icon",
contentScale = ContentScale.Crop,
)
}
if (thumbnails.size > 4) {
val carry = thumbnails.size - 3
Box(
modifier = Modifier.layoutId("Text"),
contentAlignment = Alignment.Center
) {
Text(text = "+$carry", fontSize = 20.sp)
}
}
}
}
}
And using GridImageLayout
Column {
val icons5 = listOf(
R.drawable.landscape1,
R.drawable.landscape2,
R.drawable.landscape3,
R.drawable.landscape4,
R.drawable.landscape5,
R.drawable.landscape6,
R.drawable.landscape7,
)
GridImageLayout(
modifier = Modifier
.border(1.dp, Color.Red, RoundedCornerShape(10))
.clip(RoundedCornerShape(10))
.size(150.dp),
thumbnails = icons5
)
}

Android - Horizontally (right-to-left) circular background color transition

I want to know how can I implement this animation?
Small image frame and text movement is independent from the animation. It's just like simple pager action with some scale and alpha transformation. Only problem is the color change of background like this.
I'm open to both XML and Jetpack Compose way solutions. Please..
The solution
After lots of hours searching, I've found the perfect one;
https://github.com/2307vivek/BubblePager
My Solution
import android.graphics.Path
import android.view.MotionEvent
import androidx.annotation.FloatRange
import androidx.compose.animation.core.*
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.*
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Outline
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.asComposePath
import androidx.compose.ui.input.pointer.pointerInteropFilter
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
import kotlin.math.hypot
#Composable
fun <T> CircularReveal(
targetState: T,
modifier: Modifier = Modifier,
animationSpec: FiniteAnimationSpec<Float> = tween(),
content: #Composable (T) -> Unit,
) {
val transition = updateTransition(targetState, label = "Circular reveal")
transition.CircularReveal(modifier, animationSpec, content = content)
}
#Composable
fun <T> Transition<T>.CircularReveal(
modifier: Modifier = Modifier,
animationSpec: FiniteAnimationSpec<Float> = tween(),
content: #Composable (targetState: T) -> Unit,
) {
var offset: Offset? by remember { mutableStateOf(null) }
val currentlyVisible = remember { mutableStateListOf<T>().apply { add(currentState) } }
val contentMap = remember {
mutableMapOf<T, #Composable () -> Unit>()
}
if (currentState == targetState) {
// If not animating, just display the current state
if (currentlyVisible.size != 1 || currentlyVisible[0] != targetState) {
// Remove all the intermediate items from the list once the animation is finished.
currentlyVisible.removeAll { it != targetState }
contentMap.clear()
}
}
if (!contentMap.contains(targetState)) {
// Replace target with the same key if any
val replacementId = currentlyVisible.indexOfFirst {
it == targetState
}
if (replacementId == -1) {
currentlyVisible.add(targetState)
} else {
currentlyVisible[replacementId] = targetState
}
contentMap.clear()
currentlyVisible.forEach { stateForContent ->
contentMap[stateForContent] = {
val progress by animateFloat(
label = "Progress",
transitionSpec = { animationSpec }
) {
val targetedContent = stateForContent != currentlyVisible.last() || it == stateForContent
if (targetedContent) 1f else 0f
}
Box(Modifier.circularReveal(progress = progress, offset = offset)) {
content(stateForContent)
}
}
}
}
Box(
modifier = modifier.pointerInteropFilter {
if (it.action == MotionEvent.ACTION_DOWN) {
if (!started) offset = Offset(it.x, it.y)
}
started
}
) {
currentlyVisible.forEach {
key(it) {
contentMap[it]?.invoke()
}
}
}
}
private val <T> Transition<T>.started get() =
currentState != targetState || isRunning
fun Modifier.circularReveal(
#FloatRange(from = 0.0, to = 1.0) progress: Float,
offset: Offset? = null,
) = clip(CircularRevealShape(progress, offset))
class CircularRevealShape(
#FloatRange(from = 0.0, to = 1.0) private val progress: Float,
private val offset: Offset? = null,
) : Shape {
override fun createOutline(
size: Size,
layoutDirection: LayoutDirection,
density: Density,
): Outline {
return Outline.Generic(Path().apply {
addCircle(
offset?.x ?: (size.width / 2f),
offset?.y ?: (size.height / 2f),
longestDistanceToACorner(size, offset) * progress,
Path.Direction.CW
)
}.asComposePath())
}
private fun longestDistanceToACorner(size: Size, offset: Offset?): Float {
if (offset == null) {
return hypot(size.width / 2f, size.height / 2f)
}
val topLeft = hypot(offset.x, offset.y)
val topRight = hypot(size.width - offset.x, offset.y)
val bottomLeft = hypot(offset.x, size.height - offset.y)
val bottomRight = hypot(size.width - offset.x, size.height - offset.y)
return topLeft.coerceAtLeast(topRight).coerceAtLeast(bottomLeft).coerceAtLeast(bottomRight)
}
}
#Preview
#Composable
fun CircularRevealAnimationPreview() {
val isSystemDark = isSystemInDarkTheme()
var darkTheme by remember { mutableStateOf(isSystemDark) }
val onThemeToggle = { darkTheme = !darkTheme }
CircularReveal(
targetState = darkTheme,
animationSpec = tween(1500)
) { isDark ->
MyAppTheme(darkTheme = isDark) {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background,
onClick = onThemeToggle
) {
Box(
contentAlignment = Alignment.Center
) {
Icon(
modifier = Modifier.size(120.dp),
imageVector = if (isDark) Icons.Default.DarkMode else Icons.Default.LightMode,
contentDescription = "Toggle",
)
}
}
}
}
}

Using Modifier.fillMaxHeight() in Box doesn't display the element

I am trying to display a shadow at the start and end of a list to indicate that the list still has contents to the left/right.
I want the shadow to be as tall as the whole row so therefore I use Modifier.fillMaxHeight(). Unfortunately this doesn't work unless I specifiy the size of the component at call time.
When I call the function as:
LazyRowWithShadows(modifier = Modifier
.fillMaxWidth(),
contentPadding = PaddingValues(horizontal = 12.dp),
shadowColor = colorResource(R.color.blue_white),
shadowAlphaStart = 0f, shadowAlphaEnd = 0.8f,
shadowWidth = 48.dp
) {
// Content
}
The shadows are not displayed, but the rest of the content is. I have to give the modifier a height to make it work eg:
LazyRowWithShadows(modifier = Modifier
.fillMaxWidth()
.height(X.dp), // This is what makes it display, but requires knowing the height of the row
contentPadding = PaddingValues(horizontal = 12.dp),
shadowColor = colorResource(R.color.blue_white),
shadowAlphaStart = 0f, shadowAlphaEnd = 0.8f,
shadowWidth = 48.dp
) {
// Content
}
I would prefer if the LazyRowWithShadows handled displaying the shadows even if no size is specified when calling it. Is there an easy way to do this?
This is the code for the LazyRowWithShadows:
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
/** A LazyRow that shows start & end shadows when content is not fully scrolled. */
#ExperimentalAnimationApi
#Composable
fun LazyRowWithShadows(
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(0.dp),
shadowColor: Color = Color.Black,
shadowAlphaStart: Float = 0f,
shadowAlphaEnd: Float = 0.3f,
shadowWidth: Dp = 32.dp,
content: LazyListScope.() -> Unit
) {
val listState = rememberLazyListState()
val showStartShadow by remember {
derivedStateOf {
listState.firstVisibleItemScrollOffset > 0 || listState.firstVisibleItemIndex > 0
}
}
val showEndShadow by remember {
derivedStateOf {
val lastItemIfVisible = listState.layoutInfo.visibleItemsInfo.maxByOrNull { it.index }?.takeIf { it.index == listState.layoutInfo.totalItemsCount - 1 }
if (lastItemIfVisible != null) {
val lastItemEndX = lastItemIfVisible.offset + lastItemIfVisible.size
lastItemEndX > listState.layoutInfo.viewportEndOffset
} else {
true
}
}
}
Box(modifier = modifier) {
LazyRow(
state = listState,
contentPadding = contentPadding,
content = content)
// Start scroll shadow
AnimatedVisibility(visible = showStartShadow, modifier = Modifier.align(Alignment.CenterStart)) {
Box(modifier = Modifier
.fillMaxHeight()
.width(shadowWidth)
.background(brush = Brush.horizontalGradient(colors = listOf(shadowColor.copy(alpha = shadowAlphaEnd), shadowColor.copy(alpha = shadowAlphaStart))))
)
}
// End scroll shadow
AnimatedVisibility(visible = showEndShadow, modifier = Modifier.align(Alignment.CenterEnd)) {
Box(modifier = Modifier
.fillMaxHeight()
.width(shadowWidth)
.background(brush = Brush.horizontalGradient(colors = listOf(shadowColor.copy(alpha = shadowAlphaStart), shadowColor.copy(alpha = shadowAlphaEnd))))
)
}
}
}
Edit: I have tried the solution proposed in fillMaxSize modifier not working when combined with VerticalScroll in Jetpack Compose, but it didn't solve my issue. The changes I made to try to solve it using that answer as inspiration are:
BoxWithConstraints(modifier = modifier) { // Changed to BoxWithConstraints
LazyRow(
state = listState,
contentPadding = contentPadding,
content = content)
// Start scroll shadow
AnimatedVisibility(visible = showStartShadow, modifier = Modifier.align(Alignment.CenterStart)) {
Box(modifier = Modifier
.height(this#BoxWithConstraints.maxHeight) // Changed from fillMaxHeight
.width(shadowWidth)
.background(brush = Brush.horizontalGradient(colors = listOf(shadowColor.copy(alpha = shadowAlphaEnd), shadowColor.copy(alpha = shadowAlphaStart))))
)
}
// End scroll shadow
AnimatedVisibility(visible = showEndShadow, modifier = Modifier.align(Alignment.CenterEnd)) {
Box(modifier = Modifier
.height(this#BoxWithConstraints.maxHeight) // Changed from fillMaxHeight
.width(shadowWidth)
.background(brush = Brush.horizontalGradient(colors = listOf(shadowColor.copy(alpha = shadowAlphaStart), shadowColor.copy(alpha = shadowAlphaEnd))))
)
}
}
I also tried adding a height to the AnimatedVisibility elements, but that didn't change anything.
After trying out a couple of ways, using Modifier.onSizeChanged to get the height of LazyRow finally made it work.
Convert this height to dp and pass it to Modifier.height for shadow's Box(s).
Box(modifier = modifier) {
val density = LocalDensity.current
var height by remember { mutableStateOf(0) }
val heightDp = remember(height) { with(density){ height.toDp() } }
LazyRow(
modifier = Modifier.onSizeChanged {
height = it.height
},
state = listState,
contentPadding = contentPadding,
content = content)
// Start scroll shadow
AnimatedVisibility(visible = showStartShadow, modifier = Modifier.align(Alignment.CenterStart)) {
Box(modifier = Modifier
.height(heightDp)
.width(shadowWidth)
.background(brush = Brush.horizontalGradient(colors = listOf(shadowColor.copy(alpha = shadowAlphaEnd), shadowColor.copy(alpha = shadowAlphaStart))))
)
}
// End scroll shadow
AnimatedVisibility(visible = showEndShadow, modifier = Modifier.align(Alignment.CenterEnd)) {
Box(modifier = Modifier
.height(heightDp)
.width(shadowWidth)
.background(brush = Brush.horizontalGradient(colors = listOf(shadowColor.copy(alpha = shadowAlphaStart), shadowColor.copy(alpha = shadowAlphaEnd))))
)
}
}

Categories

Resources