blob: 7e41878b3d4eb808adaa29d28d02cded094e6212 [file] [log] [blame]
Seigo Nonaka9d3bd082018-01-11 10:02:12 -08001/*
2 * Copyright (C) 2010 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
17package android.text;
18
19import android.annotation.FloatRange;
20import android.annotation.IntRange;
21import android.annotation.NonNull;
22import android.annotation.Nullable;
23import android.graphics.Paint;
Seigo Nonakaa5534772018-03-15 00:22:20 -070024import android.graphics.Rect;
Seigo Nonaka70200b02018-10-01 16:04:11 -070025import android.graphics.text.MeasuredText;
Seigo Nonaka9d3bd082018-01-11 10:02:12 -080026import android.text.AutoGrowArray.ByteArray;
27import android.text.AutoGrowArray.FloatArray;
28import android.text.AutoGrowArray.IntArray;
29import android.text.Layout.Directions;
30import android.text.style.MetricAffectingSpan;
31import android.text.style.ReplacementSpan;
32import android.util.Pools.SynchronizedPool;
33
Seigo Nonaka9d3bd082018-01-11 10:02:12 -080034import java.util.Arrays;
35
36/**
37 * MeasuredParagraph provides text information for rendering purpose.
38 *
39 * The first motivation of this class is identify the text directions and retrieving individual
40 * character widths. However retrieving character widths is slower than identifying text directions.
41 * Thus, this class provides several builder methods for specific purposes.
42 *
43 * - buildForBidi:
44 * Compute only text directions.
45 * - buildForMeasurement:
46 * Compute text direction and all character widths.
47 * - buildForStaticLayout:
48 * This is bit special. StaticLayout also needs to know text direction and character widths for
49 * line breaking, but all things are done in native code. Similarly, text measurement is done
50 * in native code. So instead of storing result to Java array, this keeps the result in native
51 * code since there is no good reason to move the results to Java layer.
52 *
53 * In addition to the character widths, some additional information is computed for each purposes,
54 * e.g. whole text length for measurement or font metrics for static layout.
55 *
56 * MeasuredParagraph is NOT a thread safe object.
57 * @hide
58 */
59public class MeasuredParagraph {
60 private static final char OBJECT_REPLACEMENT_CHARACTER = '\uFFFC';
61
Seigo Nonaka9d3bd082018-01-11 10:02:12 -080062 private MeasuredParagraph() {} // Use build static functions instead.
63
64 private static final SynchronizedPool<MeasuredParagraph> sPool = new SynchronizedPool<>(1);
65
66 private static @NonNull MeasuredParagraph obtain() { // Use build static functions instead.
67 final MeasuredParagraph mt = sPool.acquire();
68 return mt != null ? mt : new MeasuredParagraph();
69 }
70
71 /**
72 * Recycle the MeasuredParagraph.
73 *
74 * Do not call any methods after you call this method.
75 */
76 public void recycle() {
77 release();
78 sPool.release(this);
79 }
80
81 // The casted original text.
82 //
83 // This may be null if the passed text is not a Spanned.
84 private @Nullable Spanned mSpanned;
85
86 // The start offset of the target range in the original text (mSpanned);
87 private @IntRange(from = 0) int mTextStart;
88
89 // The length of the target range in the original text.
90 private @IntRange(from = 0) int mTextLength;
91
92 // The copied character buffer for measuring text.
93 //
94 // The length of this array is mTextLength.
95 private @Nullable char[] mCopiedBuffer;
96
97 // The whole paragraph direction.
98 private @Layout.Direction int mParaDir;
99
100 // True if the text is LTR direction and doesn't contain any bidi characters.
101 private boolean mLtrWithoutBidi;
102
103 // The bidi level for individual characters.
104 //
105 // This is empty if mLtrWithoutBidi is true.
106 private @NonNull ByteArray mLevels = new ByteArray();
107
108 // The whole width of the text.
109 // See getWholeWidth comments.
110 private @FloatRange(from = 0.0f) float mWholeWidth;
111
112 // Individual characters' widths.
113 // See getWidths comments.
114 private @Nullable FloatArray mWidths = new FloatArray();
115
116 // The span end positions.
117 // See getSpanEndCache comments.
118 private @Nullable IntArray mSpanEndCache = new IntArray(4);
119
120 // The font metrics.
121 // See getFontMetrics comments.
122 private @Nullable IntArray mFontMetrics = new IntArray(4 * 4);
123
124 // The native MeasuredParagraph.
Seigo Nonaka70200b02018-10-01 16:04:11 -0700125 private @Nullable MeasuredText mMeasuredText;
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800126
127 // Following two objects are for avoiding object allocation.
128 private @NonNull TextPaint mCachedPaint = new TextPaint();
129 private @Nullable Paint.FontMetricsInt mCachedFm;
130
131 /**
132 * Releases internal buffers.
133 */
134 public void release() {
135 reset();
136 mLevels.clearWithReleasingLargeArray();
137 mWidths.clearWithReleasingLargeArray();
138 mFontMetrics.clearWithReleasingLargeArray();
139 mSpanEndCache.clearWithReleasingLargeArray();
140 }
141
142 /**
143 * Resets the internal state for starting new text.
144 */
145 private void reset() {
146 mSpanned = null;
147 mCopiedBuffer = null;
148 mWholeWidth = 0;
149 mLevels.clear();
150 mWidths.clear();
151 mFontMetrics.clear();
152 mSpanEndCache.clear();
Seigo Nonaka70200b02018-10-01 16:04:11 -0700153 mMeasuredText = null;
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800154 }
155
156 /**
Seigo Nonaka783f9612018-01-20 12:11:13 -0800157 * Returns the length of the paragraph.
158 *
159 * This is always available.
160 */
161 public int getTextLength() {
162 return mTextLength;
163 }
164
165 /**
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800166 * Returns the characters to be measured.
167 *
168 * This is always available.
169 */
170 public @NonNull char[] getChars() {
171 return mCopiedBuffer;
172 }
173
174 /**
175 * Returns the paragraph direction.
176 *
177 * This is always available.
178 */
179 public @Layout.Direction int getParagraphDir() {
180 return mParaDir;
181 }
182
183 /**
184 * Returns the directions.
185 *
186 * This is always available.
187 */
188 public Directions getDirections(@IntRange(from = 0) int start, // inclusive
189 @IntRange(from = 0) int end) { // exclusive
190 if (mLtrWithoutBidi) {
191 return Layout.DIRS_ALL_LEFT_TO_RIGHT;
192 }
193
194 final int length = end - start;
195 return AndroidBidi.directions(mParaDir, mLevels.getRawArray(), start, mCopiedBuffer, start,
196 length);
197 }
198
199 /**
200 * Returns the whole text width.
201 *
Seigo Nonaka783f9612018-01-20 12:11:13 -0800202 * This is available only if the MeasuredParagraph is computed with buildForMeasurement.
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800203 * Returns 0 in other cases.
204 */
205 public @FloatRange(from = 0.0f) float getWholeWidth() {
206 return mWholeWidth;
207 }
208
209 /**
210 * Returns the individual character's width.
211 *
Seigo Nonaka783f9612018-01-20 12:11:13 -0800212 * This is available only if the MeasuredParagraph is computed with buildForMeasurement.
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800213 * Returns empty array in other cases.
214 */
215 public @NonNull FloatArray getWidths() {
216 return mWidths;
217 }
218
219 /**
220 * Returns the MetricsAffectingSpan end indices.
221 *
222 * If the input text is not a spanned string, this has one value that is the length of the text.
223 *
Seigo Nonaka783f9612018-01-20 12:11:13 -0800224 * This is available only if the MeasuredParagraph is computed with buildForStaticLayout.
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800225 * Returns empty array in other cases.
226 */
227 public @NonNull IntArray getSpanEndCache() {
228 return mSpanEndCache;
229 }
230
231 /**
232 * Returns the int array which holds FontMetrics.
233 *
234 * This array holds the repeat of top, bottom, ascent, descent of font metrics value.
235 *
Seigo Nonaka783f9612018-01-20 12:11:13 -0800236 * This is available only if the MeasuredParagraph is computed with buildForStaticLayout.
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800237 * Returns empty array in other cases.
238 */
239 public @NonNull IntArray getFontMetrics() {
240 return mFontMetrics;
241 }
242
243 /**
244 * Returns the native ptr of the MeasuredParagraph.
245 *
Seigo Nonaka783f9612018-01-20 12:11:13 -0800246 * This is available only if the MeasuredParagraph is computed with buildForStaticLayout.
Seigo Nonaka6f11c6e2018-07-24 11:26:18 -0700247 * Returns null in other cases.
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800248 */
Seigo Nonaka70200b02018-10-01 16:04:11 -0700249 public MeasuredText getMeasuredText() {
250 return mMeasuredText;
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800251 }
252
253 /**
Seigo Nonaka783f9612018-01-20 12:11:13 -0800254 * Returns the width of the given range.
255 *
256 * This is not available if the MeasuredParagraph is computed with buildForBidi.
257 * Returns 0 if the MeasuredParagraph is computed with buildForBidi.
258 *
259 * @param start the inclusive start offset of the target region in the text
260 * @param end the exclusive end offset of the target region in the text
261 */
262 public float getWidth(int start, int end) {
Seigo Nonaka70200b02018-10-01 16:04:11 -0700263 if (mMeasuredText == null) {
Seigo Nonaka783f9612018-01-20 12:11:13 -0800264 // We have result in Java.
265 final float[] widths = mWidths.getRawArray();
266 float r = 0.0f;
267 for (int i = start; i < end; ++i) {
268 r += widths[i];
269 }
270 return r;
271 } else {
272 // We have result in native.
Seigo Nonaka70200b02018-10-01 16:04:11 -0700273 return mMeasuredText.getWidth(start, end);
Seigo Nonaka783f9612018-01-20 12:11:13 -0800274 }
275 }
276
277 /**
Seigo Nonakaa5534772018-03-15 00:22:20 -0700278 * Retrieves the bounding rectangle that encloses all of the characters, with an implied origin
279 * at (0, 0).
280 *
281 * This is available only if the MeasuredParagraph is computed with buildForStaticLayout.
282 */
Seigo Nonakafb0abe12018-04-02 23:25:38 -0700283 public void getBounds(@IntRange(from = 0) int start, @IntRange(from = 0) int end,
284 @NonNull Rect bounds) {
Seigo Nonaka70200b02018-10-01 16:04:11 -0700285 mMeasuredText.getBounds(start, end, bounds);
Seigo Nonaka6f11c6e2018-07-24 11:26:18 -0700286 }
287
288 /**
289 * Returns a width of the character at the offset.
290 *
291 * This is available only if the MeasuredParagraph is computed with buildForStaticLayout.
292 */
293 public float getCharWidthAt(@IntRange(from = 0) int offset) {
Seigo Nonaka70200b02018-10-01 16:04:11 -0700294 return mMeasuredText.getCharWidthAt(offset);
Seigo Nonakaa5534772018-03-15 00:22:20 -0700295 }
296
297 /**
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800298 * Generates new MeasuredParagraph for Bidi computation.
299 *
300 * If recycle is null, this returns new instance. If recycle is not null, this fills computed
301 * result to recycle and returns recycle.
302 *
303 * @param text the character sequence to be measured
304 * @param start the inclusive start offset of the target region in the text
305 * @param end the exclusive end offset of the target region in the text
306 * @param textDir the text direction
307 * @param recycle pass existing MeasuredParagraph if you want to recycle it.
308 *
309 * @return measured text
310 */
311 public static @NonNull MeasuredParagraph buildForBidi(@NonNull CharSequence text,
312 @IntRange(from = 0) int start,
313 @IntRange(from = 0) int end,
314 @NonNull TextDirectionHeuristic textDir,
315 @Nullable MeasuredParagraph recycle) {
316 final MeasuredParagraph mt = recycle == null ? obtain() : recycle;
317 mt.resetAndAnalyzeBidi(text, start, end, textDir);
318 return mt;
319 }
320
321 /**
322 * Generates new MeasuredParagraph for measuring texts.
323 *
324 * If recycle is null, this returns new instance. If recycle is not null, this fills computed
325 * result to recycle and returns recycle.
326 *
327 * @param paint the paint to be used for rendering the text.
328 * @param text the character sequence to be measured
329 * @param start the inclusive start offset of the target region in the text
330 * @param end the exclusive end offset of the target region in the text
331 * @param textDir the text direction
332 * @param recycle pass existing MeasuredParagraph if you want to recycle it.
333 *
334 * @return measured text
335 */
336 public static @NonNull MeasuredParagraph buildForMeasurement(@NonNull TextPaint paint,
337 @NonNull CharSequence text,
338 @IntRange(from = 0) int start,
339 @IntRange(from = 0) int end,
340 @NonNull TextDirectionHeuristic textDir,
341 @Nullable MeasuredParagraph recycle) {
342 final MeasuredParagraph mt = recycle == null ? obtain() : recycle;
343 mt.resetAndAnalyzeBidi(text, start, end, textDir);
344
345 mt.mWidths.resize(mt.mTextLength);
346 if (mt.mTextLength == 0) {
347 return mt;
348 }
349
350 if (mt.mSpanned == null) {
351 // No style change by MetricsAffectingSpan. Just measure all text.
352 mt.applyMetricsAffectingSpan(
Seigo Nonaka6f11c6e2018-07-24 11:26:18 -0700353 paint, null /* spans */, start, end, null /* native builder ptr */);
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800354 } else {
355 // There may be a MetricsAffectingSpan. Split into span transitions and apply styles.
356 int spanEnd;
357 for (int spanStart = start; spanStart < end; spanStart = spanEnd) {
358 spanEnd = mt.mSpanned.nextSpanTransition(spanStart, end, MetricAffectingSpan.class);
359 MetricAffectingSpan[] spans = mt.mSpanned.getSpans(spanStart, spanEnd,
360 MetricAffectingSpan.class);
361 spans = TextUtils.removeEmptySpans(spans, mt.mSpanned, MetricAffectingSpan.class);
362 mt.applyMetricsAffectingSpan(
Seigo Nonaka6f11c6e2018-07-24 11:26:18 -0700363 paint, spans, spanStart, spanEnd, null /* native builder ptr */);
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800364 }
365 }
366 return mt;
367 }
368
369 /**
370 * Generates new MeasuredParagraph for StaticLayout.
371 *
372 * If recycle is null, this returns new instance. If recycle is not null, this fills computed
373 * result to recycle and returns recycle.
374 *
375 * @param paint the paint to be used for rendering the text.
376 * @param text the character sequence to be measured
377 * @param start the inclusive start offset of the target region in the text
378 * @param end the exclusive end offset of the target region in the text
379 * @param textDir the text direction
Seigo Nonaka291ef052018-11-16 10:41:32 -0800380 * @param computeHyphenation true if need to compute hyphenation, otherwise false
381 * @param computeLayout true if need to compute full layout, otherwise false.
382 * @param hint pass if you already have measured paragraph.
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800383 * @param recycle pass existing MeasuredParagraph if you want to recycle it.
384 *
385 * @return measured text
386 */
387 public static @NonNull MeasuredParagraph buildForStaticLayout(
388 @NonNull TextPaint paint,
389 @NonNull CharSequence text,
390 @IntRange(from = 0) int start,
391 @IntRange(from = 0) int end,
392 @NonNull TextDirectionHeuristic textDir,
Seigo Nonaka87b15472018-01-12 14:06:29 -0800393 boolean computeHyphenation,
Seigo Nonaka783f9612018-01-20 12:11:13 -0800394 boolean computeLayout,
Seigo Nonaka291ef052018-11-16 10:41:32 -0800395 @Nullable MeasuredParagraph hint,
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800396 @Nullable MeasuredParagraph recycle) {
397 final MeasuredParagraph mt = recycle == null ? obtain() : recycle;
398 mt.resetAndAnalyzeBidi(text, start, end, textDir);
Seigo Nonaka291ef052018-11-16 10:41:32 -0800399 final MeasuredText.Builder builder;
400 if (hint == null) {
401 builder = new MeasuredText.Builder(mt.mCopiedBuffer)
402 .setComputeHyphenation(computeHyphenation)
403 .setComputeLayout(computeLayout);
404 } else {
405 builder = new MeasuredText.Builder(hint.mMeasuredText);
406 }
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800407 if (mt.mTextLength == 0) {
408 // Need to build empty native measured text for StaticLayout.
409 // TODO: Stop creating empty measured text for empty lines.
Seigo Nonaka70200b02018-10-01 16:04:11 -0700410 mt.mMeasuredText = builder.build();
Seigo Nonaka6f11c6e2018-07-24 11:26:18 -0700411 } else {
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800412 if (mt.mSpanned == null) {
413 // No style change by MetricsAffectingSpan. Just measure all text.
Seigo Nonaka6f11c6e2018-07-24 11:26:18 -0700414 mt.applyMetricsAffectingSpan(paint, null /* spans */, start, end, builder);
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800415 mt.mSpanEndCache.append(end);
416 } else {
417 // There may be a MetricsAffectingSpan. Split into span transitions and apply
418 // styles.
419 int spanEnd;
420 for (int spanStart = start; spanStart < end; spanStart = spanEnd) {
421 spanEnd = mt.mSpanned.nextSpanTransition(spanStart, end,
422 MetricAffectingSpan.class);
423 MetricAffectingSpan[] spans = mt.mSpanned.getSpans(spanStart, spanEnd,
424 MetricAffectingSpan.class);
425 spans = TextUtils.removeEmptySpans(spans, mt.mSpanned,
426 MetricAffectingSpan.class);
Seigo Nonaka6f11c6e2018-07-24 11:26:18 -0700427 mt.applyMetricsAffectingSpan(paint, spans, spanStart, spanEnd, builder);
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800428 mt.mSpanEndCache.append(spanEnd);
429 }
430 }
Seigo Nonaka70200b02018-10-01 16:04:11 -0700431 mt.mMeasuredText = builder.build();
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800432 }
433
434 return mt;
435 }
436
437 /**
438 * Reset internal state and analyzes text for bidirectional runs.
439 *
440 * @param text the character sequence to be measured
441 * @param start the inclusive start offset of the target region in the text
442 * @param end the exclusive end offset of the target region in the text
443 * @param textDir the text direction
444 */
445 private void resetAndAnalyzeBidi(@NonNull CharSequence text,
446 @IntRange(from = 0) int start, // inclusive
447 @IntRange(from = 0) int end, // exclusive
448 @NonNull TextDirectionHeuristic textDir) {
449 reset();
450 mSpanned = text instanceof Spanned ? (Spanned) text : null;
451 mTextStart = start;
452 mTextLength = end - start;
453
454 if (mCopiedBuffer == null || mCopiedBuffer.length != mTextLength) {
455 mCopiedBuffer = new char[mTextLength];
456 }
457 TextUtils.getChars(text, start, end, mCopiedBuffer, 0);
458
459 // Replace characters associated with ReplacementSpan to U+FFFC.
460 if (mSpanned != null) {
461 ReplacementSpan[] spans = mSpanned.getSpans(start, end, ReplacementSpan.class);
462
463 for (int i = 0; i < spans.length; i++) {
464 int startInPara = mSpanned.getSpanStart(spans[i]) - start;
465 int endInPara = mSpanned.getSpanEnd(spans[i]) - start;
466 // The span interval may be larger and must be restricted to [start, end)
467 if (startInPara < 0) startInPara = 0;
468 if (endInPara > mTextLength) endInPara = mTextLength;
469 Arrays.fill(mCopiedBuffer, startInPara, endInPara, OBJECT_REPLACEMENT_CHARACTER);
470 }
471 }
472
473 if ((textDir == TextDirectionHeuristics.LTR
474 || textDir == TextDirectionHeuristics.FIRSTSTRONG_LTR
475 || textDir == TextDirectionHeuristics.ANYRTL_LTR)
476 && TextUtils.doesNotNeedBidi(mCopiedBuffer, 0, mTextLength)) {
477 mLevels.clear();
478 mParaDir = Layout.DIR_LEFT_TO_RIGHT;
479 mLtrWithoutBidi = true;
480 } else {
481 final int bidiRequest;
482 if (textDir == TextDirectionHeuristics.LTR) {
483 bidiRequest = Layout.DIR_REQUEST_LTR;
484 } else if (textDir == TextDirectionHeuristics.RTL) {
485 bidiRequest = Layout.DIR_REQUEST_RTL;
486 } else if (textDir == TextDirectionHeuristics.FIRSTSTRONG_LTR) {
487 bidiRequest = Layout.DIR_REQUEST_DEFAULT_LTR;
488 } else if (textDir == TextDirectionHeuristics.FIRSTSTRONG_RTL) {
489 bidiRequest = Layout.DIR_REQUEST_DEFAULT_RTL;
490 } else {
491 final boolean isRtl = textDir.isRtl(mCopiedBuffer, 0, mTextLength);
492 bidiRequest = isRtl ? Layout.DIR_REQUEST_RTL : Layout.DIR_REQUEST_LTR;
493 }
494 mLevels.resize(mTextLength);
495 mParaDir = AndroidBidi.bidi(bidiRequest, mCopiedBuffer, mLevels.getRawArray());
496 mLtrWithoutBidi = false;
497 }
498 }
499
500 private void applyReplacementRun(@NonNull ReplacementSpan replacement,
501 @IntRange(from = 0) int start, // inclusive, in copied buffer
502 @IntRange(from = 0) int end, // exclusive, in copied buffer
Seigo Nonaka70200b02018-10-01 16:04:11 -0700503 @Nullable MeasuredText.Builder builder) {
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800504 // Use original text. Shouldn't matter.
505 // TODO: passing uninitizlied FontMetrics to developers. Do we need to keep this for
506 // backward compatibility? or Should we initialize them for getFontMetricsInt?
507 final float width = replacement.getSize(
508 mCachedPaint, mSpanned, start + mTextStart, end + mTextStart, mCachedFm);
Seigo Nonaka6f11c6e2018-07-24 11:26:18 -0700509 if (builder == null) {
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800510 // Assigns all width to the first character. This is the same behavior as minikin.
511 mWidths.set(start, width);
512 if (end > start + 1) {
513 Arrays.fill(mWidths.getRawArray(), start + 1, end, 0.0f);
514 }
515 mWholeWidth += width;
516 } else {
Seigo Nonaka7b86fe52018-10-16 18:02:32 -0700517 builder.appendReplacementRun(mCachedPaint, end - start, width);
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800518 }
519 }
520
521 private void applyStyleRun(@IntRange(from = 0) int start, // inclusive, in copied buffer
522 @IntRange(from = 0) int end, // exclusive, in copied buffer
Seigo Nonaka70200b02018-10-01 16:04:11 -0700523 @Nullable MeasuredText.Builder builder) {
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800524
525 if (mLtrWithoutBidi) {
526 // If the whole text is LTR direction, just apply whole region.
Seigo Nonaka6f11c6e2018-07-24 11:26:18 -0700527 if (builder == null) {
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800528 mWholeWidth += mCachedPaint.getTextRunAdvances(
529 mCopiedBuffer, start, end - start, start, end - start, false /* isRtl */,
530 mWidths.getRawArray(), start);
531 } else {
Seigo Nonaka7b86fe52018-10-16 18:02:32 -0700532 builder.appendStyleRun(mCachedPaint, end - start, false /* isRtl */);
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800533 }
534 } else {
535 // If there is multiple bidi levels, split into individual bidi level and apply style.
536 byte level = mLevels.get(start);
537 // Note that the empty text or empty range won't reach this method.
538 // Safe to search from start + 1.
539 for (int levelStart = start, levelEnd = start + 1;; ++levelEnd) {
540 if (levelEnd == end || mLevels.get(levelEnd) != level) { // transition point
541 final boolean isRtl = (level & 0x1) != 0;
Seigo Nonaka6f11c6e2018-07-24 11:26:18 -0700542 if (builder == null) {
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800543 final int levelLength = levelEnd - levelStart;
544 mWholeWidth += mCachedPaint.getTextRunAdvances(
545 mCopiedBuffer, levelStart, levelLength, levelStart, levelLength,
546 isRtl, mWidths.getRawArray(), levelStart);
547 } else {
Seigo Nonaka7b86fe52018-10-16 18:02:32 -0700548 builder.appendStyleRun(mCachedPaint, levelEnd - levelStart, isRtl);
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800549 }
550 if (levelEnd == end) {
551 break;
552 }
553 levelStart = levelEnd;
554 level = mLevels.get(levelEnd);
555 }
556 }
557 }
558 }
559
560 private void applyMetricsAffectingSpan(
561 @NonNull TextPaint paint,
562 @Nullable MetricAffectingSpan[] spans,
563 @IntRange(from = 0) int start, // inclusive, in original text buffer
564 @IntRange(from = 0) int end, // exclusive, in original text buffer
Seigo Nonaka70200b02018-10-01 16:04:11 -0700565 @Nullable MeasuredText.Builder builder) {
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800566 mCachedPaint.set(paint);
567 // XXX paint should not have a baseline shift, but...
568 mCachedPaint.baselineShift = 0;
569
Seigo Nonaka6f11c6e2018-07-24 11:26:18 -0700570 final boolean needFontMetrics = builder != null;
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800571
572 if (needFontMetrics && mCachedFm == null) {
573 mCachedFm = new Paint.FontMetricsInt();
574 }
575
576 ReplacementSpan replacement = null;
577 if (spans != null) {
578 for (int i = 0; i < spans.length; i++) {
579 MetricAffectingSpan span = spans[i];
580 if (span instanceof ReplacementSpan) {
581 // The last ReplacementSpan is effective for backward compatibility reasons.
582 replacement = (ReplacementSpan) span;
583 } else {
584 // TODO: No need to call updateMeasureState for ReplacementSpan as well?
585 span.updateMeasureState(mCachedPaint);
586 }
587 }
588 }
589
590 final int startInCopiedBuffer = start - mTextStart;
591 final int endInCopiedBuffer = end - mTextStart;
592
Seigo Nonaka6f11c6e2018-07-24 11:26:18 -0700593 if (builder != null) {
Seigo Nonaka09036652018-03-30 10:46:52 -0700594 mCachedPaint.getFontMetricsInt(mCachedFm);
595 }
596
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800597 if (replacement != null) {
Seigo Nonaka6f11c6e2018-07-24 11:26:18 -0700598 applyReplacementRun(replacement, startInCopiedBuffer, endInCopiedBuffer, builder);
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800599 } else {
Seigo Nonaka6f11c6e2018-07-24 11:26:18 -0700600 applyStyleRun(startInCopiedBuffer, endInCopiedBuffer, builder);
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800601 }
602
603 if (needFontMetrics) {
604 if (mCachedPaint.baselineShift < 0) {
605 mCachedFm.ascent += mCachedPaint.baselineShift;
606 mCachedFm.top += mCachedPaint.baselineShift;
607 } else {
608 mCachedFm.descent += mCachedPaint.baselineShift;
609 mCachedFm.bottom += mCachedPaint.baselineShift;
610 }
611
612 mFontMetrics.append(mCachedFm.top);
613 mFontMetrics.append(mCachedFm.bottom);
614 mFontMetrics.append(mCachedFm.ascent);
615 mFontMetrics.append(mCachedFm.descent);
616 }
617 }
618
619 /**
620 * Returns the maximum index that the accumulated width not exceeds the width.
621 *
622 * If forward=false is passed, returns the minimum index from the end instead.
623 *
Seigo Nonaka783f9612018-01-20 12:11:13 -0800624 * This only works if the MeasuredParagraph is computed with buildForMeasurement.
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800625 * Undefined behavior in other case.
626 */
627 @IntRange(from = 0) int breakText(int limit, boolean forwards, float width) {
628 float[] w = mWidths.getRawArray();
629 if (forwards) {
630 int i = 0;
631 while (i < limit) {
632 width -= w[i];
633 if (width < 0.0f) break;
634 i++;
635 }
636 while (i > 0 && mCopiedBuffer[i - 1] == ' ') i--;
637 return i;
638 } else {
639 int i = limit - 1;
640 while (i >= 0) {
641 width -= w[i];
642 if (width < 0.0f) break;
643 i--;
644 }
645 while (i < limit - 1 && (mCopiedBuffer[i + 1] == ' ' || w[i + 1] == 0.0f)) {
646 i++;
647 }
648 return limit - i - 1;
649 }
650 }
651
652 /**
653 * Returns the length of the substring.
654 *
Seigo Nonaka783f9612018-01-20 12:11:13 -0800655 * This only works if the MeasuredParagraph is computed with buildForMeasurement.
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800656 * Undefined behavior in other case.
657 */
658 @FloatRange(from = 0.0f) float measure(int start, int limit) {
659 float width = 0;
660 float[] w = mWidths.getRawArray();
661 for (int i = start; i < limit; ++i) {
662 width += w[i];
663 }
664 return width;
665 }
666
Seigo Nonaka49ca0242018-01-24 16:46:14 -0800667 /**
668 * This only works if the MeasuredParagraph is computed with buildForStaticLayout.
669 */
Seigo Nonakac3328d62018-03-20 15:18:59 -0700670 public @IntRange(from = 0) int getMemoryUsage() {
Seigo Nonaka70200b02018-10-01 16:04:11 -0700671 return mMeasuredText.getMemoryUsage();
Seigo Nonaka49ca0242018-01-24 16:46:14 -0800672 }
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800673}