1 /* 2 * Copyright 2024 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 package androidx.compose.foundation.layout 17 18 import androidx.collection.IntIntPair 19 import androidx.compose.ui.layout.Measurable 20 import androidx.compose.ui.layout.Placeable 21 import kotlin.math.max 22 23 @OptIn(ExperimentalLayoutApi::class) 24 internal class FlowLayoutBuildingBlocks( 25 private val maxItemsInMainAxis: Int, 26 private val overflow: FlowLayoutOverflowState, 27 private val constraints: OrientationIndependentConstraints, 28 private val maxLines: Int, 29 private val mainAxisSpacing: Int, 30 private val crossAxisSpacing: Int, 31 ) { 32 class WrapInfo( 33 val isLastItemInLine: Boolean = false, 34 val isLastItemInContainer: Boolean = false 35 ) 36 37 class WrapEllipsisInfo( 38 val ellipsis: Measurable, 39 val placeable: Placeable?, 40 val ellipsisSize: IntIntPair, 41 var placeEllipsisOnLastContentLine: Boolean = true, 42 ) 43 getWrapEllipsisInfonull44 fun getWrapEllipsisInfo( 45 wrapInfo: WrapInfo, 46 hasNext: Boolean, 47 lastContentLineIndex: Int, 48 totalCrossAxisSize: Int, 49 leftOverMainAxis: Int, 50 nextIndexInLine: Int 51 ): WrapEllipsisInfo? { 52 if (!wrapInfo.isLastItemInContainer) return null 53 54 val ellipsisInfo = 55 overflow.ellipsisInfo(hasNext, lastContentLineIndex, totalCrossAxisSize) ?: return null 56 57 val canFitLine = 58 lastContentLineIndex >= 0 && 59 (nextIndexInLine == 0 || 60 !(leftOverMainAxis - ellipsisInfo.ellipsisSize.first < 0 || 61 nextIndexInLine >= maxItemsInMainAxis)) 62 63 ellipsisInfo.placeEllipsisOnLastContentLine = canFitLine 64 return ellipsisInfo 65 } 66 67 @Suppress("DEPRECATION") getWrapInfonull68 fun getWrapInfo( 69 nextItemHasNext: Boolean, 70 nextIndexInLine: Int, 71 leftOver: IntIntPair, 72 nextSize: IntIntPair?, 73 lineIndex: Int, 74 totalCrossAxisSize: Int, 75 currentLineCrossAxisSize: Int, 76 isWrappingRound: Boolean, 77 isEllipsisWrap: Boolean, 78 ): WrapInfo { 79 var totalContainerCrossAxisSize = totalCrossAxisSize + currentLineCrossAxisSize 80 if (nextSize == null) { 81 return WrapInfo(isLastItemInLine = true, isLastItemInContainer = true) 82 } 83 84 val willOverflowCrossAxis = 85 when { 86 overflow.type == FlowLayoutOverflow.OverflowType.Visible -> false 87 lineIndex >= maxLines -> true 88 leftOver.second - nextSize.second < 0 -> true 89 else -> false 90 } 91 92 if (willOverflowCrossAxis) { 93 return WrapInfo(isLastItemInLine = true, isLastItemInContainer = true) 94 } 95 96 val shouldWrapItem = 97 when { 98 nextIndexInLine == 0 -> false 99 nextIndexInLine >= maxItemsInMainAxis -> true 100 leftOver.first - nextSize.first < 0 -> true 101 else -> false 102 } 103 104 if (shouldWrapItem) { 105 if (isWrappingRound) { 106 return WrapInfo(isLastItemInLine = true, isLastItemInContainer = true) 107 } 108 val wrapInfo = 109 getWrapInfo( 110 nextItemHasNext, 111 nextIndexInLine = 0, 112 leftOver = 113 IntIntPair( 114 constraints.mainAxisMax, 115 leftOver.second - crossAxisSpacing - currentLineCrossAxisSize 116 ), 117 // remove the mainAxisSpacing added to 2nd position or more indexed items. 118 IntIntPair( 119 first = nextSize.first.minus(mainAxisSpacing), 120 second = nextSize.second 121 ), 122 lineIndex = lineIndex + 1, 123 totalCrossAxisSize = totalContainerCrossAxisSize, 124 currentLineCrossAxisSize = 0, 125 isWrappingRound = true, 126 isEllipsisWrap = false 127 ) 128 return WrapInfo( 129 isLastItemInLine = true, 130 isLastItemInContainer = wrapInfo.isLastItemInContainer 131 ) 132 } 133 134 totalContainerCrossAxisSize = 135 totalCrossAxisSize + max(currentLineCrossAxisSize, nextSize.second) 136 137 val ellipsis = 138 if (isEllipsisWrap) { 139 null 140 } else { 141 overflow.ellipsisSize(nextItemHasNext, lineIndex, totalContainerCrossAxisSize) 142 } 143 val shouldWrapEllipsis = 144 ellipsis?.run { 145 when { 146 nextIndexInLine + 1 >= maxItemsInMainAxis -> true 147 leftOver.first - nextSize.first - mainAxisSpacing - ellipsis.first < 0 -> true 148 else -> false 149 } 150 } ?: false 151 152 if (shouldWrapEllipsis) { 153 if (isEllipsisWrap) { 154 return WrapInfo(isLastItemInLine = true, isLastItemInContainer = true) 155 } 156 val wrapInfo = 157 getWrapInfo( 158 nextItemHasNext = false, 159 nextIndexInLine = 0, 160 IntIntPair( 161 constraints.mainAxisMax, 162 leftOver.second - 163 crossAxisSpacing - 164 max(currentLineCrossAxisSize, nextSize.second) 165 ), 166 ellipsis, 167 lineIndex = lineIndex + 1, 168 totalCrossAxisSize = totalContainerCrossAxisSize, 169 currentLineCrossAxisSize = 0, 170 isWrappingRound = true, 171 isEllipsisWrap = true 172 ) 173 174 return WrapInfo( 175 isLastItemInLine = wrapInfo.isLastItemInContainer, 176 isLastItemInContainer = wrapInfo.isLastItemInContainer 177 ) 178 } 179 180 return WrapInfo(isLastItemInLine = false, isLastItemInContainer = false) 181 } 182 } 183