All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.androidx.compose.foundation.ClipScrollableContainer.kt Maven / Gradle / Ivy

/*
 * Copyright 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.compose.foundation

import androidx.compose.foundation.gestures.Orientation
import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Outline
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp

/**
 * Clips bounds of scrollable container on main axis while leaving space for background effects
 * (like shadows) on cross axis.
 *
 * @param orientation orientation of the scrolling
 */
@Stable
fun Modifier.clipScrollableContainer(orientation: Orientation) =
    then(
        if (orientation == Orientation.Vertical) {
            VerticalScrollableClipModifier
        } else {
            HorizontalScrollableClipModifier
        }
    )

/**
 * In the scrollable containers we want to clip the main axis sides in order to not display the
 * content which is scrolled out. But once we apply clipToBounds() modifier on such containers it
 * causes unexpected behavior as we also clip the content on the cross axis sides. It is unexpected
 * as Compose components are not clipping by default. The most common case how it could be
 * reproduced is a horizontally scrolling list of Cards. Cards have the elevation by default and
 * such Cards will be drawn with clipped shadows on top and bottom. This was harder to reproduce in
 * the Views system as usually scrolling containers like RecyclerView didn't have an opaque
 * background which means the ripple was drawn on the surface on the first parent with background.
 * In Compose as we don't clip by default we draw shadows right in place. We faced similar issue in
 * Compose already with Androids Popups and Dialogs where we decided to just predefine some constant
 * with a maximum elevation size we are not going to clip. We are going to reuse this technique
 * here. This will improve how it works in most common cases. If the user will need to have a larger
 * unclipped area for some reason they can always add the needed padding inside the scrollable area.
 */
internal val MaxSupportedElevation = 30.dp

private val HorizontalScrollableClipModifier =
    Modifier.clip(
        object : Shape {
            override fun createOutline(
                size: Size,
                layoutDirection: LayoutDirection,
                density: Density
            ): Outline {
                val inflateSize = with(density) { MaxSupportedElevation.roundToPx().toFloat() }
                return Outline.Rectangle(
                    Rect(
                        left = 0f,
                        top = -inflateSize,
                        right = size.width,
                        bottom = size.height + inflateSize
                    )
                )
            }
        }
    )

private val VerticalScrollableClipModifier =
    Modifier.clip(
        object : Shape {
            override fun createOutline(
                size: Size,
                layoutDirection: LayoutDirection,
                density: Density
            ): Outline {
                val inflateSize = with(density) { MaxSupportedElevation.roundToPx().toFloat() }
                return Outline.Rectangle(
                    Rect(
                        left = -inflateSize,
                        top = 0f,
                        right = size.width + inflateSize,
                        bottom = size.height
                    )
                )
            }
        }
    )




© 2015 - 2025 Weber Informatics LLC | Privacy Policy