blob: 86cc0141b0a44dd16dd5f3d89b7ca483f7ce6de9 [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;
Doug Felte8e45f22010-03-29 14:58:40 -070021import android.graphics.Canvas;
22import android.graphics.Paint;
Doug Felte8e45f22010-03-29 14:58:40 -070023import android.graphics.Paint.FontMetricsInt;
Doug Felte8e45f22010-03-29 14:58:40 -070024import android.text.Layout.Directions;
Doug Feltc982f602010-05-25 11:51:40 -070025import android.text.Layout.TabStops;
Doug Felte8e45f22010-03-29 14:58:40 -070026import android.text.style.CharacterStyle;
27import android.text.style.MetricAffectingSpan;
28import android.text.style.ReplacementSpan;
Doug Felte8e45f22010-03-29 14:58:40 -070029import android.util.Log;
30
Roozbeh Pournader3630d082017-10-23 12:16:32 -070031import com.android.internal.annotations.VisibleForTesting;
Gilles Debunnedd8f5ed2011-08-10 18:25:49 -070032import com.android.internal.util.ArrayUtils;
33
Roozbeh Pournader538de5b2017-05-24 09:07:52 -070034import java.util.ArrayList;
35
Doug Felte8e45f22010-03-29 14:58:40 -070036/**
37 * Represents a line of styled text, for measuring in visual order and
38 * for rendering.
39 *
40 * <p>Get a new instance using obtain(), and when finished with it, return it
41 * to the pool using recycle().
42 *
43 * <p>Call set to prepare the instance for use, then either draw, measure,
44 * metrics, or caretToLeftRightOf.
45 *
46 * @hide
47 */
Roozbeh Pournader3630d082017-10-23 12:16:32 -070048@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
49public class TextLine {
Romain Guybc7cdb62011-05-26 18:48:49 -070050 private static final boolean DEBUG = false;
51
Doug Felte8e45f22010-03-29 14:58:40 -070052 private TextPaint mPaint;
53 private CharSequence mText;
54 private int mStart;
55 private int mLen;
56 private int mDir;
57 private Directions mDirections;
58 private boolean mHasTabs;
Doug Feltc982f602010-05-25 11:51:40 -070059 private TabStops mTabs;
Doug Felte8e45f22010-03-29 14:58:40 -070060 private char[] mChars;
61 private boolean mCharsValid;
62 private Spanned mSpanned;
Seigo Nonaka09da71a2016-11-28 16:24:14 +090063
64 // Additional width of whitespace for justification. This value is per whitespace, thus
65 // the line width will increase by mAddedWidth x (number of stretchable whitespaces).
66 private float mAddedWidth;
Roozbeh Pournader538de5b2017-05-24 09:07:52 -070067
Gilles Debunne345cb032010-06-16 17:13:23 -070068 private final TextPaint mWorkPaint = new TextPaint();
Roozbeh Pournader538de5b2017-05-24 09:07:52 -070069 private final TextPaint mActivePaint = new TextPaint();
Gilles Debunnec1f44832011-12-08 16:03:00 -080070 private final SpanSet<MetricAffectingSpan> mMetricAffectingSpanSpanSet =
71 new SpanSet<MetricAffectingSpan>(MetricAffectingSpan.class);
72 private final SpanSet<CharacterStyle> mCharacterStyleSpanSet =
73 new SpanSet<CharacterStyle>(CharacterStyle.class);
74 private final SpanSet<ReplacementSpan> mReplacementSpanSpanSet =
75 new SpanSet<ReplacementSpan>(ReplacementSpan.class);
Doug Felte8e45f22010-03-29 14:58:40 -070076
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -070077 private final DecorationInfo mDecorationInfo = new DecorationInfo();
Siyamed Sinira273a702017-10-05 11:22:12 -070078 private final ArrayList<DecorationInfo> mDecorations = new ArrayList<>();
Roozbeh Pournader538de5b2017-05-24 09:07:52 -070079
Romain Guybc7cdb62011-05-26 18:48:49 -070080 private static final TextLine[] sCached = new TextLine[3];
Doug Felte8e45f22010-03-29 14:58:40 -070081
82 /**
83 * Returns a new TextLine from the shared pool.
84 *
85 * @return an uninitialized TextLine
86 */
Roozbeh Pournader3630d082017-10-23 12:16:32 -070087 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
88 public static TextLine obtain() {
Doug Felte8e45f22010-03-29 14:58:40 -070089 TextLine tl;
Romain Guybc7cdb62011-05-26 18:48:49 -070090 synchronized (sCached) {
91 for (int i = sCached.length; --i >= 0;) {
92 if (sCached[i] != null) {
93 tl = sCached[i];
94 sCached[i] = null;
Doug Felte8e45f22010-03-29 14:58:40 -070095 return tl;
96 }
97 }
98 }
99 tl = new TextLine();
Romain Guybc7cdb62011-05-26 18:48:49 -0700100 if (DEBUG) {
101 Log.v("TLINE", "new: " + tl);
102 }
Doug Felte8e45f22010-03-29 14:58:40 -0700103 return tl;
104 }
105
106 /**
107 * Puts a TextLine back into the shared pool. Do not use this TextLine once
108 * it has been returned.
109 * @param tl the textLine
110 * @return null, as a convenience from clearing references to the provided
111 * TextLine
112 */
Roozbeh Pournader3630d082017-10-23 12:16:32 -0700113 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
114 public static TextLine recycle(TextLine tl) {
Doug Felte8e45f22010-03-29 14:58:40 -0700115 tl.mText = null;
116 tl.mPaint = null;
117 tl.mDirections = null;
Svet Ganov893d6fe2015-01-16 10:10:15 -0800118 tl.mSpanned = null;
119 tl.mTabs = null;
120 tl.mChars = null;
Gilles Debunnec3fb7a12011-12-12 14:03:40 -0800121
122 tl.mMetricAffectingSpanSpanSet.recycle();
123 tl.mCharacterStyleSpanSet.recycle();
124 tl.mReplacementSpanSpanSet.recycle();
125
Romain Guybc7cdb62011-05-26 18:48:49 -0700126 synchronized(sCached) {
127 for (int i = 0; i < sCached.length; ++i) {
128 if (sCached[i] == null) {
129 sCached[i] = tl;
Gilles Debunnef902d7b2011-01-25 09:09:46 -0800130 break;
Doug Felte8e45f22010-03-29 14:58:40 -0700131 }
132 }
133 }
134 return null;
135 }
136
137 /**
138 * Initializes a TextLine and prepares it for use.
139 *
140 * @param paint the base paint for the line
141 * @param text the text, can be Styled
142 * @param start the start of the line relative to the text
143 * @param limit the limit of the line relative to the text
144 * @param dir the paragraph direction of this line
145 * @param directions the directions information of this line
Roozbeh Pournader112d9c72015-08-07 12:44:41 -0700146 * @param hasTabs true if the line might contain tabs
Doug Feltc982f602010-05-25 11:51:40 -0700147 * @param tabStops the tabStops. Can be null.
Doug Felte8e45f22010-03-29 14:58:40 -0700148 */
Roozbeh Pournader3630d082017-10-23 12:16:32 -0700149 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
150 public void set(TextPaint paint, CharSequence text, int start, int limit, int dir,
Doug Feltc982f602010-05-25 11:51:40 -0700151 Directions directions, boolean hasTabs, TabStops tabStops) {
Doug Felte8e45f22010-03-29 14:58:40 -0700152 mPaint = paint;
153 mText = text;
154 mStart = start;
155 mLen = limit - start;
156 mDir = dir;
157 mDirections = directions;
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -0700158 if (mDirections == null) {
159 throw new IllegalArgumentException("Directions cannot be null");
160 }
Doug Felte8e45f22010-03-29 14:58:40 -0700161 mHasTabs = hasTabs;
162 mSpanned = null;
Doug Felte8e45f22010-03-29 14:58:40 -0700163
164 boolean hasReplacement = false;
165 if (text instanceof Spanned) {
166 mSpanned = (Spanned) text;
Gilles Debunnec1f44832011-12-08 16:03:00 -0800167 mReplacementSpanSpanSet.init(mSpanned, start, limit);
168 hasReplacement = mReplacementSpanSpanSet.numberOfSpans > 0;
Doug Felte8e45f22010-03-29 14:58:40 -0700169 }
170
Gilles Debunne1e3ac182011-03-08 14:22:34 -0800171 mCharsValid = hasReplacement || hasTabs || directions != Layout.DIRS_ALL_LEFT_TO_RIGHT;
Doug Felte8e45f22010-03-29 14:58:40 -0700172
173 if (mCharsValid) {
174 if (mChars == null || mChars.length < mLen) {
Adam Lesinski776abc22014-03-07 11:30:59 -0500175 mChars = ArrayUtils.newUnpaddedCharArray(mLen);
Doug Felte8e45f22010-03-29 14:58:40 -0700176 }
177 TextUtils.getChars(text, start, limit, mChars, 0);
Doug Felt0c702b82010-05-14 10:55:42 -0700178 if (hasReplacement) {
179 // Handle these all at once so we don't have to do it as we go.
180 // Replace the first character of each replacement run with the
181 // object-replacement character and the remainder with zero width
182 // non-break space aka BOM. Cursor movement code skips these
183 // zero-width characters.
184 char[] chars = mChars;
185 for (int i = start, inext; i < limit; i = inext) {
Gilles Debunnec1f44832011-12-08 16:03:00 -0800186 inext = mReplacementSpanSpanSet.getNextTransition(i, limit);
187 if (mReplacementSpanSpanSet.hasSpansIntersecting(i, inext)) {
Gilles Debunne1e3ac182011-03-08 14:22:34 -0800188 // transition into a span
Doug Felt0c702b82010-05-14 10:55:42 -0700189 chars[i - start] = '\ufffc';
190 for (int j = i - start + 1, e = inext - start; j < e; ++j) {
191 chars[j] = '\ufeff'; // used as ZWNBS, marks positions to skip
192 }
193 }
194 }
195 }
Doug Felte8e45f22010-03-29 14:58:40 -0700196 }
Doug Feltc982f602010-05-25 11:51:40 -0700197 mTabs = tabStops;
Seigo Nonaka09da71a2016-11-28 16:24:14 +0900198 mAddedWidth = 0;
199 }
200
201 /**
202 * Justify the line to the given width.
203 */
Roozbeh Pournader3630d082017-10-23 12:16:32 -0700204 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
205 public void justify(float justifyWidth) {
Seigo Nonaka09da71a2016-11-28 16:24:14 +0900206 int end = mLen;
207 while (end > 0 && isLineEndSpace(mText.charAt(mStart + end - 1))) {
208 end--;
209 }
210 final int spaces = countStretchableSpaces(0, end);
211 if (spaces == 0) {
212 // There are no stretchable spaces, so we can't help the justification by adding any
213 // width.
214 return;
215 }
216 final float width = Math.abs(measure(end, false, null));
217 mAddedWidth = (justifyWidth - width) / spaces;
Doug Felte8e45f22010-03-29 14:58:40 -0700218 }
219
220 /**
221 * Renders the TextLine.
222 *
223 * @param c the canvas to render on
224 * @param x the leading margin position
225 * @param top the top of the line
226 * @param y the baseline
227 * @param bottom the bottom of the line
228 */
229 void draw(Canvas c, float x, int top, int y, int bottom) {
230 if (!mHasTabs) {
231 if (mDirections == Layout.DIRS_ALL_LEFT_TO_RIGHT) {
Romain Guybc7cdb62011-05-26 18:48:49 -0700232 drawRun(c, 0, mLen, false, x, top, y, bottom, false);
Doug Felte8e45f22010-03-29 14:58:40 -0700233 return;
234 }
235 if (mDirections == Layout.DIRS_ALL_RIGHT_TO_LEFT) {
Romain Guybc7cdb62011-05-26 18:48:49 -0700236 drawRun(c, 0, mLen, true, x, top, y, bottom, false);
Doug Felte8e45f22010-03-29 14:58:40 -0700237 return;
238 }
239 }
240
241 float h = 0;
242 int[] runs = mDirections.mDirections;
Doug Felte8e45f22010-03-29 14:58:40 -0700243
244 int lastRunIndex = runs.length - 2;
245 for (int i = 0; i < runs.length; i += 2) {
246 int runStart = runs[i];
247 int runLimit = runStart + (runs[i+1] & Layout.RUN_LENGTH_MASK);
248 if (runLimit > mLen) {
249 runLimit = mLen;
250 }
251 boolean runIsRtl = (runs[i+1] & Layout.RUN_RTL_FLAG) != 0;
252
253 int segstart = runStart;
Doug Felte8e45f22010-03-29 14:58:40 -0700254 for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
255 int codept = 0;
Doug Felte8e45f22010-03-29 14:58:40 -0700256 if (mHasTabs && j < runLimit) {
257 codept = mChars[j];
Roozbeh Pournader112d9c72015-08-07 12:44:41 -0700258 if (codept >= 0xD800 && codept < 0xDC00 && j + 1 < runLimit) {
Doug Felte8e45f22010-03-29 14:58:40 -0700259 codept = Character.codePointAt(mChars, j);
Roozbeh Pournader112d9c72015-08-07 12:44:41 -0700260 if (codept > 0xFFFF) {
Doug Felte8e45f22010-03-29 14:58:40 -0700261 ++j;
262 continue;
263 }
264 }
265 }
266
Roozbeh Pournader112d9c72015-08-07 12:44:41 -0700267 if (j == runLimit || codept == '\t') {
Romain Guybc7cdb62011-05-26 18:48:49 -0700268 h += drawRun(c, segstart, j, runIsRtl, x+h, top, y, bottom,
Doug Felte8e45f22010-03-29 14:58:40 -0700269 i != lastRunIndex || j != mLen);
270
271 if (codept == '\t') {
272 h = mDir * nextTab(h * mDir);
Doug Felte8e45f22010-03-29 14:58:40 -0700273 }
274 segstart = j + 1;
275 }
276 }
277 }
278 }
279
280 /**
281 * Returns metrics information for the entire line.
282 *
283 * @param fmi receives font metrics information, can be null
284 * @return the signed width of the line
285 */
Roozbeh Pournader3630d082017-10-23 12:16:32 -0700286 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
287 public float metrics(FontMetricsInt fmi) {
Doug Felte8e45f22010-03-29 14:58:40 -0700288 return measure(mLen, false, fmi);
289 }
290
291 /**
292 * Returns information about a position on the line.
293 *
294 * @param offset the line-relative character offset, between 0 and the
295 * line length, inclusive
296 * @param trailing true to measure the trailing edge of the character
297 * before offset, false to measure the leading edge of the character
298 * at offset.
299 * @param fmi receives metrics information about the requested
300 * character, can be null.
301 * @return the signed offset from the leading margin to the requested
302 * character edge.
303 */
304 float measure(int offset, boolean trailing, FontMetricsInt fmi) {
305 int target = trailing ? offset - 1 : offset;
306 if (target < 0) {
307 return 0;
308 }
309
310 float h = 0;
311
312 if (!mHasTabs) {
313 if (mDirections == Layout.DIRS_ALL_LEFT_TO_RIGHT) {
Romain Guybc7cdb62011-05-26 18:48:49 -0700314 return measureRun(0, offset, mLen, false, fmi);
Doug Felte8e45f22010-03-29 14:58:40 -0700315 }
316 if (mDirections == Layout.DIRS_ALL_RIGHT_TO_LEFT) {
Romain Guybc7cdb62011-05-26 18:48:49 -0700317 return measureRun(0, offset, mLen, true, fmi);
Doug Felte8e45f22010-03-29 14:58:40 -0700318 }
319 }
320
321 char[] chars = mChars;
322 int[] runs = mDirections.mDirections;
323 for (int i = 0; i < runs.length; i += 2) {
324 int runStart = runs[i];
325 int runLimit = runStart + (runs[i+1] & Layout.RUN_LENGTH_MASK);
326 if (runLimit > mLen) {
327 runLimit = mLen;
328 }
329 boolean runIsRtl = (runs[i+1] & Layout.RUN_RTL_FLAG) != 0;
330
331 int segstart = runStart;
332 for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
333 int codept = 0;
Doug Felte8e45f22010-03-29 14:58:40 -0700334 if (mHasTabs && j < runLimit) {
335 codept = chars[j];
Roozbeh Pournader112d9c72015-08-07 12:44:41 -0700336 if (codept >= 0xD800 && codept < 0xDC00 && j + 1 < runLimit) {
Doug Felte8e45f22010-03-29 14:58:40 -0700337 codept = Character.codePointAt(chars, j);
Roozbeh Pournader112d9c72015-08-07 12:44:41 -0700338 if (codept > 0xFFFF) {
Doug Felte8e45f22010-03-29 14:58:40 -0700339 ++j;
340 continue;
341 }
342 }
343 }
344
Roozbeh Pournader112d9c72015-08-07 12:44:41 -0700345 if (j == runLimit || codept == '\t') {
Doug Felte8e45f22010-03-29 14:58:40 -0700346 boolean inSegment = target >= segstart && target < j;
347
348 boolean advance = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl;
349 if (inSegment && advance) {
Siyamed Sinira273a702017-10-05 11:22:12 -0700350 return h + measureRun(segstart, offset, j, runIsRtl, fmi);
Doug Felte8e45f22010-03-29 14:58:40 -0700351 }
352
Romain Guybc7cdb62011-05-26 18:48:49 -0700353 float w = measureRun(segstart, j, j, runIsRtl, fmi);
Doug Felte8e45f22010-03-29 14:58:40 -0700354 h += advance ? w : -w;
355
356 if (inSegment) {
Siyamed Sinira273a702017-10-05 11:22:12 -0700357 return h + measureRun(segstart, offset, j, runIsRtl, null);
Doug Felte8e45f22010-03-29 14:58:40 -0700358 }
359
360 if (codept == '\t') {
361 if (offset == j) {
362 return h;
363 }
364 h = mDir * nextTab(h * mDir);
365 if (target == j) {
366 return h;
367 }
368 }
369
Doug Felte8e45f22010-03-29 14:58:40 -0700370 segstart = j + 1;
371 }
372 }
373 }
374
375 return h;
376 }
377
378 /**
379 * Draws a unidirectional (but possibly multi-styled) run of text.
380 *
Romain Guybc7cdb62011-05-26 18:48:49 -0700381 *
Doug Felte8e45f22010-03-29 14:58:40 -0700382 * @param c the canvas to draw on
Doug Felte8e45f22010-03-29 14:58:40 -0700383 * @param start the line-relative start
384 * @param limit the line-relative limit
385 * @param runIsRtl true if the run is right-to-left
386 * @param x the position of the run that is closest to the leading margin
387 * @param top the top of the line
388 * @param y the baseline
389 * @param bottom the bottom of the line
390 * @param needWidth true if the width value is required.
391 * @return the signed width of the run, based on the paragraph direction.
392 * Only valid if needWidth is true.
393 */
Romain Guybc7cdb62011-05-26 18:48:49 -0700394 private float drawRun(Canvas c, int start,
Doug Felte8e45f22010-03-29 14:58:40 -0700395 int limit, boolean runIsRtl, float x, int top, int y, int bottom,
396 boolean needWidth) {
397
398 if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) {
Romain Guybc7cdb62011-05-26 18:48:49 -0700399 float w = -measureRun(start, limit, limit, runIsRtl, null);
400 handleRun(start, limit, limit, runIsRtl, c, x + w, top,
Doug Felt0c702b82010-05-14 10:55:42 -0700401 y, bottom, null, false);
Doug Felte8e45f22010-03-29 14:58:40 -0700402 return w;
403 }
404
Romain Guybc7cdb62011-05-26 18:48:49 -0700405 return handleRun(start, limit, limit, runIsRtl, c, x, top,
Doug Felt0c702b82010-05-14 10:55:42 -0700406 y, bottom, null, needWidth);
Doug Felte8e45f22010-03-29 14:58:40 -0700407 }
408
409 /**
410 * Measures a unidirectional (but possibly multi-styled) run of text.
411 *
Romain Guybc7cdb62011-05-26 18:48:49 -0700412 *
Doug Felte8e45f22010-03-29 14:58:40 -0700413 * @param start the line-relative start of the run
414 * @param offset the offset to measure to, between start and limit inclusive
415 * @param limit the line-relative limit of the run
416 * @param runIsRtl true if the run is right-to-left
417 * @param fmi receives metrics information about the requested
418 * run, can be null.
419 * @return the signed width from the start of the run to the leading edge
420 * of the character at offset, based on the run (not paragraph) direction
421 */
Romain Guybc7cdb62011-05-26 18:48:49 -0700422 private float measureRun(int start, int offset, int limit, boolean runIsRtl,
423 FontMetricsInt fmi) {
424 return handleRun(start, offset, limit, runIsRtl, null, 0, 0, 0, 0, fmi, true);
Doug Felte8e45f22010-03-29 14:58:40 -0700425 }
426
427 /**
428 * Walk the cursor through this line, skipping conjuncts and
429 * zero-width characters.
430 *
431 * <p>This function cannot properly walk the cursor off the ends of the line
432 * since it does not know about any shaping on the previous/following line
433 * that might affect the cursor position. Callers must either avoid these
434 * situations or handle the result specially.
435 *
Doug Felte8e45f22010-03-29 14:58:40 -0700436 * @param cursor the starting position of the cursor, between 0 and the
437 * length of the line, inclusive
438 * @param toLeft true if the caret is moving to the left.
439 * @return the new offset. If it is less than 0 or greater than the length
440 * of the line, the previous/following line should be examined to get the
441 * actual offset.
442 */
443 int getOffsetToLeftRightOf(int cursor, boolean toLeft) {
444 // 1) The caret marks the leading edge of a character. The character
445 // logically before it might be on a different level, and the active caret
446 // position is on the character at the lower level. If that character
447 // was the previous character, the caret is on its trailing edge.
448 // 2) Take this character/edge and move it in the indicated direction.
449 // This gives you a new character and a new edge.
450 // 3) This position is between two visually adjacent characters. One of
451 // these might be at a lower level. The active position is on the
452 // character at the lower level.
453 // 4) If the active position is on the trailing edge of the character,
454 // the new caret position is the following logical character, else it
455 // is the character.
456
457 int lineStart = 0;
458 int lineEnd = mLen;
459 boolean paraIsRtl = mDir == -1;
460 int[] runs = mDirections.mDirections;
461
462 int runIndex, runLevel = 0, runStart = lineStart, runLimit = lineEnd, newCaret = -1;
463 boolean trailing = false;
464
465 if (cursor == lineStart) {
466 runIndex = -2;
467 } else if (cursor == lineEnd) {
468 runIndex = runs.length;
469 } else {
470 // First, get information about the run containing the character with
471 // the active caret.
472 for (runIndex = 0; runIndex < runs.length; runIndex += 2) {
473 runStart = lineStart + runs[runIndex];
474 if (cursor >= runStart) {
475 runLimit = runStart + (runs[runIndex+1] & Layout.RUN_LENGTH_MASK);
476 if (runLimit > lineEnd) {
477 runLimit = lineEnd;
478 }
479 if (cursor < runLimit) {
480 runLevel = (runs[runIndex+1] >>> Layout.RUN_LEVEL_SHIFT) &
481 Layout.RUN_LEVEL_MASK;
482 if (cursor == runStart) {
483 // The caret is on a run boundary, see if we should
484 // use the position on the trailing edge of the previous
485 // logical character instead.
486 int prevRunIndex, prevRunLevel, prevRunStart, prevRunLimit;
487 int pos = cursor - 1;
488 for (prevRunIndex = 0; prevRunIndex < runs.length; prevRunIndex += 2) {
489 prevRunStart = lineStart + runs[prevRunIndex];
490 if (pos >= prevRunStart) {
491 prevRunLimit = prevRunStart +
492 (runs[prevRunIndex+1] & Layout.RUN_LENGTH_MASK);
493 if (prevRunLimit > lineEnd) {
494 prevRunLimit = lineEnd;
495 }
496 if (pos < prevRunLimit) {
497 prevRunLevel = (runs[prevRunIndex+1] >>> Layout.RUN_LEVEL_SHIFT)
498 & Layout.RUN_LEVEL_MASK;
499 if (prevRunLevel < runLevel) {
500 // Start from logically previous character.
501 runIndex = prevRunIndex;
502 runLevel = prevRunLevel;
503 runStart = prevRunStart;
504 runLimit = prevRunLimit;
505 trailing = true;
506 break;
507 }
508 }
509 }
510 }
511 }
512 break;
513 }
514 }
515 }
516
517 // caret might be == lineEnd. This is generally a space or paragraph
518 // separator and has an associated run, but might be the end of
519 // text, in which case it doesn't. If that happens, we ran off the
520 // end of the run list, and runIndex == runs.length. In this case,
521 // we are at a run boundary so we skip the below test.
522 if (runIndex != runs.length) {
523 boolean runIsRtl = (runLevel & 0x1) != 0;
524 boolean advance = toLeft == runIsRtl;
525 if (cursor != (advance ? runLimit : runStart) || advance != trailing) {
526 // Moving within or into the run, so we can move logically.
Doug Felt0c702b82010-05-14 10:55:42 -0700527 newCaret = getOffsetBeforeAfter(runIndex, runStart, runLimit,
528 runIsRtl, cursor, advance);
Doug Felte8e45f22010-03-29 14:58:40 -0700529 // If the new position is internal to the run, we're at the strong
530 // position already so we're finished.
531 if (newCaret != (advance ? runLimit : runStart)) {
532 return newCaret;
533 }
534 }
535 }
536 }
537
538 // If newCaret is -1, we're starting at a run boundary and crossing
539 // into another run. Otherwise we've arrived at a run boundary, and
540 // need to figure out which character to attach to. Note we might
541 // need to run this twice, if we cross a run boundary and end up at
542 // another run boundary.
543 while (true) {
544 boolean advance = toLeft == paraIsRtl;
545 int otherRunIndex = runIndex + (advance ? 2 : -2);
546 if (otherRunIndex >= 0 && otherRunIndex < runs.length) {
547 int otherRunStart = lineStart + runs[otherRunIndex];
548 int otherRunLimit = otherRunStart +
549 (runs[otherRunIndex+1] & Layout.RUN_LENGTH_MASK);
550 if (otherRunLimit > lineEnd) {
551 otherRunLimit = lineEnd;
552 }
553 int otherRunLevel = (runs[otherRunIndex+1] >>> Layout.RUN_LEVEL_SHIFT) &
554 Layout.RUN_LEVEL_MASK;
555 boolean otherRunIsRtl = (otherRunLevel & 1) != 0;
556
557 advance = toLeft == otherRunIsRtl;
558 if (newCaret == -1) {
Doug Felt0c702b82010-05-14 10:55:42 -0700559 newCaret = getOffsetBeforeAfter(otherRunIndex, otherRunStart,
560 otherRunLimit, otherRunIsRtl,
Doug Felte8e45f22010-03-29 14:58:40 -0700561 advance ? otherRunStart : otherRunLimit, advance);
562 if (newCaret == (advance ? otherRunLimit : otherRunStart)) {
563 // Crossed and ended up at a new boundary,
564 // repeat a second and final time.
565 runIndex = otherRunIndex;
566 runLevel = otherRunLevel;
567 continue;
568 }
569 break;
570 }
571
572 // The new caret is at a boundary.
573 if (otherRunLevel < runLevel) {
574 // The strong character is in the other run.
575 newCaret = advance ? otherRunStart : otherRunLimit;
576 }
577 break;
578 }
579
580 if (newCaret == -1) {
581 // We're walking off the end of the line. The paragraph
582 // level is always equal to or lower than any internal level, so
583 // the boundaries get the strong caret.
Doug Felt0c702b82010-05-14 10:55:42 -0700584 newCaret = advance ? mLen + 1 : -1;
Doug Felte8e45f22010-03-29 14:58:40 -0700585 break;
586 }
587
588 // Else we've arrived at the end of the line. That's a strong position.
589 // We might have arrived here by crossing over a run with no internal
590 // breaks and dropping out of the above loop before advancing one final
591 // time, so reset the caret.
592 // Note, we use '<=' below to handle a situation where the only run
593 // on the line is a counter-directional run. If we're not advancing,
594 // we can end up at the 'lineEnd' position but the caret we want is at
595 // the lineStart.
596 if (newCaret <= lineEnd) {
597 newCaret = advance ? lineEnd : lineStart;
598 }
599 break;
600 }
601
602 return newCaret;
603 }
604
605 /**
606 * Returns the next valid offset within this directional run, skipping
607 * conjuncts and zero-width characters. This should not be called to walk
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -0700608 * off the end of the line, since the returned values might not be valid
Doug Felt0c702b82010-05-14 10:55:42 -0700609 * on neighboring lines. If the returned offset is less than zero or
610 * greater than the line length, the offset should be recomputed on the
611 * preceding or following line, respectively.
Doug Felte8e45f22010-03-29 14:58:40 -0700612 *
613 * @param runIndex the run index
Doug Felt0c702b82010-05-14 10:55:42 -0700614 * @param runStart the start of the run
615 * @param runLimit the limit of the run
616 * @param runIsRtl true if the run is right-to-left
Doug Felte8e45f22010-03-29 14:58:40 -0700617 * @param offset the offset
618 * @param after true if the new offset should logically follow the provided
619 * offset
620 * @return the new offset
621 */
Doug Felt0c702b82010-05-14 10:55:42 -0700622 private int getOffsetBeforeAfter(int runIndex, int runStart, int runLimit,
623 boolean runIsRtl, int offset, boolean after) {
Doug Felte8e45f22010-03-29 14:58:40 -0700624
Doug Felt0c702b82010-05-14 10:55:42 -0700625 if (runIndex < 0 || offset == (after ? mLen : 0)) {
626 // Walking off end of line. Since we don't know
627 // what cursor positions are available on other lines, we can't
628 // return accurate values. These are a guess.
Doug Felte8e45f22010-03-29 14:58:40 -0700629 if (after) {
Doug Felt0c702b82010-05-14 10:55:42 -0700630 return TextUtils.getOffsetAfter(mText, offset + mStart) - mStart;
631 }
632 return TextUtils.getOffsetBefore(mText, offset + mStart) - mStart;
633 }
634
635 TextPaint wp = mWorkPaint;
636 wp.set(mPaint);
Seigo Nonaka09da71a2016-11-28 16:24:14 +0900637 wp.setWordSpacing(mAddedWidth);
Doug Felt0c702b82010-05-14 10:55:42 -0700638
639 int spanStart = runStart;
640 int spanLimit;
641 if (mSpanned == null) {
642 spanLimit = runLimit;
643 } else {
644 int target = after ? offset + 1 : offset;
645 int limit = mStart + runLimit;
646 while (true) {
647 spanLimit = mSpanned.nextSpanTransition(mStart + spanStart, limit,
648 MetricAffectingSpan.class) - mStart;
649 if (spanLimit >= target) {
650 break;
Doug Felte8e45f22010-03-29 14:58:40 -0700651 }
Doug Felt0c702b82010-05-14 10:55:42 -0700652 spanStart = spanLimit;
653 }
654
655 MetricAffectingSpan[] spans = mSpanned.getSpans(mStart + spanStart,
656 mStart + spanLimit, MetricAffectingSpan.class);
Gilles Debunne1e3ac182011-03-08 14:22:34 -0800657 spans = TextUtils.removeEmptySpans(spans, mSpanned, MetricAffectingSpan.class);
Doug Felt0c702b82010-05-14 10:55:42 -0700658
659 if (spans.length > 0) {
660 ReplacementSpan replacement = null;
661 for (int j = 0; j < spans.length; j++) {
662 MetricAffectingSpan span = spans[j];
663 if (span instanceof ReplacementSpan) {
664 replacement = (ReplacementSpan)span;
665 } else {
666 span.updateMeasureState(wp);
667 }
668 }
669
670 if (replacement != null) {
671 // If we have a replacement span, we're moving either to
672 // the start or end of this span.
673 return after ? spanLimit : spanStart;
Doug Felte8e45f22010-03-29 14:58:40 -0700674 }
675 }
Doug Felte8e45f22010-03-29 14:58:40 -0700676 }
677
Raph Levien051910b2014-06-15 18:25:29 -0700678 int dir = runIsRtl ? Paint.DIRECTION_RTL : Paint.DIRECTION_LTR;
Doug Felt0c702b82010-05-14 10:55:42 -0700679 int cursorOpt = after ? Paint.CURSOR_AFTER : Paint.CURSOR_BEFORE;
680 if (mCharsValid) {
681 return wp.getTextRunCursor(mChars, spanStart, spanLimit - spanStart,
Raph Levien051910b2014-06-15 18:25:29 -0700682 dir, offset, cursorOpt);
Doug Felt0c702b82010-05-14 10:55:42 -0700683 } else {
684 return wp.getTextRunCursor(mText, mStart + spanStart,
Raph Levien051910b2014-06-15 18:25:29 -0700685 mStart + spanLimit, dir, mStart + offset, cursorOpt) - mStart;
Doug Felte8e45f22010-03-29 14:58:40 -0700686 }
Doug Felte8e45f22010-03-29 14:58:40 -0700687 }
688
689 /**
Gilles Debunne0bb00092010-12-02 15:50:26 -0800690 * @param wp
691 */
692 private static void expandMetricsFromPaint(FontMetricsInt fmi, TextPaint wp) {
693 final int previousTop = fmi.top;
694 final int previousAscent = fmi.ascent;
695 final int previousDescent = fmi.descent;
696 final int previousBottom = fmi.bottom;
697 final int previousLeading = fmi.leading;
698
699 wp.getFontMetricsInt(fmi);
700
Fabrice Di Meglio8a5137a2011-09-22 18:49:43 -0700701 updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom,
702 previousLeading);
703 }
704
705 static void updateMetrics(FontMetricsInt fmi, int previousTop, int previousAscent,
706 int previousDescent, int previousBottom, int previousLeading) {
Gilles Debunne0bb00092010-12-02 15:50:26 -0800707 fmi.top = Math.min(fmi.top, previousTop);
708 fmi.ascent = Math.min(fmi.ascent, previousAscent);
709 fmi.descent = Math.max(fmi.descent, previousDescent);
710 fmi.bottom = Math.max(fmi.bottom, previousBottom);
711 fmi.leading = Math.max(fmi.leading, previousLeading);
712 }
713
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700714 private static void drawStroke(TextPaint wp, Canvas c, int color, float position,
715 float thickness, float xleft, float xright, float baseline) {
716 final float strokeTop = baseline + wp.baselineShift + position;
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700717
718 final int previousColor = wp.getColor();
719 final Paint.Style previousStyle = wp.getStyle();
720 final boolean previousAntiAlias = wp.isAntiAlias();
721
722 wp.setStyle(Paint.Style.FILL);
723 wp.setAntiAlias(true);
724
725 wp.setColor(color);
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700726 c.drawRect(xleft, strokeTop, xright, strokeTop + thickness, wp);
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700727
728 wp.setStyle(previousStyle);
729 wp.setColor(previousColor);
730 wp.setAntiAlias(previousAntiAlias);
731 }
732
733 private float getRunAdvance(TextPaint wp, int start, int end, int contextStart, int contextEnd,
734 boolean runIsRtl, int offset) {
735 if (mCharsValid) {
736 return wp.getRunAdvance(mChars, start, end, contextStart, contextEnd, runIsRtl, offset);
737 } else {
738 final int delta = mStart;
739 return wp.getRunAdvance(mText, delta + start, delta + end,
740 delta + contextStart, delta + contextEnd, runIsRtl, delta + offset);
741 }
742 }
743
Gilles Debunne0bb00092010-12-02 15:50:26 -0800744 /**
Doug Felte8e45f22010-03-29 14:58:40 -0700745 * Utility function for measuring and rendering text. The text must
Roozbeh Pournader112d9c72015-08-07 12:44:41 -0700746 * not include a tab.
Doug Felte8e45f22010-03-29 14:58:40 -0700747 *
748 * @param wp the working paint
749 * @param start the start of the text
Doug Felt0c702b82010-05-14 10:55:42 -0700750 * @param end the end of the text
Doug Felte8e45f22010-03-29 14:58:40 -0700751 * @param runIsRtl true if the run is right-to-left
752 * @param c the canvas, can be null if rendering is not needed
753 * @param x the edge of the run closest to the leading margin
754 * @param top the top of the line
755 * @param y the baseline
756 * @param bottom the bottom of the line
757 * @param fmi receives metrics information, can be null
758 * @param needWidth true if the width of the run is needed
Raph Levien909c7bc2015-11-30 21:05:46 -0800759 * @param offset the offset for the purpose of measuring
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700760 * @param decorations the list of locations and paremeters for drawing decorations
Doug Felte8e45f22010-03-29 14:58:40 -0700761 * @return the signed width of the run based on the run direction; only
762 * valid if needWidth is true
763 */
Doug Felt0c702b82010-05-14 10:55:42 -0700764 private float handleText(TextPaint wp, int start, int end,
765 int contextStart, int contextEnd, boolean runIsRtl,
766 Canvas c, float x, int top, int y, int bottom,
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700767 FontMetricsInt fmi, boolean needWidth, int offset,
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700768 @Nullable ArrayList<DecorationInfo> decorations) {
Doug Felte8e45f22010-03-29 14:58:40 -0700769
Seigo Nonaka09da71a2016-11-28 16:24:14 +0900770 wp.setWordSpacing(mAddedWidth);
Fabrice Di Meglio850dffa2011-08-08 09:45:09 -0700771 // Get metrics first (even for empty strings or "0" width runs)
772 if (fmi != null) {
773 expandMetricsFromPaint(fmi, wp);
774 }
Doug Felte8e45f22010-03-29 14:58:40 -0700775
Fabrice Di Meglio850dffa2011-08-08 09:45:09 -0700776 // No need to do anything if the run width is "0"
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700777 if (end == start) {
Fabrice Di Meglio850dffa2011-08-08 09:45:09 -0700778 return 0f;
779 }
780
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700781 float totalWidth = 0;
Fabrice Di Meglio850dffa2011-08-08 09:45:09 -0700782
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700783 final int numDecorations = decorations == null ? 0 : decorations.size();
784 if (needWidth || (c != null && (wp.bgColor != 0 || numDecorations != 0 || runIsRtl))) {
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700785 totalWidth = getRunAdvance(wp, start, end, contextStart, contextEnd, runIsRtl, offset);
Doug Felte8e45f22010-03-29 14:58:40 -0700786 }
787
Doug Felte8e45f22010-03-29 14:58:40 -0700788 if (c != null) {
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700789 final float leftX, rightX;
Doug Felte8e45f22010-03-29 14:58:40 -0700790 if (runIsRtl) {
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700791 leftX = x - totalWidth;
792 rightX = x;
793 } else {
794 leftX = x;
795 rightX = x + totalWidth;
Doug Felte8e45f22010-03-29 14:58:40 -0700796 }
797
798 if (wp.bgColor != 0) {
Gilles Debunnedd8f5ed2011-08-10 18:25:49 -0700799 int previousColor = wp.getColor();
800 Paint.Style previousStyle = wp.getStyle();
801
Doug Felte8e45f22010-03-29 14:58:40 -0700802 wp.setColor(wp.bgColor);
803 wp.setStyle(Paint.Style.FILL);
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700804 c.drawRect(leftX, top, rightX, bottom, wp);
Doug Felte8e45f22010-03-29 14:58:40 -0700805
Gilles Debunnedd8f5ed2011-08-10 18:25:49 -0700806 wp.setStyle(previousStyle);
807 wp.setColor(previousColor);
808 }
809
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700810 if (numDecorations != 0) {
811 for (int i = 0; i < numDecorations; i++) {
812 final DecorationInfo info = decorations.get(i);
Gilles Debunnedd8f5ed2011-08-10 18:25:49 -0700813
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700814 final int decorationStart = Math.max(info.start, start);
815 final int decorationEnd = Math.min(info.end, offset);
816 float decorationStartAdvance = getRunAdvance(
817 wp, start, end, contextStart, contextEnd, runIsRtl, decorationStart);
818 float decorationEndAdvance = getRunAdvance(
819 wp, start, end, contextStart, contextEnd, runIsRtl, decorationEnd);
820 final float decorationXLeft, decorationXRight;
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700821 if (runIsRtl) {
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700822 decorationXLeft = rightX - decorationEndAdvance;
823 decorationXRight = rightX - decorationStartAdvance;
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700824 } else {
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700825 decorationXLeft = leftX + decorationStartAdvance;
826 decorationXRight = leftX + decorationEndAdvance;
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700827 }
Siyamed Sinir702c9f92017-06-01 18:25:40 +0000828
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700829 // Theoretically, there could be cases where both Paint's and TextPaint's
830 // setUnderLineText() are called. For backward compatibility, we need to draw
831 // both underlines, the one with custom color first.
832 if (info.underlineColor != 0) {
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700833 drawStroke(wp, c, info.underlineColor, wp.getUnderlinePosition(),
834 info.underlineThickness, decorationXLeft, decorationXRight, y);
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700835 }
836 if (info.isUnderlineText) {
Roozbeh Pournaderca8a04a2017-06-06 18:30:29 -0700837 final float thickness =
Siyamed Sinira273a702017-10-05 11:22:12 -0700838 Math.max(wp.getUnderlineThickness(), 1.0f);
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700839 drawStroke(wp, c, wp.getColor(), wp.getUnderlinePosition(), thickness,
840 decorationXLeft, decorationXRight, y);
841 }
842
843 if (info.isStrikeThruText) {
844 final float thickness =
Siyamed Sinira273a702017-10-05 11:22:12 -0700845 Math.max(wp.getStrikeThruThickness(), 1.0f);
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700846 drawStroke(wp, c, wp.getColor(), wp.getStrikeThruPosition(), thickness,
847 decorationXLeft, decorationXRight, y);
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700848 }
849 }
Doug Felte8e45f22010-03-29 14:58:40 -0700850 }
851
Fabrice Di Meglioda12f382013-03-15 11:26:56 -0700852 drawTextRun(c, wp, start, end, contextStart, contextEnd, runIsRtl,
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700853 leftX, y + wp.baselineShift);
Doug Felte8e45f22010-03-29 14:58:40 -0700854 }
855
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700856 return runIsRtl ? -totalWidth : totalWidth;
Doug Felte8e45f22010-03-29 14:58:40 -0700857 }
858
859 /**
860 * Utility function for measuring and rendering a replacement.
861 *
Romain Guybc7cdb62011-05-26 18:48:49 -0700862 *
Doug Felte8e45f22010-03-29 14:58:40 -0700863 * @param replacement the replacement
864 * @param wp the work paint
Doug Felte8e45f22010-03-29 14:58:40 -0700865 * @param start the start of the run
866 * @param limit the limit of the run
867 * @param runIsRtl true if the run is right-to-left
868 * @param c the canvas, can be null if not rendering
869 * @param x the edge of the replacement closest to the leading margin
870 * @param top the top of the line
871 * @param y the baseline
872 * @param bottom the bottom of the line
873 * @param fmi receives metrics information, can be null
874 * @param needWidth true if the width of the replacement is needed
Doug Felte8e45f22010-03-29 14:58:40 -0700875 * @return the signed width of the run based on the run direction; only
876 * valid if needWidth is true
877 */
878 private float handleReplacement(ReplacementSpan replacement, TextPaint wp,
Romain Guybc7cdb62011-05-26 18:48:49 -0700879 int start, int limit, boolean runIsRtl, Canvas c,
Doug Felte8e45f22010-03-29 14:58:40 -0700880 float x, int top, int y, int bottom, FontMetricsInt fmi,
Doug Felt0c702b82010-05-14 10:55:42 -0700881 boolean needWidth) {
Doug Felte8e45f22010-03-29 14:58:40 -0700882
883 float ret = 0;
884
Doug Felt0c702b82010-05-14 10:55:42 -0700885 int textStart = mStart + start;
886 int textLimit = mStart + limit;
887
888 if (needWidth || (c != null && runIsRtl)) {
Fabrice Di Meglio8a5137a2011-09-22 18:49:43 -0700889 int previousTop = 0;
890 int previousAscent = 0;
891 int previousDescent = 0;
892 int previousBottom = 0;
893 int previousLeading = 0;
894
895 boolean needUpdateMetrics = (fmi != null);
896
897 if (needUpdateMetrics) {
898 previousTop = fmi.top;
899 previousAscent = fmi.ascent;
900 previousDescent = fmi.descent;
901 previousBottom = fmi.bottom;
902 previousLeading = fmi.leading;
903 }
904
Doug Felt0c702b82010-05-14 10:55:42 -0700905 ret = replacement.getSize(wp, mText, textStart, textLimit, fmi);
Fabrice Di Meglio8a5137a2011-09-22 18:49:43 -0700906
907 if (needUpdateMetrics) {
908 updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom,
909 previousLeading);
910 }
Doug Felte8e45f22010-03-29 14:58:40 -0700911 }
912
Doug Felt0c702b82010-05-14 10:55:42 -0700913 if (c != null) {
914 if (runIsRtl) {
915 x -= ret;
Doug Felte8e45f22010-03-29 14:58:40 -0700916 }
Doug Felt0c702b82010-05-14 10:55:42 -0700917 replacement.draw(c, mText, textStart, textLimit,
918 x, top, y, bottom, wp);
Doug Felte8e45f22010-03-29 14:58:40 -0700919 }
920
921 return runIsRtl ? -ret : ret;
922 }
923
Roozbeh Pournader46c6f4c2017-02-21 12:18:31 -0800924 private int adjustHyphenEdit(int start, int limit, int hyphenEdit) {
925 int result = hyphenEdit;
926 // Only draw hyphens on first or last run in line. Disable them otherwise.
927 if (start > 0) { // not the first run
928 result &= ~Paint.HYPHENEDIT_MASK_START_OF_LINE;
929 }
930 if (limit < mLen) { // not the last run
931 result &= ~Paint.HYPHENEDIT_MASK_END_OF_LINE;
932 }
933 return result;
934 }
935
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700936 private static final class DecorationInfo {
937 public boolean isStrikeThruText;
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700938 public boolean isUnderlineText;
939 public int underlineColor;
940 public float underlineThickness;
941 public int start = -1;
942 public int end = -1;
943
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700944 public boolean hasDecoration() {
945 return isStrikeThruText || isUnderlineText || underlineColor != 0;
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700946 }
947
948 // Copies the info, but not the start and end range.
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700949 public DecorationInfo copyInfo() {
950 final DecorationInfo copy = new DecorationInfo();
951 copy.isStrikeThruText = isStrikeThruText;
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700952 copy.isUnderlineText = isUnderlineText;
953 copy.underlineColor = underlineColor;
954 copy.underlineThickness = underlineThickness;
955 return copy;
956 }
957 }
958
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700959 private void extractDecorationInfo(@NonNull TextPaint paint, @NonNull DecorationInfo info) {
960 info.isStrikeThruText = paint.isStrikeThruText();
961 if (info.isStrikeThruText) {
962 paint.setStrikeThruText(false);
963 }
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700964 info.isUnderlineText = paint.isUnderlineText();
965 if (info.isUnderlineText) {
966 paint.setUnderlineText(false);
967 }
968 info.underlineColor = paint.underlineColor;
969 info.underlineThickness = paint.underlineThickness;
970 paint.setUnderlineText(0, 0.0f);
971 }
972
Doug Felte8e45f22010-03-29 14:58:40 -0700973 /**
974 * Utility function for handling a unidirectional run. The run must not
Roozbeh Pournader112d9c72015-08-07 12:44:41 -0700975 * contain tabs but can contain styles.
Doug Felte8e45f22010-03-29 14:58:40 -0700976 *
Romain Guybc7cdb62011-05-26 18:48:49 -0700977 *
Doug Felte8e45f22010-03-29 14:58:40 -0700978 * @param start the line-relative start of the run
Doug Felt0c702b82010-05-14 10:55:42 -0700979 * @param measureLimit the offset to measure to, between start and limit inclusive
Doug Felte8e45f22010-03-29 14:58:40 -0700980 * @param limit the limit of the run
981 * @param runIsRtl true if the run is right-to-left
982 * @param c the canvas, can be null
983 * @param x the end of the run closest to the leading margin
984 * @param top the top of the line
985 * @param y the baseline
986 * @param bottom the bottom of the line
987 * @param fmi receives metrics information, can be null
988 * @param needWidth true if the width is required
Doug Felte8e45f22010-03-29 14:58:40 -0700989 * @return the signed width of the run based on the run direction; only
990 * valid if needWidth is true
991 */
Romain Guybc7cdb62011-05-26 18:48:49 -0700992 private float handleRun(int start, int measureLimit,
Doug Felte8e45f22010-03-29 14:58:40 -0700993 int limit, boolean runIsRtl, Canvas c, float x, int top, int y,
Doug Felt0c702b82010-05-14 10:55:42 -0700994 int bottom, FontMetricsInt fmi, boolean needWidth) {
Doug Felte8e45f22010-03-29 14:58:40 -0700995
Siyamed Sinir9f3958c2016-08-26 15:23:47 -0700996 if (measureLimit < start || measureLimit > limit) {
997 throw new IndexOutOfBoundsException("measureLimit (" + measureLimit + ") is out of "
998 + "start (" + start + ") and limit (" + limit + ") bounds");
999 }
1000
Gilles Debunnef483e512011-04-28 15:08:54 -07001001 // Case of an empty line, make sure we update fmi according to mPaint
1002 if (start == measureLimit) {
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001003 final TextPaint wp = mWorkPaint;
Gilles Debunnef483e512011-04-28 15:08:54 -07001004 wp.set(mPaint);
Fabrice Di Meglio15c097a2011-08-08 14:42:41 -07001005 if (fmi != null) {
1006 expandMetricsFromPaint(fmi, wp);
1007 }
1008 return 0f;
Gilles Debunnef483e512011-04-28 15:08:54 -07001009 }
1010
Roozbeh Pournader08836d42017-06-06 16:23:09 -07001011 final boolean needsSpanMeasurement;
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001012 if (mSpanned == null) {
Roozbeh Pournader08836d42017-06-06 16:23:09 -07001013 needsSpanMeasurement = false;
1014 } else {
1015 mMetricAffectingSpanSpanSet.init(mSpanned, mStart + start, mStart + limit);
1016 mCharacterStyleSpanSet.init(mSpanned, mStart + start, mStart + limit);
1017 needsSpanMeasurement = mMetricAffectingSpanSpanSet.numberOfSpans != 0
1018 || mCharacterStyleSpanSet.numberOfSpans != 0;
1019 }
1020
1021 if (!needsSpanMeasurement) {
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001022 final TextPaint wp = mWorkPaint;
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001023 wp.set(mPaint);
Roozbeh Pournader46c6f4c2017-02-21 12:18:31 -08001024 wp.setHyphenEdit(adjustHyphenEdit(start, limit, wp.getHyphenEdit()));
Raph Levien909c7bc2015-11-30 21:05:46 -08001025 return handleText(wp, start, limit, start, limit, runIsRtl, c, x, top,
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001026 y, bottom, fmi, needWidth, measureLimit, null);
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001027 }
1028
Doug Felte8e45f22010-03-29 14:58:40 -07001029 // Shaping needs to take into account context up to metric boundaries,
1030 // but rendering needs to take into account character style boundaries.
Doug Felt0c702b82010-05-14 10:55:42 -07001031 // So we iterate through metric runs to get metric bounds,
1032 // then within each metric run iterate through character style runs
1033 // for the run bounds.
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001034 final float originalX = x;
Doug Felt0c702b82010-05-14 10:55:42 -07001035 for (int i = start, inext; i < measureLimit; i = inext) {
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001036 final TextPaint wp = mWorkPaint;
Doug Felte8e45f22010-03-29 14:58:40 -07001037 wp.set(mPaint);
1038
Gilles Debunnec1f44832011-12-08 16:03:00 -08001039 inext = mMetricAffectingSpanSpanSet.getNextTransition(mStart + i, mStart + limit) -
1040 mStart;
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001041 int mlimit = Math.min(inext, measureLimit);
Doug Felte8e45f22010-03-29 14:58:40 -07001042
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001043 ReplacementSpan replacement = null;
Doug Felte8e45f22010-03-29 14:58:40 -07001044
Gilles Debunnec1f44832011-12-08 16:03:00 -08001045 for (int j = 0; j < mMetricAffectingSpanSpanSet.numberOfSpans; j++) {
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001046 // Both intervals [spanStarts..spanEnds] and [mStart + i..mStart + mlimit] are NOT
1047 // empty by construction. This special case in getSpans() explains the >= & <= tests
Gilles Debunnec1f44832011-12-08 16:03:00 -08001048 if ((mMetricAffectingSpanSpanSet.spanStarts[j] >= mStart + mlimit) ||
1049 (mMetricAffectingSpanSpanSet.spanEnds[j] <= mStart + i)) continue;
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001050 final MetricAffectingSpan span = mMetricAffectingSpanSpanSet.spans[j];
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001051 if (span instanceof ReplacementSpan) {
1052 replacement = (ReplacementSpan)span;
1053 } else {
1054 // We might have a replacement that uses the draw
1055 // state, otherwise measure state would suffice.
1056 span.updateDrawState(wp);
Doug Felte8e45f22010-03-29 14:58:40 -07001057 }
1058 }
1059
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001060 if (replacement != null) {
1061 x += handleReplacement(replacement, wp, i, mlimit, runIsRtl, c, x, top, y,
1062 bottom, fmi, needWidth || mlimit < measureLimit);
1063 continue;
1064 }
1065
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001066 final TextPaint activePaint = mActivePaint;
1067 activePaint.set(mPaint);
1068 int activeStart = i;
1069 int activeEnd = mlimit;
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001070 final DecorationInfo decorationInfo = mDecorationInfo;
1071 mDecorations.clear();
Raph Levien42ef515d2012-10-22 15:01:17 -07001072 for (int j = i, jnext; j < mlimit; j = jnext) {
Siyamed Sinir702c9f92017-06-01 18:25:40 +00001073 jnext = mCharacterStyleSpanSet.getNextTransition(mStart + j, mStart + inext) -
1074 mStart;
Doug Felte8e45f22010-03-29 14:58:40 -07001075
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001076 final int offset = Math.min(jnext, mlimit);
Raph Levien42ef515d2012-10-22 15:01:17 -07001077 wp.set(mPaint);
1078 for (int k = 0; k < mCharacterStyleSpanSet.numberOfSpans; k++) {
1079 // Intentionally using >= and <= as explained above
Siyamed Sinir702c9f92017-06-01 18:25:40 +00001080 if ((mCharacterStyleSpanSet.spanStarts[k] >= mStart + offset) ||
1081 (mCharacterStyleSpanSet.spanEnds[k] <= mStart + j)) continue;
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001082
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001083 final CharacterStyle span = mCharacterStyleSpanSet.spans[k];
Siyamed Sinir702c9f92017-06-01 18:25:40 +00001084 span.updateDrawState(wp);
Doug Felte8e45f22010-03-29 14:58:40 -07001085 }
Raph Levien42ef515d2012-10-22 15:01:17 -07001086
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001087 extractDecorationInfo(wp, decorationInfo);
Roozbeh Pournader46c6f4c2017-02-21 12:18:31 -08001088
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001089 if (j == i) {
1090 // First chunk of text. We can't handle it yet, since we may need to merge it
1091 // with the next chunk. So we just save the TextPaint for future comparisons
1092 // and use.
1093 activePaint.set(wp);
1094 } else if (!wp.hasEqualAttributes(activePaint)) {
1095 // The style of the present chunk of text is substantially different from the
1096 // style of the previous chunk. We need to handle the active piece of text
1097 // and restart with the present chunk.
1098 activePaint.setHyphenEdit(adjustHyphenEdit(
1099 activeStart, activeEnd, mPaint.getHyphenEdit()));
1100 x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, x,
1101 top, y, bottom, fmi, needWidth || activeEnd < measureLimit,
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001102 Math.min(activeEnd, mlimit), mDecorations);
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001103
1104 activeStart = j;
1105 activePaint.set(wp);
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001106 mDecorations.clear();
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001107 } else {
1108 // The present TextPaint is substantially equal to the last TextPaint except
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001109 // perhaps for decorations. We just need to expand the active piece of text to
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001110 // include the present chunk, which we always do anyway. We don't need to save
1111 // wp to activePaint, since they are already equal.
1112 }
1113
1114 activeEnd = jnext;
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001115 if (decorationInfo.hasDecoration()) {
1116 final DecorationInfo copy = decorationInfo.copyInfo();
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001117 copy.start = j;
1118 copy.end = jnext;
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001119 mDecorations.add(copy);
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001120 }
Doug Felte8e45f22010-03-29 14:58:40 -07001121 }
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001122 // Handle the final piece of text.
1123 activePaint.setHyphenEdit(adjustHyphenEdit(
1124 activeStart, activeEnd, mPaint.getHyphenEdit()));
1125 x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, x,
1126 top, y, bottom, fmi, needWidth || activeEnd < measureLimit,
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001127 Math.min(activeEnd, mlimit), mDecorations);
Doug Felte8e45f22010-03-29 14:58:40 -07001128 }
1129
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001130 return x - originalX;
Doug Felte8e45f22010-03-29 14:58:40 -07001131 }
1132
Doug Felte8e45f22010-03-29 14:58:40 -07001133 /**
1134 * Render a text run with the set-up paint.
1135 *
1136 * @param c the canvas
1137 * @param wp the paint used to render the text
Doug Felt0c702b82010-05-14 10:55:42 -07001138 * @param start the start of the run
1139 * @param end the end of the run
1140 * @param contextStart the start of context for the run
1141 * @param contextEnd the end of the context for the run
Fabrice Di Meglioda12f382013-03-15 11:26:56 -07001142 * @param runIsRtl true if the run is right-to-left
Doug Felte8e45f22010-03-29 14:58:40 -07001143 * @param x the x position of the left edge of the run
1144 * @param y the baseline of the run
1145 */
Doug Felt0c702b82010-05-14 10:55:42 -07001146 private void drawTextRun(Canvas c, TextPaint wp, int start, int end,
Fabrice Di Meglioda12f382013-03-15 11:26:56 -07001147 int contextStart, int contextEnd, boolean runIsRtl, float x, int y) {
Doug Felte8e45f22010-03-29 14:58:40 -07001148
Doug Felte8e45f22010-03-29 14:58:40 -07001149 if (mCharsValid) {
Doug Felt0c702b82010-05-14 10:55:42 -07001150 int count = end - start;
1151 int contextCount = contextEnd - contextStart;
1152 c.drawTextRun(mChars, start, count, contextStart, contextCount,
Raph Levien051910b2014-06-15 18:25:29 -07001153 x, y, runIsRtl, wp);
Doug Felte8e45f22010-03-29 14:58:40 -07001154 } else {
Doug Felt0c702b82010-05-14 10:55:42 -07001155 int delta = mStart;
1156 c.drawTextRun(mText, delta + start, delta + end,
Raph Levien051910b2014-06-15 18:25:29 -07001157 delta + contextStart, delta + contextEnd, x, y, runIsRtl, wp);
Doug Felte8e45f22010-03-29 14:58:40 -07001158 }
1159 }
1160
1161 /**
Doug Felte8e45f22010-03-29 14:58:40 -07001162 * Returns the next tab position.
1163 *
1164 * @param h the (unsigned) offset from the leading margin
1165 * @return the (unsigned) tab position after this offset
1166 */
1167 float nextTab(float h) {
Doug Feltc982f602010-05-25 11:51:40 -07001168 if (mTabs != null) {
1169 return mTabs.nextTab(h);
Doug Felte8e45f22010-03-29 14:58:40 -07001170 }
Doug Feltc982f602010-05-25 11:51:40 -07001171 return TabStops.nextDefaultStop(h, TAB_INCREMENT);
Doug Felte8e45f22010-03-29 14:58:40 -07001172 }
1173
Seigo Nonaka09da71a2016-11-28 16:24:14 +09001174 private boolean isStretchableWhitespace(int ch) {
Roozbeh Pournader3630d082017-10-23 12:16:32 -07001175 // TODO: Support NBSP and other stretchable whitespace (b/34013491 and b/68204709).
1176 return ch == 0x0020;
Seigo Nonaka09da71a2016-11-28 16:24:14 +09001177 }
1178
1179 /* Return the number of spaces in the text line, for the purpose of justification */
1180 private int countStretchableSpaces(int start, int end) {
1181 int count = 0;
Roozbeh Pournader3630d082017-10-23 12:16:32 -07001182 for (int i = start; i < end; i++) {
1183 final char c = mCharsValid ? mChars[i] : mText.charAt(i + mStart);
1184 if (isStretchableWhitespace(c)) {
1185 count++;
1186 }
Seigo Nonaka09da71a2016-11-28 16:24:14 +09001187 }
1188 return count;
1189 }
1190
1191 // Note: keep this in sync with Minikin LineBreaker::isLineEndSpace()
1192 public static boolean isLineEndSpace(char ch) {
1193 return ch == ' ' || ch == '\t' || ch == 0x1680
1194 || (0x2000 <= ch && ch <= 0x200A && ch != 0x2007)
1195 || ch == 0x205F || ch == 0x3000;
1196 }
1197
Doug Felte8e45f22010-03-29 14:58:40 -07001198 private static final int TAB_INCREMENT = 20;
1199}