blob: c2c3182c2abd48394526753d68ad0ffb2ee23b7d [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
33import dalvik.annotation.optimization.CriticalNative;
34
35import libcore.util.NativeAllocationRegistry;
36
37import java.util.Arrays;
38
39/**
40 * MeasuredParagraph provides text information for rendering purpose.
41 *
42 * The first motivation of this class is identify the text directions and retrieving individual
43 * character widths. However retrieving character widths is slower than identifying text directions.
44 * Thus, this class provides several builder methods for specific purposes.
45 *
46 * - buildForBidi:
47 * Compute only text directions.
48 * - buildForMeasurement:
49 * Compute text direction and all character widths.
50 * - buildForStaticLayout:
51 * This is bit special. StaticLayout also needs to know text direction and character widths for
52 * line breaking, but all things are done in native code. Similarly, text measurement is done
53 * in native code. So instead of storing result to Java array, this keeps the result in native
54 * code since there is no good reason to move the results to Java layer.
55 *
56 * In addition to the character widths, some additional information is computed for each purposes,
57 * e.g. whole text length for measurement or font metrics for static layout.
58 *
59 * MeasuredParagraph is NOT a thread safe object.
60 * @hide
61 */
62public class MeasuredParagraph {
63 private static final char OBJECT_REPLACEMENT_CHARACTER = '\uFFFC';
64
65 private static final NativeAllocationRegistry sRegistry = new NativeAllocationRegistry(
66 MeasuredParagraph.class.getClassLoader(), nGetReleaseFunc(), 1024);
67
68 private MeasuredParagraph() {} // Use build static functions instead.
69
70 private static final SynchronizedPool<MeasuredParagraph> sPool = new SynchronizedPool<>(1);
71
72 private static @NonNull MeasuredParagraph obtain() { // Use build static functions instead.
73 final MeasuredParagraph mt = sPool.acquire();
74 return mt != null ? mt : new MeasuredParagraph();
75 }
76
77 /**
78 * Recycle the MeasuredParagraph.
79 *
80 * Do not call any methods after you call this method.
81 */
82 public void recycle() {
83 release();
84 sPool.release(this);
85 }
86
87 // The casted original text.
88 //
89 // This may be null if the passed text is not a Spanned.
90 private @Nullable Spanned mSpanned;
91
92 // The start offset of the target range in the original text (mSpanned);
93 private @IntRange(from = 0) int mTextStart;
94
95 // The length of the target range in the original text.
96 private @IntRange(from = 0) int mTextLength;
97
98 // The copied character buffer for measuring text.
99 //
100 // The length of this array is mTextLength.
101 private @Nullable char[] mCopiedBuffer;
102
103 // The whole paragraph direction.
104 private @Layout.Direction int mParaDir;
105
106 // True if the text is LTR direction and doesn't contain any bidi characters.
107 private boolean mLtrWithoutBidi;
108
109 // The bidi level for individual characters.
110 //
111 // This is empty if mLtrWithoutBidi is true.
112 private @NonNull ByteArray mLevels = new ByteArray();
113
114 // The whole width of the text.
115 // See getWholeWidth comments.
116 private @FloatRange(from = 0.0f) float mWholeWidth;
117
118 // Individual characters' widths.
119 // See getWidths comments.
120 private @Nullable FloatArray mWidths = new FloatArray();
121
122 // The span end positions.
123 // See getSpanEndCache comments.
124 private @Nullable IntArray mSpanEndCache = new IntArray(4);
125
126 // The font metrics.
127 // See getFontMetrics comments.
128 private @Nullable IntArray mFontMetrics = new IntArray(4 * 4);
129
130 // The native MeasuredParagraph.
131 // See getNativePtr comments.
132 // Do not modify these members directly. Use bindNativeObject/unbindNativeObject instead.
133 private /* Maybe Zero */ long mNativePtr = 0;
134 private @Nullable Runnable mNativeObjectCleaner;
135
136 // Associate the native object to this Java object.
137 private void bindNativeObject(/* Non Zero*/ long nativePtr) {
138 mNativePtr = nativePtr;
139 mNativeObjectCleaner = sRegistry.registerNativeAllocation(this, nativePtr);
140 }
141
142 // Decouple the native object from this Java object and release the native object.
143 private void unbindNativeObject() {
144 if (mNativePtr != 0) {
145 mNativeObjectCleaner.run();
146 mNativePtr = 0;
147 }
148 }
149
150 // Following two objects are for avoiding object allocation.
151 private @NonNull TextPaint mCachedPaint = new TextPaint();
152 private @Nullable Paint.FontMetricsInt mCachedFm;
153
154 /**
155 * Releases internal buffers.
156 */
157 public void release() {
158 reset();
159 mLevels.clearWithReleasingLargeArray();
160 mWidths.clearWithReleasingLargeArray();
161 mFontMetrics.clearWithReleasingLargeArray();
162 mSpanEndCache.clearWithReleasingLargeArray();
163 }
164
165 /**
166 * Resets the internal state for starting new text.
167 */
168 private void reset() {
169 mSpanned = null;
170 mCopiedBuffer = null;
171 mWholeWidth = 0;
172 mLevels.clear();
173 mWidths.clear();
174 mFontMetrics.clear();
175 mSpanEndCache.clear();
176 unbindNativeObject();
177 }
178
179 /**
Seigo Nonaka783f9612018-01-20 12:11:13 -0800180 * Returns the length of the paragraph.
181 *
182 * This is always available.
183 */
184 public int getTextLength() {
185 return mTextLength;
186 }
187
188 /**
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800189 * Returns the characters to be measured.
190 *
191 * This is always available.
192 */
193 public @NonNull char[] getChars() {
194 return mCopiedBuffer;
195 }
196
197 /**
198 * Returns the paragraph direction.
199 *
200 * This is always available.
201 */
202 public @Layout.Direction int getParagraphDir() {
203 return mParaDir;
204 }
205
206 /**
207 * Returns the directions.
208 *
209 * This is always available.
210 */
211 public Directions getDirections(@IntRange(from = 0) int start, // inclusive
212 @IntRange(from = 0) int end) { // exclusive
213 if (mLtrWithoutBidi) {
214 return Layout.DIRS_ALL_LEFT_TO_RIGHT;
215 }
216
217 final int length = end - start;
218 return AndroidBidi.directions(mParaDir, mLevels.getRawArray(), start, mCopiedBuffer, start,
219 length);
220 }
221
222 /**
223 * Returns the whole text width.
224 *
Seigo Nonaka783f9612018-01-20 12:11:13 -0800225 * This is available only if the MeasuredParagraph is computed with buildForMeasurement.
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800226 * Returns 0 in other cases.
227 */
228 public @FloatRange(from = 0.0f) float getWholeWidth() {
229 return mWholeWidth;
230 }
231
232 /**
233 * Returns the individual character's width.
234 *
Seigo Nonaka783f9612018-01-20 12:11:13 -0800235 * This is available only if the MeasuredParagraph is computed with buildForMeasurement.
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800236 * Returns empty array in other cases.
237 */
238 public @NonNull FloatArray getWidths() {
239 return mWidths;
240 }
241
242 /**
243 * Returns the MetricsAffectingSpan end indices.
244 *
245 * If the input text is not a spanned string, this has one value that is the length of the text.
246 *
Seigo Nonaka783f9612018-01-20 12:11:13 -0800247 * This is available only if the MeasuredParagraph is computed with buildForStaticLayout.
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800248 * Returns empty array in other cases.
249 */
250 public @NonNull IntArray getSpanEndCache() {
251 return mSpanEndCache;
252 }
253
254 /**
255 * Returns the int array which holds FontMetrics.
256 *
257 * This array holds the repeat of top, bottom, ascent, descent of font metrics value.
258 *
Seigo Nonaka783f9612018-01-20 12:11:13 -0800259 * This is available only if the MeasuredParagraph is computed with buildForStaticLayout.
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800260 * Returns empty array in other cases.
261 */
262 public @NonNull IntArray getFontMetrics() {
263 return mFontMetrics;
264 }
265
266 /**
267 * Returns the native ptr of the MeasuredParagraph.
268 *
Seigo Nonaka783f9612018-01-20 12:11:13 -0800269 * This is available only if the MeasuredParagraph is computed with buildForStaticLayout.
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800270 * Returns 0 in other cases.
271 */
272 public /* Maybe Zero */ long getNativePtr() {
273 return mNativePtr;
274 }
275
276 /**
Seigo Nonaka783f9612018-01-20 12:11:13 -0800277 * Returns the width of the given range.
278 *
279 * This is not available if the MeasuredParagraph is computed with buildForBidi.
280 * Returns 0 if the MeasuredParagraph is computed with buildForBidi.
281 *
282 * @param start the inclusive start offset of the target region in the text
283 * @param end the exclusive end offset of the target region in the text
284 */
285 public float getWidth(int start, int end) {
286 if (mNativePtr == 0) {
287 // We have result in Java.
288 final float[] widths = mWidths.getRawArray();
289 float r = 0.0f;
290 for (int i = start; i < end; ++i) {
291 r += widths[i];
292 }
293 return r;
294 } else {
295 // We have result in native.
296 return nGetWidth(mNativePtr, start, end);
297 }
298 }
299
300 /**
Seigo Nonakaa5534772018-03-15 00:22:20 -0700301 * Retrieves the bounding rectangle that encloses all of the characters, with an implied origin
302 * at (0, 0).
303 *
304 * This is available only if the MeasuredParagraph is computed with buildForStaticLayout.
305 */
Seigo Nonakafb0abe12018-04-02 23:25:38 -0700306 public void getBounds(@IntRange(from = 0) int start, @IntRange(from = 0) int end,
307 @NonNull Rect bounds) {
308 nGetBounds(mNativePtr, mCopiedBuffer, start, end, bounds);
Seigo Nonakaa5534772018-03-15 00:22:20 -0700309 }
310
311 /**
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800312 * Generates new MeasuredParagraph for Bidi computation.
313 *
314 * If recycle is null, this returns new instance. If recycle is not null, this fills computed
315 * result to recycle and returns recycle.
316 *
317 * @param text the character sequence to be measured
318 * @param start the inclusive start offset of the target region in the text
319 * @param end the exclusive end offset of the target region in the text
320 * @param textDir the text direction
321 * @param recycle pass existing MeasuredParagraph if you want to recycle it.
322 *
323 * @return measured text
324 */
325 public static @NonNull MeasuredParagraph buildForBidi(@NonNull CharSequence text,
326 @IntRange(from = 0) int start,
327 @IntRange(from = 0) int end,
328 @NonNull TextDirectionHeuristic textDir,
329 @Nullable MeasuredParagraph recycle) {
330 final MeasuredParagraph mt = recycle == null ? obtain() : recycle;
331 mt.resetAndAnalyzeBidi(text, start, end, textDir);
332 return mt;
333 }
334
335 /**
336 * Generates new MeasuredParagraph for measuring texts.
337 *
338 * If recycle is null, this returns new instance. If recycle is not null, this fills computed
339 * result to recycle and returns recycle.
340 *
341 * @param paint the paint to be used for rendering the text.
342 * @param text the character sequence to be measured
343 * @param start the inclusive start offset of the target region in the text
344 * @param end the exclusive end offset of the target region in the text
345 * @param textDir the text direction
346 * @param recycle pass existing MeasuredParagraph if you want to recycle it.
347 *
348 * @return measured text
349 */
350 public static @NonNull MeasuredParagraph buildForMeasurement(@NonNull TextPaint paint,
351 @NonNull CharSequence text,
352 @IntRange(from = 0) int start,
353 @IntRange(from = 0) int end,
354 @NonNull TextDirectionHeuristic textDir,
355 @Nullable MeasuredParagraph recycle) {
356 final MeasuredParagraph mt = recycle == null ? obtain() : recycle;
357 mt.resetAndAnalyzeBidi(text, start, end, textDir);
358
359 mt.mWidths.resize(mt.mTextLength);
360 if (mt.mTextLength == 0) {
361 return mt;
362 }
363
364 if (mt.mSpanned == null) {
365 // No style change by MetricsAffectingSpan. Just measure all text.
366 mt.applyMetricsAffectingSpan(
367 paint, null /* spans */, start, end, 0 /* native static layout ptr */);
368 } else {
369 // There may be a MetricsAffectingSpan. Split into span transitions and apply styles.
370 int spanEnd;
371 for (int spanStart = start; spanStart < end; spanStart = spanEnd) {
372 spanEnd = mt.mSpanned.nextSpanTransition(spanStart, end, MetricAffectingSpan.class);
373 MetricAffectingSpan[] spans = mt.mSpanned.getSpans(spanStart, spanEnd,
374 MetricAffectingSpan.class);
375 spans = TextUtils.removeEmptySpans(spans, mt.mSpanned, MetricAffectingSpan.class);
376 mt.applyMetricsAffectingSpan(
377 paint, spans, spanStart, spanEnd, 0 /* native static layout ptr */);
378 }
379 }
380 return mt;
381 }
382
383 /**
384 * Generates new MeasuredParagraph for StaticLayout.
385 *
386 * If recycle is null, this returns new instance. If recycle is not null, this fills computed
387 * result to recycle and returns recycle.
388 *
389 * @param paint the paint to be used for rendering the text.
390 * @param text the character sequence to be measured
391 * @param start the inclusive start offset of the target region in the text
392 * @param end the exclusive end offset of the target region in the text
393 * @param textDir the text direction
394 * @param recycle pass existing MeasuredParagraph if you want to recycle it.
395 *
396 * @return measured text
397 */
398 public static @NonNull MeasuredParagraph buildForStaticLayout(
399 @NonNull TextPaint paint,
400 @NonNull CharSequence text,
401 @IntRange(from = 0) int start,
402 @IntRange(from = 0) int end,
403 @NonNull TextDirectionHeuristic textDir,
Seigo Nonaka87b15472018-01-12 14:06:29 -0800404 boolean computeHyphenation,
Seigo Nonaka783f9612018-01-20 12:11:13 -0800405 boolean computeLayout,
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800406 @Nullable MeasuredParagraph recycle) {
407 final MeasuredParagraph mt = recycle == null ? obtain() : recycle;
408 mt.resetAndAnalyzeBidi(text, start, end, textDir);
409 if (mt.mTextLength == 0) {
410 // Need to build empty native measured text for StaticLayout.
411 // TODO: Stop creating empty measured text for empty lines.
412 long nativeBuilderPtr = nInitBuilder();
413 try {
414 mt.bindNativeObject(
Seigo Nonaka87b15472018-01-12 14:06:29 -0800415 nBuildNativeMeasuredParagraph(nativeBuilderPtr, mt.mCopiedBuffer,
Seigo Nonaka783f9612018-01-20 12:11:13 -0800416 computeHyphenation, computeLayout));
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800417 } finally {
418 nFreeBuilder(nativeBuilderPtr);
419 }
420 return mt;
421 }
422
423 long nativeBuilderPtr = nInitBuilder();
424 try {
425 if (mt.mSpanned == null) {
426 // No style change by MetricsAffectingSpan. Just measure all text.
427 mt.applyMetricsAffectingSpan(paint, null /* spans */, start, end, nativeBuilderPtr);
428 mt.mSpanEndCache.append(end);
429 } else {
430 // There may be a MetricsAffectingSpan. Split into span transitions and apply
431 // styles.
432 int spanEnd;
433 for (int spanStart = start; spanStart < end; spanStart = spanEnd) {
434 spanEnd = mt.mSpanned.nextSpanTransition(spanStart, end,
435 MetricAffectingSpan.class);
436 MetricAffectingSpan[] spans = mt.mSpanned.getSpans(spanStart, spanEnd,
437 MetricAffectingSpan.class);
438 spans = TextUtils.removeEmptySpans(spans, mt.mSpanned,
439 MetricAffectingSpan.class);
440 mt.applyMetricsAffectingSpan(paint, spans, spanStart, spanEnd,
441 nativeBuilderPtr);
442 mt.mSpanEndCache.append(spanEnd);
443 }
444 }
Seigo Nonaka87b15472018-01-12 14:06:29 -0800445 mt.bindNativeObject(nBuildNativeMeasuredParagraph(nativeBuilderPtr, mt.mCopiedBuffer,
Seigo Nonaka783f9612018-01-20 12:11:13 -0800446 computeHyphenation, computeLayout));
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800447 } finally {
448 nFreeBuilder(nativeBuilderPtr);
449 }
450
451 return mt;
452 }
453
454 /**
455 * Reset internal state and analyzes text for bidirectional runs.
456 *
457 * @param text the character sequence to be measured
458 * @param start the inclusive start offset of the target region in the text
459 * @param end the exclusive end offset of the target region in the text
460 * @param textDir the text direction
461 */
462 private void resetAndAnalyzeBidi(@NonNull CharSequence text,
463 @IntRange(from = 0) int start, // inclusive
464 @IntRange(from = 0) int end, // exclusive
465 @NonNull TextDirectionHeuristic textDir) {
466 reset();
467 mSpanned = text instanceof Spanned ? (Spanned) text : null;
468 mTextStart = start;
469 mTextLength = end - start;
470
471 if (mCopiedBuffer == null || mCopiedBuffer.length != mTextLength) {
472 mCopiedBuffer = new char[mTextLength];
473 }
474 TextUtils.getChars(text, start, end, mCopiedBuffer, 0);
475
476 // Replace characters associated with ReplacementSpan to U+FFFC.
477 if (mSpanned != null) {
478 ReplacementSpan[] spans = mSpanned.getSpans(start, end, ReplacementSpan.class);
479
480 for (int i = 0; i < spans.length; i++) {
481 int startInPara = mSpanned.getSpanStart(spans[i]) - start;
482 int endInPara = mSpanned.getSpanEnd(spans[i]) - start;
483 // The span interval may be larger and must be restricted to [start, end)
484 if (startInPara < 0) startInPara = 0;
485 if (endInPara > mTextLength) endInPara = mTextLength;
486 Arrays.fill(mCopiedBuffer, startInPara, endInPara, OBJECT_REPLACEMENT_CHARACTER);
487 }
488 }
489
490 if ((textDir == TextDirectionHeuristics.LTR
491 || textDir == TextDirectionHeuristics.FIRSTSTRONG_LTR
492 || textDir == TextDirectionHeuristics.ANYRTL_LTR)
493 && TextUtils.doesNotNeedBidi(mCopiedBuffer, 0, mTextLength)) {
494 mLevels.clear();
495 mParaDir = Layout.DIR_LEFT_TO_RIGHT;
496 mLtrWithoutBidi = true;
497 } else {
498 final int bidiRequest;
499 if (textDir == TextDirectionHeuristics.LTR) {
500 bidiRequest = Layout.DIR_REQUEST_LTR;
501 } else if (textDir == TextDirectionHeuristics.RTL) {
502 bidiRequest = Layout.DIR_REQUEST_RTL;
503 } else if (textDir == TextDirectionHeuristics.FIRSTSTRONG_LTR) {
504 bidiRequest = Layout.DIR_REQUEST_DEFAULT_LTR;
505 } else if (textDir == TextDirectionHeuristics.FIRSTSTRONG_RTL) {
506 bidiRequest = Layout.DIR_REQUEST_DEFAULT_RTL;
507 } else {
508 final boolean isRtl = textDir.isRtl(mCopiedBuffer, 0, mTextLength);
509 bidiRequest = isRtl ? Layout.DIR_REQUEST_RTL : Layout.DIR_REQUEST_LTR;
510 }
511 mLevels.resize(mTextLength);
512 mParaDir = AndroidBidi.bidi(bidiRequest, mCopiedBuffer, mLevels.getRawArray());
513 mLtrWithoutBidi = false;
514 }
515 }
516
517 private void applyReplacementRun(@NonNull ReplacementSpan replacement,
518 @IntRange(from = 0) int start, // inclusive, in copied buffer
519 @IntRange(from = 0) int end, // exclusive, in copied buffer
520 /* Maybe Zero */ long nativeBuilderPtr) {
521 // Use original text. Shouldn't matter.
522 // TODO: passing uninitizlied FontMetrics to developers. Do we need to keep this for
523 // backward compatibility? or Should we initialize them for getFontMetricsInt?
524 final float width = replacement.getSize(
525 mCachedPaint, mSpanned, start + mTextStart, end + mTextStart, mCachedFm);
526 if (nativeBuilderPtr == 0) {
527 // Assigns all width to the first character. This is the same behavior as minikin.
528 mWidths.set(start, width);
529 if (end > start + 1) {
530 Arrays.fill(mWidths.getRawArray(), start + 1, end, 0.0f);
531 }
532 mWholeWidth += width;
533 } else {
534 nAddReplacementRun(nativeBuilderPtr, mCachedPaint.getNativeInstance(), start, end,
535 width);
536 }
537 }
538
539 private void applyStyleRun(@IntRange(from = 0) int start, // inclusive, in copied buffer
540 @IntRange(from = 0) int end, // exclusive, in copied buffer
541 /* Maybe Zero */ long nativeBuilderPtr) {
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800542
543 if (mLtrWithoutBidi) {
544 // If the whole text is LTR direction, just apply whole region.
545 if (nativeBuilderPtr == 0) {
546 mWholeWidth += mCachedPaint.getTextRunAdvances(
547 mCopiedBuffer, start, end - start, start, end - start, false /* isRtl */,
548 mWidths.getRawArray(), start);
549 } else {
550 nAddStyleRun(nativeBuilderPtr, mCachedPaint.getNativeInstance(), start, end,
551 false /* isRtl */);
552 }
553 } else {
554 // If there is multiple bidi levels, split into individual bidi level and apply style.
555 byte level = mLevels.get(start);
556 // Note that the empty text or empty range won't reach this method.
557 // Safe to search from start + 1.
558 for (int levelStart = start, levelEnd = start + 1;; ++levelEnd) {
559 if (levelEnd == end || mLevels.get(levelEnd) != level) { // transition point
560 final boolean isRtl = (level & 0x1) != 0;
561 if (nativeBuilderPtr == 0) {
562 final int levelLength = levelEnd - levelStart;
563 mWholeWidth += mCachedPaint.getTextRunAdvances(
564 mCopiedBuffer, levelStart, levelLength, levelStart, levelLength,
565 isRtl, mWidths.getRawArray(), levelStart);
566 } else {
567 nAddStyleRun(nativeBuilderPtr, mCachedPaint.getNativeInstance(), levelStart,
568 levelEnd, isRtl);
569 }
570 if (levelEnd == end) {
571 break;
572 }
573 levelStart = levelEnd;
574 level = mLevels.get(levelEnd);
575 }
576 }
577 }
578 }
579
580 private void applyMetricsAffectingSpan(
581 @NonNull TextPaint paint,
582 @Nullable MetricAffectingSpan[] spans,
583 @IntRange(from = 0) int start, // inclusive, in original text buffer
584 @IntRange(from = 0) int end, // exclusive, in original text buffer
585 /* Maybe Zero */ long nativeBuilderPtr) {
586 mCachedPaint.set(paint);
587 // XXX paint should not have a baseline shift, but...
588 mCachedPaint.baselineShift = 0;
589
590 final boolean needFontMetrics = nativeBuilderPtr != 0;
591
592 if (needFontMetrics && mCachedFm == null) {
593 mCachedFm = new Paint.FontMetricsInt();
594 }
595
596 ReplacementSpan replacement = null;
597 if (spans != null) {
598 for (int i = 0; i < spans.length; i++) {
599 MetricAffectingSpan span = spans[i];
600 if (span instanceof ReplacementSpan) {
601 // The last ReplacementSpan is effective for backward compatibility reasons.
602 replacement = (ReplacementSpan) span;
603 } else {
604 // TODO: No need to call updateMeasureState for ReplacementSpan as well?
605 span.updateMeasureState(mCachedPaint);
606 }
607 }
608 }
609
610 final int startInCopiedBuffer = start - mTextStart;
611 final int endInCopiedBuffer = end - mTextStart;
612
Seigo Nonaka09036652018-03-30 10:46:52 -0700613 if (nativeBuilderPtr != 0) {
614 mCachedPaint.getFontMetricsInt(mCachedFm);
615 }
616
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800617 if (replacement != null) {
618 applyReplacementRun(replacement, startInCopiedBuffer, endInCopiedBuffer,
619 nativeBuilderPtr);
620 } else {
621 applyStyleRun(startInCopiedBuffer, endInCopiedBuffer, nativeBuilderPtr);
622 }
623
624 if (needFontMetrics) {
625 if (mCachedPaint.baselineShift < 0) {
626 mCachedFm.ascent += mCachedPaint.baselineShift;
627 mCachedFm.top += mCachedPaint.baselineShift;
628 } else {
629 mCachedFm.descent += mCachedPaint.baselineShift;
630 mCachedFm.bottom += mCachedPaint.baselineShift;
631 }
632
633 mFontMetrics.append(mCachedFm.top);
634 mFontMetrics.append(mCachedFm.bottom);
635 mFontMetrics.append(mCachedFm.ascent);
636 mFontMetrics.append(mCachedFm.descent);
637 }
638 }
639
640 /**
641 * Returns the maximum index that the accumulated width not exceeds the width.
642 *
643 * If forward=false is passed, returns the minimum index from the end instead.
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 @IntRange(from = 0) int breakText(int limit, boolean forwards, float width) {
649 float[] w = mWidths.getRawArray();
650 if (forwards) {
651 int i = 0;
652 while (i < limit) {
653 width -= w[i];
654 if (width < 0.0f) break;
655 i++;
656 }
657 while (i > 0 && mCopiedBuffer[i - 1] == ' ') i--;
658 return i;
659 } else {
660 int i = limit - 1;
661 while (i >= 0) {
662 width -= w[i];
663 if (width < 0.0f) break;
664 i--;
665 }
666 while (i < limit - 1 && (mCopiedBuffer[i + 1] == ' ' || w[i + 1] == 0.0f)) {
667 i++;
668 }
669 return limit - i - 1;
670 }
671 }
672
673 /**
674 * Returns the length of the substring.
675 *
Seigo Nonaka783f9612018-01-20 12:11:13 -0800676 * This only works if the MeasuredParagraph is computed with buildForMeasurement.
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800677 * Undefined behavior in other case.
678 */
679 @FloatRange(from = 0.0f) float measure(int start, int limit) {
680 float width = 0;
681 float[] w = mWidths.getRawArray();
682 for (int i = start; i < limit; ++i) {
683 width += w[i];
684 }
685 return width;
686 }
687
Seigo Nonaka49ca0242018-01-24 16:46:14 -0800688 /**
689 * This only works if the MeasuredParagraph is computed with buildForStaticLayout.
690 */
Seigo Nonakac3328d62018-03-20 15:18:59 -0700691 public @IntRange(from = 0) int getMemoryUsage() {
Seigo Nonaka49ca0242018-01-24 16:46:14 -0800692 return nGetMemoryUsage(mNativePtr);
693 }
694
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800695 private static native /* Non Zero */ long nInitBuilder();
696
697 /**
698 * Apply style to make native measured text.
699 *
700 * @param nativeBuilderPtr The native MeasuredParagraph builder pointer.
701 * @param paintPtr The native paint pointer to be applied.
702 * @param start The start offset in the copied buffer.
703 * @param end The end offset in the copied buffer.
704 * @param isRtl True if the text is RTL.
705 */
706 private static native void nAddStyleRun(/* Non Zero */ long nativeBuilderPtr,
707 /* Non Zero */ long paintPtr,
708 @IntRange(from = 0) int start,
709 @IntRange(from = 0) int end,
710 boolean isRtl);
711
712 /**
713 * Apply ReplacementRun to make native measured text.
714 *
715 * @param nativeBuilderPtr The native MeasuredParagraph builder pointer.
716 * @param paintPtr The native paint pointer to be applied.
717 * @param start The start offset in the copied buffer.
718 * @param end The end offset in the copied buffer.
719 * @param width The width of the replacement.
720 */
721 private static native void nAddReplacementRun(/* Non Zero */ long nativeBuilderPtr,
722 /* Non Zero */ long paintPtr,
723 @IntRange(from = 0) int start,
724 @IntRange(from = 0) int end,
725 @FloatRange(from = 0) float width);
726
727 private static native long nBuildNativeMeasuredParagraph(/* Non Zero */ long nativeBuilderPtr,
Seigo Nonaka87b15472018-01-12 14:06:29 -0800728 @NonNull char[] text,
Seigo Nonaka783f9612018-01-20 12:11:13 -0800729 boolean computeHyphenation,
730 boolean computeLayout);
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800731
732 private static native void nFreeBuilder(/* Non Zero */ long nativeBuilderPtr);
733
734 @CriticalNative
Seigo Nonaka783f9612018-01-20 12:11:13 -0800735 private static native float nGetWidth(/* Non Zero */ long nativePtr,
736 @IntRange(from = 0) int start,
737 @IntRange(from = 0) int end);
738
739 @CriticalNative
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800740 private static native /* Non Zero */ long nGetReleaseFunc();
Seigo Nonaka49ca0242018-01-24 16:46:14 -0800741
742 @CriticalNative
743 private static native int nGetMemoryUsage(/* Non Zero */ long nativePtr);
Seigo Nonakaa5534772018-03-15 00:22:20 -0700744
Seigo Nonakafb0abe12018-04-02 23:25:38 -0700745 private static native void nGetBounds(long nativePtr, char[] buf, int start, int end,
746 Rect rect);
Seigo Nonaka9d3bd082018-01-11 10:02:12 -0800747}