1 /*
2 * Copyright 2019 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package androidx.compose.ui.text
18
19 import androidx.compose.ui.geometry.Offset
20 import androidx.compose.ui.geometry.Rect
21 import androidx.compose.ui.geometry.Size
22 import androidx.compose.ui.geometry.isUnspecified
23 import androidx.compose.ui.graphics.BlendMode
24 import androidx.compose.ui.graphics.Brush
25 import androidx.compose.ui.graphics.Canvas
26 import androidx.compose.ui.graphics.Color
27 import androidx.compose.ui.graphics.Shadow
28 import androidx.compose.ui.graphics.drawscope.DrawScope
29 import androidx.compose.ui.graphics.drawscope.DrawStyle
30 import androidx.compose.ui.graphics.drawscope.DrawTransform
31 import androidx.compose.ui.graphics.drawscope.Fill
32 import androidx.compose.ui.graphics.drawscope.withTransform
33 import androidx.compose.ui.graphics.isUnspecified
34 import androidx.compose.ui.graphics.takeOrElse
35 import androidx.compose.ui.text.style.TextDecoration
36 import androidx.compose.ui.text.style.TextForegroundStyle.Unspecified
37 import androidx.compose.ui.text.style.TextOverflow
38 import androidx.compose.ui.text.style.modulate
39 import androidx.compose.ui.unit.Constraints
40 import androidx.compose.ui.util.fastRoundToInt
41 import kotlin.math.ceil
42
43 object TextPainter {
44
45 // TODO(b/236964276): Deprecate when TextMeasurer and drawText are no longer Experimental
46 /**
47 * Paints the text onto the given canvas.
48 *
49 * @param canvas a canvas to be drawn
50 * @param textLayoutResult a result of text layout
51 */
paintnull52 fun paint(canvas: Canvas, textLayoutResult: TextLayoutResult) {
53 val needClipping =
54 textLayoutResult.hasVisualOverflow &&
55 textLayoutResult.layoutInput.overflow != TextOverflow.Visible
56 if (needClipping) {
57 val width = textLayoutResult.size.width.toFloat()
58 val height = textLayoutResult.size.height.toFloat()
59 val bounds = Rect(Offset.Zero, Size(width, height))
60 canvas.save()
61 canvas.clipRect(bounds)
62 }
63
64 /* inline resolveSpanStyleDefaults to avoid an allocation in draw */
65 val style = textLayoutResult.layoutInput.style.spanStyle
66 val textDecoration = style.textDecoration ?: TextDecoration.None
67 val shadow = style.shadow ?: Shadow.None
68 val drawStyle = style.drawStyle ?: Fill
69 try {
70 val brush = style.brush
71 if (brush != null) {
72 val alpha =
73 if (style.textForegroundStyle !== Unspecified) {
74 style.textForegroundStyle.alpha
75 } else {
76 1.0f
77 }
78 textLayoutResult.multiParagraph.paint(
79 canvas = canvas,
80 brush = brush,
81 alpha = alpha,
82 shadow = shadow,
83 decoration = textDecoration,
84 drawStyle = drawStyle
85 )
86 } else {
87 val color =
88 if (style.textForegroundStyle !== Unspecified) {
89 style.textForegroundStyle.color
90 } else {
91 Color.Black
92 }
93 textLayoutResult.multiParagraph.paint(
94 canvas = canvas,
95 color = color,
96 shadow = shadow,
97 decoration = textDecoration,
98 drawStyle = drawStyle
99 )
100 }
101 } finally {
102 if (needClipping) {
103 canvas.restore()
104 }
105 }
106 }
107 }
108
109 /**
110 * Draw styled text using a TextMeasurer.
111 *
112 * This draw function supports multi-styling and async font loading.
113 *
114 * TextMeasurer carries an internal cache to optimize text layout measurement for repeated calls in
115 * draw phase. If layout affecting attributes like font size, font weight, overflow, softWrap, etc.
116 * are changed in consecutive calls to this method, TextMeasurer and its internal cache that holds
117 * layout results may not offer any benefits. Check out [TextMeasurer] and drawText overloads that
118 * take [TextLayoutResult] to learn more about text layout and draw phase optimizations.
119 *
120 * @param textMeasurer Measures and lays out the text
121 * @param text Text to be drawn
122 * @param topLeft Offsets the text from top left point of the current coordinate system.
123 * @param style the [TextStyle] to be applied to the text
124 * @param overflow How visual overflow should be handled.
125 * @param softWrap Whether the text should break at soft line breaks. If false, the glyphs in the
126 * text will be positioned as if there was unlimited horizontal space. If [softWrap] is false,
127 * [overflow] and TextAlign may have unexpected effects.
128 * @param maxLines An optional maximum number of lines for the text to span, wrapping if necessary.
129 * If the text exceeds the given number of lines, it will be truncated according to [overflow] and
130 * [softWrap]. If it is not null, then it must be greater than zero.
131 * @param placeholders a list of [Placeholder]s that specify ranges of text which will be skipped
132 * during layout and replaced with [Placeholder]. It's required that the range of each
133 * [Placeholder] doesn't cross paragraph boundary, otherwise [IllegalArgumentException] is thrown.
134 * @param size how wide and tall the text should be. If left [Size.Unspecified] as its default
135 * value, text will be forced to fit inside the total drawing area from where it's placed. If size
136 * is specified, [Size.width] will define the width of the text. [Size.height] helps defining the
137 * number of lines that fit if [softWrap] is enabled and [overflow] is [TextOverflow.Ellipsis].
138 * Otherwise, [Size.height] either defines where the text is clipped ([TextOverflow.Clip]) or
139 * becomes no-op.
140 * @param blendMode Blending algorithm to be applied to the text
141 * @sample androidx.compose.ui.text.samples.DrawTextAnnotatedStringSample
142 */
DrawScopenull143 fun DrawScope.drawText(
144 textMeasurer: TextMeasurer,
145 text: AnnotatedString,
146 topLeft: Offset = Offset.Zero,
147 style: TextStyle = TextStyle.Default,
148 overflow: TextOverflow = TextOverflow.Clip,
149 softWrap: Boolean = true,
150 maxLines: Int = Int.MAX_VALUE,
151 placeholders: List<AnnotatedString.Range<Placeholder>> = listOf(),
152 size: Size = Size.Unspecified,
153 blendMode: BlendMode = DrawScope.DefaultBlendMode
154 ) {
155 val textLayoutResult =
156 textMeasurer.measure(
157 text = text,
158 style = style,
159 overflow = overflow,
160 softWrap = softWrap,
161 maxLines = maxLines,
162 placeholders = placeholders,
163 constraints = textLayoutConstraints(size, topLeft),
164 layoutDirection = layoutDirection,
165 density = this
166 )
167
168 withTransform({
169 translate(topLeft.x, topLeft.y)
170 clip(textLayoutResult)
171 }) {
172 textLayoutResult.multiParagraph.paint(canvas = drawContext.canvas, blendMode = blendMode)
173 }
174 }
175
176 /**
177 * Draw text using a TextMeasurer.
178 *
179 * This draw function supports only one text style, and async font loading.
180 *
181 * TextMeasurer carries an internal cache to optimize text layout measurement for repeated calls in
182 * draw phase. If layout affecting attributes like font size, font weight, overflow, softWrap, etc.
183 * are changed in consecutive calls to this method, TextMeasurer and its internal cache that holds
184 * layout results may not offer any benefits. Check out [TextMeasurer] and drawText overloads that
185 * take [TextLayoutResult] to learn more about text layout and draw phase optimizations.
186 *
187 * @param textMeasurer Measures and lays out the text
188 * @param text Text to be drawn
189 * @param topLeft Offsets the text from top left point of the current coordinate system.
190 * @param style the [TextStyle] to be applied to the text
191 * @param overflow How visual overflow should be handled.
192 * @param softWrap Whether the text should break at soft line breaks. If false, the glyphs in the
193 * text will be positioned as if there was unlimited horizontal space. If [softWrap] is false,
194 * [overflow] and TextAlign may have unexpected effects.
195 * @param maxLines An optional maximum number of lines for the text to span, wrapping if necessary.
196 * If the text exceeds the given number of lines, it will be truncated according to [overflow] and
197 * [softWrap]. If it is not null, then it must be greater than zero.
198 * @param size how wide and tall the text should be. If left [Size.Unspecified] as its default
199 * value, text will be forced to fit inside the total drawing area from where it's placed. If size
200 * is specified, [Size.width] will define the width of the text. [Size.height] helps defining the
201 * number of lines that fit if [softWrap] is enabled and [overflow] is [TextOverflow.Ellipsis].
202 * Otherwise, [Size.height] either defines where the text is clipped ([TextOverflow.Clip]) or
203 * becomes no-op.
204 * @param blendMode Blending algorithm to be applied to the text
205 * @sample androidx.compose.ui.text.samples.DrawTextSample
206 * @sample androidx.compose.ui.text.samples.DrawTextStyledSample
207 */
DrawScopenull208 fun DrawScope.drawText(
209 textMeasurer: TextMeasurer,
210 text: String,
211 topLeft: Offset = Offset.Zero,
212 style: TextStyle = TextStyle.Default,
213 overflow: TextOverflow = TextOverflow.Clip,
214 softWrap: Boolean = true,
215 maxLines: Int = Int.MAX_VALUE,
216 size: Size = Size.Unspecified,
217 blendMode: BlendMode = DrawScope.DefaultBlendMode
218 ) {
219 val textLayoutResult =
220 textMeasurer.measure(
221 text = AnnotatedString(text),
222 style = style,
223 overflow = overflow,
224 softWrap = softWrap,
225 maxLines = maxLines,
226 constraints = textLayoutConstraints(size, topLeft),
227 layoutDirection = layoutDirection,
228 density = this
229 )
230
231 withTransform({
232 translate(topLeft.x, topLeft.y)
233 clip(textLayoutResult)
234 }) {
235 textLayoutResult.multiParagraph.paint(canvas = drawContext.canvas, blendMode = blendMode)
236 }
237 }
238
239 /**
240 * Draw an existing text layout as produced by [TextMeasurer].
241 *
242 * This draw function cannot relayout when async font loading resolves. If using async fonts or
243 * other dynamic text layout, you are responsible for invalidating layout on changes.
244 *
245 * @param textLayoutResult Text Layout to be drawn
246 * @param color Text color to use
247 * @param topLeft Offsets the text from top left point of the current coordinate system.
248 * @param alpha opacity to be applied to the [color] from 0.0f to 1.0f representing fully
249 * transparent to fully opaque respectively
250 * @param shadow The shadow effect applied on the text.
251 * @param textDecoration The decorations to paint on the text (e.g., an underline).
252 * @param drawStyle Whether or not the text is stroked or filled in.
253 * @param blendMode Blending algorithm to be applied to the text
254 * @sample androidx.compose.ui.text.samples.DrawTextMeasureInLayoutSample
255 * @sample androidx.compose.ui.text.samples.DrawTextDrawWithCacheSample
256 */
DrawScopenull257 fun DrawScope.drawText(
258 textLayoutResult: TextLayoutResult,
259 color: Color = Color.Unspecified,
260 topLeft: Offset = Offset.Zero,
261 alpha: Float = Float.NaN,
262 shadow: Shadow? = null,
263 textDecoration: TextDecoration? = null,
264 drawStyle: DrawStyle? = null,
265 blendMode: BlendMode = DrawScope.DefaultBlendMode
266 ) {
267 val newShadow = shadow ?: textLayoutResult.layoutInput.style.shadow
268 val newTextDecoration = textDecoration ?: textLayoutResult.layoutInput.style.textDecoration
269 val newDrawStyle = drawStyle ?: textLayoutResult.layoutInput.style.drawStyle
270
271 withTransform({
272 translate(topLeft.x, topLeft.y)
273 clip(textLayoutResult)
274 }) {
275 // if text layout was created using brush, and [color] is unspecified, we should treat this
276 // like drawText(brush) call
277 val brush = textLayoutResult.layoutInput.style.brush
278 if (brush != null && color.isUnspecified) {
279 textLayoutResult.multiParagraph.paint(
280 drawContext.canvas,
281 brush,
282 if (!alpha.isNaN()) alpha else textLayoutResult.layoutInput.style.alpha,
283 newShadow,
284 newTextDecoration,
285 newDrawStyle,
286 blendMode
287 )
288 } else {
289 textLayoutResult.multiParagraph.paint(
290 drawContext.canvas,
291 color.takeOrElse { textLayoutResult.layoutInput.style.color }.modulate(alpha),
292 newShadow,
293 newTextDecoration,
294 newDrawStyle,
295 blendMode
296 )
297 }
298 }
299 }
300
301 /**
302 * Draw an existing text layout as produced by [TextMeasurer].
303 *
304 * This draw function cannot relayout when async font loading resolves. If using async fonts or
305 * other dynamic text layout, you are responsible for invalidating layout on changes.
306 *
307 * @param textLayoutResult Text Layout to be drawn
308 * @param brush The brush to use when drawing the text.
309 * @param topLeft Offsets the text from top left point of the current coordinate system.
310 * @param alpha Opacity to be applied to [brush] from 0.0f to 1.0f representing fully transparent to
311 * fully opaque respectively.
312 * @param shadow The shadow effect applied on the text.
313 * @param textDecoration The decorations to paint on the text (e.g., an underline).
314 * @param drawStyle Whether or not the text is stroked or filled in.
315 * @param blendMode Blending algorithm to be applied to the text
316 */
DrawScopenull317 fun DrawScope.drawText(
318 textLayoutResult: TextLayoutResult,
319 brush: Brush,
320 topLeft: Offset = Offset.Zero,
321 alpha: Float = Float.NaN,
322 shadow: Shadow? = null,
323 textDecoration: TextDecoration? = null,
324 drawStyle: DrawStyle? = null,
325 blendMode: BlendMode = DrawScope.DefaultBlendMode
326 ) {
327 val newShadow = shadow ?: textLayoutResult.layoutInput.style.shadow
328 val newTextDecoration = textDecoration ?: textLayoutResult.layoutInput.style.textDecoration
329 val newDrawStyle = drawStyle ?: textLayoutResult.layoutInput.style.drawStyle
330
331 withTransform({
332 translate(topLeft.x, topLeft.y)
333 clip(textLayoutResult)
334 }) {
335 textLayoutResult.multiParagraph.paint(
336 drawContext.canvas,
337 brush,
338 if (!alpha.isNaN()) alpha else textLayoutResult.layoutInput.style.alpha,
339 newShadow,
340 newTextDecoration,
341 newDrawStyle,
342 blendMode
343 )
344 }
345 }
346
DrawTransformnull347 private fun DrawTransform.clip(textLayoutResult: TextLayoutResult) {
348 if (
349 textLayoutResult.hasVisualOverflow &&
350 textLayoutResult.layoutInput.overflow != TextOverflow.Visible
351 ) {
352 clipRect(
353 left = 0f,
354 top = 0f,
355 right = textLayoutResult.size.width.toFloat(),
356 bottom = textLayoutResult.size.height.toFloat()
357 )
358 }
359 }
360
361 /** Converts given size and placement preferences to Constraints for measuring text layout. */
DrawScopenull362 private fun DrawScope.textLayoutConstraints(size: Size, topLeft: Offset): Constraints {
363 val minWidth: Int
364 val maxWidth: Int
365 val isWidthNaN = size.isUnspecified || size.width.isNaN()
366 if (isWidthNaN) {
367 minWidth = 0
368 maxWidth = ceil(this.size.width - topLeft.x).fastRoundToInt()
369 } else {
370 val fixedWidth = ceil(size.width).fastRoundToInt()
371 minWidth = fixedWidth
372 maxWidth = fixedWidth
373 }
374
375 val minHeight: Int
376 val maxHeight: Int
377 val isHeightNaN = size.isUnspecified || size.height.isNaN()
378 if (isHeightNaN) {
379 minHeight = 0
380 maxHeight = ceil(this.size.height - topLeft.y).fastRoundToInt()
381 } else {
382 val fixedHeight = ceil(size.height).fastRoundToInt()
383 minHeight = fixedHeight
384 maxHeight = fixedHeight
385 }
386
387 return Constraints(minWidth, maxWidth, minHeight, maxHeight)
388 }
389