| /* |
| * 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 |
| ) |
| ) |
| } |
| }) |