blob: 90d8d43a36ccc821195586ad5d26ff190c059862 [file] [log] [blame]
Doug Felte8e45f22010-03-29 14:58:40 -07001/*
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
Roozbeh Pournader538de5b2017-05-24 09:07:52 -070019import android.annotation.NonNull;
20import android.annotation.Nullable;
Mathew Inwood9cf99fd2018-08-14 15:31:03 +010021import android.annotation.UnsupportedAppUsage;
Doug Felte8e45f22010-03-29 14:58:40 -070022import android.graphics.Canvas;
23import android.graphics.Paint;
Doug Felte8e45f22010-03-29 14:58:40 -070024import android.graphics.Paint.FontMetricsInt;
Doug Felte8e45f22010-03-29 14:58:40 -070025import android.text.Layout.Directions;
Doug Feltc982f602010-05-25 11:51:40 -070026import android.text.Layout.TabStops;
Doug Felte8e45f22010-03-29 14:58:40 -070027import android.text.style.CharacterStyle;
28import android.text.style.MetricAffectingSpan;
29import android.text.style.ReplacementSpan;
Doug Felte8e45f22010-03-29 14:58:40 -070030import android.util.Log;
31
Roozbeh Pournader3630d082017-10-23 12:16:32 -070032import com.android.internal.annotations.VisibleForTesting;
Gilles Debunnedd8f5ed2011-08-10 18:25:49 -070033import com.android.internal.util.ArrayUtils;
34
Roozbeh Pournader538de5b2017-05-24 09:07:52 -070035import java.util.ArrayList;
36
Doug Felte8e45f22010-03-29 14:58:40 -070037/**
38 * Represents a line of styled text, for measuring in visual order and
39 * for rendering.
40 *
41 * <p>Get a new instance using obtain(), and when finished with it, return it
42 * to the pool using recycle().
43 *
44 * <p>Call set to prepare the instance for use, then either draw, measure,
45 * metrics, or caretToLeftRightOf.
46 *
47 * @hide
48 */
Roozbeh Pournader3630d082017-10-23 12:16:32 -070049@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
50public class TextLine {
Romain Guybc7cdb62011-05-26 18:48:49 -070051 private static final boolean DEBUG = false;
52
Doug Felte8e45f22010-03-29 14:58:40 -070053 private TextPaint mPaint;
Mathew Inwood9cf99fd2018-08-14 15:31:03 +010054 @UnsupportedAppUsage
Doug Felte8e45f22010-03-29 14:58:40 -070055 private CharSequence mText;
56 private int mStart;
57 private int mLen;
58 private int mDir;
59 private Directions mDirections;
60 private boolean mHasTabs;
Doug Feltc982f602010-05-25 11:51:40 -070061 private TabStops mTabs;
Doug Felte8e45f22010-03-29 14:58:40 -070062 private char[] mChars;
63 private boolean mCharsValid;
Mathew Inwood9cf99fd2018-08-14 15:31:03 +010064 @UnsupportedAppUsage
Doug Felte8e45f22010-03-29 14:58:40 -070065 private Spanned mSpanned;
Seigo Nonakabeafa1f2018-02-01 21:39:24 -080066 private PrecomputedText mComputed;
Seigo Nonaka09da71a2016-11-28 16:24:14 +090067
68 // Additional width of whitespace for justification. This value is per whitespace, thus
69 // the line width will increase by mAddedWidth x (number of stretchable whitespaces).
70 private float mAddedWidth;
Roozbeh Pournader538de5b2017-05-24 09:07:52 -070071
Gilles Debunne345cb032010-06-16 17:13:23 -070072 private final TextPaint mWorkPaint = new TextPaint();
Roozbeh Pournader538de5b2017-05-24 09:07:52 -070073 private final TextPaint mActivePaint = new TextPaint();
Mathew Inwood9cf99fd2018-08-14 15:31:03 +010074 @UnsupportedAppUsage
Gilles Debunnec1f44832011-12-08 16:03:00 -080075 private final SpanSet<MetricAffectingSpan> mMetricAffectingSpanSpanSet =
76 new SpanSet<MetricAffectingSpan>(MetricAffectingSpan.class);
Mathew Inwood9cf99fd2018-08-14 15:31:03 +010077 @UnsupportedAppUsage
Gilles Debunnec1f44832011-12-08 16:03:00 -080078 private final SpanSet<CharacterStyle> mCharacterStyleSpanSet =
79 new SpanSet<CharacterStyle>(CharacterStyle.class);
Mathew Inwood9cf99fd2018-08-14 15:31:03 +010080 @UnsupportedAppUsage
Gilles Debunnec1f44832011-12-08 16:03:00 -080081 private final SpanSet<ReplacementSpan> mReplacementSpanSpanSet =
82 new SpanSet<ReplacementSpan>(ReplacementSpan.class);
Doug Felte8e45f22010-03-29 14:58:40 -070083
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -070084 private final DecorationInfo mDecorationInfo = new DecorationInfo();
Siyamed Sinira273a702017-10-05 11:22:12 -070085 private final ArrayList<DecorationInfo> mDecorations = new ArrayList<>();
Roozbeh Pournader538de5b2017-05-24 09:07:52 -070086
Mathew Inwood9cf99fd2018-08-14 15:31:03 +010087 @UnsupportedAppUsage
Romain Guybc7cdb62011-05-26 18:48:49 -070088 private static final TextLine[] sCached = new TextLine[3];
Doug Felte8e45f22010-03-29 14:58:40 -070089
90 /**
91 * Returns a new TextLine from the shared pool.
92 *
93 * @return an uninitialized TextLine
94 */
Roozbeh Pournader3630d082017-10-23 12:16:32 -070095 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
Mathew Inwood9cf99fd2018-08-14 15:31:03 +010096 @UnsupportedAppUsage
Roozbeh Pournader3630d082017-10-23 12:16:32 -070097 public static TextLine obtain() {
Doug Felte8e45f22010-03-29 14:58:40 -070098 TextLine tl;
Romain Guybc7cdb62011-05-26 18:48:49 -070099 synchronized (sCached) {
100 for (int i = sCached.length; --i >= 0;) {
101 if (sCached[i] != null) {
102 tl = sCached[i];
103 sCached[i] = null;
Doug Felte8e45f22010-03-29 14:58:40 -0700104 return tl;
105 }
106 }
107 }
108 tl = new TextLine();
Romain Guybc7cdb62011-05-26 18:48:49 -0700109 if (DEBUG) {
110 Log.v("TLINE", "new: " + tl);
111 }
Doug Felte8e45f22010-03-29 14:58:40 -0700112 return tl;
113 }
114
115 /**
116 * Puts a TextLine back into the shared pool. Do not use this TextLine once
117 * it has been returned.
118 * @param tl the textLine
119 * @return null, as a convenience from clearing references to the provided
120 * TextLine
121 */
Roozbeh Pournader3630d082017-10-23 12:16:32 -0700122 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
123 public static TextLine recycle(TextLine tl) {
Doug Felte8e45f22010-03-29 14:58:40 -0700124 tl.mText = null;
125 tl.mPaint = null;
126 tl.mDirections = null;
Svet Ganov893d6fe2015-01-16 10:10:15 -0800127 tl.mSpanned = null;
128 tl.mTabs = null;
129 tl.mChars = null;
Seigo Nonakabeafa1f2018-02-01 21:39:24 -0800130 tl.mComputed = null;
Gilles Debunnec3fb7a12011-12-12 14:03:40 -0800131
132 tl.mMetricAffectingSpanSpanSet.recycle();
133 tl.mCharacterStyleSpanSet.recycle();
134 tl.mReplacementSpanSpanSet.recycle();
135
Romain Guybc7cdb62011-05-26 18:48:49 -0700136 synchronized(sCached) {
137 for (int i = 0; i < sCached.length; ++i) {
138 if (sCached[i] == null) {
139 sCached[i] = tl;
Gilles Debunnef902d7b2011-01-25 09:09:46 -0800140 break;
Doug Felte8e45f22010-03-29 14:58:40 -0700141 }
142 }
143 }
144 return null;
145 }
146
147 /**
148 * Initializes a TextLine and prepares it for use.
149 *
150 * @param paint the base paint for the line
151 * @param text the text, can be Styled
152 * @param start the start of the line relative to the text
153 * @param limit the limit of the line relative to the text
154 * @param dir the paragraph direction of this line
155 * @param directions the directions information of this line
Roozbeh Pournader112d9c72015-08-07 12:44:41 -0700156 * @param hasTabs true if the line might contain tabs
Doug Feltc982f602010-05-25 11:51:40 -0700157 * @param tabStops the tabStops. Can be null.
Doug Felte8e45f22010-03-29 14:58:40 -0700158 */
Roozbeh Pournader3630d082017-10-23 12:16:32 -0700159 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
Seigo Nonaka4e90fa22018-02-13 21:40:01 +0000160 public void set(TextPaint paint, CharSequence text, int start, int limit, int dir,
161 Directions directions, boolean hasTabs, TabStops tabStops) {
Doug Felte8e45f22010-03-29 14:58:40 -0700162 mPaint = paint;
163 mText = text;
164 mStart = start;
165 mLen = limit - start;
166 mDir = dir;
167 mDirections = directions;
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -0700168 if (mDirections == null) {
169 throw new IllegalArgumentException("Directions cannot be null");
170 }
Doug Felte8e45f22010-03-29 14:58:40 -0700171 mHasTabs = hasTabs;
172 mSpanned = null;
Doug Felte8e45f22010-03-29 14:58:40 -0700173
174 boolean hasReplacement = false;
175 if (text instanceof Spanned) {
176 mSpanned = (Spanned) text;
Gilles Debunnec1f44832011-12-08 16:03:00 -0800177 mReplacementSpanSpanSet.init(mSpanned, start, limit);
178 hasReplacement = mReplacementSpanSpanSet.numberOfSpans > 0;
Doug Felte8e45f22010-03-29 14:58:40 -0700179 }
180
Seigo Nonakabeafa1f2018-02-01 21:39:24 -0800181 mComputed = null;
182 if (text instanceof PrecomputedText) {
Seigo Nonaka53145632018-03-23 17:22:54 -0700183 // Here, no need to check line break strategy or hyphenation frequency since there is no
184 // line break concept here.
Seigo Nonakabeafa1f2018-02-01 21:39:24 -0800185 mComputed = (PrecomputedText) text;
Seigo Nonaka53145632018-03-23 17:22:54 -0700186 if (!mComputed.getParams().getTextPaint().equalsForTextMeasurement(paint)) {
187 mComputed = null;
188 }
Seigo Nonaka4e90fa22018-02-13 21:40:01 +0000189 }
190
Gilles Debunne1e3ac182011-03-08 14:22:34 -0800191 mCharsValid = hasReplacement || hasTabs || directions != Layout.DIRS_ALL_LEFT_TO_RIGHT;
Doug Felte8e45f22010-03-29 14:58:40 -0700192
193 if (mCharsValid) {
194 if (mChars == null || mChars.length < mLen) {
Adam Lesinski776abc22014-03-07 11:30:59 -0500195 mChars = ArrayUtils.newUnpaddedCharArray(mLen);
Doug Felte8e45f22010-03-29 14:58:40 -0700196 }
197 TextUtils.getChars(text, start, limit, mChars, 0);
Doug Felt0c702b82010-05-14 10:55:42 -0700198 if (hasReplacement) {
199 // Handle these all at once so we don't have to do it as we go.
200 // Replace the first character of each replacement run with the
201 // object-replacement character and the remainder with zero width
202 // non-break space aka BOM. Cursor movement code skips these
203 // zero-width characters.
204 char[] chars = mChars;
205 for (int i = start, inext; i < limit; i = inext) {
Gilles Debunnec1f44832011-12-08 16:03:00 -0800206 inext = mReplacementSpanSpanSet.getNextTransition(i, limit);
207 if (mReplacementSpanSpanSet.hasSpansIntersecting(i, inext)) {
Gilles Debunne1e3ac182011-03-08 14:22:34 -0800208 // transition into a span
Doug Felt0c702b82010-05-14 10:55:42 -0700209 chars[i - start] = '\ufffc';
210 for (int j = i - start + 1, e = inext - start; j < e; ++j) {
211 chars[j] = '\ufeff'; // used as ZWNBS, marks positions to skip
212 }
213 }
214 }
215 }
Doug Felte8e45f22010-03-29 14:58:40 -0700216 }
Doug Feltc982f602010-05-25 11:51:40 -0700217 mTabs = tabStops;
Seigo Nonaka09da71a2016-11-28 16:24:14 +0900218 mAddedWidth = 0;
219 }
220
221 /**
222 * Justify the line to the given width.
223 */
Roozbeh Pournader3630d082017-10-23 12:16:32 -0700224 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
225 public void justify(float justifyWidth) {
Seigo Nonaka09da71a2016-11-28 16:24:14 +0900226 int end = mLen;
227 while (end > 0 && isLineEndSpace(mText.charAt(mStart + end - 1))) {
228 end--;
229 }
230 final int spaces = countStretchableSpaces(0, end);
231 if (spaces == 0) {
232 // There are no stretchable spaces, so we can't help the justification by adding any
233 // width.
234 return;
235 }
236 final float width = Math.abs(measure(end, false, null));
237 mAddedWidth = (justifyWidth - width) / spaces;
Doug Felte8e45f22010-03-29 14:58:40 -0700238 }
239
240 /**
241 * Renders the TextLine.
242 *
243 * @param c the canvas to render on
244 * @param x the leading margin position
245 * @param top the top of the line
246 * @param y the baseline
247 * @param bottom the bottom of the line
248 */
249 void draw(Canvas c, float x, int top, int y, int bottom) {
250 if (!mHasTabs) {
251 if (mDirections == Layout.DIRS_ALL_LEFT_TO_RIGHT) {
Romain Guybc7cdb62011-05-26 18:48:49 -0700252 drawRun(c, 0, mLen, false, x, top, y, bottom, false);
Doug Felte8e45f22010-03-29 14:58:40 -0700253 return;
254 }
255 if (mDirections == Layout.DIRS_ALL_RIGHT_TO_LEFT) {
Romain Guybc7cdb62011-05-26 18:48:49 -0700256 drawRun(c, 0, mLen, true, x, top, y, bottom, false);
Doug Felte8e45f22010-03-29 14:58:40 -0700257 return;
258 }
259 }
260
261 float h = 0;
262 int[] runs = mDirections.mDirections;
Doug Felte8e45f22010-03-29 14:58:40 -0700263
264 int lastRunIndex = runs.length - 2;
265 for (int i = 0; i < runs.length; i += 2) {
266 int runStart = runs[i];
267 int runLimit = runStart + (runs[i+1] & Layout.RUN_LENGTH_MASK);
268 if (runLimit > mLen) {
269 runLimit = mLen;
270 }
271 boolean runIsRtl = (runs[i+1] & Layout.RUN_RTL_FLAG) != 0;
272
273 int segstart = runStart;
Doug Felte8e45f22010-03-29 14:58:40 -0700274 for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
275 int codept = 0;
Doug Felte8e45f22010-03-29 14:58:40 -0700276 if (mHasTabs && j < runLimit) {
277 codept = mChars[j];
Roozbeh Pournader112d9c72015-08-07 12:44:41 -0700278 if (codept >= 0xD800 && codept < 0xDC00 && j + 1 < runLimit) {
Doug Felte8e45f22010-03-29 14:58:40 -0700279 codept = Character.codePointAt(mChars, j);
Roozbeh Pournader112d9c72015-08-07 12:44:41 -0700280 if (codept > 0xFFFF) {
Doug Felte8e45f22010-03-29 14:58:40 -0700281 ++j;
282 continue;
283 }
284 }
285 }
286
Roozbeh Pournader112d9c72015-08-07 12:44:41 -0700287 if (j == runLimit || codept == '\t') {
Romain Guybc7cdb62011-05-26 18:48:49 -0700288 h += drawRun(c, segstart, j, runIsRtl, x+h, top, y, bottom,
Doug Felte8e45f22010-03-29 14:58:40 -0700289 i != lastRunIndex || j != mLen);
290
291 if (codept == '\t') {
292 h = mDir * nextTab(h * mDir);
Doug Felte8e45f22010-03-29 14:58:40 -0700293 }
294 segstart = j + 1;
295 }
296 }
297 }
298 }
299
300 /**
301 * Returns metrics information for the entire line.
302 *
303 * @param fmi receives font metrics information, can be null
304 * @return the signed width of the line
305 */
Roozbeh Pournader3630d082017-10-23 12:16:32 -0700306 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
307 public float metrics(FontMetricsInt fmi) {
Doug Felte8e45f22010-03-29 14:58:40 -0700308 return measure(mLen, false, fmi);
309 }
310
311 /**
312 * Returns information about a position on the line.
313 *
314 * @param offset the line-relative character offset, between 0 and the
315 * line length, inclusive
316 * @param trailing true to measure the trailing edge of the character
317 * before offset, false to measure the leading edge of the character
318 * at offset.
319 * @param fmi receives metrics information about the requested
320 * character, can be null.
321 * @return the signed offset from the leading margin to the requested
322 * character edge.
323 */
324 float measure(int offset, boolean trailing, FontMetricsInt fmi) {
325 int target = trailing ? offset - 1 : offset;
326 if (target < 0) {
327 return 0;
328 }
329
330 float h = 0;
331
332 if (!mHasTabs) {
333 if (mDirections == Layout.DIRS_ALL_LEFT_TO_RIGHT) {
Romain Guybc7cdb62011-05-26 18:48:49 -0700334 return measureRun(0, offset, mLen, false, fmi);
Doug Felte8e45f22010-03-29 14:58:40 -0700335 }
336 if (mDirections == Layout.DIRS_ALL_RIGHT_TO_LEFT) {
Romain Guybc7cdb62011-05-26 18:48:49 -0700337 return measureRun(0, offset, mLen, true, fmi);
Doug Felte8e45f22010-03-29 14:58:40 -0700338 }
339 }
340
341 char[] chars = mChars;
342 int[] runs = mDirections.mDirections;
343 for (int i = 0; i < runs.length; i += 2) {
344 int runStart = runs[i];
345 int runLimit = runStart + (runs[i+1] & Layout.RUN_LENGTH_MASK);
346 if (runLimit > mLen) {
347 runLimit = mLen;
348 }
349 boolean runIsRtl = (runs[i+1] & Layout.RUN_RTL_FLAG) != 0;
350
351 int segstart = runStart;
352 for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
353 int codept = 0;
Doug Felte8e45f22010-03-29 14:58:40 -0700354 if (mHasTabs && j < runLimit) {
355 codept = chars[j];
Roozbeh Pournader112d9c72015-08-07 12:44:41 -0700356 if (codept >= 0xD800 && codept < 0xDC00 && j + 1 < runLimit) {
Doug Felte8e45f22010-03-29 14:58:40 -0700357 codept = Character.codePointAt(chars, j);
Roozbeh Pournader112d9c72015-08-07 12:44:41 -0700358 if (codept > 0xFFFF) {
Doug Felte8e45f22010-03-29 14:58:40 -0700359 ++j;
360 continue;
361 }
362 }
363 }
364
Roozbeh Pournader112d9c72015-08-07 12:44:41 -0700365 if (j == runLimit || codept == '\t') {
Doug Felte8e45f22010-03-29 14:58:40 -0700366 boolean inSegment = target >= segstart && target < j;
367
368 boolean advance = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl;
369 if (inSegment && advance) {
Siyamed Sinira273a702017-10-05 11:22:12 -0700370 return h + measureRun(segstart, offset, j, runIsRtl, fmi);
Doug Felte8e45f22010-03-29 14:58:40 -0700371 }
372
Romain Guybc7cdb62011-05-26 18:48:49 -0700373 float w = measureRun(segstart, j, j, runIsRtl, fmi);
Doug Felte8e45f22010-03-29 14:58:40 -0700374 h += advance ? w : -w;
375
376 if (inSegment) {
Siyamed Sinira273a702017-10-05 11:22:12 -0700377 return h + measureRun(segstart, offset, j, runIsRtl, null);
Doug Felte8e45f22010-03-29 14:58:40 -0700378 }
379
380 if (codept == '\t') {
381 if (offset == j) {
382 return h;
383 }
384 h = mDir * nextTab(h * mDir);
385 if (target == j) {
386 return h;
387 }
388 }
389
Doug Felte8e45f22010-03-29 14:58:40 -0700390 segstart = j + 1;
391 }
392 }
393 }
394
395 return h;
396 }
397
398 /**
Mihai Popa138b1062018-05-09 17:31:48 +0100399 * @see #measure(int, boolean, FontMetricsInt)
400 * @return The measure results for all possible offsets
401 */
402 float[] measureAllOffsets(boolean[] trailing, FontMetricsInt fmi) {
403 float[] measurement = new float[mLen + 1];
404
405 int[] target = new int[mLen + 1];
406 for (int offset = 0; offset < target.length; ++offset) {
407 target[offset] = trailing[offset] ? offset - 1 : offset;
408 }
409 if (target[0] < 0) {
410 measurement[0] = 0;
411 }
412
413 float h = 0;
414
415 if (!mHasTabs) {
416 if (mDirections == Layout.DIRS_ALL_LEFT_TO_RIGHT) {
417 for (int offset = 0; offset <= mLen; ++offset) {
418 measurement[offset] = measureRun(0, offset, mLen, false, fmi);
419 }
420 return measurement;
421 }
422 if (mDirections == Layout.DIRS_ALL_RIGHT_TO_LEFT) {
423 for (int offset = 0; offset <= mLen; ++offset) {
424 measurement[offset] = measureRun(0, offset, mLen, true, fmi);
425 }
426 return measurement;
427 }
428 }
429
430 char[] chars = mChars;
431 int[] runs = mDirections.mDirections;
432 for (int i = 0; i < runs.length; i += 2) {
433 int runStart = runs[i];
434 int runLimit = runStart + (runs[i + 1] & Layout.RUN_LENGTH_MASK);
435 if (runLimit > mLen) {
436 runLimit = mLen;
437 }
438 boolean runIsRtl = (runs[i + 1] & Layout.RUN_RTL_FLAG) != 0;
439
440 int segstart = runStart;
441 for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; ++j) {
442 int codept = 0;
443 if (mHasTabs && j < runLimit) {
444 codept = chars[j];
445 if (codept >= 0xD800 && codept < 0xDC00 && j + 1 < runLimit) {
446 codept = Character.codePointAt(chars, j);
447 if (codept > 0xFFFF) {
448 ++j;
449 continue;
450 }
451 }
452 }
453
454 if (j == runLimit || codept == '\t') {
455 float oldh = h;
456 boolean advance = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl;
457 float w = measureRun(segstart, j, j, runIsRtl, fmi);
458 h += advance ? w : -w;
459
460 float baseh = advance ? oldh : h;
461 FontMetricsInt crtfmi = advance ? fmi : null;
462 for (int offset = segstart; offset <= j && offset <= mLen; ++offset) {
463 if (target[offset] >= segstart && target[offset] < j) {
464 measurement[offset] =
465 baseh + measureRun(segstart, offset, j, runIsRtl, crtfmi);
466 }
467 }
468
469 if (codept == '\t') {
470 if (target[j] == j) {
471 measurement[j] = h;
472 }
473 h = mDir * nextTab(h * mDir);
474 if (target[j + 1] == j) {
475 measurement[j + 1] = h;
476 }
477 }
478
479 segstart = j + 1;
480 }
481 }
482 }
483 if (target[mLen] == mLen) {
484 measurement[mLen] = h;
485 }
486
487 return measurement;
488 }
489
490 /**
Doug Felte8e45f22010-03-29 14:58:40 -0700491 * Draws a unidirectional (but possibly multi-styled) run of text.
492 *
Romain Guybc7cdb62011-05-26 18:48:49 -0700493 *
Doug Felte8e45f22010-03-29 14:58:40 -0700494 * @param c the canvas to draw on
Doug Felte8e45f22010-03-29 14:58:40 -0700495 * @param start the line-relative start
496 * @param limit the line-relative limit
497 * @param runIsRtl true if the run is right-to-left
498 * @param x the position of the run that is closest to the leading margin
499 * @param top the top of the line
500 * @param y the baseline
501 * @param bottom the bottom of the line
502 * @param needWidth true if the width value is required.
503 * @return the signed width of the run, based on the paragraph direction.
504 * Only valid if needWidth is true.
505 */
Romain Guybc7cdb62011-05-26 18:48:49 -0700506 private float drawRun(Canvas c, int start,
Doug Felte8e45f22010-03-29 14:58:40 -0700507 int limit, boolean runIsRtl, float x, int top, int y, int bottom,
508 boolean needWidth) {
509
510 if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) {
Romain Guybc7cdb62011-05-26 18:48:49 -0700511 float w = -measureRun(start, limit, limit, runIsRtl, null);
512 handleRun(start, limit, limit, runIsRtl, c, x + w, top,
Doug Felt0c702b82010-05-14 10:55:42 -0700513 y, bottom, null, false);
Doug Felte8e45f22010-03-29 14:58:40 -0700514 return w;
515 }
516
Romain Guybc7cdb62011-05-26 18:48:49 -0700517 return handleRun(start, limit, limit, runIsRtl, c, x, top,
Doug Felt0c702b82010-05-14 10:55:42 -0700518 y, bottom, null, needWidth);
Doug Felte8e45f22010-03-29 14:58:40 -0700519 }
520
521 /**
522 * Measures a unidirectional (but possibly multi-styled) run of text.
523 *
Romain Guybc7cdb62011-05-26 18:48:49 -0700524 *
Doug Felte8e45f22010-03-29 14:58:40 -0700525 * @param start the line-relative start of the run
526 * @param offset the offset to measure to, between start and limit inclusive
527 * @param limit the line-relative limit of the run
528 * @param runIsRtl true if the run is right-to-left
529 * @param fmi receives metrics information about the requested
530 * run, can be null.
531 * @return the signed width from the start of the run to the leading edge
532 * of the character at offset, based on the run (not paragraph) direction
533 */
Romain Guybc7cdb62011-05-26 18:48:49 -0700534 private float measureRun(int start, int offset, int limit, boolean runIsRtl,
535 FontMetricsInt fmi) {
536 return handleRun(start, offset, limit, runIsRtl, null, 0, 0, 0, 0, fmi, true);
Doug Felte8e45f22010-03-29 14:58:40 -0700537 }
538
539 /**
540 * Walk the cursor through this line, skipping conjuncts and
541 * zero-width characters.
542 *
543 * <p>This function cannot properly walk the cursor off the ends of the line
544 * since it does not know about any shaping on the previous/following line
545 * that might affect the cursor position. Callers must either avoid these
546 * situations or handle the result specially.
547 *
Doug Felte8e45f22010-03-29 14:58:40 -0700548 * @param cursor the starting position of the cursor, between 0 and the
549 * length of the line, inclusive
550 * @param toLeft true if the caret is moving to the left.
551 * @return the new offset. If it is less than 0 or greater than the length
552 * of the line, the previous/following line should be examined to get the
553 * actual offset.
554 */
555 int getOffsetToLeftRightOf(int cursor, boolean toLeft) {
556 // 1) The caret marks the leading edge of a character. The character
557 // logically before it might be on a different level, and the active caret
558 // position is on the character at the lower level. If that character
559 // was the previous character, the caret is on its trailing edge.
560 // 2) Take this character/edge and move it in the indicated direction.
561 // This gives you a new character and a new edge.
562 // 3) This position is between two visually adjacent characters. One of
563 // these might be at a lower level. The active position is on the
564 // character at the lower level.
565 // 4) If the active position is on the trailing edge of the character,
566 // the new caret position is the following logical character, else it
567 // is the character.
568
569 int lineStart = 0;
570 int lineEnd = mLen;
571 boolean paraIsRtl = mDir == -1;
572 int[] runs = mDirections.mDirections;
573
574 int runIndex, runLevel = 0, runStart = lineStart, runLimit = lineEnd, newCaret = -1;
575 boolean trailing = false;
576
577 if (cursor == lineStart) {
578 runIndex = -2;
579 } else if (cursor == lineEnd) {
580 runIndex = runs.length;
581 } else {
582 // First, get information about the run containing the character with
583 // the active caret.
584 for (runIndex = 0; runIndex < runs.length; runIndex += 2) {
585 runStart = lineStart + runs[runIndex];
586 if (cursor >= runStart) {
587 runLimit = runStart + (runs[runIndex+1] & Layout.RUN_LENGTH_MASK);
588 if (runLimit > lineEnd) {
589 runLimit = lineEnd;
590 }
591 if (cursor < runLimit) {
592 runLevel = (runs[runIndex+1] >>> Layout.RUN_LEVEL_SHIFT) &
593 Layout.RUN_LEVEL_MASK;
594 if (cursor == runStart) {
595 // The caret is on a run boundary, see if we should
596 // use the position on the trailing edge of the previous
597 // logical character instead.
598 int prevRunIndex, prevRunLevel, prevRunStart, prevRunLimit;
599 int pos = cursor - 1;
600 for (prevRunIndex = 0; prevRunIndex < runs.length; prevRunIndex += 2) {
601 prevRunStart = lineStart + runs[prevRunIndex];
602 if (pos >= prevRunStart) {
603 prevRunLimit = prevRunStart +
604 (runs[prevRunIndex+1] & Layout.RUN_LENGTH_MASK);
605 if (prevRunLimit > lineEnd) {
606 prevRunLimit = lineEnd;
607 }
608 if (pos < prevRunLimit) {
609 prevRunLevel = (runs[prevRunIndex+1] >>> Layout.RUN_LEVEL_SHIFT)
610 & Layout.RUN_LEVEL_MASK;
611 if (prevRunLevel < runLevel) {
612 // Start from logically previous character.
613 runIndex = prevRunIndex;
614 runLevel = prevRunLevel;
615 runStart = prevRunStart;
616 runLimit = prevRunLimit;
617 trailing = true;
618 break;
619 }
620 }
621 }
622 }
623 }
624 break;
625 }
626 }
627 }
628
629 // caret might be == lineEnd. This is generally a space or paragraph
630 // separator and has an associated run, but might be the end of
631 // text, in which case it doesn't. If that happens, we ran off the
632 // end of the run list, and runIndex == runs.length. In this case,
633 // we are at a run boundary so we skip the below test.
634 if (runIndex != runs.length) {
635 boolean runIsRtl = (runLevel & 0x1) != 0;
636 boolean advance = toLeft == runIsRtl;
637 if (cursor != (advance ? runLimit : runStart) || advance != trailing) {
638 // Moving within or into the run, so we can move logically.
Doug Felt0c702b82010-05-14 10:55:42 -0700639 newCaret = getOffsetBeforeAfter(runIndex, runStart, runLimit,
640 runIsRtl, cursor, advance);
Doug Felte8e45f22010-03-29 14:58:40 -0700641 // If the new position is internal to the run, we're at the strong
642 // position already so we're finished.
643 if (newCaret != (advance ? runLimit : runStart)) {
644 return newCaret;
645 }
646 }
647 }
648 }
649
650 // If newCaret is -1, we're starting at a run boundary and crossing
651 // into another run. Otherwise we've arrived at a run boundary, and
652 // need to figure out which character to attach to. Note we might
653 // need to run this twice, if we cross a run boundary and end up at
654 // another run boundary.
655 while (true) {
656 boolean advance = toLeft == paraIsRtl;
657 int otherRunIndex = runIndex + (advance ? 2 : -2);
658 if (otherRunIndex >= 0 && otherRunIndex < runs.length) {
659 int otherRunStart = lineStart + runs[otherRunIndex];
660 int otherRunLimit = otherRunStart +
661 (runs[otherRunIndex+1] & Layout.RUN_LENGTH_MASK);
662 if (otherRunLimit > lineEnd) {
663 otherRunLimit = lineEnd;
664 }
665 int otherRunLevel = (runs[otherRunIndex+1] >>> Layout.RUN_LEVEL_SHIFT) &
666 Layout.RUN_LEVEL_MASK;
667 boolean otherRunIsRtl = (otherRunLevel & 1) != 0;
668
669 advance = toLeft == otherRunIsRtl;
670 if (newCaret == -1) {
Doug Felt0c702b82010-05-14 10:55:42 -0700671 newCaret = getOffsetBeforeAfter(otherRunIndex, otherRunStart,
672 otherRunLimit, otherRunIsRtl,
Doug Felte8e45f22010-03-29 14:58:40 -0700673 advance ? otherRunStart : otherRunLimit, advance);
674 if (newCaret == (advance ? otherRunLimit : otherRunStart)) {
675 // Crossed and ended up at a new boundary,
676 // repeat a second and final time.
677 runIndex = otherRunIndex;
678 runLevel = otherRunLevel;
679 continue;
680 }
681 break;
682 }
683
684 // The new caret is at a boundary.
685 if (otherRunLevel < runLevel) {
686 // The strong character is in the other run.
687 newCaret = advance ? otherRunStart : otherRunLimit;
688 }
689 break;
690 }
691
692 if (newCaret == -1) {
693 // We're walking off the end of the line. The paragraph
694 // level is always equal to or lower than any internal level, so
695 // the boundaries get the strong caret.
Doug Felt0c702b82010-05-14 10:55:42 -0700696 newCaret = advance ? mLen + 1 : -1;
Doug Felte8e45f22010-03-29 14:58:40 -0700697 break;
698 }
699
700 // Else we've arrived at the end of the line. That's a strong position.
701 // We might have arrived here by crossing over a run with no internal
702 // breaks and dropping out of the above loop before advancing one final
703 // time, so reset the caret.
704 // Note, we use '<=' below to handle a situation where the only run
705 // on the line is a counter-directional run. If we're not advancing,
706 // we can end up at the 'lineEnd' position but the caret we want is at
707 // the lineStart.
708 if (newCaret <= lineEnd) {
709 newCaret = advance ? lineEnd : lineStart;
710 }
711 break;
712 }
713
714 return newCaret;
715 }
716
717 /**
718 * Returns the next valid offset within this directional run, skipping
719 * conjuncts and zero-width characters. This should not be called to walk
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -0700720 * off the end of the line, since the returned values might not be valid
Doug Felt0c702b82010-05-14 10:55:42 -0700721 * on neighboring lines. If the returned offset is less than zero or
722 * greater than the line length, the offset should be recomputed on the
723 * preceding or following line, respectively.
Doug Felte8e45f22010-03-29 14:58:40 -0700724 *
725 * @param runIndex the run index
Doug Felt0c702b82010-05-14 10:55:42 -0700726 * @param runStart the start of the run
727 * @param runLimit the limit of the run
728 * @param runIsRtl true if the run is right-to-left
Doug Felte8e45f22010-03-29 14:58:40 -0700729 * @param offset the offset
730 * @param after true if the new offset should logically follow the provided
731 * offset
732 * @return the new offset
733 */
Doug Felt0c702b82010-05-14 10:55:42 -0700734 private int getOffsetBeforeAfter(int runIndex, int runStart, int runLimit,
735 boolean runIsRtl, int offset, boolean after) {
Doug Felte8e45f22010-03-29 14:58:40 -0700736
Doug Felt0c702b82010-05-14 10:55:42 -0700737 if (runIndex < 0 || offset == (after ? mLen : 0)) {
738 // Walking off end of line. Since we don't know
739 // what cursor positions are available on other lines, we can't
740 // return accurate values. These are a guess.
Doug Felte8e45f22010-03-29 14:58:40 -0700741 if (after) {
Doug Felt0c702b82010-05-14 10:55:42 -0700742 return TextUtils.getOffsetAfter(mText, offset + mStart) - mStart;
743 }
744 return TextUtils.getOffsetBefore(mText, offset + mStart) - mStart;
745 }
746
747 TextPaint wp = mWorkPaint;
748 wp.set(mPaint);
Seigo Nonaka09da71a2016-11-28 16:24:14 +0900749 wp.setWordSpacing(mAddedWidth);
Doug Felt0c702b82010-05-14 10:55:42 -0700750
751 int spanStart = runStart;
752 int spanLimit;
753 if (mSpanned == null) {
754 spanLimit = runLimit;
755 } else {
756 int target = after ? offset + 1 : offset;
757 int limit = mStart + runLimit;
758 while (true) {
759 spanLimit = mSpanned.nextSpanTransition(mStart + spanStart, limit,
760 MetricAffectingSpan.class) - mStart;
761 if (spanLimit >= target) {
762 break;
Doug Felte8e45f22010-03-29 14:58:40 -0700763 }
Doug Felt0c702b82010-05-14 10:55:42 -0700764 spanStart = spanLimit;
765 }
766
767 MetricAffectingSpan[] spans = mSpanned.getSpans(mStart + spanStart,
768 mStart + spanLimit, MetricAffectingSpan.class);
Gilles Debunne1e3ac182011-03-08 14:22:34 -0800769 spans = TextUtils.removeEmptySpans(spans, mSpanned, MetricAffectingSpan.class);
Doug Felt0c702b82010-05-14 10:55:42 -0700770
771 if (spans.length > 0) {
772 ReplacementSpan replacement = null;
773 for (int j = 0; j < spans.length; j++) {
774 MetricAffectingSpan span = spans[j];
775 if (span instanceof ReplacementSpan) {
776 replacement = (ReplacementSpan)span;
777 } else {
778 span.updateMeasureState(wp);
779 }
780 }
781
782 if (replacement != null) {
783 // If we have a replacement span, we're moving either to
784 // the start or end of this span.
785 return after ? spanLimit : spanStart;
Doug Felte8e45f22010-03-29 14:58:40 -0700786 }
787 }
Doug Felte8e45f22010-03-29 14:58:40 -0700788 }
789
Raph Levien051910b2014-06-15 18:25:29 -0700790 int dir = runIsRtl ? Paint.DIRECTION_RTL : Paint.DIRECTION_LTR;
Doug Felt0c702b82010-05-14 10:55:42 -0700791 int cursorOpt = after ? Paint.CURSOR_AFTER : Paint.CURSOR_BEFORE;
792 if (mCharsValid) {
793 return wp.getTextRunCursor(mChars, spanStart, spanLimit - spanStart,
Raph Levien051910b2014-06-15 18:25:29 -0700794 dir, offset, cursorOpt);
Doug Felt0c702b82010-05-14 10:55:42 -0700795 } else {
796 return wp.getTextRunCursor(mText, mStart + spanStart,
Raph Levien051910b2014-06-15 18:25:29 -0700797 mStart + spanLimit, dir, mStart + offset, cursorOpt) - mStart;
Doug Felte8e45f22010-03-29 14:58:40 -0700798 }
Doug Felte8e45f22010-03-29 14:58:40 -0700799 }
800
801 /**
Gilles Debunne0bb00092010-12-02 15:50:26 -0800802 * @param wp
803 */
804 private static void expandMetricsFromPaint(FontMetricsInt fmi, TextPaint wp) {
805 final int previousTop = fmi.top;
806 final int previousAscent = fmi.ascent;
807 final int previousDescent = fmi.descent;
808 final int previousBottom = fmi.bottom;
809 final int previousLeading = fmi.leading;
810
811 wp.getFontMetricsInt(fmi);
812
Fabrice Di Meglio8a5137a2011-09-22 18:49:43 -0700813 updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom,
814 previousLeading);
815 }
816
817 static void updateMetrics(FontMetricsInt fmi, int previousTop, int previousAscent,
818 int previousDescent, int previousBottom, int previousLeading) {
Gilles Debunne0bb00092010-12-02 15:50:26 -0800819 fmi.top = Math.min(fmi.top, previousTop);
820 fmi.ascent = Math.min(fmi.ascent, previousAscent);
821 fmi.descent = Math.max(fmi.descent, previousDescent);
822 fmi.bottom = Math.max(fmi.bottom, previousBottom);
823 fmi.leading = Math.max(fmi.leading, previousLeading);
824 }
825
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700826 private static void drawStroke(TextPaint wp, Canvas c, int color, float position,
827 float thickness, float xleft, float xright, float baseline) {
828 final float strokeTop = baseline + wp.baselineShift + position;
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700829
830 final int previousColor = wp.getColor();
831 final Paint.Style previousStyle = wp.getStyle();
832 final boolean previousAntiAlias = wp.isAntiAlias();
833
834 wp.setStyle(Paint.Style.FILL);
835 wp.setAntiAlias(true);
836
837 wp.setColor(color);
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700838 c.drawRect(xleft, strokeTop, xright, strokeTop + thickness, wp);
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700839
840 wp.setStyle(previousStyle);
841 wp.setColor(previousColor);
842 wp.setAntiAlias(previousAntiAlias);
843 }
844
845 private float getRunAdvance(TextPaint wp, int start, int end, int contextStart, int contextEnd,
846 boolean runIsRtl, int offset) {
847 if (mCharsValid) {
848 return wp.getRunAdvance(mChars, start, end, contextStart, contextEnd, runIsRtl, offset);
849 } else {
850 final int delta = mStart;
Seigo Nonakabeafa1f2018-02-01 21:39:24 -0800851 if (mComputed == null) {
Seigo Nonaka783f9612018-01-20 12:11:13 -0800852 // TODO: Enable measured getRunAdvance for ReplacementSpan and RTL text.
853 return wp.getRunAdvance(mText, delta + start, delta + end,
854 delta + contextStart, delta + contextEnd, runIsRtl, delta + offset);
855 } else {
Seigo Nonakabeafa1f2018-02-01 21:39:24 -0800856 return mComputed.getWidth(start + delta, end + delta);
Seigo Nonaka783f9612018-01-20 12:11:13 -0800857 }
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700858 }
859 }
860
Gilles Debunne0bb00092010-12-02 15:50:26 -0800861 /**
Doug Felte8e45f22010-03-29 14:58:40 -0700862 * Utility function for measuring and rendering text. The text must
Roozbeh Pournader112d9c72015-08-07 12:44:41 -0700863 * not include a tab.
Doug Felte8e45f22010-03-29 14:58:40 -0700864 *
865 * @param wp the working paint
866 * @param start the start of the text
Doug Felt0c702b82010-05-14 10:55:42 -0700867 * @param end the end of the text
Doug Felte8e45f22010-03-29 14:58:40 -0700868 * @param runIsRtl true if the run is right-to-left
869 * @param c the canvas, can be null if rendering is not needed
870 * @param x the edge of the run closest to the leading margin
871 * @param top the top of the line
872 * @param y the baseline
873 * @param bottom the bottom of the line
874 * @param fmi receives metrics information, can be null
875 * @param needWidth true if the width of the run is needed
Raph Levien909c7bc2015-11-30 21:05:46 -0800876 * @param offset the offset for the purpose of measuring
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700877 * @param decorations the list of locations and paremeters for drawing decorations
Doug Felte8e45f22010-03-29 14:58:40 -0700878 * @return the signed width of the run based on the run direction; only
879 * valid if needWidth is true
880 */
Doug Felt0c702b82010-05-14 10:55:42 -0700881 private float handleText(TextPaint wp, int start, int end,
882 int contextStart, int contextEnd, boolean runIsRtl,
883 Canvas c, float x, int top, int y, int bottom,
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700884 FontMetricsInt fmi, boolean needWidth, int offset,
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700885 @Nullable ArrayList<DecorationInfo> decorations) {
Doug Felte8e45f22010-03-29 14:58:40 -0700886
Seigo Nonaka09da71a2016-11-28 16:24:14 +0900887 wp.setWordSpacing(mAddedWidth);
Fabrice Di Meglio850dffa2011-08-08 09:45:09 -0700888 // Get metrics first (even for empty strings or "0" width runs)
889 if (fmi != null) {
890 expandMetricsFromPaint(fmi, wp);
891 }
Doug Felte8e45f22010-03-29 14:58:40 -0700892
Fabrice Di Meglio850dffa2011-08-08 09:45:09 -0700893 // No need to do anything if the run width is "0"
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700894 if (end == start) {
Fabrice Di Meglio850dffa2011-08-08 09:45:09 -0700895 return 0f;
896 }
897
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700898 float totalWidth = 0;
Fabrice Di Meglio850dffa2011-08-08 09:45:09 -0700899
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700900 final int numDecorations = decorations == null ? 0 : decorations.size();
901 if (needWidth || (c != null && (wp.bgColor != 0 || numDecorations != 0 || runIsRtl))) {
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700902 totalWidth = getRunAdvance(wp, start, end, contextStart, contextEnd, runIsRtl, offset);
Doug Felte8e45f22010-03-29 14:58:40 -0700903 }
904
Doug Felte8e45f22010-03-29 14:58:40 -0700905 if (c != null) {
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700906 final float leftX, rightX;
Doug Felte8e45f22010-03-29 14:58:40 -0700907 if (runIsRtl) {
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700908 leftX = x - totalWidth;
909 rightX = x;
910 } else {
911 leftX = x;
912 rightX = x + totalWidth;
Doug Felte8e45f22010-03-29 14:58:40 -0700913 }
914
915 if (wp.bgColor != 0) {
Gilles Debunnedd8f5ed2011-08-10 18:25:49 -0700916 int previousColor = wp.getColor();
917 Paint.Style previousStyle = wp.getStyle();
918
Doug Felte8e45f22010-03-29 14:58:40 -0700919 wp.setColor(wp.bgColor);
920 wp.setStyle(Paint.Style.FILL);
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700921 c.drawRect(leftX, top, rightX, bottom, wp);
Doug Felte8e45f22010-03-29 14:58:40 -0700922
Gilles Debunnedd8f5ed2011-08-10 18:25:49 -0700923 wp.setStyle(previousStyle);
924 wp.setColor(previousColor);
925 }
926
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700927 if (numDecorations != 0) {
928 for (int i = 0; i < numDecorations; i++) {
929 final DecorationInfo info = decorations.get(i);
Gilles Debunnedd8f5ed2011-08-10 18:25:49 -0700930
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700931 final int decorationStart = Math.max(info.start, start);
932 final int decorationEnd = Math.min(info.end, offset);
933 float decorationStartAdvance = getRunAdvance(
934 wp, start, end, contextStart, contextEnd, runIsRtl, decorationStart);
935 float decorationEndAdvance = getRunAdvance(
936 wp, start, end, contextStart, contextEnd, runIsRtl, decorationEnd);
937 final float decorationXLeft, decorationXRight;
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700938 if (runIsRtl) {
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700939 decorationXLeft = rightX - decorationEndAdvance;
940 decorationXRight = rightX - decorationStartAdvance;
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700941 } else {
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700942 decorationXLeft = leftX + decorationStartAdvance;
943 decorationXRight = leftX + decorationEndAdvance;
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700944 }
Siyamed Sinir702c9f92017-06-01 18:25:40 +0000945
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700946 // Theoretically, there could be cases where both Paint's and TextPaint's
947 // setUnderLineText() are called. For backward compatibility, we need to draw
948 // both underlines, the one with custom color first.
949 if (info.underlineColor != 0) {
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700950 drawStroke(wp, c, info.underlineColor, wp.getUnderlinePosition(),
951 info.underlineThickness, decorationXLeft, decorationXRight, y);
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700952 }
953 if (info.isUnderlineText) {
Roozbeh Pournaderca8a04a2017-06-06 18:30:29 -0700954 final float thickness =
Siyamed Sinira273a702017-10-05 11:22:12 -0700955 Math.max(wp.getUnderlineThickness(), 1.0f);
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700956 drawStroke(wp, c, wp.getColor(), wp.getUnderlinePosition(), thickness,
957 decorationXLeft, decorationXRight, y);
958 }
959
960 if (info.isStrikeThruText) {
961 final float thickness =
Siyamed Sinira273a702017-10-05 11:22:12 -0700962 Math.max(wp.getStrikeThruThickness(), 1.0f);
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700963 drawStroke(wp, c, wp.getColor(), wp.getStrikeThruPosition(), thickness,
964 decorationXLeft, decorationXRight, y);
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700965 }
966 }
Doug Felte8e45f22010-03-29 14:58:40 -0700967 }
968
Fabrice Di Meglioda12f382013-03-15 11:26:56 -0700969 drawTextRun(c, wp, start, end, contextStart, contextEnd, runIsRtl,
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700970 leftX, y + wp.baselineShift);
Doug Felte8e45f22010-03-29 14:58:40 -0700971 }
972
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700973 return runIsRtl ? -totalWidth : totalWidth;
Doug Felte8e45f22010-03-29 14:58:40 -0700974 }
975
976 /**
977 * Utility function for measuring and rendering a replacement.
978 *
Romain Guybc7cdb62011-05-26 18:48:49 -0700979 *
Doug Felte8e45f22010-03-29 14:58:40 -0700980 * @param replacement the replacement
981 * @param wp the work paint
Doug Felte8e45f22010-03-29 14:58:40 -0700982 * @param start the start of the run
983 * @param limit the limit of the run
984 * @param runIsRtl true if the run is right-to-left
985 * @param c the canvas, can be null if not rendering
986 * @param x the edge of the replacement closest to the leading margin
987 * @param top the top of the line
988 * @param y the baseline
989 * @param bottom the bottom of the line
990 * @param fmi receives metrics information, can be null
991 * @param needWidth true if the width of the replacement is needed
Doug Felte8e45f22010-03-29 14:58:40 -0700992 * @return the signed width of the run based on the run direction; only
993 * valid if needWidth is true
994 */
995 private float handleReplacement(ReplacementSpan replacement, TextPaint wp,
Romain Guybc7cdb62011-05-26 18:48:49 -0700996 int start, int limit, boolean runIsRtl, Canvas c,
Doug Felte8e45f22010-03-29 14:58:40 -0700997 float x, int top, int y, int bottom, FontMetricsInt fmi,
Doug Felt0c702b82010-05-14 10:55:42 -0700998 boolean needWidth) {
Doug Felte8e45f22010-03-29 14:58:40 -0700999
1000 float ret = 0;
1001
Doug Felt0c702b82010-05-14 10:55:42 -07001002 int textStart = mStart + start;
1003 int textLimit = mStart + limit;
1004
1005 if (needWidth || (c != null && runIsRtl)) {
Fabrice Di Meglio8a5137a2011-09-22 18:49:43 -07001006 int previousTop = 0;
1007 int previousAscent = 0;
1008 int previousDescent = 0;
1009 int previousBottom = 0;
1010 int previousLeading = 0;
1011
1012 boolean needUpdateMetrics = (fmi != null);
1013
1014 if (needUpdateMetrics) {
1015 previousTop = fmi.top;
1016 previousAscent = fmi.ascent;
1017 previousDescent = fmi.descent;
1018 previousBottom = fmi.bottom;
1019 previousLeading = fmi.leading;
1020 }
1021
Doug Felt0c702b82010-05-14 10:55:42 -07001022 ret = replacement.getSize(wp, mText, textStart, textLimit, fmi);
Fabrice Di Meglio8a5137a2011-09-22 18:49:43 -07001023
1024 if (needUpdateMetrics) {
1025 updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom,
1026 previousLeading);
1027 }
Doug Felte8e45f22010-03-29 14:58:40 -07001028 }
1029
Doug Felt0c702b82010-05-14 10:55:42 -07001030 if (c != null) {
1031 if (runIsRtl) {
1032 x -= ret;
Doug Felte8e45f22010-03-29 14:58:40 -07001033 }
Doug Felt0c702b82010-05-14 10:55:42 -07001034 replacement.draw(c, mText, textStart, textLimit,
1035 x, top, y, bottom, wp);
Doug Felte8e45f22010-03-29 14:58:40 -07001036 }
1037
1038 return runIsRtl ? -ret : ret;
1039 }
1040
Roozbeh Pournader46c6f4c2017-02-21 12:18:31 -08001041 private int adjustHyphenEdit(int start, int limit, int hyphenEdit) {
1042 int result = hyphenEdit;
1043 // Only draw hyphens on first or last run in line. Disable them otherwise.
1044 if (start > 0) { // not the first run
1045 result &= ~Paint.HYPHENEDIT_MASK_START_OF_LINE;
1046 }
1047 if (limit < mLen) { // not the last run
1048 result &= ~Paint.HYPHENEDIT_MASK_END_OF_LINE;
1049 }
1050 return result;
1051 }
1052
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001053 private static final class DecorationInfo {
1054 public boolean isStrikeThruText;
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001055 public boolean isUnderlineText;
1056 public int underlineColor;
1057 public float underlineThickness;
1058 public int start = -1;
1059 public int end = -1;
1060
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001061 public boolean hasDecoration() {
1062 return isStrikeThruText || isUnderlineText || underlineColor != 0;
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001063 }
1064
1065 // Copies the info, but not the start and end range.
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001066 public DecorationInfo copyInfo() {
1067 final DecorationInfo copy = new DecorationInfo();
1068 copy.isStrikeThruText = isStrikeThruText;
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001069 copy.isUnderlineText = isUnderlineText;
1070 copy.underlineColor = underlineColor;
1071 copy.underlineThickness = underlineThickness;
1072 return copy;
1073 }
1074 }
1075
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001076 private void extractDecorationInfo(@NonNull TextPaint paint, @NonNull DecorationInfo info) {
1077 info.isStrikeThruText = paint.isStrikeThruText();
1078 if (info.isStrikeThruText) {
1079 paint.setStrikeThruText(false);
1080 }
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001081 info.isUnderlineText = paint.isUnderlineText();
1082 if (info.isUnderlineText) {
1083 paint.setUnderlineText(false);
1084 }
1085 info.underlineColor = paint.underlineColor;
1086 info.underlineThickness = paint.underlineThickness;
1087 paint.setUnderlineText(0, 0.0f);
1088 }
1089
Doug Felte8e45f22010-03-29 14:58:40 -07001090 /**
1091 * Utility function for handling a unidirectional run. The run must not
Roozbeh Pournader112d9c72015-08-07 12:44:41 -07001092 * contain tabs but can contain styles.
Doug Felte8e45f22010-03-29 14:58:40 -07001093 *
Romain Guybc7cdb62011-05-26 18:48:49 -07001094 *
Doug Felte8e45f22010-03-29 14:58:40 -07001095 * @param start the line-relative start of the run
Doug Felt0c702b82010-05-14 10:55:42 -07001096 * @param measureLimit the offset to measure to, between start and limit inclusive
Doug Felte8e45f22010-03-29 14:58:40 -07001097 * @param limit the limit of the run
1098 * @param runIsRtl true if the run is right-to-left
1099 * @param c the canvas, can be null
1100 * @param x the end of the run closest to the leading margin
1101 * @param top the top of the line
1102 * @param y the baseline
1103 * @param bottom the bottom of the line
1104 * @param fmi receives metrics information, can be null
1105 * @param needWidth true if the width is required
Doug Felte8e45f22010-03-29 14:58:40 -07001106 * @return the signed width of the run based on the run direction; only
1107 * valid if needWidth is true
1108 */
Romain Guybc7cdb62011-05-26 18:48:49 -07001109 private float handleRun(int start, int measureLimit,
Doug Felte8e45f22010-03-29 14:58:40 -07001110 int limit, boolean runIsRtl, Canvas c, float x, int top, int y,
Doug Felt0c702b82010-05-14 10:55:42 -07001111 int bottom, FontMetricsInt fmi, boolean needWidth) {
Doug Felte8e45f22010-03-29 14:58:40 -07001112
Siyamed Sinir9f3958c2016-08-26 15:23:47 -07001113 if (measureLimit < start || measureLimit > limit) {
1114 throw new IndexOutOfBoundsException("measureLimit (" + measureLimit + ") is out of "
1115 + "start (" + start + ") and limit (" + limit + ") bounds");
1116 }
1117
Gilles Debunnef483e512011-04-28 15:08:54 -07001118 // Case of an empty line, make sure we update fmi according to mPaint
1119 if (start == measureLimit) {
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001120 final TextPaint wp = mWorkPaint;
Gilles Debunnef483e512011-04-28 15:08:54 -07001121 wp.set(mPaint);
Fabrice Di Meglio15c097a2011-08-08 14:42:41 -07001122 if (fmi != null) {
1123 expandMetricsFromPaint(fmi, wp);
1124 }
1125 return 0f;
Gilles Debunnef483e512011-04-28 15:08:54 -07001126 }
1127
Roozbeh Pournader08836d42017-06-06 16:23:09 -07001128 final boolean needsSpanMeasurement;
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001129 if (mSpanned == null) {
Roozbeh Pournader08836d42017-06-06 16:23:09 -07001130 needsSpanMeasurement = false;
1131 } else {
1132 mMetricAffectingSpanSpanSet.init(mSpanned, mStart + start, mStart + limit);
1133 mCharacterStyleSpanSet.init(mSpanned, mStart + start, mStart + limit);
1134 needsSpanMeasurement = mMetricAffectingSpanSpanSet.numberOfSpans != 0
1135 || mCharacterStyleSpanSet.numberOfSpans != 0;
1136 }
1137
1138 if (!needsSpanMeasurement) {
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001139 final TextPaint wp = mWorkPaint;
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001140 wp.set(mPaint);
Roozbeh Pournader46c6f4c2017-02-21 12:18:31 -08001141 wp.setHyphenEdit(adjustHyphenEdit(start, limit, wp.getHyphenEdit()));
Raph Levien909c7bc2015-11-30 21:05:46 -08001142 return handleText(wp, start, limit, start, limit, runIsRtl, c, x, top,
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001143 y, bottom, fmi, needWidth, measureLimit, null);
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001144 }
1145
Doug Felte8e45f22010-03-29 14:58:40 -07001146 // Shaping needs to take into account context up to metric boundaries,
1147 // but rendering needs to take into account character style boundaries.
Doug Felt0c702b82010-05-14 10:55:42 -07001148 // So we iterate through metric runs to get metric bounds,
1149 // then within each metric run iterate through character style runs
1150 // for the run bounds.
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001151 final float originalX = x;
Doug Felt0c702b82010-05-14 10:55:42 -07001152 for (int i = start, inext; i < measureLimit; i = inext) {
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001153 final TextPaint wp = mWorkPaint;
Doug Felte8e45f22010-03-29 14:58:40 -07001154 wp.set(mPaint);
1155
Gilles Debunnec1f44832011-12-08 16:03:00 -08001156 inext = mMetricAffectingSpanSpanSet.getNextTransition(mStart + i, mStart + limit) -
1157 mStart;
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001158 int mlimit = Math.min(inext, measureLimit);
Doug Felte8e45f22010-03-29 14:58:40 -07001159
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001160 ReplacementSpan replacement = null;
Doug Felte8e45f22010-03-29 14:58:40 -07001161
Gilles Debunnec1f44832011-12-08 16:03:00 -08001162 for (int j = 0; j < mMetricAffectingSpanSpanSet.numberOfSpans; j++) {
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001163 // Both intervals [spanStarts..spanEnds] and [mStart + i..mStart + mlimit] are NOT
1164 // empty by construction. This special case in getSpans() explains the >= & <= tests
Gilles Debunnec1f44832011-12-08 16:03:00 -08001165 if ((mMetricAffectingSpanSpanSet.spanStarts[j] >= mStart + mlimit) ||
1166 (mMetricAffectingSpanSpanSet.spanEnds[j] <= mStart + i)) continue;
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001167 final MetricAffectingSpan span = mMetricAffectingSpanSpanSet.spans[j];
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001168 if (span instanceof ReplacementSpan) {
1169 replacement = (ReplacementSpan)span;
1170 } else {
1171 // We might have a replacement that uses the draw
1172 // state, otherwise measure state would suffice.
1173 span.updateDrawState(wp);
Doug Felte8e45f22010-03-29 14:58:40 -07001174 }
1175 }
1176
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001177 if (replacement != null) {
1178 x += handleReplacement(replacement, wp, i, mlimit, runIsRtl, c, x, top, y,
1179 bottom, fmi, needWidth || mlimit < measureLimit);
1180 continue;
1181 }
1182
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001183 final TextPaint activePaint = mActivePaint;
1184 activePaint.set(mPaint);
1185 int activeStart = i;
1186 int activeEnd = mlimit;
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001187 final DecorationInfo decorationInfo = mDecorationInfo;
1188 mDecorations.clear();
Raph Levien42ef515d2012-10-22 15:01:17 -07001189 for (int j = i, jnext; j < mlimit; j = jnext) {
Siyamed Sinir702c9f92017-06-01 18:25:40 +00001190 jnext = mCharacterStyleSpanSet.getNextTransition(mStart + j, mStart + inext) -
1191 mStart;
Doug Felte8e45f22010-03-29 14:58:40 -07001192
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001193 final int offset = Math.min(jnext, mlimit);
Raph Levien42ef515d2012-10-22 15:01:17 -07001194 wp.set(mPaint);
1195 for (int k = 0; k < mCharacterStyleSpanSet.numberOfSpans; k++) {
1196 // Intentionally using >= and <= as explained above
Siyamed Sinir702c9f92017-06-01 18:25:40 +00001197 if ((mCharacterStyleSpanSet.spanStarts[k] >= mStart + offset) ||
1198 (mCharacterStyleSpanSet.spanEnds[k] <= mStart + j)) continue;
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001199
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001200 final CharacterStyle span = mCharacterStyleSpanSet.spans[k];
Siyamed Sinir702c9f92017-06-01 18:25:40 +00001201 span.updateDrawState(wp);
Doug Felte8e45f22010-03-29 14:58:40 -07001202 }
Raph Levien42ef515d2012-10-22 15:01:17 -07001203
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001204 extractDecorationInfo(wp, decorationInfo);
Roozbeh Pournader46c6f4c2017-02-21 12:18:31 -08001205
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001206 if (j == i) {
1207 // First chunk of text. We can't handle it yet, since we may need to merge it
1208 // with the next chunk. So we just save the TextPaint for future comparisons
1209 // and use.
1210 activePaint.set(wp);
1211 } else if (!wp.hasEqualAttributes(activePaint)) {
1212 // The style of the present chunk of text is substantially different from the
1213 // style of the previous chunk. We need to handle the active piece of text
1214 // and restart with the present chunk.
1215 activePaint.setHyphenEdit(adjustHyphenEdit(
1216 activeStart, activeEnd, mPaint.getHyphenEdit()));
1217 x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, x,
1218 top, y, bottom, fmi, needWidth || activeEnd < measureLimit,
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001219 Math.min(activeEnd, mlimit), mDecorations);
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001220
1221 activeStart = j;
1222 activePaint.set(wp);
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001223 mDecorations.clear();
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001224 } else {
1225 // The present TextPaint is substantially equal to the last TextPaint except
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001226 // perhaps for decorations. We just need to expand the active piece of text to
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001227 // include the present chunk, which we always do anyway. We don't need to save
1228 // wp to activePaint, since they are already equal.
1229 }
1230
1231 activeEnd = jnext;
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001232 if (decorationInfo.hasDecoration()) {
1233 final DecorationInfo copy = decorationInfo.copyInfo();
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001234 copy.start = j;
1235 copy.end = jnext;
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001236 mDecorations.add(copy);
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001237 }
Doug Felte8e45f22010-03-29 14:58:40 -07001238 }
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001239 // Handle the final piece of text.
1240 activePaint.setHyphenEdit(adjustHyphenEdit(
1241 activeStart, activeEnd, mPaint.getHyphenEdit()));
1242 x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, x,
1243 top, y, bottom, fmi, needWidth || activeEnd < measureLimit,
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001244 Math.min(activeEnd, mlimit), mDecorations);
Doug Felte8e45f22010-03-29 14:58:40 -07001245 }
1246
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001247 return x - originalX;
Doug Felte8e45f22010-03-29 14:58:40 -07001248 }
1249
Doug Felte8e45f22010-03-29 14:58:40 -07001250 /**
1251 * Render a text run with the set-up paint.
1252 *
1253 * @param c the canvas
1254 * @param wp the paint used to render the text
Doug Felt0c702b82010-05-14 10:55:42 -07001255 * @param start the start of the run
1256 * @param end the end of the run
1257 * @param contextStart the start of context for the run
1258 * @param contextEnd the end of the context for the run
Fabrice Di Meglioda12f382013-03-15 11:26:56 -07001259 * @param runIsRtl true if the run is right-to-left
Doug Felte8e45f22010-03-29 14:58:40 -07001260 * @param x the x position of the left edge of the run
1261 * @param y the baseline of the run
1262 */
Doug Felt0c702b82010-05-14 10:55:42 -07001263 private void drawTextRun(Canvas c, TextPaint wp, int start, int end,
Fabrice Di Meglioda12f382013-03-15 11:26:56 -07001264 int contextStart, int contextEnd, boolean runIsRtl, float x, int y) {
Doug Felte8e45f22010-03-29 14:58:40 -07001265
Doug Felte8e45f22010-03-29 14:58:40 -07001266 if (mCharsValid) {
Doug Felt0c702b82010-05-14 10:55:42 -07001267 int count = end - start;
1268 int contextCount = contextEnd - contextStart;
1269 c.drawTextRun(mChars, start, count, contextStart, contextCount,
Raph Levien051910b2014-06-15 18:25:29 -07001270 x, y, runIsRtl, wp);
Doug Felte8e45f22010-03-29 14:58:40 -07001271 } else {
Doug Felt0c702b82010-05-14 10:55:42 -07001272 int delta = mStart;
1273 c.drawTextRun(mText, delta + start, delta + end,
Raph Levien051910b2014-06-15 18:25:29 -07001274 delta + contextStart, delta + contextEnd, x, y, runIsRtl, wp);
Doug Felte8e45f22010-03-29 14:58:40 -07001275 }
1276 }
1277
1278 /**
Doug Felte8e45f22010-03-29 14:58:40 -07001279 * Returns the next tab position.
1280 *
1281 * @param h the (unsigned) offset from the leading margin
1282 * @return the (unsigned) tab position after this offset
1283 */
1284 float nextTab(float h) {
Doug Feltc982f602010-05-25 11:51:40 -07001285 if (mTabs != null) {
1286 return mTabs.nextTab(h);
Doug Felte8e45f22010-03-29 14:58:40 -07001287 }
Doug Feltc982f602010-05-25 11:51:40 -07001288 return TabStops.nextDefaultStop(h, TAB_INCREMENT);
Doug Felte8e45f22010-03-29 14:58:40 -07001289 }
1290
Seigo Nonaka09da71a2016-11-28 16:24:14 +09001291 private boolean isStretchableWhitespace(int ch) {
Roozbeh Pournader3630d082017-10-23 12:16:32 -07001292 // TODO: Support NBSP and other stretchable whitespace (b/34013491 and b/68204709).
1293 return ch == 0x0020;
Seigo Nonaka09da71a2016-11-28 16:24:14 +09001294 }
1295
1296 /* Return the number of spaces in the text line, for the purpose of justification */
1297 private int countStretchableSpaces(int start, int end) {
1298 int count = 0;
Roozbeh Pournader3630d082017-10-23 12:16:32 -07001299 for (int i = start; i < end; i++) {
1300 final char c = mCharsValid ? mChars[i] : mText.charAt(i + mStart);
1301 if (isStretchableWhitespace(c)) {
1302 count++;
1303 }
Seigo Nonaka09da71a2016-11-28 16:24:14 +09001304 }
1305 return count;
1306 }
1307
1308 // Note: keep this in sync with Minikin LineBreaker::isLineEndSpace()
1309 public static boolean isLineEndSpace(char ch) {
1310 return ch == ' ' || ch == '\t' || ch == 0x1680
1311 || (0x2000 <= ch && ch <= 0x200A && ch != 0x2007)
1312 || ch == 0x205F || ch == 0x3000;
1313 }
1314
Doug Felte8e45f22010-03-29 14:58:40 -07001315 private static final int TAB_INCREMENT = 20;
1316}