blob: f0002644f295489bceba9d7a98a1360a80c31fdd [file] [log] [blame]
/*
* Copyright 2019 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
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.geometry.isUnspecified
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.DrawStyle
import androidx.compose.ui.graphics.drawscope.DrawTransform
import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.graphics.drawscope.withTransform
import androidx.compose.ui.graphics.isUnspecified
import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextForegroundStyle.Unspecified
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.style.modulate
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.util.fastRoundToInt
import kotlin.math.ceil
object TextPainter {
// TODO(b/236964276): Deprecate when TextMeasurer and drawText are no longer Experimental
/**
* Paints the text onto the given canvas.
*
* @param canvas a canvas to be drawn
* @param textLayoutResult a result of text layout
*/
fun paint(canvas: Canvas, textLayoutResult: TextLayoutResult) {
val needClipping = textLayoutResult.hasVisualOverflow &&
textLayoutResult.layoutInput.overflow != TextOverflow.Visible
if (needClipping) {
val width = textLayoutResult.size.width.toFloat()
val height = textLayoutResult.size.height.toFloat()
val bounds = Rect(Offset.Zero, Size(width, height))
canvas.save()
canvas.clipRect(bounds)
}
/* inline resolveSpanStyleDefaults to avoid an allocation in draw */
val style = textLayoutResult.layoutInput.style.spanStyle
val textDecoration = style.textDecoration ?: TextDecoration.None
val shadow = style.shadow ?: Shadow.None
val drawStyle = style.drawStyle ?: Fill
try {
val brush = style.brush
if (brush != null) {
val alpha = if (style.textForegroundStyle !== Unspecified) {
style.textForegroundStyle.alpha
} else {
1.0f
}
textLayoutResult.multiParagraph.paint(
canvas = canvas,
brush = brush,
alpha = alpha,
shadow = shadow,
decoration = textDecoration,
drawStyle = drawStyle
)
} else {
val color = if (style.textForegroundStyle !== Unspecified) {
style.textForegroundStyle.color
} else {
Color.Black
}
textLayoutResult.multiParagraph.paint(
canvas = canvas,
color = color,
shadow = shadow,
decoration = textDecoration,
drawStyle = drawStyle
)
}
} finally {
if (needClipping) {
canvas.restore()
}
}
}
}
/**
* Draw styled text using a TextMeasurer.
*
* This draw function supports multi-styling and async font loading.
*
* TextMeasurer carries an internal cache to optimize text layout measurement for repeated calls
* in draw phase. If layout affecting attributes like font size, font weight, overflow, softWrap,
* etc. are changed in consecutive calls to this method, TextMeasurer and its internal cache that
* holds layout results may not offer any benefits. Check out [TextMeasurer] and drawText
* overloads that take [TextLayoutResult] to learn more about text layout and draw phase
* optimizations.
*
* @param textMeasurer Measures and lays out the text
* @param text Text to be drawn
* @param topLeft Offsets the text from top left point of the current coordinate system.
* @param style the [TextStyle] to be applied to the text
* @param overflow How visual overflow should be handled.
* @param softWrap Whether the text should break at soft line breaks. If false, the glyphs in
* the text will be positioned as if there was unlimited horizontal space. If [softWrap] is
* false, [overflow] and TextAlign may have unexpected effects.
* @param maxLines An optional maximum number of lines for the text to span, wrapping if
* necessary. If the text exceeds the given number of lines, it will be truncated according to
* [overflow] and [softWrap]. If it is not null, then it must be greater than zero.
* @param placeholders a list of [Placeholder]s that specify ranges of text which will be
* skipped during layout and replaced with [Placeholder]. It's required that the range of each
* [Placeholder] doesn't cross paragraph boundary, otherwise [IllegalArgumentException] is
* thrown.
* @param size how wide and tall the text should be. If left [Size.Unspecified] as its default
* value, text will be forced to fit inside the total drawing area from where it's placed.
* If size is specified, [Size.width] will define the width of the text. [Size.height] helps
* defining the number of lines that fit if [softWrap] is enabled and [overflow] is
* [TextOverflow.Ellipsis]. Otherwise, [Size.height] either defines where the text is clipped
* ([TextOverflow.Clip]) or becomes no-op.
* @param blendMode Blending algorithm to be applied to the text
*
* @sample androidx.compose.ui.text.samples.DrawTextAnnotatedStringSample
*/
fun DrawScope.drawText(
textMeasurer: TextMeasurer,
text: AnnotatedString,
topLeft: Offset = Offset.Zero,
style: TextStyle = TextStyle.Default,
overflow: TextOverflow = TextOverflow.Clip,
softWrap: Boolean = true,
maxLines: Int = Int.MAX_VALUE,
placeholders: List<AnnotatedString.Range<Placeholder>> = emptyList(),
size: Size = Size.Unspecified,
blendMode: BlendMode = DrawScope.DefaultBlendMode
) {
val textLayoutResult = textMeasurer.measure(
text = text,
style = style,
overflow = overflow,
softWrap = softWrap,
maxLines = maxLines,
placeholders = placeholders,
constraints = textLayoutConstraints(size, topLeft),
layoutDirection = layoutDirection,
density = this
)
withTransform({
translate(topLeft.x, topLeft.y)
clip(textLayoutResult)
}) {
textLayoutResult.multiParagraph.paint(
canvas = drawContext.canvas,
blendMode = blendMode
)
}
}
/**
* Draw text using a TextMeasurer.
*
* This draw function supports only one text style, and async font loading.
*
* TextMeasurer carries an internal cache to optimize text layout measurement for repeated calls
* in draw phase. If layout affecting attributes like font size, font weight, overflow, softWrap,
* etc. are changed in consecutive calls to this method, TextMeasurer and its internal cache that
* holds layout results may not offer any benefits. Check out [TextMeasurer] and drawText overloads that take [TextLayoutResult] to learn
* more about text layout and draw phase optimizations.
*
* @param textMeasurer Measures and lays out the text
* @param text Text to be drawn
* @param topLeft Offsets the text from top left point of the current coordinate system.
* @param style the [TextStyle] to be applied to the text
* @param overflow How visual overflow should be handled.
* @param softWrap Whether the text should break at soft line breaks. If false, the glyphs in
* the text will be positioned as if there was unlimited horizontal space. If [softWrap] is
* false, [overflow] and TextAlign may have unexpected effects.
* @param maxLines An optional maximum number of lines for the text to span, wrapping if
* necessary. If the text exceeds the given number of lines, it will be truncated according to
* [overflow] and [softWrap]. If it is not null, then it must be greater than zero.
* @param size how wide and tall the text should be. If left [Size.Unspecified] as its default
* value, text will be forced to fit inside the total drawing area from where it's placed.
* If size is specified, [Size.width] will define the width of the text. [Size.height] helps
* defining the number of lines that fit if [softWrap] is enabled and [overflow] is
* [TextOverflow.Ellipsis]. Otherwise, [Size.height] either defines where the text is clipped
* ([TextOverflow.Clip]) or becomes no-op.
* @param blendMode Blending algorithm to be applied to the text
*
* @sample androidx.compose.ui.text.samples.DrawTextSample
* @sample androidx.compose.ui.text.samples.DrawTextStyledSample
*/
fun DrawScope.drawText(
textMeasurer: TextMeasurer,
text: String,
topLeft: Offset = Offset.Zero,
style: TextStyle = TextStyle.Default,
overflow: TextOverflow = TextOverflow.Clip,
softWrap: Boolean = true,
maxLines: Int = Int.MAX_VALUE,
size: Size = Size.Unspecified,
blendMode: BlendMode = DrawScope.DefaultBlendMode
) {
val textLayoutResult = textMeasurer.measure(
text = AnnotatedString(text),
style = style,
overflow = overflow,
softWrap = softWrap,
maxLines = maxLines,
constraints = textLayoutConstraints(size, topLeft),
layoutDirection = layoutDirection,
density = this
)
withTransform({
translate(topLeft.x, topLeft.y)
clip(textLayoutResult)
}) {
textLayoutResult.multiParagraph.paint(
canvas = drawContext.canvas,
blendMode = blendMode
)
}
}
/**
* Draw an existing text layout as produced by [TextMeasurer].
*
* This draw function cannot relayout when async font loading resolves. If using async fonts or
* other dynamic text layout, you are responsible for invalidating layout on changes.
*
* @param textLayoutResult Text Layout to be drawn
* @param color Text color to use
* @param topLeft Offsets the text from top left point of the current coordinate system.
* @param alpha opacity to be applied to the [color] from 0.0f to 1.0f representing fully
* transparent to fully opaque respectively
* @param shadow The shadow effect applied on the text.
* @param textDecoration The decorations to paint on the text (e.g., an underline).
* @param drawStyle Whether or not the text is stroked or filled in.
* @param blendMode Blending algorithm to be applied to the text
*
* @sample androidx.compose.ui.text.samples.DrawTextMeasureInLayoutSample
* @sample androidx.compose.ui.text.samples.DrawTextDrawWithCacheSample
*/
fun DrawScope.drawText(
textLayoutResult: TextLayoutResult,
color: Color = Color.Unspecified,
topLeft: Offset = Offset.Zero,
alpha: Float = Float.NaN,
shadow: Shadow? = null,
textDecoration: TextDecoration? = null,
drawStyle: DrawStyle? = null,
blendMode: BlendMode = DrawScope.DefaultBlendMode
) {
val newShadow = shadow ?: textLayoutResult.layoutInput.style.shadow
val newTextDecoration = textDecoration ?: textLayoutResult.layoutInput.style.textDecoration
val newDrawStyle = drawStyle ?: textLayoutResult.layoutInput.style.drawStyle
withTransform({
translate(topLeft.x, topLeft.y)
clip(textLayoutResult)
}) {
// if text layout was created using brush, and [color] is unspecified, we should treat this
// like drawText(brush) call
val brush = textLayoutResult.layoutInput.style.brush
if (brush != null && color.isUnspecified) {
textLayoutResult.multiParagraph.paint(
drawContext.canvas,
brush,
if (!alpha.isNaN()) alpha else textLayoutResult.layoutInput.style.alpha,
newShadow,
newTextDecoration,
newDrawStyle,
blendMode
)
} else {
textLayoutResult.multiParagraph.paint(
drawContext.canvas,
color.takeOrElse { textLayoutResult.layoutInput.style.color }.modulate(alpha),
newShadow,
newTextDecoration,
newDrawStyle,
blendMode
)
}
}
}
/**
* Draw an existing text layout as produced by [TextMeasurer].
*
* This draw function cannot relayout when async font loading resolves. If using async fonts or
* other dynamic text layout, you are responsible for invalidating layout on changes.
*
* @param textLayoutResult Text Layout to be drawn
* @param brush The brush to use when drawing the text.
* @param topLeft Offsets the text from top left point of the current coordinate system.
* @param alpha Opacity to be applied to [brush] from 0.0f to 1.0f representing fully
* transparent to fully opaque respectively.
* @param shadow The shadow effect applied on the text.
* @param textDecoration The decorations to paint on the text (e.g., an underline).
* @param drawStyle Whether or not the text is stroked or filled in.
* @param blendMode Blending algorithm to be applied to the text
*/
fun DrawScope.drawText(
textLayoutResult: TextLayoutResult,
brush: Brush,
topLeft: Offset = Offset.Zero,
alpha: Float = Float.NaN,
shadow: Shadow? = null,
textDecoration: TextDecoration? = null,
drawStyle: DrawStyle? = null,
blendMode: BlendMode = DrawScope.DefaultBlendMode
) {
val newShadow = shadow ?: textLayoutResult.layoutInput.style.shadow
val newTextDecoration = textDecoration ?: textLayoutResult.layoutInput.style.textDecoration
val newDrawStyle = drawStyle ?: textLayoutResult.layoutInput.style.drawStyle
withTransform({
translate(topLeft.x, topLeft.y)
clip(textLayoutResult)
}) {
textLayoutResult.multiParagraph.paint(
drawContext.canvas,
brush,
if (!alpha.isNaN()) alpha else textLayoutResult.layoutInput.style.alpha,
newShadow,
newTextDecoration,
newDrawStyle,
blendMode
)
}
}
private fun DrawTransform.clip(textLayoutResult: TextLayoutResult) {
if (textLayoutResult.hasVisualOverflow &&
textLayoutResult.layoutInput.overflow != TextOverflow.Visible
) {
clipRect(
left = 0f,
top = 0f,
right = textLayoutResult.size.width.toFloat(),
bottom = textLayoutResult.size.height.toFloat()
)
}
}
/**
* Converts given size and placement preferences to Constraints for measuring text layout.
*/
private fun DrawScope.textLayoutConstraints(
size: Size,
topLeft: Offset
): Constraints {
val minWidth: Int
val maxWidth: Int
val isWidthNaN = size.isUnspecified || size.width.isNaN()
if (isWidthNaN) {
minWidth = 0
maxWidth = ceil(this.size.width - topLeft.x).fastRoundToInt()
} else {
val fixedWidth = ceil(size.width).fastRoundToInt()
minWidth = fixedWidth
maxWidth = fixedWidth
}
val minHeight: Int
val maxHeight: Int
val isHeightNaN = size.isUnspecified || size.height.isNaN()
if (isHeightNaN) {
minHeight = 0
maxHeight = ceil(this.size.height - topLeft.y).fastRoundToInt()
} else {
val fixedHeight = ceil(size.height).fastRoundToInt()
minHeight = fixedHeight
maxHeight = fixedHeight
}
return Constraints(minWidth, maxWidth, minHeight, maxHeight)
}