[go: nahoru, domu]

blob: f3b8b8526817b1bd399d0b68a5ffc1a007982a35 [file] [log] [blame]
/*
* Copyright 2018 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.platform
import android.text.Spanned
import android.text.TextPaint
import android.text.TextUtils
import androidx.compose.ui.text.android.InternalPlatformTextApi
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.asComposePath
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.Paragraph
import androidx.compose.ui.text.ParagraphConstraints
import androidx.compose.ui.text.ParagraphIntrinsics
import androidx.compose.ui.text.Placeholder
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.android.LayoutCompat.ALIGN_CENTER
import androidx.compose.ui.text.android.LayoutCompat.ALIGN_LEFT
import androidx.compose.ui.text.android.LayoutCompat.ALIGN_NORMAL
import androidx.compose.ui.text.android.LayoutCompat.ALIGN_OPPOSITE
import androidx.compose.ui.text.android.LayoutCompat.ALIGN_RIGHT
import androidx.compose.ui.text.android.LayoutCompat.DEFAULT_ALIGNMENT
import androidx.compose.ui.text.android.LayoutCompat.DEFAULT_JUSTIFICATION_MODE
import androidx.compose.ui.text.android.LayoutCompat.DEFAULT_LINESPACING_MULTIPLIER
import androidx.compose.ui.text.android.LayoutCompat.JUSTIFICATION_MODE_INTER_WORD
import androidx.compose.ui.text.android.TextLayout
import androidx.compose.ui.text.android.selection.WordBoundary
import androidx.compose.ui.text.android.style.PlaceholderSpan
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.ResolvedTextDirection
import androidx.ui.unit.Density
import androidx.compose.ui.geometry.Offset
import androidx.ui.util.annotation.VisibleForTesting
import java.util.Locale as JavaLocale
/**
* Android specific implementation for [Paragraph]
*/
@OptIn(InternalPlatformTextApi::class)
internal class AndroidParagraph constructor(
val paragraphIntrinsics: AndroidParagraphIntrinsics,
val maxLines: Int,
val ellipsis: Boolean,
val constraints: ParagraphConstraints
) : Paragraph {
constructor(
text: String,
style: TextStyle,
spanStyles: List<AnnotatedString.Range<SpanStyle>>,
placeholders: List<AnnotatedString.Range<Placeholder>>,
maxLines: Int,
ellipsis: Boolean,
constraints: ParagraphConstraints,
typefaceAdapter: TypefaceAdapter,
density: Density
) : this(
paragraphIntrinsics = AndroidParagraphIntrinsics(
text = text,
style = style,
placeholders = placeholders,
spanStyles = spanStyles,
typefaceAdapter = typefaceAdapter,
density = density
),
maxLines = maxLines,
ellipsis = ellipsis,
constraints = constraints
)
private val layout: TextLayout
override val width: Float
init {
require(maxLines >= 1) { "maxLines should be greater than 0" }
val style = paragraphIntrinsics.style
val alignment = toLayoutAlign(style.textAlign)
val justificationMode = when (style.textAlign) {
TextAlign.Justify -> JUSTIFICATION_MODE_INTER_WORD
else -> DEFAULT_JUSTIFICATION_MODE
}
val ellipsize = if (ellipsis) {
TextUtils.TruncateAt.END
} else {
null
}
this.width = constraints.width
layout = TextLayout(
charSequence = paragraphIntrinsics.charSequence,
width = constraints.width,
textPaint = textPaint,
ellipsize = ellipsize,
alignment = alignment,
textDirectionHeuristic = paragraphIntrinsics.textDirectionHeuristic,
lineSpacingMultiplier = DEFAULT_LINESPACING_MULTIPLIER,
maxLines = maxLines,
justificationMode = justificationMode,
layoutIntrinsics = paragraphIntrinsics.layoutIntrinsics
)
}
override val height: Float
get() = layout.height.toFloat()
override val maxIntrinsicWidth: Float
get() = paragraphIntrinsics.maxIntrinsicWidth
override val minIntrinsicWidth: Float
get() = paragraphIntrinsics.minIntrinsicWidth
override val firstBaseline: Float
get() = layout.getLineBaseline(0)
override val lastBaseline: Float
get() = if (maxLines < lineCount) {
layout.getLineBaseline(maxLines - 1)
} else {
layout.getLineBaseline(lineCount - 1)
}
override val didExceedMaxLines: Boolean
get() = layout.didExceedMaxLines
@VisibleForTesting
internal val textLocale: JavaLocale
get() = paragraphIntrinsics.textPaint.textLocale
override val lineCount: Int
get() = layout.lineCount
override val placeholderRects: List<Rect?> =
with(paragraphIntrinsics.charSequence) {
if (this !is Spanned) return@with listOf()
getSpans(0, length, PlaceholderSpan::class.java).map { span ->
val start = getSpanStart(span)
val end = getSpanEnd(span)
val line = layout.getLineForOffset(start)
// This Placeholder is ellipsized, return null instead.
if (layout.getLineEllipsisCount(line) > 0 &&
end > layout.getLineEllipsisOffset(line)) {
return@map null
}
val direction = getBidiRunDirection(start)
val left = when (direction) {
ResolvedTextDirection.Ltr ->
getHorizontalPosition(start, true)
ResolvedTextDirection.Rtl ->
getHorizontalPosition(start, true) - span.widthPx
}
val right = left + span.widthPx
val top = with(layout) {
when (span.verticalAlign) {
PlaceholderSpan.ALIGN_ABOVE_BASELINE ->
getLineBaseline(line) - span.heightPx
PlaceholderSpan.ALIGN_TOP -> getLineTop(line)
PlaceholderSpan.ALIGN_BOTTOM -> getLineBottom(line) - span.heightPx
PlaceholderSpan.ALIGN_CENTER ->
(getLineTop(line) + getLineBottom(line) - span.heightPx) / 2
PlaceholderSpan.ALIGN_TEXT_TOP ->
span.fontMetrics.ascent + getLineBaseline(line)
PlaceholderSpan.ALIGN_TEXT_BOTTOM ->
span.fontMetrics.descent + getLineBaseline(line) - span.heightPx
PlaceholderSpan.ALIGN_TEXT_CENTER ->
with(span.fontMetrics) {
(ascent + descent - span.heightPx) / 2 + getLineBaseline(line)
}
else -> throw IllegalStateException("unexpected verticalAlignment")
}
}
val bottom = top + span.heightPx
Rect(left, top, right, bottom)
}
}
@VisibleForTesting
internal val charSequence: CharSequence
get() = paragraphIntrinsics.charSequence
@VisibleForTesting
internal val textPaint: TextPaint
get() = paragraphIntrinsics.textPaint
override fun getLineForVerticalPosition(vertical: Float): Int {
return layout.getLineForVertical(vertical.toInt())
}
override fun getOffsetForPosition(position: Offset): Int {
val line = layout.getLineForVertical(position.y.toInt())
return layout.getOffsetForHorizontal(line, position.x)
}
/**
* Returns the bounding box as Rect of the character for given character offset. Rect includes
* the top, bottom, left and right of a character.
*/
// TODO:(qqd) Implement RTL case.
override fun getBoundingBox(offset: Int): Rect {
val left = layout.getPrimaryHorizontal(offset)
val right = layout.getPrimaryHorizontal(offset + 1)
val line = layout.getLineForOffset(offset)
val top = layout.getLineTop(line)
val bottom = layout.getLineBottom(line)
return Rect(top = top, bottom = bottom, left = left, right = right)
}
override fun getPathForRange(start: Int, end: Int): Path {
if (start !in 0..end || end > charSequence.length) {
throw AssertionError(
"Start($start) or End($end) is out of Range(0..${charSequence.length})," +
" or start > end!"
)
}
val path = android.graphics.Path()
layout.getSelectionPath(start, end, path)
return path.asComposePath()
}
override fun getCursorRect(offset: Int): Rect {
if (offset !in 0..charSequence.length) {
throw AssertionError("offset($offset) is out of bounds (0,${charSequence.length}")
}
val cursorWidth = 4.0f
val horizontal = layout.getPrimaryHorizontal(offset)
val line = layout.getLineForOffset(offset)
return Rect(
horizontal - 0.5f * cursorWidth,
layout.getLineTop(line),
horizontal + 0.5f * cursorWidth,
layout.getLineBottom(line)
)
}
private val wordBoundary: WordBoundary by lazy {
WordBoundary(textLocale, layout.text)
}
override fun getWordBoundary(offset: Int): TextRange {
return TextRange(wordBoundary.getWordStart(offset), wordBoundary.getWordEnd(offset))
}
override fun getLineLeft(lineIndex: Int): Float = layout.getLineLeft(lineIndex)
override fun getLineRight(lineIndex: Int): Float = layout.getLineRight(lineIndex)
override fun getLineTop(lineIndex: Int): Float = layout.getLineTop(lineIndex)
override fun getLineBottom(lineIndex: Int): Float = layout.getLineBottom(lineIndex)
override fun getLineHeight(lineIndex: Int): Float = layout.getLineHeight(lineIndex)
override fun getLineWidth(lineIndex: Int): Float = layout.getLineWidth(lineIndex)
override fun getLineStart(lineIndex: Int): Int = layout.getLineStart(lineIndex)
override fun getLineEnd(lineIndex: Int): Int = layout.getLineEnd(lineIndex)
override fun getLineEllipsisOffset(lineIndex: Int): Int =
layout.getLineEllipsisOffset(lineIndex)
override fun getLineEllipsisCount(lineIndex: Int): Int = layout.getLineEllipsisCount(lineIndex)
override fun getLineForOffset(offset: Int): Int = layout.getLineForOffset(offset)
override fun getHorizontalPosition(offset: Int, usePrimaryDirection: Boolean): Float =
if (usePrimaryDirection) {
layout.getPrimaryHorizontal(offset)
} else {
layout.getSecondaryHorizontal(offset)
}
override fun getParagraphDirection(offset: Int): ResolvedTextDirection {
val lineIndex = layout.getLineForOffset(offset)
val direction = layout.getParagraphDirection(lineIndex)
return if (direction == 1) ResolvedTextDirection.Ltr else ResolvedTextDirection.Rtl
}
override fun getBidiRunDirection(offset: Int): ResolvedTextDirection {
return if (layout.isRtlCharAt(offset))
ResolvedTextDirection.Rtl
else
ResolvedTextDirection.Ltr
}
/**
* @return true if the given line is ellipsized, else false.
*/
@VisibleForTesting
internal fun isEllipsisApplied(lineIndex: Int): Boolean =
layout.isEllipsisApplied(lineIndex)
override fun paint(canvas: Canvas) {
val nativeCanvas = canvas.nativeCanvas
if (didExceedMaxLines) {
nativeCanvas.save()
nativeCanvas.clipRect(0f, 0f, width, height)
}
layout.paint(nativeCanvas)
if (didExceedMaxLines) {
nativeCanvas.restore()
}
}
}
/**
* Converts [TextAlign] into [TextLayout] alignment constants.
*/
@OptIn(InternalPlatformTextApi::class)
private fun toLayoutAlign(align: TextAlign?): Int = when (align) {
TextAlign.Left -> ALIGN_LEFT
TextAlign.Right -> ALIGN_RIGHT
TextAlign.Center -> ALIGN_CENTER
TextAlign.Start -> ALIGN_NORMAL
TextAlign.End -> ALIGN_OPPOSITE
else -> DEFAULT_ALIGNMENT
}
// TODO(b/159152328): temporary workaround for desktop. remove when full support of MPP will
// be in-place
@Deprecated(
"Temporary workaround. Supposed to be used only in desktop before MPP",
level = DeprecationLevel.ERROR
)
@InternalPlatformTextApi
var paragraphActualFactory: ((
paragraphIntrinsics: ParagraphIntrinsics,
maxLines: Int,
ellipsis: Boolean,
constraints: ParagraphConstraints
) -> Paragraph) = { paragraphIntrinsics, maxLines, ellipsis, constraints ->
AndroidParagraph(
paragraphIntrinsics as AndroidParagraphIntrinsics,
maxLines,
ellipsis,
constraints)
}
@OptIn(InternalPlatformTextApi::class)
internal actual fun ActualParagraph(
text: String,
style: TextStyle,
spanStyles: List<AnnotatedString.Range<SpanStyle>>,
placeholders: List<AnnotatedString.Range<Placeholder>>,
maxLines: Int,
ellipsis: Boolean,
constraints: ParagraphConstraints,
density: Density,
resourceLoader: Font.ResourceLoader
): Paragraph =
@Suppress("DEPRECATION_ERROR")
paragraphActualFactory(
ActualParagraphIntrinsics(
text,
style,
spanStyles,
placeholders,
density,
resourceLoader
),
maxLines,
ellipsis,
constraints
)
@OptIn(InternalPlatformTextApi::class)
internal actual fun ActualParagraph(
paragraphIntrinsics: ParagraphIntrinsics,
maxLines: Int,
ellipsis: Boolean,
constraints: ParagraphConstraints
): Paragraph =
@Suppress("DEPRECATION_ERROR")
paragraphActualFactory(
paragraphIntrinsics,
maxLines,
ellipsis,
constraints)