blob: 9bf8cd20f44126d87620a423ad0cd570274ce138 [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 Nonaka9d3bd082018-01-11 10:02:12 -080025import android.text.AutoGrowArray.ByteArray;
26import android.text.AutoGrowArray.FloatArray;
27import android.text.AutoGrowArray.IntArray;
28import android.text.Layout.Directions;
29import android.text.style.MetricAffectingSpan;
30import android.text.style.ReplacementSpan;
31import android.util.Pools.SynchronizedPool;
32
Seigo Nonaka9d3bd082018-01-11 10:02:12 -080033import java.util.Arrays;
34
35/**
36 * MeasuredParagraph provides text information for rendering purpose.
37 *
38 * The first motivation of this class is identify the text directions and retrieving individual
39 * character widths. However retrieving character widths is slower than identifying text directions.
40 * Thus, this class provides several builder methods for specific purposes.
41 *
42 * - buildForBidi:
43 * Compute only text directions.
44 * - buildForMeasurement:
45 * Compute text direction and all character widths.
46 * - buildForStaticLayout:
47 * This is bit special. StaticLayout also needs to know text direction and character widths for
48 * line breaking, but all things are done in native code. Similarly, text measurement is done
49 * in native code. So instead of storing result to Java array, this keeps the result in native
50 * code since there is no good reason to move the results to Java layer.
51 *
52 * In addition to the character widths, some additional information is computed for each purposes,
53 * e.g. whole text length for measurement or font metrics for static layout.
54 *
55 * MeasuredParagraph is NOT a thread safe object.
56 * @hide
57 */
58public class MeasuredParagraph {
59 private static final char OBJECT_REPLACEMENT_CHARACTER = '\uFFFC';
60
Seigo Nonaka9d3bd082018-01-11 10:02:12 -080061 private MeasuredParagraph() {} // Use build static functions instead.
62
63 private static final SynchronizedPool<MeasuredParagraph> sPool = new SynchronizedPool<>(1);
64
65 private static @NonNull MeasuredParagraph obtain() { // Use build static functions instead.
66 final MeasuredParagraph mt = sPool.acquire();
67 return mt != null ? mt : new MeasuredParagraph();
68 }
69
70 /**
71 * Recycle the MeasuredParagraph.
72 *
73 * Do not call any methods after you call this method.
74 */
75 public void recycle() {
76 release();
77 sPool.release(this);
78 }
79
80 // The casted original text.
81 //
82 // This may be null if the passed text is not a Spanned.
83 private @Nullable Spanned mSpanned;
84
85 // The start offset of the target range in the original text (mSpanned);
86 private @IntRange(from = 0) int mTextStart;
87
88 // The length of the target range in the original text.
89 private @IntRange(from = 0) int mTextLength;
90
91 // The copied character buffer for measuring text.
92 //
93 // The length of this array is mTextLength.
94 private @Nullable char[] mCopiedBuffer;
95
96 // The whole paragraph direction.
97 private @Layout.Direction int mParaDir;
98
99 // True if the text is LTR direction and doesn't contain any bidi characters.
100 private boolean mLtrWithoutBidi;
101
102 // The bidi level for individual characters.
103 //
104 // This is empty if mLtrWithoutBidi is true.
105 private @NonNull ByteArray mLevels = new ByteArray();
106
107 // The whole width of the text.
108 // See getWholeWidth comments.
109 private @FloatRange(from = 0.0f) float mWholeWidth;
110
111 // Individual characters' widths.
112 // See getWidths comments.
113 private @Nullable FloatArray mWidths = new FloatArray();
114
115 // The span end positions.
116 // See getSpanEndCache comments.
117 private @Nullable IntArray mSpanEndCache = new IntArray(4);
118
119 // The font metrics.
120 // See getFontMetrics comments.
121 private @Nullable IntArray mFontMetrics = new IntArray(4 * 4);
122
123 // The native MeasuredParagraph.
Seigo Nonaka6f11c6e2018-07-24 11:26:18 -0700124 private @Nullable NativeMeasuredParagraph mNativeMeasuredParagraph;
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800125
126 // Following two objects are for avoiding object allocation.
127 private @NonNull TextPaint mCachedPaint = new TextPaint();
128 private @Nullable Paint.FontMetricsInt mCachedFm;
129
130 /**
131 * Releases internal buffers.
132 */
133 public void release() {
134 reset();
135 mLevels.clearWithReleasingLargeArray();
136 mWidths.clearWithReleasingLargeArray();
137 mFontMetrics.clearWithReleasingLargeArray();
138 mSpanEndCache.clearWithReleasingLargeArray();
139 }
140
141 /**
142 * Resets the internal state for starting new text.
143 */
144 private void reset() {
145 mSpanned = null;
146 mCopiedBuffer = null;
147 mWholeWidth = 0;
148 mLevels.clear();
149 mWidths.clear();
150 mFontMetrics.clear();
151 mSpanEndCache.clear();
Seigo Nonaka6f11c6e2018-07-24 11:26:18 -0700152 mNativeMeasuredParagraph = null;
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800153 }
154
155 /**
Seigo Nonaka783f9612018-01-20 12:11:13 -0800156 * Returns the length of the paragraph.
157 *
158 * This is always available.
159 */
160 public int getTextLength() {
161 return mTextLength;
162 }
163
164 /**
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800165 * Returns the characters to be measured.
166 *
167 * This is always available.
168 */
169 public @NonNull char[] getChars() {
170 return mCopiedBuffer;
171 }
172
173 /**
174 * Returns the paragraph direction.
175 *
176 * This is always available.
177 */
178 public @Layout.Direction int getParagraphDir() {
179 return mParaDir;
180 }
181
182 /**
183 * Returns the directions.
184 *
185 * This is always available.
186 */
187 public Directions getDirections(@IntRange(from = 0) int start, // inclusive
188 @IntRange(from = 0) int end) { // exclusive
189 if (mLtrWithoutBidi) {
190 return Layout.DIRS_ALL_LEFT_TO_RIGHT;
191 }
192
193 final int length = end - start;
194 return AndroidBidi.directions(mParaDir, mLevels.getRawArray(), start, mCopiedBuffer, start,
195 length);
196 }
197
198 /**
199 * Returns the whole text width.
200 *
Seigo Nonaka783f9612018-01-20 12:11:13 -0800201 * This is available only if the MeasuredParagraph is computed with buildForMeasurement.
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800202 * Returns 0 in other cases.
203 */
204 public @FloatRange(from = 0.0f) float getWholeWidth() {
205 return mWholeWidth;
206 }
207
208 /**
209 * Returns the individual character's width.
210 *
Seigo Nonaka783f9612018-01-20 12:11:13 -0800211 * This is available only if the MeasuredParagraph is computed with buildForMeasurement.
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800212 * Returns empty array in other cases.
213 */
214 public @NonNull FloatArray getWidths() {
215 return mWidths;
216 }
217
218 /**
219 * Returns the MetricsAffectingSpan end indices.
220 *
221 * If the input text is not a spanned string, this has one value that is the length of the text.
222 *
Seigo Nonaka783f9612018-01-20 12:11:13 -0800223 * This is available only if the MeasuredParagraph is computed with buildForStaticLayout.
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800224 * Returns empty array in other cases.
225 */
226 public @NonNull IntArray getSpanEndCache() {
227 return mSpanEndCache;
228 }
229
230 /**
231 * Returns the int array which holds FontMetrics.
232 *
233 * This array holds the repeat of top, bottom, ascent, descent of font metrics value.
234 *
Seigo Nonaka783f9612018-01-20 12:11:13 -0800235 * This is available only if the MeasuredParagraph is computed with buildForStaticLayout.
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800236 * Returns empty array in other cases.
237 */
238 public @NonNull IntArray getFontMetrics() {
239 return mFontMetrics;
240 }
241
242 /**
243 * Returns the native ptr of the MeasuredParagraph.
244 *
Seigo Nonaka783f9612018-01-20 12:11:13 -0800245 * This is available only if the MeasuredParagraph is computed with buildForStaticLayout.
Seigo Nonaka6f11c6e2018-07-24 11:26:18 -0700246 * Returns null in other cases.
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800247 */
Seigo Nonaka6f11c6e2018-07-24 11:26:18 -0700248 public NativeMeasuredParagraph getNativeMeasuredParagraph() {
249 return mNativeMeasuredParagraph;
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800250 }
251
252 /**
Seigo Nonaka783f9612018-01-20 12:11:13 -0800253 * Returns the width of the given range.
254 *
255 * This is not available if the MeasuredParagraph is computed with buildForBidi.
256 * Returns 0 if the MeasuredParagraph is computed with buildForBidi.
257 *
258 * @param start the inclusive start offset of the target region in the text
259 * @param end the exclusive end offset of the target region in the text
260 */
261 public float getWidth(int start, int end) {
Seigo Nonaka6f11c6e2018-07-24 11:26:18 -0700262 if (mNativeMeasuredParagraph == null) {
Seigo Nonaka783f9612018-01-20 12:11:13 -0800263 // We have result in Java.
264 final float[] widths = mWidths.getRawArray();
265 float r = 0.0f;
266 for (int i = start; i < end; ++i) {
267 r += widths[i];
268 }
269 return r;
270 } else {
271 // We have result in native.
Seigo Nonaka6f11c6e2018-07-24 11:26:18 -0700272 return mNativeMeasuredParagraph.getWidth(start, end);
Seigo Nonaka783f9612018-01-20 12:11:13 -0800273 }
274 }
275
276 /**
Seigo Nonakaa5534772018-03-15 00:22:20 -0700277 * Retrieves the bounding rectangle that encloses all of the characters, with an implied origin
278 * at (0, 0).
279 *
280 * This is available only if the MeasuredParagraph is computed with buildForStaticLayout.
281 */
Seigo Nonakafb0abe12018-04-02 23:25:38 -0700282 public void getBounds(@IntRange(from = 0) int start, @IntRange(from = 0) int end,
283 @NonNull Rect bounds) {
Seigo Nonaka6f11c6e2018-07-24 11:26:18 -0700284 mNativeMeasuredParagraph.getBounds(mCopiedBuffer, start, end, bounds);
285 }
286
287 /**
288 * Returns a width of the character at the offset.
289 *
290 * This is available only if the MeasuredParagraph is computed with buildForStaticLayout.
291 */
292 public float getCharWidthAt(@IntRange(from = 0) int offset) {
293 return mNativeMeasuredParagraph.getCharWidthAt(offset);
Seigo Nonakaa5534772018-03-15 00:22:20 -0700294 }
295
296 /**
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800297 * Generates new MeasuredParagraph for Bidi computation.
298 *
299 * If recycle is null, this returns new instance. If recycle is not null, this fills computed
300 * result to recycle and returns recycle.
301 *
302 * @param text the character sequence to be measured
303 * @param start the inclusive start offset of the target region in the text
304 * @param end the exclusive end offset of the target region in the text
305 * @param textDir the text direction
306 * @param recycle pass existing MeasuredParagraph if you want to recycle it.
307 *
308 * @return measured text
309 */
310 public static @NonNull MeasuredParagraph buildForBidi(@NonNull CharSequence text,
311 @IntRange(from = 0) int start,
312 @IntRange(from = 0) int end,
313 @NonNull TextDirectionHeuristic textDir,
314 @Nullable MeasuredParagraph recycle) {
315 final MeasuredParagraph mt = recycle == null ? obtain() : recycle;
316 mt.resetAndAnalyzeBidi(text, start, end, textDir);
317 return mt;
318 }
319
320 /**
321 * Generates new MeasuredParagraph for measuring texts.
322 *
323 * If recycle is null, this returns new instance. If recycle is not null, this fills computed
324 * result to recycle and returns recycle.
325 *
326 * @param paint the paint to be used for rendering the text.
327 * @param text the character sequence to be measured
328 * @param start the inclusive start offset of the target region in the text
329 * @param end the exclusive end offset of the target region in the text
330 * @param textDir the text direction
331 * @param recycle pass existing MeasuredParagraph if you want to recycle it.
332 *
333 * @return measured text
334 */
335 public static @NonNull MeasuredParagraph buildForMeasurement(@NonNull TextPaint paint,
336 @NonNull CharSequence text,
337 @IntRange(from = 0) int start,
338 @IntRange(from = 0) int end,
339 @NonNull TextDirectionHeuristic textDir,
340 @Nullable MeasuredParagraph recycle) {
341 final MeasuredParagraph mt = recycle == null ? obtain() : recycle;
342 mt.resetAndAnalyzeBidi(text, start, end, textDir);
343
344 mt.mWidths.resize(mt.mTextLength);
345 if (mt.mTextLength == 0) {
346 return mt;
347 }
348
349 if (mt.mSpanned == null) {
350 // No style change by MetricsAffectingSpan. Just measure all text.
351 mt.applyMetricsAffectingSpan(
Seigo Nonaka6f11c6e2018-07-24 11:26:18 -0700352 paint, null /* spans */, start, end, null /* native builder ptr */);
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800353 } else {
354 // There may be a MetricsAffectingSpan. Split into span transitions and apply styles.
355 int spanEnd;
356 for (int spanStart = start; spanStart < end; spanStart = spanEnd) {
357 spanEnd = mt.mSpanned.nextSpanTransition(spanStart, end, MetricAffectingSpan.class);
358 MetricAffectingSpan[] spans = mt.mSpanned.getSpans(spanStart, spanEnd,
359 MetricAffectingSpan.class);
360 spans = TextUtils.removeEmptySpans(spans, mt.mSpanned, MetricAffectingSpan.class);
361 mt.applyMetricsAffectingSpan(
Seigo Nonaka6f11c6e2018-07-24 11:26:18 -0700362 paint, spans, spanStart, spanEnd, null /* native builder ptr */);
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800363 }
364 }
365 return mt;
366 }
367
368 /**
369 * Generates new MeasuredParagraph for StaticLayout.
370 *
371 * If recycle is null, this returns new instance. If recycle is not null, this fills computed
372 * result to recycle and returns recycle.
373 *
374 * @param paint the paint to be used for rendering the text.
375 * @param text the character sequence to be measured
376 * @param start the inclusive start offset of the target region in the text
377 * @param end the exclusive end offset of the target region in the text
378 * @param textDir the text direction
379 * @param recycle pass existing MeasuredParagraph if you want to recycle it.
380 *
381 * @return measured text
382 */
383 public static @NonNull MeasuredParagraph buildForStaticLayout(
384 @NonNull TextPaint paint,
385 @NonNull CharSequence text,
386 @IntRange(from = 0) int start,
387 @IntRange(from = 0) int end,
388 @NonNull TextDirectionHeuristic textDir,
Seigo Nonaka87b15472018-01-12 14:06:29 -0800389 boolean computeHyphenation,
Seigo Nonaka783f9612018-01-20 12:11:13 -0800390 boolean computeLayout,
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800391 @Nullable MeasuredParagraph recycle) {
392 final MeasuredParagraph mt = recycle == null ? obtain() : recycle;
393 mt.resetAndAnalyzeBidi(text, start, end, textDir);
Seigo Nonaka6f11c6e2018-07-24 11:26:18 -0700394 final NativeMeasuredParagraph.Builder builder = new NativeMeasuredParagraph.Builder();
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800395 if (mt.mTextLength == 0) {
396 // Need to build empty native measured text for StaticLayout.
397 // TODO: Stop creating empty measured text for empty lines.
Seigo Nonaka6f11c6e2018-07-24 11:26:18 -0700398 mt.mNativeMeasuredParagraph = builder.build(mt.mCopiedBuffer, computeHyphenation,
399 computeLayout);
400 } else {
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800401 if (mt.mSpanned == null) {
402 // No style change by MetricsAffectingSpan. Just measure all text.
Seigo Nonaka6f11c6e2018-07-24 11:26:18 -0700403 mt.applyMetricsAffectingSpan(paint, null /* spans */, start, end, builder);
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800404 mt.mSpanEndCache.append(end);
405 } else {
406 // There may be a MetricsAffectingSpan. Split into span transitions and apply
407 // styles.
408 int spanEnd;
409 for (int spanStart = start; spanStart < end; spanStart = spanEnd) {
410 spanEnd = mt.mSpanned.nextSpanTransition(spanStart, end,
411 MetricAffectingSpan.class);
412 MetricAffectingSpan[] spans = mt.mSpanned.getSpans(spanStart, spanEnd,
413 MetricAffectingSpan.class);
414 spans = TextUtils.removeEmptySpans(spans, mt.mSpanned,
415 MetricAffectingSpan.class);
Seigo Nonaka6f11c6e2018-07-24 11:26:18 -0700416 mt.applyMetricsAffectingSpan(paint, spans, spanStart, spanEnd, builder);
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800417 mt.mSpanEndCache.append(spanEnd);
418 }
419 }
Seigo Nonaka6f11c6e2018-07-24 11:26:18 -0700420 mt.mNativeMeasuredParagraph = builder.build(mt.mCopiedBuffer, computeHyphenation,
421 computeLayout);
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800422 }
423
424 return mt;
425 }
426
427 /**
428 * Reset internal state and analyzes text for bidirectional runs.
429 *
430 * @param text the character sequence to be measured
431 * @param start the inclusive start offset of the target region in the text
432 * @param end the exclusive end offset of the target region in the text
433 * @param textDir the text direction
434 */
435 private void resetAndAnalyzeBidi(@NonNull CharSequence text,
436 @IntRange(from = 0) int start, // inclusive
437 @IntRange(from = 0) int end, // exclusive
438 @NonNull TextDirectionHeuristic textDir) {
439 reset();
440 mSpanned = text instanceof Spanned ? (Spanned) text : null;
441 mTextStart = start;
442 mTextLength = end - start;
443
444 if (mCopiedBuffer == null || mCopiedBuffer.length != mTextLength) {
445 mCopiedBuffer = new char[mTextLength];
446 }
447 TextUtils.getChars(text, start, end, mCopiedBuffer, 0);
448
449 // Replace characters associated with ReplacementSpan to U+FFFC.
450 if (mSpanned != null) {
451 ReplacementSpan[] spans = mSpanned.getSpans(start, end, ReplacementSpan.class);
452
453 for (int i = 0; i < spans.length; i++) {
454 int startInPara = mSpanned.getSpanStart(spans[i]) - start;
455 int endInPara = mSpanned.getSpanEnd(spans[i]) - start;
456 // The span interval may be larger and must be restricted to [start, end)
457 if (startInPara < 0) startInPara = 0;
458 if (endInPara > mTextLength) endInPara = mTextLength;
459 Arrays.fill(mCopiedBuffer, startInPara, endInPara, OBJECT_REPLACEMENT_CHARACTER);
460 }
461 }
462
463 if ((textDir == TextDirectionHeuristics.LTR
464 || textDir == TextDirectionHeuristics.FIRSTSTRONG_LTR
465 || textDir == TextDirectionHeuristics.ANYRTL_LTR)
466 && TextUtils.doesNotNeedBidi(mCopiedBuffer, 0, mTextLength)) {
467 mLevels.clear();
468 mParaDir = Layout.DIR_LEFT_TO_RIGHT;
469 mLtrWithoutBidi = true;
470 } else {
471 final int bidiRequest;
472 if (textDir == TextDirectionHeuristics.LTR) {
473 bidiRequest = Layout.DIR_REQUEST_LTR;
474 } else if (textDir == TextDirectionHeuristics.RTL) {
475 bidiRequest = Layout.DIR_REQUEST_RTL;
476 } else if (textDir == TextDirectionHeuristics.FIRSTSTRONG_LTR) {
477 bidiRequest = Layout.DIR_REQUEST_DEFAULT_LTR;
478 } else if (textDir == TextDirectionHeuristics.FIRSTSTRONG_RTL) {
479 bidiRequest = Layout.DIR_REQUEST_DEFAULT_RTL;
480 } else {
481 final boolean isRtl = textDir.isRtl(mCopiedBuffer, 0, mTextLength);
482 bidiRequest = isRtl ? Layout.DIR_REQUEST_RTL : Layout.DIR_REQUEST_LTR;
483 }
484 mLevels.resize(mTextLength);
485 mParaDir = AndroidBidi.bidi(bidiRequest, mCopiedBuffer, mLevels.getRawArray());
486 mLtrWithoutBidi = false;
487 }
488 }
489
490 private void applyReplacementRun(@NonNull ReplacementSpan replacement,
491 @IntRange(from = 0) int start, // inclusive, in copied buffer
492 @IntRange(from = 0) int end, // exclusive, in copied buffer
Seigo Nonaka6f11c6e2018-07-24 11:26:18 -0700493 @Nullable NativeMeasuredParagraph.Builder builder) {
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800494 // Use original text. Shouldn't matter.
495 // TODO: passing uninitizlied FontMetrics to developers. Do we need to keep this for
496 // backward compatibility? or Should we initialize them for getFontMetricsInt?
497 final float width = replacement.getSize(
498 mCachedPaint, mSpanned, start + mTextStart, end + mTextStart, mCachedFm);
Seigo Nonaka6f11c6e2018-07-24 11:26:18 -0700499 if (builder == null) {
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800500 // Assigns all width to the first character. This is the same behavior as minikin.
501 mWidths.set(start, width);
502 if (end > start + 1) {
503 Arrays.fill(mWidths.getRawArray(), start + 1, end, 0.0f);
504 }
505 mWholeWidth += width;
506 } else {
Seigo Nonaka6f11c6e2018-07-24 11:26:18 -0700507 builder.addReplacementRun(mCachedPaint, start, end, width);
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800508 }
509 }
510
511 private void applyStyleRun(@IntRange(from = 0) int start, // inclusive, in copied buffer
512 @IntRange(from = 0) int end, // exclusive, in copied buffer
Seigo Nonaka6f11c6e2018-07-24 11:26:18 -0700513 @Nullable NativeMeasuredParagraph.Builder builder) {
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800514
515 if (mLtrWithoutBidi) {
516 // If the whole text is LTR direction, just apply whole region.
Seigo Nonaka6f11c6e2018-07-24 11:26:18 -0700517 if (builder == null) {
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800518 mWholeWidth += mCachedPaint.getTextRunAdvances(
519 mCopiedBuffer, start, end - start, start, end - start, false /* isRtl */,
520 mWidths.getRawArray(), start);
521 } else {
Seigo Nonaka6f11c6e2018-07-24 11:26:18 -0700522 builder.addStyleRun(mCachedPaint, start, end, false /* isRtl */);
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800523 }
524 } else {
525 // If there is multiple bidi levels, split into individual bidi level and apply style.
526 byte level = mLevels.get(start);
527 // Note that the empty text or empty range won't reach this method.
528 // Safe to search from start + 1.
529 for (int levelStart = start, levelEnd = start + 1;; ++levelEnd) {
530 if (levelEnd == end || mLevels.get(levelEnd) != level) { // transition point
531 final boolean isRtl = (level & 0x1) != 0;
Seigo Nonaka6f11c6e2018-07-24 11:26:18 -0700532 if (builder == null) {
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800533 final int levelLength = levelEnd - levelStart;
534 mWholeWidth += mCachedPaint.getTextRunAdvances(
535 mCopiedBuffer, levelStart, levelLength, levelStart, levelLength,
536 isRtl, mWidths.getRawArray(), levelStart);
537 } else {
Seigo Nonaka6f11c6e2018-07-24 11:26:18 -0700538 builder.addStyleRun(mCachedPaint, levelStart, levelEnd, isRtl);
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800539 }
540 if (levelEnd == end) {
541 break;
542 }
543 levelStart = levelEnd;
544 level = mLevels.get(levelEnd);
545 }
546 }
547 }
548 }
549
550 private void applyMetricsAffectingSpan(
551 @NonNull TextPaint paint,
552 @Nullable MetricAffectingSpan[] spans,
553 @IntRange(from = 0) int start, // inclusive, in original text buffer
554 @IntRange(from = 0) int end, // exclusive, in original text buffer
Seigo Nonaka6f11c6e2018-07-24 11:26:18 -0700555 @Nullable NativeMeasuredParagraph.Builder builder) {
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800556 mCachedPaint.set(paint);
557 // XXX paint should not have a baseline shift, but...
558 mCachedPaint.baselineShift = 0;
559
Seigo Nonaka6f11c6e2018-07-24 11:26:18 -0700560 final boolean needFontMetrics = builder != null;
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800561
562 if (needFontMetrics && mCachedFm == null) {
563 mCachedFm = new Paint.FontMetricsInt();
564 }
565
566 ReplacementSpan replacement = null;
567 if (spans != null) {
568 for (int i = 0; i < spans.length; i++) {
569 MetricAffectingSpan span = spans[i];
570 if (span instanceof ReplacementSpan) {
571 // The last ReplacementSpan is effective for backward compatibility reasons.
572 replacement = (ReplacementSpan) span;
573 } else {
574 // TODO: No need to call updateMeasureState for ReplacementSpan as well?
575 span.updateMeasureState(mCachedPaint);
576 }
577 }
578 }
579
580 final int startInCopiedBuffer = start - mTextStart;
581 final int endInCopiedBuffer = end - mTextStart;
582
Seigo Nonaka6f11c6e2018-07-24 11:26:18 -0700583 if (builder != null) {
Seigo Nonaka09036652018-03-30 10:46:52 -0700584 mCachedPaint.getFontMetricsInt(mCachedFm);
585 }
586
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800587 if (replacement != null) {
Seigo Nonaka6f11c6e2018-07-24 11:26:18 -0700588 applyReplacementRun(replacement, startInCopiedBuffer, endInCopiedBuffer, builder);
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800589 } else {
Seigo Nonaka6f11c6e2018-07-24 11:26:18 -0700590 applyStyleRun(startInCopiedBuffer, endInCopiedBuffer, builder);
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800591 }
592
593 if (needFontMetrics) {
594 if (mCachedPaint.baselineShift < 0) {
595 mCachedFm.ascent += mCachedPaint.baselineShift;
596 mCachedFm.top += mCachedPaint.baselineShift;
597 } else {
598 mCachedFm.descent += mCachedPaint.baselineShift;
599 mCachedFm.bottom += mCachedPaint.baselineShift;
600 }
601
602 mFontMetrics.append(mCachedFm.top);
603 mFontMetrics.append(mCachedFm.bottom);
604 mFontMetrics.append(mCachedFm.ascent);
605 mFontMetrics.append(mCachedFm.descent);
606 }
607 }
608
609 /**
610 * Returns the maximum index that the accumulated width not exceeds the width.
611 *
612 * If forward=false is passed, returns the minimum index from the end instead.
613 *
Seigo Nonaka783f9612018-01-20 12:11:13 -0800614 * This only works if the MeasuredParagraph is computed with buildForMeasurement.
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800615 * Undefined behavior in other case.
616 */
617 @IntRange(from = 0) int breakText(int limit, boolean forwards, float width) {
618 float[] w = mWidths.getRawArray();
619 if (forwards) {
620 int i = 0;
621 while (i < limit) {
622 width -= w[i];
623 if (width < 0.0f) break;
624 i++;
625 }
626 while (i > 0 && mCopiedBuffer[i - 1] == ' ') i--;
627 return i;
628 } else {
629 int i = limit - 1;
630 while (i >= 0) {
631 width -= w[i];
632 if (width < 0.0f) break;
633 i--;
634 }
635 while (i < limit - 1 && (mCopiedBuffer[i + 1] == ' ' || w[i + 1] == 0.0f)) {
636 i++;
637 }
638 return limit - i - 1;
639 }
640 }
641
642 /**
643 * Returns the length of the substring.
644 *
Seigo Nonaka783f9612018-01-20 12:11:13 -0800645 * This only works if the MeasuredParagraph is computed with buildForMeasurement.
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800646 * Undefined behavior in other case.
647 */
648 @FloatRange(from = 0.0f) float measure(int start, int limit) {
649 float width = 0;
650 float[] w = mWidths.getRawArray();
651 for (int i = start; i < limit; ++i) {
652 width += w[i];
653 }
654 return width;
655 }
656
Seigo Nonaka49ca0242018-01-24 16:46:14 -0800657 /**
658 * This only works if the MeasuredParagraph is computed with buildForStaticLayout.
659 */
Seigo Nonakac3328d62018-03-20 15:18:59 -0700660 public @IntRange(from = 0) int getMemoryUsage() {
Seigo Nonaka6f11c6e2018-07-24 11:26:18 -0700661 return mNativeMeasuredParagraph.getMemoryUsage();
Seigo Nonaka49ca0242018-01-24 16:46:14 -0800662 }
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800663}