LazyListHeaders.kt

/*
 * Copyright 2020 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.lazy

import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.util.fastFirstOrNull

/**
 * @param itemProvider the provider so we can compose a header if it wasn't composed already
 * @param headerIndexes list of indexes of headers. Must be sorted.
 * @param measureResult the result of the measuring.
 */
internal class LazyListHeaders(
    private val itemProvider: LazyMeasuredItemProvider,
    headerIndexes: List<Int>,
    measureResult: LazyListMeasureResult,
    private val startContentPadding: Int
) {
    private val currentHeaderListPosition: Int
    private val nextHeaderListPosition: Int

    private val notUsedButComposedItems: MutableList<LazyMeasuredItem>?

    private var currentHeaderItem: LazyMeasuredItem? = null
    private var currentHeaderOffset: Int = Int.MIN_VALUE
    private var nextHeaderOffset: Int = Int.MIN_VALUE
    private var nextHeaderSize: Int = Int.MIN_VALUE

    init {
        var currentHeaderListPosition = -1
        var nextHeaderListPosition = -1
        // we use visibleItemsInfo and not firstVisibleItemIndex as visibleItemsInfo list also
        // contains all the items which are visible in the start content padding area
        val firstVisible = measureResult.visibleItemsInfo.first().index
        // find the header which can be displayed
        for (index in headerIndexes.indices) {
            if (headerIndexes[index] <= firstVisible) {
                currentHeaderListPosition = headerIndexes[index]
                nextHeaderListPosition = headerIndexes.getOrElse(index + 1) { -1 }
            } else {
                break
            }
        }
        this.currentHeaderListPosition = currentHeaderListPosition
        this.nextHeaderListPosition = nextHeaderListPosition

        notUsedButComposedItems = measureResult.notUsedButComposedItems
    }

    fun onBeforeItemsPlacing() {
        currentHeaderItem = null
        currentHeaderOffset = Int.MIN_VALUE
        nextHeaderOffset = Int.MIN_VALUE
    }

    fun place(
        item: LazyMeasuredItem,
        scope: Placeable.PlacementScope,
        layoutWidth: Int,
        layoutHeight: Int,
        offset: Int
    ) {
        if (item.index == currentHeaderListPosition) {
            currentHeaderItem = item
            currentHeaderOffset = offset
        } else {
            item.place(scope, layoutWidth, layoutHeight, offset)
            if (item.index == nextHeaderListPosition) {
                nextHeaderOffset = offset
                nextHeaderSize = item.size
            }
        }
    }

    fun onAfterItemsPlacing(
        scope: Placeable.PlacementScope,
        layoutWidth: Int,
        layoutHeight: Int
    ) {
        if (currentHeaderListPosition == -1) {
            // we have no headers needing special handling
            return
        }

        val headerItem = currentHeaderItem
            ?: notUsedButComposedItems?.fastFirstOrNull { it.index == currentHeaderListPosition }
            ?: itemProvider.getAndMeasure(DataIndex(currentHeaderListPosition))

        var headerOffset = if (currentHeaderOffset != Int.MIN_VALUE) {
            maxOf(-startContentPadding, currentHeaderOffset)
        } else {
            -startContentPadding
        }
        // if we have a next header overlapping with the current header, the next one will be
        // pushing the current one away from the viewport.
        if (nextHeaderOffset != Int.MIN_VALUE) {
            headerOffset = minOf(headerOffset, nextHeaderOffset - headerItem.size)
        }

        headerItem.place(scope, layoutWidth, layoutHeight, headerOffset)
    }
}