All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.androidx.compose.ui.layout.LayoutCoordinates.kt Maven / Gradle / Ivy

/*
 * 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.
 */

@file:JvmName("LayoutCoordinatesKt")
@file:JvmMultifileClass

package androidx.compose.ui.layout

import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Matrix
import androidx.compose.ui.internal.JvmDefaultWithCompatibility
import androidx.compose.ui.internal.throwUnsupportedOperationException
import androidx.compose.ui.node.NodeCoordinator
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.util.fastCoerceIn
import androidx.compose.ui.util.fastMaxOf
import androidx.compose.ui.util.fastMinOf
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName

/** A holder of the measured bounds for the [Layout]. */
@JvmDefaultWithCompatibility
interface LayoutCoordinates {
    /** The size of this layout in the local coordinates space. */
    val size: IntSize

    /** The alignment lines provided for this layout, not including inherited lines. */
    val providedAlignmentLines: Set

    /** The coordinates of the parent layout. Null if there is no parent. */
    val parentLayoutCoordinates: LayoutCoordinates?

    /**
     * The coordinates of the parent layout modifier or parent layout if there is no parent layout
     * modifier, or `null` if there is no parent.
     */
    val parentCoordinates: LayoutCoordinates?

    /** Returns false if the corresponding layout was detached from the hierarchy. */
    val isAttached: Boolean

    /**
     * Indicates whether the corresponding Layout is expected to change its [Offset] in small
     * increments (such as when its parent is a `Scroll`).
     *
     * In those situations, the corresponding placed [LayoutCoordinates] will have their
     * [introducesMotionFrameOfReference] return `true`.
     *
     * Custom Layouts that are expected to have similar behaviors should place their children using
     * [Placeable.PlacementScope.withMotionFrameOfReferencePlacement].
     *
     * You may then use [localPositionOf] with `includeMotionFrameOfReference = false` to query a
     * Layout's position such that it excludes all [Offset] introduced by those Layouts.
     *
     * This is typically helpful when deciding when to animate an [approachLayout] using
     * [LookaheadScope] coordinates. As you probably don't want to trigger animations on small
     * positional increments.
     *
     * @see Placeable.PlacementScope.withMotionFrameOfReferencePlacement
     * @see localPositionOf
     */
    @Suppress("GetterSetterNames") // Preferred name
    val introducesMotionFrameOfReference: Boolean
        get() = false

    /**
     * Converts [relativeToScreen] relative to the device's screen's origin into an [Offset]
     * relative to this layout. Returns [Offset.Unspecified] if the conversion cannot be performed.
     */
    fun screenToLocal(relativeToScreen: Offset): Offset = Offset.Unspecified

    /**
     * Converts [relativeToLocal] position within this layout into an [Offset] relative to the
     * device's screen. Returns [Offset.Unspecified] if the conversion cannot be performed.
     */
    fun localToScreen(relativeToLocal: Offset): Offset = Offset.Unspecified

    /**
     * Converts [relativeToWindow] relative to the window's origin into an [Offset] relative to this
     * layout.
     */
    fun windowToLocal(relativeToWindow: Offset): Offset

    /**
     * Converts [relativeToLocal] position within this layout into an [Offset] relative to the
     * window's origin.
     */
    fun localToWindow(relativeToLocal: Offset): Offset

    /** Converts a local position within this layout into an offset from the root composable. */
    fun localToRoot(relativeToLocal: Offset): Offset

    /**
     * Converts an [relativeToSource] in [sourceCoordinates] space into local coordinates.
     * [sourceCoordinates] may be any [LayoutCoordinates] that belong to the same compose layout
     * hierarchy.
     *
     * By default, includes the [Offset] when [introducesMotionFrameOfReference] is `true`. But you
     * may exclude it from the calculation by using the overload that takes
     * `includeMotionFrameOfReference` and passing it as `false`.
     */
    fun localPositionOf(sourceCoordinates: LayoutCoordinates, relativeToSource: Offset): Offset

    /**
     * Converts an [relativeToSource] in [sourceCoordinates] space into local coordinates.
     * [sourceCoordinates] may be any [LayoutCoordinates] that belong to the same compose layout
     * hierarchy.
     *
     * Use [includeMotionFrameOfReference] to decide whether to include the [Offset] of any
     * `LayoutCoordinate` that returns `true` in the [includeMotionFrameOfReference] flag.
     *
     * In other words, passing [includeMotionFrameOfReference] as `false`, returns a calculation
     * that excludes the [Offset] set from Layouts that place their children using
     * [Placeable.PlacementScope.withMotionFrameOfReferencePlacement].
     */
    fun localPositionOf(
        sourceCoordinates: LayoutCoordinates,
        relativeToSource: Offset = Offset.Zero,
        includeMotionFrameOfReference: Boolean = true
    ): Offset {
        throw UnsupportedOperationException(
            "localPositionOf is not implemented on this LayoutCoordinates"
        )
    }

