1 /* <lambda>null2 * Copyright 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.compose.foundation.pager 18 19 import androidx.compose.foundation.ExperimentalFoundationApi 20 import androidx.compose.foundation.checkScrollableContainerConstraints 21 import androidx.compose.foundation.gestures.Orientation 22 import androidx.compose.foundation.gestures.snapping.SnapPosition 23 import androidx.compose.foundation.layout.PaddingValues 24 import androidx.compose.foundation.layout.calculateEndPadding 25 import androidx.compose.foundation.layout.calculateStartPadding 26 import androidx.compose.foundation.lazy.layout.LazyLayoutMeasureScope 27 import androidx.compose.foundation.lazy.layout.calculateLazyLayoutPinnedIndices 28 import androidx.compose.runtime.Composable 29 import androidx.compose.runtime.remember 30 import androidx.compose.runtime.snapshots.Snapshot 31 import androidx.compose.ui.Alignment 32 import androidx.compose.ui.layout.MeasureResult 33 import androidx.compose.ui.unit.Constraints 34 import androidx.compose.ui.unit.Dp 35 import androidx.compose.ui.unit.IntOffset 36 import androidx.compose.ui.unit.constrainHeight 37 import androidx.compose.ui.unit.constrainWidth 38 import androidx.compose.ui.unit.offset 39 import kotlinx.coroutines.CoroutineScope 40 41 @OptIn(ExperimentalFoundationApi::class) 42 @Composable 43 internal fun rememberPagerMeasurePolicy( 44 itemProviderLambda: () -> PagerLazyLayoutItemProvider, 45 state: PagerState, 46 contentPadding: PaddingValues, 47 reverseLayout: Boolean, 48 orientation: Orientation, 49 beyondViewportPageCount: Int, 50 pageSpacing: Dp, 51 pageSize: PageSize, 52 horizontalAlignment: Alignment.Horizontal?, 53 verticalAlignment: Alignment.Vertical?, 54 snapPosition: SnapPosition, 55 coroutineScope: CoroutineScope, 56 pageCount: () -> Int, 57 ) = 58 remember<LazyLayoutMeasureScope.(Constraints) -> MeasureResult>( 59 state, 60 contentPadding, 61 reverseLayout, 62 orientation, 63 horizontalAlignment, 64 verticalAlignment, 65 pageSpacing, 66 pageSize, 67 snapPosition, 68 pageCount, 69 beyondViewportPageCount, 70 coroutineScope 71 ) { 72 { containerConstraints -> 73 state.measurementScopeInvalidator.attachToScope() 74 val isVertical = orientation == Orientation.Vertical 75 checkScrollableContainerConstraints( 76 containerConstraints, 77 if (isVertical) Orientation.Vertical else Orientation.Horizontal 78 ) 79 80 // resolve content paddings 81 val startPadding = 82 if (isVertical) { 83 contentPadding.calculateLeftPadding(layoutDirection).roundToPx() 84 } else { 85 // in horizontal configuration, padding is reversed by placeRelative 86 contentPadding.calculateStartPadding(layoutDirection).roundToPx() 87 } 88 89 val endPadding = 90 if (isVertical) { 91 contentPadding.calculateRightPadding(layoutDirection).roundToPx() 92 } else { 93 // in horizontal configuration, padding is reversed by placeRelative 94 contentPadding.calculateEndPadding(layoutDirection).roundToPx() 95 } 96 val topPadding = contentPadding.calculateTopPadding().roundToPx() 97 val bottomPadding = contentPadding.calculateBottomPadding().roundToPx() 98 val totalVerticalPadding = topPadding + bottomPadding 99 val totalHorizontalPadding = startPadding + endPadding 100 val totalMainAxisPadding = 101 if (isVertical) totalVerticalPadding else totalHorizontalPadding 102 val beforeContentPadding = 103 when { 104 isVertical && !reverseLayout -> topPadding 105 isVertical && reverseLayout -> bottomPadding 106 !isVertical && !reverseLayout -> startPadding 107 else -> endPadding // !isVertical && reverseLayout 108 } 109 val afterContentPadding = totalMainAxisPadding - beforeContentPadding 110 val contentConstraints = 111 containerConstraints.offset(-totalHorizontalPadding, -totalVerticalPadding) 112 113 state.density = this 114 115 val spaceBetweenPages = pageSpacing.roundToPx() 116 117 // can be negative if the content padding is larger than the max size from constraints 118 val mainAxisAvailableSize = 119 if (isVertical) { 120 containerConstraints.maxHeight - totalVerticalPadding 121 } else { 122 containerConstraints.maxWidth - totalHorizontalPadding 123 } 124 val visualItemOffset = 125 if (!reverseLayout || mainAxisAvailableSize > 0) { 126 IntOffset(startPadding, topPadding) 127 } else { 128 // When layout is reversed and paddings together take >100% of the available 129 // space, 130 // layout size is coerced to 0 when positioning. To take that space into 131 // account, 132 // we offset start padding by negative space between paddings. 133 IntOffset( 134 if (isVertical) startPadding else startPadding + mainAxisAvailableSize, 135 if (isVertical) topPadding + mainAxisAvailableSize else topPadding 136 ) 137 } 138 139 val pageAvailableSize = 140 with(pageSize) { 141 calculateMainAxisPageSize(mainAxisAvailableSize, spaceBetweenPages) 142 .coerceAtLeast(0) 143 } 144 145 state.premeasureConstraints = 146 Constraints( 147 maxWidth = 148 if (orientation == Orientation.Vertical) { 149 contentConstraints.maxWidth 150 } else { 151 pageAvailableSize 152 }, 153 maxHeight = 154 if (orientation != Orientation.Vertical) { 155 contentConstraints.maxHeight 156 } else { 157 pageAvailableSize 158 } 159 ) 160 val itemProvider = itemProviderLambda() 161 162 val currentPage: Int 163 val currentPageOffset: Int 164 val layoutSize = mainAxisAvailableSize + beforeContentPadding + afterContentPadding 165 166 Snapshot.withoutReadObservation { 167 currentPage = state.matchScrollPositionWithKey(itemProvider, state.currentPage) 168 currentPageOffset = 169 snapPosition.currentPageOffset( 170 layoutSize, 171 pageAvailableSize, 172 spaceBetweenPages, 173 beforeContentPadding, 174 afterContentPadding, 175 state.currentPage, 176 state.currentPageOffsetFraction, 177 state.pageCount 178 ) 179 } 180 181 val pinnedPages = 182 itemProvider.calculateLazyLayoutPinnedIndices( 183 pinnedItemList = state.pinnedPages, 184 beyondBoundsInfo = state.beyondBoundsInfo 185 ) 186 187 // todo: wrap with snapshot when b/341782245 is resolved 188 val measureResult = 189 measurePager( 190 beforeContentPadding = beforeContentPadding, 191 afterContentPadding = afterContentPadding, 192 constraints = contentConstraints, 193 pageCount = pageCount(), 194 spaceBetweenPages = spaceBetweenPages, 195 mainAxisAvailableSize = mainAxisAvailableSize, 196 visualPageOffset = visualItemOffset, 197 pageAvailableSize = pageAvailableSize, 198 beyondViewportPageCount = beyondViewportPageCount, 199 orientation = orientation, 200 currentPage = currentPage, 201 currentPageOffset = currentPageOffset, 202 horizontalAlignment = horizontalAlignment, 203 verticalAlignment = verticalAlignment, 204 pagerItemProvider = itemProvider, 205 reverseLayout = reverseLayout, 206 pinnedPages = pinnedPages, 207 snapPosition = snapPosition, 208 placementScopeInvalidator = state.placementScopeInvalidator, 209 coroutineScope = coroutineScope, 210 layout = { width, height, placement -> 211 layout( 212 containerConstraints.constrainWidth(width + totalHorizontalPadding), 213 containerConstraints.constrainHeight(height + totalVerticalPadding), 214 emptyMap(), 215 placement 216 ) 217 } 218 ) 219 state.applyMeasureResult(measureResult, isLookingAhead = isLookingAhead) 220 measureResult 221 } 222 } 223