ScaleFactor.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.ui.layout

import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.util.packFloats
import androidx.compose.ui.util.toStringAsFixed
import androidx.compose.ui.util.unpackFloat1
import androidx.compose.ui.util.unpackFloat2

/**
 * Constructs a [ScaleFactor] from the given x and y scale values
 */
@Stable
fun ScaleFactor(scaleX: Float, scaleY: Float) = ScaleFactor(packFloats(scaleX, scaleY))

/**
 * Holds 2 dimensional scaling factors for horizontal and vertical axes
 */
@Suppress("EXPERIMENTAL_FEATURE_WARNING")
@Immutable
inline class ScaleFactor(@PublishedApi internal val packedValue: Long) {

    /**
     * Returns the scale factor to apply along the horizontal axis
     */
    @Stable
    val scaleX: Float
        get() {
            // Explicitly compare against packed values to avoid
            // auto-boxing of ScaleFactor.Unspecified
            check(this.packedValue != ScaleFactor.Unspecified.packedValue) {
                "ScaleFactor is unspecified"
            }
            return unpackFloat1(packedValue)
        }

    /**
     * Returns the scale factor to apply along the vertical axis
     */
    @Stable
    val scaleY: Float
        get() {
            // Explicitly compare against packed values to avoid
            // auto-boxing of Size.Unspecified
            check(this.packedValue != ScaleFactor.Unspecified.packedValue) {
                "ScaleFactor is unspecified"
            }
            return unpackFloat2(packedValue)
        }

    @Suppress("NOTHING_TO_INLINE")
    @Stable
    inline operator fun component1(): Float = scaleX

    @Suppress("NOTHING_TO_INLINE")
    @Stable
    inline operator fun component2(): Float = scaleY

    /**
     * Returns a copy of this ScaleFactor instance optionally overriding the
     * scaleX or scaleY parameters
     */
    fun copy(scaleX: Float = this.scaleX, scaleY: Float = this.scaleY) = ScaleFactor(scaleX, scaleY)

    /**
     * Multiplication operator.
     *
     * Returns a [ScaleFactor] with scale x and y values multiplied by the operand
     */
    @Stable
    operator fun times(operand: Float) = ScaleFactor(scaleX * operand, scaleY * operand)

    /**
     * Division operator.
     *
     * Returns a [ScaleFactor] with scale x and y values divided by the operand
     */
    @Stable
    operator fun div(operand: Float) = ScaleFactor(scaleX / operand, scaleY / operand)

    override fun toString() = "ScaleFactor(${scaleX.toStringAsFixed(1)}, " +
        "${scaleY.toStringAsFixed(1)})"

    companion object {

        /**
         * A ScaleFactor whose [scaleX] and [scaleY] parameters are unspecified. This is a sentinel
         * value used to initialize a non-null parameter.
         * Access to scaleX or scaleY on an unspecified size is not allowed
         */
        @Stable
        val Unspecified = ScaleFactor(Float.NaN, Float.NaN)
    }
}

/**
 * Multiplication operator with [Size].
 *
 * Return a new [Size] with the width and height multiplied by the [ScaleFactor.scaleX] and
 * [ScaleFactor.scaleY] respectively
 */
@Stable
operator fun Size.times(scaleFactor: ScaleFactor): Size =
    Size(this.width * scaleFactor.scaleX, this.height * scaleFactor.scaleY)

/**
 * Multiplication operator with [Size] with reverse parameter types to maintain
 * commutative properties of multiplication
 *
 * Return a new [Size] with the width and height multiplied by the [ScaleFactor.scaleX] and
 * [ScaleFactor.scaleY] respectively
 */
@Stable
operator fun ScaleFactor.times(size: Size): Size = size * this

/**
 * Division operator with [Size]
 *
 * Return a new [Size] with the width and height divided by [ScaleFactor.scaleX] and
 * [ScaleFactor.scaleY] respectively
 */
@Stable
operator fun Size.div(scaleFactor: ScaleFactor): Size =
    Size(width / scaleFactor.scaleX, height / scaleFactor.scaleY)

/**
 * Linearly interpolate between two [ScaleFactor] parameters
 *
 * The [fraction] argument represents position on the timeline, with 0.0 meaning
 * that the interpolation has not started, returning [start] (or something
 * equivalent to [start]), 1.0 meaning that the interpolation has finished,
 * returning [stop] (or something equivalent to [stop]), and values in between
 * meaning that the interpolation is at the relevant point on the timeline
 * between [start] and [stop]. The interpolation can be extrapolated beyond 0.0 and
 * 1.0, so negative values and values greater than 1.0 are valid (and can
 * easily be generated by curves).
 *
 * Values for [fraction] are usually obtained from an [Animation<Float>], such as
 * an `AnimationController`.
 */
@Stable
fun lerp(start: ScaleFactor, stop: ScaleFactor, fraction: Float): ScaleFactor {
    return ScaleFactor(
        androidx.compose.ui.util.lerp(start.scaleX, stop.scaleX, fraction),
        androidx.compose.ui.util.lerp(start.scaleY, stop.scaleY, fraction)
    )
}