Skip to content

Instantly share code, notes, and snippets.

@dovahkiin98
Last active July 16, 2024 05:04
Show Gist options
  • Save dovahkiin98/85acb72ab0c4ddfc6b53413c955bcd10 to your computer and use it in GitHub Desktop.
Save dovahkiin98/85acb72ab0c4ddfc6b53413c955bcd10 to your computer and use it in GitHub Desktop.
A Jetpack Compose implementation of the `fadingEdge` effect.
import androidx.compose.foundation.ScrollState
import androidx.compose.material.MaterialTheme
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.unit.Dp
fun Modifier.horizontalFadingEdge(
scrollState: ScrollState,
length: Dp,
edgeColor: Color? = null,
) = composed(
debugInspectorInfo {
name = "length"
value = length
}
) {
val color = edgeColor ?: MaterialTheme.colors.surface
drawWithContent {
val lengthValue = length.toPx()
val scrollFromStart = scrollState.value
val scrollFromEnd = scrollState.maxValue - scrollState.value
val startFadingEdgeStrength = lengthValue * (scrollFromStart / lengthValue).coerceAtMost(1f)
val endFadingEdgeStrength = lengthValue * (scrollFromEnd / lengthValue).coerceAtMost(1f)
drawContent()
drawRect(
brush = Brush.horizontalGradient(
colors = listOf(
color,
Color.Transparent,
),
startX = 0f,
endX = startFadingEdgeStrength,
),
size = Size(
startFadingEdgeStrength,
this.size.height,
),
)
drawRect(
brush = Brush.horizontalGradient(
colors = listOf(
Color.Transparent,
color,
),
startX = size.width - endFadingEdgeStrength,
endX = size.width,
),
topLeft = Offset(x = size.width - endFadingEdgeStrength, y = 0f),
)
}
}
fun Modifier.verticalFadingEdge(
scrollState: ScrollState,
length: Dp,
edgeColor: Color? = null,
) = composed(
debugInspectorInfo {
name = "length"
value = length
}
) {
val color = edgeColor ?: MaterialTheme.colors.surface
drawWithContent {
val lengthValue = length.toPx()
val scrollFromTop = scrollState.value
val scrollFromBottom = scrollState.maxValue - scrollState.value
val topFadingEdgeStrength = lengthValue * (scrollFromTop / lengthValue).coerceAtMost(1f)
val bottomFadingEdgeStrength = lengthValue * (scrollFromBottom / lengthValue).coerceAtMost(1f)
drawContent()
drawRect(
brush = Brush.verticalGradient(
colors = listOf(
color,
Color.Transparent,
),
startY = 0f,
endY = topFadingEdgeStrength,
),
size = Size(
this.size.width,
topFadingEdgeStrength
),
)
drawRect(
brush = Brush.verticalGradient(
colors = listOf(
Color.Transparent,
color,
),
startY = size.height - bottomFadingEdgeStrength,
endY = size.height,
),
topLeft = Offset(x = 0f, y = size.height - bottomFadingEdgeStrength),
)
}
}
@fargus9
Copy link

fargus9 commented Apr 8, 2022

    Modifier.drawWithContent {

Should be:

   drawWithContent {

otherwise you're not chaining properly to the extension receiver.

@fargus9
Copy link

fargus9 commented Apr 8, 2022

        val scrollFromTop = scrollState.value
        val scrollFromBottom = scrollState.maxValue - scrollState.value

Should be

        val scrollFromTop by derivedStateOf { scrollState.value }
        val scrollFromBottom by derivedStateOf { scrollState.maxValue - scrollState.value }

so that you can properly share the scroll state with Modifier.verticalScroll .

You need to make similar updates to the horizontal.

@fargus9
Copy link

fargus9 commented Apr 8, 2022

I also made a version that works for LazyColumn

fun Modifier.verticalFadingEdge(
    lazyListState: LazyListState,
    length: Dp,
    edgeColor: Color? = null,
) = composed(
    debugInspectorInfo {
        name = "length"
        value = length
    }
) {
    val color = edgeColor ?: MaterialTheme.colors.surface

    drawWithContent {
        val topFadingEdgeStrength by derivedStateOf {
            lazyListState.layoutInfo.run {
                when {
                    visibleItemsInfo.size in 0..1 -> 0f
                    visibleItemsInfo.first().offset == viewportStartOffset -> 0f
                    visibleItemsInfo.first().offset < viewportStartOffset -> visibleItemsInfo.first().run {
                        abs(offset) / size.toFloat()
                    }
                    else -> 1f
                }
            }.coerceAtMost(1f) * lengthValue
        }
        val bottomFadingEdgeStrength by derivedStateOf {
            lazyListState.layoutInfo.run {
                when {
                    visibleItemsInfo.size in 0..1 -> 0f
                    visibleItemsInfo.last().run { offset + size } == viewportEndOffset -> 0f
                        visibleItemsInfo.last().run { offset + size } > viewportEndOffset -> visibleItemsInfo.last().run {
                        (viewportEndOffset - offset) / size.toFloat()
                    }
                    else -> 1f
                }
            }.coerceAtMost(1f) * lengthValue
        }
       ...
     }

@SylpheM
Copy link

SylpheM commented Nov 2, 2022

Hi @dovahkiin98 and @fargus9, thank you both for sharing your solutions! 

I’ve made some small corrections to your solution fargus9, so that the fade does not go away between two items: when the last visible item is aligned with the end of the viewport. Also the fading was not properly drawn on the last item.

fun Modifier.verticalFadingEdge(
    lazyListState: LazyListState,
    length: Dp,
    edgeColor: Color? = null,
) = composed(
    debugInspectorInfo {
        name = "length"
        value = length
    }
) {
    val color = edgeColor ?: MaterialTheme.colorScheme.surface

    drawWithContent {
        val topFadingEdgeStrength by derivedStateOf {
            lazyListState.layoutInfo.run {
                val firstItem = visibleItemsInfo.first()
                when {
                    visibleItemsInfo.size in 0..1 -> 0f
                    firstItem.index > 0 -> 1f // Added
                    firstItem.offset == viewportStartOffset -> 0f
                    firstItem.offset < viewportStartOffset -> firstItem.run {
                        abs(offset) / size.toFloat()
                    }
                    else -> 1f
                }
            }.coerceAtMost(1f) * length.value
        }
        val bottomFadingEdgeStrength by derivedStateOf {
            lazyListState.layoutInfo.run {
                val lastItem = visibleItemsInfo.last()
                when {
                    visibleItemsInfo.size in 0..1 -> 0f
                    lastItem.index < totalItemsCount - 1 -> 1f // Added
                    lastItem.offset + lastItem.size <= viewportEndOffset -> 0f // added the <=
                    lastItem.offset + lastItem.size > viewportEndOffset -> lastItem.run {
                        (size - (viewportEndOffset - offset)) / size.toFloat()  // Fixed the percentage computation
                    }
                    else -> 1f
                }
            }.coerceAtMost(1f) * length.value
        }

        drawContent()

        …

@fargus9
Copy link

fargus9 commented Nov 20, 2022

Thanks for the fixes!

@EaseTheWorld
Copy link

For material3, I used default edgeColor to MaterialTheme.colorScheme.background.

@EaseTheWorld
Copy link

visibleItemsInfo.size in 0..1 should be before visibleItemsInfo.first()/last()
otherwise NoSuchElementException("List is empty.")

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment