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

androidMain.androidx.compose.ui.text.android.style.PlaceholderSpan.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.
 */

package androidx.compose.ui.text.android.style

import android.annotation.SuppressLint
import android.graphics.Canvas
import android.graphics.Paint
import android.text.style.ReplacementSpan
import androidx.annotation.IntDef
import androidx.compose.ui.text.android.InternalPlatformTextApi
import kotlin.math.ceil
import kotlin.math.max
import kotlin.math.min

/**
 * A span that is used to reserve empty spaces for inline element. It's used only to tell the
 * text processor the size and vertical alignment of the inline element.
 *
 * @param width The width needed by the inline element.
 * @param widthUnit The unit of the [width]; can be Sp, Em or inherit, if it's inherit, by
 * default it will be 1.em
 * @param height The height needed by the inline element.
 * @param heightUnit The unit of the [height]; can be Sp, Em or inherit, if it's inherit, by
 * default it will be computed from the context FontMetrics by
 * height = fontMetrics.descent - fontMetrics.ascent.
 * @param pxPerSp The number of pixels 1 Sp equals to.
 * @param verticalAlign How the inline element is aligned with the text.
 *
 * @suppress
 */
@InternalPlatformTextApi
class PlaceholderSpan(
    private val width: Float,
    @Unit
    private val widthUnit: Int,
    private val height: Float,
    @Unit
    private val heightUnit: Int,
    private val pxPerSp: Float,
    @VerticalAlign
    val verticalAlign: Int
) : ReplacementSpan() {
    companion object {
        const val ALIGN_ABOVE_BASELINE = 0
        const val ALIGN_TOP = 1
        const val ALIGN_BOTTOM = 2
        const val ALIGN_CENTER = 3
        const val ALIGN_TEXT_TOP = 4
        const val ALIGN_TEXT_BOTTOM = 5
        const val ALIGN_TEXT_CENTER = 6

        @Retention(AnnotationRetention.SOURCE)
        @IntDef(
            ALIGN_ABOVE_BASELINE,
            ALIGN_TOP,
            ALIGN_BOTTOM,
            ALIGN_CENTER,
            ALIGN_TEXT_TOP,
            ALIGN_TEXT_BOTTOM,
            ALIGN_TEXT_CENTER
        )
        internal annotation class VerticalAlign

        const val UNIT_SP = 0
        const val UNIT_EM = 1
        const val UNIT_UNSPECIFIED = 2

        @Retention(AnnotationRetention.SOURCE)
        @IntDef(
            UNIT_SP,
            UNIT_EM,
            UNIT_UNSPECIFIED
        )
        internal annotation class Unit
    }

    /* Used to compute bounding box when verticalAlign is ALIGN_TEXT_TOP / ALIGN_TEXT_BOTTOM. */
    lateinit var fontMetrics: Paint.FontMetricsInt
        private set

    /* The laid out width of the placeholder, in the unit of pixel. */
    var widthPx: Int = 0
        private set
        get() {
            check(isLaidOut) { "PlaceholderSpan is not laid out yet." }
            return field
        }

    /* The laid out height of the placeholder, in the unit of pixel. */
    var heightPx: Int = 0
        private set
        get() {
            check(isLaidOut) { "PlaceholderSpan is not laid out yet." }
            return field
        }

    /* Primitives can't be "lateinit", use this boolean to work around. */
    private var isLaidOut: Boolean = false

    @SuppressLint("DocumentExceptions")
    override fun getSize(
        paint: Paint,
        text: CharSequence?,
        start: Int,
        end: Int,
        fm: Paint.FontMetricsInt?
    ): Int {
        isLaidOut = true
        val fontSize = paint.textSize
        fontMetrics = paint.fontMetricsInt
        require(fontMetrics.descent > fontMetrics.ascent) {
            "Invalid fontMetrics: line height can not be negative."
        }

        widthPx = when (widthUnit) {
            UNIT_SP -> width * pxPerSp
            UNIT_EM -> width * fontSize
            else -> throw IllegalArgumentException("Unsupported unit.")
        }.ceilToInt()

        heightPx = when (heightUnit) {
            UNIT_SP -> (height * pxPerSp).ceilToInt()
            UNIT_EM -> (height * fontSize).ceilToInt()
            else -> throw IllegalArgumentException("Unsupported unit.")
        }

        fm?.apply {
            ascent = fontMetrics.ascent
            descent = fontMetrics.descent
            leading = fontMetrics.leading

            when (verticalAlign) {
                // If align top and inline element is too tall, expand descent.
                ALIGN_TOP, ALIGN_TEXT_TOP -> if (ascent + heightPx > descent) {
                    descent = ascent + heightPx
                }
                // If align bottom and inline element is too tall, expand ascent.
                ALIGN_BOTTOM, ALIGN_TEXT_BOTTOM -> if (ascent > descent - heightPx) {
                    ascent = descent - heightPx
                }
                // If align center and inline element is too tall, evenly expand ascent and descent.
                ALIGN_CENTER, ALIGN_TEXT_CENTER -> if (descent - ascent < heightPx) {
                    ascent -= (heightPx - (descent - ascent)) / 2
                    descent = ascent + heightPx
                }
                // If align baseline and inline element is too tall, expand ascent
                ALIGN_ABOVE_BASELINE -> if (ascent > -heightPx) {
                    ascent = -heightPx
                }
                else -> throw IllegalArgumentException("Unknown verticalAlign.")
            }
            // make top/bottom at least same as ascent/descent.
            top = min(fontMetrics.top, ascent)
            bottom = max(fontMetrics.bottom, descent)
        }

        return widthPx
    }

    /* Empty implementation, not used. */
    override fun draw(
        canvas: Canvas,
        text: CharSequence?,
        start: Int,
        end: Int,
        x: Float,
        top: Int,
        y: Int,
        bottom: Int,
        paint: Paint
    ) {}
}

internal fun Float.ceilToInt(): Int = ceil(this).toInt()




© 2015 - 2025 Weber Informatics LLC | Privacy Policy