    /**
     * Returns the bounding box of [sourceCoordinates] in the local coordinates. If [clipBounds] is
     * `true`, any clipping that occurs between [sourceCoordinates] and this layout will affect the
     * returned bounds, and can even result in an empty rectangle if clipped regions do not overlap.
     * If [clipBounds] is false, the bounding box of [sourceCoordinates] will be converted to local
     * coordinates irrespective of any clipping applied between the layouts.
     *
     * When rotation or scaling is applied, the bounding box of the rotated or scaled value will be
     * computed in the local coordinates. For example, if a 40 pixels x 20 pixel layout is rotated
     * 90 degrees, the bounding box will be 20 pixels x 40 pixels in its parent's coordinates.
     */
    fun localBoundingBoxOf(sourceCoordinates: LayoutCoordinates, clipBounds: Boolean = true): Rect

    /**
     * Modifies [matrix] to be a transform to convert a coordinate in [sourceCoordinates] to a
     * coordinate in `this` [LayoutCoordinates].
     */
    @Suppress("DocumentExceptions")
    fun transformFrom(sourceCoordinates: LayoutCoordinates, matrix: Matrix) {
        throwUnsupportedOperationException(
            "transformFrom is not implemented on this LayoutCoordinates"
        )
    }

    /**
     * Returns the position in pixels of an [alignment line][AlignmentLine], or
     * [AlignmentLine.Unspecified] if the line is not provided.
     */
    operator fun get(alignmentLine: AlignmentLine): Int
}

/** The position of this layout inside the root composable. */
fun LayoutCoordinates.positionInRoot(): Offset = localToRoot(Offset.Zero)

/** The position of this layout relative to the window. */
fun LayoutCoordinates.positionInWindow(): Offset = localToWindow(Offset.Zero)

/**
 * The position of this layout on the device's screen. Returns [Offset.Unspecified] if the
 * conversion cannot be performed.
 */
fun LayoutCoordinates.positionOnScreen(): Offset = localToScreen(Offset.Zero)

/** The boundaries of this layout inside the root composable. */
fun LayoutCoordinates.boundsInRoot(): Rect = findRootCoordinates().localBoundingBoxOf(this)

/** The boundaries of this layout relative to the window's origin. */
fun LayoutCoordinates.boundsInWindow(): Rect {
    val root = findRootCoordinates()
    val rootWidth = root.size.width.toFloat()
    val rootHeight = root.size.height.toFloat()

    val bounds = boundsInRoot()
    val boundsLeft = bounds.left.fastCoerceIn(0f, rootWidth)
    val boundsTop = bounds.top.fastCoerceIn(0f, rootHeight)
    val boundsRight = bounds.right.fastCoerceIn(0f, rootWidth)
    val boundsBottom = bounds.bottom.fastCoerceIn(0f, rootHeight)

    if (boundsLeft == boundsRight || boundsTop == boundsBottom) {
        return Rect.Zero
    }

    val topLeft = root.localToWindow(Offset(boundsLeft, boundsTop))
    val topRight = root.localToWindow(Offset(boundsRight, boundsTop))
    val bottomRight = root.localToWindow(Offset(boundsRight, boundsBottom))
    val bottomLeft = root.localToWindow(Offset(boundsLeft, boundsBottom))

    val topLeftX = topLeft.x
    val topRightX = topRight.x
    val bottomLeftX = bottomLeft.x
    val bottomRightX = bottomRight.x

    val left = fastMinOf(topLeftX, topRightX, bottomLeftX, bottomRightX)
    val right = fastMaxOf(topLeftX, topRightX, bottomLeftX, bottomRightX)

    val topLeftY = topLeft.y
    val topRightY = topRight.y
    val bottomLeftY = bottomLeft.y
    val bottomRightY = bottomRight.y

    val top = fastMinOf(topLeftY, topRightY, bottomLeftY, bottomRightY)
    val bottom = fastMaxOf(topLeftY, topRightY, bottomLeftY, bottomRightY)

    return Rect(left, top, right, bottom)
}

/** Returns the position of the top-left in the parent's content area or (0, 0) for the root. */
fun LayoutCoordinates.positionInParent(): Offset =
    parentLayoutCoordinates?.localPositionOf(this, Offset.Zero) ?: Offset.Zero

/**
 * Returns the bounding box of the child in the parent's content area, including any clipping done
 * with respect to the parent. For the root, the bounds is positioned at (0, 0) and sized to the
 * size of the root.
 */
fun LayoutCoordinates.boundsInParent(): Rect =
    parentLayoutCoordinates?.localBoundingBoxOf(this)
        ?: Rect(0f, 0f, size.width.toFloat(), size.height.toFloat())

/**
 * Walks up the [LayoutCoordinates] hierarchy to find the [LayoutCoordinates] whose
 * [LayoutCoordinates.parentCoordinates] is `null` and returns it. If
 * [LayoutCoordinates.isAttached], this will have the size of the
 * [ComposeView][androidx.compose.ui.platform.ComposeView].
 */
fun LayoutCoordinates.findRootCoordinates(): LayoutCoordinates {
    var root = this
    var parent = root.parentLayoutCoordinates
    while (parent != null) {
        root = parent
        parent = root.parentLayoutCoordinates
    }
    var rootCoordinator = root as? NodeCoordinator ?: return root
    var parentCoordinator = rootCoordinator.wrappedBy
    while (parentCoordinator != null) {
        rootCoordinator = parentCoordinator
        parentCoordinator = parentCoordinator.wrappedBy
    }
    return rootCoordinator
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy