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