1 /*
<lambda>null2 * Copyright 2022 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 android.graphics.RectF
20 import android.os.Build
21 import android.text.Spannable
22 import android.text.SpannableString
23 import android.text.Spanned
24 import android.text.TextUtils
25 import androidx.annotation.IntRange
26 import androidx.annotation.VisibleForTesting
27 import androidx.compose.ui.geometry.Offset
28 import androidx.compose.ui.geometry.Rect
29 import androidx.compose.ui.geometry.Size
30 import androidx.compose.ui.graphics.BlendMode
31 import androidx.compose.ui.graphics.Brush
32 import androidx.compose.ui.graphics.Canvas
33 import androidx.compose.ui.graphics.Color
34 import androidx.compose.ui.graphics.Path
35 import androidx.compose.ui.graphics.Shadow
36 import androidx.compose.ui.graphics.asComposePath
37 import androidx.compose.ui.graphics.drawscope.DrawStyle
38 import androidx.compose.ui.graphics.nativeCanvas
39 import androidx.compose.ui.graphics.toAndroidRectF
40 import androidx.compose.ui.graphics.toComposeRect
41 import androidx.compose.ui.text.android.InternalPlatformTextApi
42 import androidx.compose.ui.text.android.LayoutCompat.ALIGN_CENTER
43 import androidx.compose.ui.text.android.LayoutCompat.ALIGN_LEFT
44 import androidx.compose.ui.text.android.LayoutCompat.ALIGN_NORMAL
45 import androidx.compose.ui.text.android.LayoutCompat.ALIGN_OPPOSITE
46 import androidx.compose.ui.text.android.LayoutCompat.ALIGN_RIGHT
47 import androidx.compose.ui.text.android.LayoutCompat.BREAK_STRATEGY_BALANCED
48 import androidx.compose.ui.text.android.LayoutCompat.BREAK_STRATEGY_HIGH_QUALITY
49 import androidx.compose.ui.text.android.LayoutCompat.BREAK_STRATEGY_SIMPLE
50 import androidx.compose.ui.text.android.LayoutCompat.DEFAULT_ALIGNMENT
51 import androidx.compose.ui.text.android.LayoutCompat.DEFAULT_BREAK_STRATEGY
52 import androidx.compose.ui.text.android.LayoutCompat.DEFAULT_HYPHENATION_FREQUENCY
53 import androidx.compose.ui.text.android.LayoutCompat.DEFAULT_JUSTIFICATION_MODE
54 import androidx.compose.ui.text.android.LayoutCompat.DEFAULT_LINESPACING_MULTIPLIER
55 import androidx.compose.ui.text.android.LayoutCompat.DEFAULT_LINE_BREAK_STYLE
56 import androidx.compose.ui.text.android.LayoutCompat.DEFAULT_LINE_BREAK_WORD_STYLE
57 import androidx.compose.ui.text.android.LayoutCompat.HYPHENATION_FREQUENCY_FULL
58 import androidx.compose.ui.text.android.LayoutCompat.HYPHENATION_FREQUENCY_FULL_FAST
59 import androidx.compose.ui.text.android.LayoutCompat.HYPHENATION_FREQUENCY_NONE
60 import androidx.compose.ui.text.android.LayoutCompat.JUSTIFICATION_MODE_INTER_WORD
61 import androidx.compose.ui.text.android.LayoutCompat.LINE_BREAK_STYLE_LOOSE
62 import androidx.compose.ui.text.android.LayoutCompat.LINE_BREAK_STYLE_NONE
63 import androidx.compose.ui.text.android.LayoutCompat.LINE_BREAK_STYLE_NORMAL
64 import androidx.compose.ui.text.android.LayoutCompat.LINE_BREAK_STYLE_STRICT
65 import androidx.compose.ui.text.android.LayoutCompat.LINE_BREAK_WORD_STYLE_NONE
66 import androidx.compose.ui.text.android.LayoutCompat.LINE_BREAK_WORD_STYLE_PHRASE
67 import androidx.compose.ui.text.android.LayoutCompat.TEXT_GRANULARITY_CHARACTER
68 import androidx.compose.ui.text.android.LayoutCompat.TEXT_GRANULARITY_WORD
69 import androidx.compose.ui.text.android.TextLayout
70 import androidx.compose.ui.text.android.hasSpan
71 import androidx.compose.ui.text.android.selection.getWordEnd
72 import androidx.compose.ui.text.android.selection.getWordStart
73 import androidx.compose.ui.text.android.style.IndentationFixSpan
74 import androidx.compose.ui.text.android.style.PlaceholderSpan
75 import androidx.compose.ui.text.font.FontFamily
76 import androidx.compose.ui.text.internal.requirePrecondition
77 import androidx.compose.ui.text.platform.AndroidParagraphIntrinsics
78 import androidx.compose.ui.text.platform.AndroidTextPaint
79 import androidx.compose.ui.text.platform.extensions.setSpan
80 import androidx.compose.ui.text.platform.isIncludeFontPaddingEnabled
81 import androidx.compose.ui.text.platform.style.ShaderBrushSpan
82 import androidx.compose.ui.text.style.Hyphens
83 import androidx.compose.ui.text.style.LineBreak
84 import androidx.compose.ui.text.style.ResolvedTextDirection
85 import androidx.compose.ui.text.style.TextAlign
86 import androidx.compose.ui.text.style.TextDecoration
87 import androidx.compose.ui.text.style.TextOverflow
88 import androidx.compose.ui.text.style.TextOverflow.Companion.Ellipsis
89 import androidx.compose.ui.text.style.TextOverflow.Companion.MiddleEllipsis
90 import androidx.compose.ui.text.style.TextOverflow.Companion.StartEllipsis
91 import androidx.compose.ui.unit.Constraints
92 import androidx.compose.ui.unit.Density
93 import androidx.compose.ui.unit.TextUnit
94 import androidx.compose.ui.unit.sp
95 import java.util.Locale as JavaLocale
96
97 /** Android specific implementation for [Paragraph] */
98 // NOTE(text-perf-review): I see most of the APIs in this class just delegate to TextLayout or to
99 // AndroidParagraphIntrinsics. Should we consider just having one TextLayout class which
100 // implements Paragraph and ParagraphIntrinsics? it seems like all of these types are immutable
101 // and have similar sets of responsibilities.
102 @OptIn(InternalPlatformTextApi::class, ExperimentalTextApi::class)
103 internal class AndroidParagraph(
104 val paragraphIntrinsics: AndroidParagraphIntrinsics,
105 val maxLines: Int,
106 val overflow: TextOverflow,
107 val constraints: Constraints
108 ) : Paragraph {
109 constructor(
110 text: String,
111 style: TextStyle,
112 annotations: List<AnnotatedString.Range<out AnnotatedString.Annotation>>,
113 placeholders: List<AnnotatedString.Range<Placeholder>>,
114 maxLines: Int,
115 overflow: TextOverflow,
116 constraints: Constraints,
117 fontFamilyResolver: FontFamily.Resolver,
118 density: Density
119 ) : this(
120 paragraphIntrinsics =
121 AndroidParagraphIntrinsics(
122 text = text,
123 style = style,
124 annotations = annotations,
125 placeholders = placeholders,
126 fontFamilyResolver = fontFamilyResolver,
127 density = density
128 ),
129 maxLines = maxLines,
130 overflow = overflow,
131 constraints = constraints
132 )
133
134 private val layout: TextLayout
135
136 @VisibleForTesting internal val charSequence: CharSequence
137
138 init {
139 requirePrecondition(constraints.minHeight == 0 && constraints.minWidth == 0) {
140 "Setting Constraints.minWidth and Constraints.minHeight is not supported, " +
141 "these should be the default zero values instead."
142 }
143 requirePrecondition(maxLines >= 1) { "maxLines should be greater than 0" }
144
145 val style = paragraphIntrinsics.style
146
147 charSequence =
148 if (shouldAttachIndentationFixSpan(style, overflow == Ellipsis)) {
149 // When letter spacing, align and ellipsize applied to text, the ellipsized line is
150 // indented wrong. This function adds the IndentationFixSpan in order to fix the
151 // issue
152 // with best effort. b/228463206
153 paragraphIntrinsics.charSequence.attachIndentationFixSpan()
154 } else {
155 paragraphIntrinsics.charSequence
156 }
157
158 val alignment = toLayoutAlign(style.textAlign)
159
160 val justificationMode =
161 when (style.textAlign) {
162 TextAlign.Justify -> JUSTIFICATION_MODE_INTER_WORD
163 else -> DEFAULT_JUSTIFICATION_MODE
164 }
165
166 val hyphens = toLayoutHyphenationFrequency(style.paragraphStyle.hyphens)
167
168 val breakStrategy = toLayoutBreakStrategy(style.lineBreak.strategy)
169 val lineBreakStyle = toLayoutLineBreakStyle(style.lineBreak.strictness)
170 val lineBreakWordStyle = toLayoutLineBreakWordStyle(style.lineBreak.wordBreak)
171
172 val ellipsize =
173 when (overflow) {
174 Ellipsis -> TextUtils.TruncateAt.END
175 MiddleEllipsis -> TextUtils.TruncateAt.MIDDLE
176 StartEllipsis -> TextUtils.TruncateAt.START
177 else -> null
178 }
179
180 var firstLayout =
181 constructTextLayout(
182 alignment = alignment,
183 justificationMode = justificationMode,
184 ellipsize = ellipsize,
185 maxLines = maxLines,
186 hyphens = hyphens,
187 breakStrategy = breakStrategy,
188 lineBreakStyle = lineBreakStyle,
189 lineBreakWordStyle = lineBreakWordStyle
190 )
191
192 // In case of start/middle ellipsis when the letter spacing is enabled and some of the
193 // characters are ellipsized away, we need to remeasure. This is because though
194 // internally ellipsized character are replaced with zero-width U+FEFF character, the
195 // letter spacing is still applied to each such character. It's been fixed on API 35
196 // where letter spacing won't be applied to some special characters including U+FEFF.
197 if (
198 Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM &&
199 textPaint.letterSpacing != 0f &&
200 (overflow == StartEllipsis || overflow == MiddleEllipsis) &&
201 firstLayout.getLineEllipsisCount(0) > 0
202 ) {
203 val beforeEllipsis = firstLayout.getLineEllipsisOffset(0)
204 val afterEllipsis = beforeEllipsis + firstLayout.getLineEllipsisCount(0)
205 val newSpannable =
206 TextUtils.concat(
207 charSequence.subSequence(0, beforeEllipsis),
208 Typography.ellipsis.toString(),
209 charSequence.subSequence(afterEllipsis, charSequence.length)
210 )
211 firstLayout =
212 constructTextLayout(
213 alignment = alignment,
214 justificationMode = justificationMode,
215 ellipsize = ellipsize,
216 maxLines = maxLines,
217 hyphens = hyphens,
218 breakStrategy = breakStrategy,
219 lineBreakStyle = lineBreakStyle,
220 lineBreakWordStyle = lineBreakWordStyle,
221 charSequence = newSpannable
222 )
223 }
224
225 // Ellipsize if there's not enough vertical space to fit all lines. Because this only makes
226 // sense for end ellipsis because start/middle only works for a single line.
227 if (overflow == Ellipsis && firstLayout.height > constraints.maxHeight && maxLines > 1) {
228 val calculatedMaxLines =
229 firstLayout.numberOfLinesThatFitMaxHeight(constraints.maxHeight)
230 layout =
231 if (calculatedMaxLines >= 0 && calculatedMaxLines != maxLines) {
232 constructTextLayout(
233 alignment = alignment,
234 justificationMode = justificationMode,
235 ellipsize = ellipsize,
236 // When we can't fully fit even a single line, measure with one line anyway.
237 // This will allow to have an ellipsis on that single line. If we measured
238 // with 0 maxLines, it would measure all lines with no ellipsis even though
239 // the first line might be partially visible
240 maxLines = calculatedMaxLines.coerceAtLeast(1),
241 hyphens = hyphens,
242 breakStrategy = breakStrategy,
243 lineBreakStyle = lineBreakStyle,
244 lineBreakWordStyle = lineBreakWordStyle
245 )
246 } else {
247 firstLayout
248 }
249 } else {
250 layout = firstLayout
251 }
252
253 // Brush is not fully realized on text until layout is complete and size information
254 // is known. Brush can now be applied to the overall textpaint and all the spans.
255 textPaint.setBrush(style.brush, Size(width, height), style.alpha)
256 val shaderBrushSpans = layout.getShaderBrushSpans()
257 if (shaderBrushSpans != null) {
258 for (shaderBrushSpan in shaderBrushSpans) {
259 shaderBrushSpan.size = Size(width, height)
260 }
261 }
262 }
263
264 override val width: Float
265 get() = constraints.maxWidth.toFloat()
266
267 override val height: Float
268 get() = layout.height.toFloat()
269
270 override val maxIntrinsicWidth: Float
271 get() = paragraphIntrinsics.maxIntrinsicWidth
272
273 override val minIntrinsicWidth: Float
274 get() = paragraphIntrinsics.minIntrinsicWidth
275
276 override val firstBaseline: Float
277 get() = getLineBaseline(0)
278
279 override val lastBaseline: Float
280 get() = getLineBaseline(lineCount - 1)
281
282 override val didExceedMaxLines: Boolean
283 get() = layout.didExceedMaxLines
284
285 @VisibleForTesting
286 internal val textLocale: JavaLocale
287 get() = paragraphIntrinsics.textPaint.textLocale
288
289 /**
290 * Resolved line count. If maxLines smaller than the real number of lines in the text, this
291 * property will return the minimum between the two
292 */
293 override val lineCount: Int
294 get() = layout.lineCount
295
296 override val placeholderRects: List<Rect?> =
297 with(charSequence) {
298 if (this !is Spanned) return@with listOf()
299 getSpans(0, length, PlaceholderSpan::class.java).map { span ->
300 val start = getSpanStart(span)
301 val end = getSpanEnd(span)
302 // The line index of the PlaceholderSpan. In the case where PlaceholderSpan is
303 // truncated due to maxLines limitation. It will return the index of last line.
304 val line = layout.getLineForOffset(start)
305 val exceedsMaxLines = line >= maxLines
306 val isPlaceholderSpanEllipsized =
307 layout.getLineEllipsisCount(line) > 0 &&
308 end > layout.getLineEllipsisOffset(line)
309 val isPlaceholderSpanTruncated = end > layout.getLineEnd(line)
310 // This Placeholder is ellipsized or truncated, return null instead.
311 if (isPlaceholderSpanEllipsized || isPlaceholderSpanTruncated || exceedsMaxLines) {
312 return@map null
313 }
314
315 val direction = getBidiRunDirection(start)
316
317 val left =
318 when (direction) {
319 ResolvedTextDirection.Ltr -> getHorizontalPosition(start, true)
320 ResolvedTextDirection.Rtl ->
321 getHorizontalPosition(start, true) - span.widthPx
322 }
323 val right = left + span.widthPx
324
325 val top =
326 with(layout) {
327 when (span.verticalAlign) {
328 PlaceholderSpan.ALIGN_ABOVE_BASELINE ->
329 getLineBaseline(line) - span.heightPx
330 PlaceholderSpan.ALIGN_TOP -> getLineTop(line)
331 PlaceholderSpan.ALIGN_BOTTOM -> getLineBottom(line) - span.heightPx
332 PlaceholderSpan.ALIGN_CENTER ->
333 (getLineTop(line) + getLineBottom(line) - span.heightPx) / 2
334 PlaceholderSpan.ALIGN_TEXT_TOP ->
335 span.fontMetrics.ascent + getLineBaseline(line)
336 PlaceholderSpan.ALIGN_TEXT_BOTTOM ->
337 span.fontMetrics.descent + getLineBaseline(line) - span.heightPx
338 PlaceholderSpan.ALIGN_TEXT_CENTER ->
339 with(span.fontMetrics) {
340 (ascent + descent - span.heightPx) / 2 + getLineBaseline(line)
341 }
342 else -> throw IllegalStateException("unexpected verticalAlignment")
343 }
344 }
345
346 val bottom = top + span.heightPx
347
348 Rect(left, top, right, bottom)
349 }
350 }
351
352 @VisibleForTesting
353 internal val textPaint: AndroidTextPaint
354 get() = paragraphIntrinsics.textPaint
355
356 override fun getLineForVerticalPosition(vertical: Float): Int {
357 return layout.getLineForVertical(vertical.toInt())
358 }
359
360 override fun getOffsetForPosition(position: Offset): Int {
361 val line = layout.getLineForVertical(position.y.toInt())
362 return layout.getOffsetForHorizontal(line, position.x)
363 }
364
365 override fun getRangeForRect(
366 rect: Rect,
367 granularity: TextGranularity,
368 inclusionStrategy: TextInclusionStrategy
369 ): TextRange {
370 val range =
371 layout.getRangeForRect(
372 rect = rect.toAndroidRectF(),
373 granularity = granularity.toLayoutTextGranularity(),
374 inclusionStrategy = { segmentBounds: RectF, area: RectF ->
375 inclusionStrategy.isIncluded(
376 segmentBounds.toComposeRect(),
377 area.toComposeRect()
378 )
379 }
380 ) ?: return TextRange.Zero
381 return TextRange(range[0], range[1])
382 }
383
384 /**
385 * Returns the bounding box as Rect of the character for given character offset. Rect includes
386 * the top, bottom, left and right of a character.
387 */
388 override fun getBoundingBox(offset: Int): Rect {
389 requirePrecondition(offset in charSequence.indices) {
390 "offset($offset) is out of bounds [0,${charSequence.length})"
391 }
392 val rectF = layout.getBoundingBox(offset)
393 return with(rectF) { Rect(left = left, top = top, right = right, bottom = bottom) }
394 }
395
396 /**
397 * Fills the bounding boxes for characters provided in the [range] into [array]. The array is
398 * filled starting from [arrayStart] (inclusive). The coordinates are in local text layout
399 * coordinates.
400 *
401 * The returned information consists of left/right of a character; line top and bottom for the
402 * same character.
403 *
404 * For the grapheme consists of multiple code points, e.g. ligatures, combining marks, the first
405 * character has the total width and the remaining are returned as zero-width.
406 *
407 * The array divided into segments of four where each index in that segment represents left,
408 * top, right, bottom of the character.
409 *
410 * The size of the provided [array] should be greater or equal than the four times * [TextRange]
411 * length.
412 *
413 * The final order of characters in the [array] is from [TextRange.min] to [TextRange.max].
414 *
415 * @param range the [TextRange] representing the start and end indices in the [Paragraph].
416 * @param array the array to fill in the values. The array divided into segments of four where
417 * each index in that segment represents left, top, right, bottom of the character.
418 * @param arrayStart the inclusive start index in the array where the function will start
419 * filling in the values from
420 */
421 override fun fillBoundingBoxes(
422 range: TextRange,
423 array: FloatArray,
424 @IntRange(from = 0) arrayStart: Int
425 ) {
426 layout.fillBoundingBoxes(range.min, range.max, array, arrayStart)
427 }
428
429 override fun getPathForRange(start: Int, end: Int): Path {
430 requirePrecondition(start in 0..end && end <= charSequence.length) {
431 "start($start) or end($end) is out of range [0..${charSequence.length}]," +
432 " or start > end!"
433 }
434 val path = android.graphics.Path()
435 layout.getSelectionPath(start, end, path)
436 return path.asComposePath()
437 }
438
439 override fun getCursorRect(offset: Int): Rect {
440 requirePrecondition(offset in 0..charSequence.length) {
441 "offset($offset) is out of bounds [0,${charSequence.length}]"
442 }
443 val horizontal = layout.getPrimaryHorizontal(offset)
444 val line = layout.getLineForOffset(offset)
445
446 // The width of the cursor is not taken into account. The callers of this API should use
447 // rect.left to get the start X position and then adjust it according to the width if needed
448 return Rect(horizontal, layout.getLineTop(line), horizontal, layout.getLineBottom(line))
449 }
450
451 override fun getWordBoundary(offset: Int): TextRange {
452 val wordIterator = layout.wordIterator
453 return TextRange(wordIterator.getWordStart(offset), wordIterator.getWordEnd(offset))
454 }
455
456 override fun getLineLeft(lineIndex: Int): Float = layout.getLineLeft(lineIndex)
457
458 override fun getLineRight(lineIndex: Int): Float = layout.getLineRight(lineIndex)
459
460 override fun getLineTop(lineIndex: Int): Float = layout.getLineTop(lineIndex)
461
462 internal fun getLineAscent(lineIndex: Int): Float = layout.getLineAscent(lineIndex)
463
464 override fun getLineBaseline(lineIndex: Int): Float = layout.getLineBaseline(lineIndex)
465
466 internal fun getLineDescent(lineIndex: Int): Float = layout.getLineDescent(lineIndex)
467
468 override fun getLineBottom(lineIndex: Int): Float = layout.getLineBottom(lineIndex)
469
470 override fun getLineHeight(lineIndex: Int): Float = layout.getLineHeight(lineIndex)
471
472 override fun getLineWidth(lineIndex: Int): Float = layout.getLineWidth(lineIndex)
473
474 override fun getLineStart(lineIndex: Int): Int = layout.getLineStart(lineIndex)
475
476 override fun getLineEnd(lineIndex: Int, visibleEnd: Boolean): Int =
477 if (visibleEnd) {
478 layout.getLineVisibleEnd(lineIndex)
479 } else {
480 layout.getLineEnd(lineIndex)
481 }
482
483 override fun isLineEllipsized(lineIndex: Int): Boolean = layout.isLineEllipsized(lineIndex)
484
485 internal fun getLineEllipsisOffset(lineIndex: Int): Int =
486 layout.getLineEllipsisOffset(lineIndex)
487
488 internal fun getLineEllipsisCount(lineIndex: Int): Int = layout.getLineEllipsisCount(lineIndex)
489
490 override fun getLineForOffset(offset: Int): Int = layout.getLineForOffset(offset)
491
492 override fun getHorizontalPosition(offset: Int, usePrimaryDirection: Boolean): Float =
493 if (usePrimaryDirection) {
494 layout.getPrimaryHorizontal(offset)
495 } else {
496 layout.getSecondaryHorizontal(offset)
497 }
498
499 override fun getParagraphDirection(offset: Int): ResolvedTextDirection {
500 val lineIndex = layout.getLineForOffset(offset)
501 val direction = layout.getParagraphDirection(lineIndex)
502 return if (direction == 1) ResolvedTextDirection.Ltr else ResolvedTextDirection.Rtl
503 }
504
505 override fun getBidiRunDirection(offset: Int): ResolvedTextDirection {
506 return if (layout.isRtlCharAt(offset)) ResolvedTextDirection.Rtl
507 else ResolvedTextDirection.Ltr
508 }
509
510 private fun TextLayout.getShaderBrushSpans(): Array<ShaderBrushSpan>? {
511 if (text !is Spanned) return null
512 if (!(text as Spanned).hasSpan(ShaderBrushSpan::class.java)) return null
513 val brushSpans = (text as Spanned).getSpans(0, text.length, ShaderBrushSpan::class.java)
514 return brushSpans
515 }
516
517 private fun Spanned.hasSpan(clazz: Class<*>): Boolean {
518 return nextSpanTransition(-1, length, clazz) != length
519 }
520
521 override fun paint(
522 canvas: Canvas,
523 color: Color,
524 shadow: Shadow?,
525 textDecoration: TextDecoration?
526 ) {
527 with(textPaint) {
528 setColor(color)
529 setShadow(shadow)
530 setTextDecoration(textDecoration)
531 }
532
533 paint(canvas)
534 }
535
536 override fun paint(
537 canvas: Canvas,
538 color: Color,
539 shadow: Shadow?,
540 textDecoration: TextDecoration?,
541 drawStyle: DrawStyle?,
542 blendMode: BlendMode
543 ) {
544 val currBlendMode = textPaint.blendMode
545 with(textPaint) {
546 setColor(color)
547 setShadow(shadow)
548 setTextDecoration(textDecoration)
549 setDrawStyle(drawStyle)
550 this.blendMode = blendMode
551 }
552
553 paint(canvas)
554
555 textPaint.blendMode = currBlendMode
556 }
557
558 override fun paint(
559 canvas: Canvas,
560 brush: Brush,
561 alpha: Float,
562 shadow: Shadow?,
563 textDecoration: TextDecoration?,
564 drawStyle: DrawStyle?,
565 blendMode: BlendMode
566 ) {
567 val currBlendMode = textPaint.blendMode
568 with(textPaint) {
569 setBrush(brush, Size(width, height), alpha)
570 setShadow(shadow)
571 setTextDecoration(textDecoration)
572 setDrawStyle(drawStyle)
573 this.blendMode = blendMode
574 }
575
576 paint(canvas)
577
578 textPaint.blendMode = currBlendMode
579 }
580
581 private fun paint(canvas: Canvas) {
582 val nativeCanvas = canvas.nativeCanvas
583 if (didExceedMaxLines) {
584 nativeCanvas.save()
585 nativeCanvas.clipRect(0f, 0f, width, height)
586 }
587 layout.paint(nativeCanvas)
588 if (didExceedMaxLines) {
589 nativeCanvas.restore()
590 }
591 }
592
593 private fun constructTextLayout(
594 alignment: Int,
595 justificationMode: Int,
596 ellipsize: TextUtils.TruncateAt?,
597 maxLines: Int,
598 hyphens: Int,
599 breakStrategy: Int,
600 lineBreakStyle: Int,
601 lineBreakWordStyle: Int,
602 charSequence: CharSequence = this.charSequence,
603 ) =
604 TextLayout(
605 charSequence = charSequence,
606 width = width,
607 textPaint = textPaint,
608 ellipsize = ellipsize,
609 alignment = alignment,
610 textDirectionHeuristic = paragraphIntrinsics.textDirectionHeuristic,
611 lineSpacingMultiplier = DEFAULT_LINESPACING_MULTIPLIER,
612 maxLines = maxLines,
613 justificationMode = justificationMode,
614 layoutIntrinsics = paragraphIntrinsics.layoutIntrinsics,
615 includePadding = paragraphIntrinsics.style.isIncludeFontPaddingEnabled(),
616 fallbackLineSpacing = true,
617 hyphenationFrequency = hyphens,
618 breakStrategy = breakStrategy,
619 lineBreakStyle = lineBreakStyle,
620 lineBreakWordStyle = lineBreakWordStyle
621 )
622 }
623
624 /** Converts [TextAlign] into [TextLayout] alignment constants. */
625 @OptIn(InternalPlatformTextApi::class)
toLayoutAlignnull626 private fun toLayoutAlign(align: TextAlign): Int =
627 when (align) {
628 TextAlign.Left -> ALIGN_LEFT
629 TextAlign.Right -> ALIGN_RIGHT
630 TextAlign.Center -> ALIGN_CENTER
631 TextAlign.Start -> ALIGN_NORMAL
632 TextAlign.End -> ALIGN_OPPOSITE
633 else -> DEFAULT_ALIGNMENT
634 }
635
636 @OptIn(InternalPlatformTextApi::class)
toLayoutHyphenationFrequencynull637 private fun toLayoutHyphenationFrequency(hyphens: Hyphens): Int =
638 when (hyphens) {
639 Hyphens.Auto ->
640 if (Build.VERSION.SDK_INT <= 32) {
641 HYPHENATION_FREQUENCY_FULL
642 } else {
643 HYPHENATION_FREQUENCY_FULL_FAST
644 }
645 Hyphens.None -> HYPHENATION_FREQUENCY_NONE
646 else -> DEFAULT_HYPHENATION_FREQUENCY
647 }
648
649 @OptIn(InternalPlatformTextApi::class)
toLayoutBreakStrategynull650 private fun toLayoutBreakStrategy(breakStrategy: LineBreak.Strategy): Int =
651 when (breakStrategy) {
652 LineBreak.Strategy.Simple -> BREAK_STRATEGY_SIMPLE
653 LineBreak.Strategy.HighQuality -> BREAK_STRATEGY_HIGH_QUALITY
654 LineBreak.Strategy.Balanced -> BREAK_STRATEGY_BALANCED
655 else -> DEFAULT_BREAK_STRATEGY
656 }
657
658 @OptIn(InternalPlatformTextApi::class)
toLayoutLineBreakStylenull659 private fun toLayoutLineBreakStyle(lineBreakStrictness: LineBreak.Strictness): Int =
660 when (lineBreakStrictness) {
661 LineBreak.Strictness.Default -> LINE_BREAK_STYLE_NONE
662 LineBreak.Strictness.Loose -> LINE_BREAK_STYLE_LOOSE
663 LineBreak.Strictness.Normal -> LINE_BREAK_STYLE_NORMAL
664 LineBreak.Strictness.Strict -> LINE_BREAK_STYLE_STRICT
665 else -> DEFAULT_LINE_BREAK_STYLE
666 }
667
668 @OptIn(InternalPlatformTextApi::class)
toLayoutLineBreakWordStylenull669 private fun toLayoutLineBreakWordStyle(lineBreakWordStyle: LineBreak.WordBreak): Int =
670 when (lineBreakWordStyle) {
671 LineBreak.WordBreak.Default -> LINE_BREAK_WORD_STYLE_NONE
672 LineBreak.WordBreak.Phrase -> LINE_BREAK_WORD_STYLE_PHRASE
673 else -> DEFAULT_LINE_BREAK_WORD_STYLE
674 }
675
676 @OptIn(InternalPlatformTextApi::class)
numberOfLinesThatFitMaxHeightnull677 private fun TextLayout.numberOfLinesThatFitMaxHeight(maxHeight: Int): Int {
678 for (lineIndex in 0 until lineCount) {
679 if (getLineBottom(lineIndex) > maxHeight) return lineIndex
680 }
681 return lineCount
682 }
683
shouldAttachIndentationFixSpannull684 private fun shouldAttachIndentationFixSpan(textStyle: TextStyle, ellipsis: Boolean) =
685 with(textStyle) {
686 ellipsis &&
687 (letterSpacing != 0.sp && letterSpacing != TextUnit.Unspecified) &&
688 (textAlign != TextAlign.Unspecified &&
689 textAlign != TextAlign.Start &&
690 textAlign != TextAlign.Justify)
691 }
692
693 // this _will_ be called multiple times on the same ParagraphIntrinsics
attachIndentationFixSpannull694 private fun CharSequence.attachIndentationFixSpan(): CharSequence {
695 if (isEmpty()) return this
696 val spannable = this as? Spannable ?: SpannableString(this)
697 if (!spannable.hasSpan(IndentationFixSpan::class.java)) {
698 spannable.setSpan(IndentationFixSpan(), spannable.length - 1, spannable.length - 1)
699 }
700 return spannable
701 }
702
TextGranularitynull703 private fun TextGranularity.toLayoutTextGranularity(): Int {
704 return when (this) {
705 TextGranularity.Character -> TEXT_GRANULARITY_CHARACTER
706 TextGranularity.Word -> TEXT_GRANULARITY_WORD
707 else -> TEXT_GRANULARITY_CHARACTER
708 }
709 }
710