blob: 55367dcce47e24ac2e192352844d066ae2643cb0 [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 Nonaka783f9612018-01-20 12:11:13 -080063 private MeasuredText mMeasured;
Seigo Nonaka09da71a2016-11-28 16:24:14 +090064
65 // Additional width of whitespace for justification. This value is per whitespace, thus
66 // the line width will increase by mAddedWidth x (number of stretchable whitespaces).
67 private float mAddedWidth;
Roozbeh Pournader538de5b2017-05-24 09:07:52 -070068
Gilles Debunne345cb032010-06-16 17:13:23 -070069 private final TextPaint mWorkPaint = new TextPaint();
Roozbeh Pournader538de5b2017-05-24 09:07:52 -070070 private final TextPaint mActivePaint = new TextPaint();
Gilles Debunnec1f44832011-12-08 16:03:00 -080071 private final SpanSet<MetricAffectingSpan> mMetricAffectingSpanSpanSet =
72 new SpanSet<MetricAffectingSpan>(MetricAffectingSpan.class);
73 private final SpanSet<CharacterStyle> mCharacterStyleSpanSet =
74 new SpanSet<CharacterStyle>(CharacterStyle.class);
75 private final SpanSet<ReplacementSpan> mReplacementSpanSpanSet =
76 new SpanSet<ReplacementSpan>(ReplacementSpan.class);
Doug Felte8e45f22010-03-29 14:58:40 -070077
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -070078 private final DecorationInfo mDecorationInfo = new DecorationInfo();
Siyamed Sinira273a702017-10-05 11:22:12 -070079 private final ArrayList<DecorationInfo> mDecorations = new ArrayList<>();
Roozbeh Pournader538de5b2017-05-24 09:07:52 -070080
Romain Guybc7cdb62011-05-26 18:48:49 -070081 private static final TextLine[] sCached = new TextLine[3];
Doug Felte8e45f22010-03-29 14:58:40 -070082
83 /**
84 * Returns a new TextLine from the shared pool.
85 *
86 * @return an uninitialized TextLine
87 */
Roozbeh Pournader3630d082017-10-23 12:16:32 -070088 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
89 public static TextLine obtain() {
Doug Felte8e45f22010-03-29 14:58:40 -070090 TextLine tl;
Romain Guybc7cdb62011-05-26 18:48:49 -070091 synchronized (sCached) {
92 for (int i = sCached.length; --i >= 0;) {
93 if (sCached[i] != null) {
94 tl = sCached[i];
95 sCached[i] = null;
Doug Felte8e45f22010-03-29 14:58:40 -070096 return tl;
97 }
98 }
99 }
100 tl = new TextLine();
Romain Guybc7cdb62011-05-26 18:48:49 -0700101 if (DEBUG) {
102 Log.v("TLINE", "new: " + tl);
103 }
Doug Felte8e45f22010-03-29 14:58:40 -0700104 return tl;
105 }
106
107 /**
108 * Puts a TextLine back into the shared pool. Do not use this TextLine once
109 * it has been returned.
110 * @param tl the textLine
111 * @return null, as a convenience from clearing references to the provided
112 * TextLine
113 */
Roozbeh Pournader3630d082017-10-23 12:16:32 -0700114 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
115 public static TextLine recycle(TextLine tl) {
Doug Felte8e45f22010-03-29 14:58:40 -0700116 tl.mText = null;
117 tl.mPaint = null;
118 tl.mDirections = null;
Svet Ganov893d6fe2015-01-16 10:10:15 -0800119 tl.mSpanned = null;
120 tl.mTabs = null;
121 tl.mChars = null;
Seigo Nonaka783f9612018-01-20 12:11:13 -0800122 tl.mMeasured = null;
Gilles Debunnec3fb7a12011-12-12 14:03:40 -0800123
124 tl.mMetricAffectingSpanSpanSet.recycle();
125 tl.mCharacterStyleSpanSet.recycle();
126 tl.mReplacementSpanSpanSet.recycle();
127
Romain Guybc7cdb62011-05-26 18:48:49 -0700128 synchronized(sCached) {
129 for (int i = 0; i < sCached.length; ++i) {
130 if (sCached[i] == null) {
131 sCached[i] = tl;
Gilles Debunnef902d7b2011-01-25 09:09:46 -0800132 break;
Doug Felte8e45f22010-03-29 14:58:40 -0700133 }
134 }
135 }
136 return null;
137 }
138
139 /**
140 * Initializes a TextLine and prepares it for use.
141 *
142 * @param paint the base paint for the line
143 * @param text the text, can be Styled
144 * @param start the start of the line relative to the text
145 * @param limit the limit of the line relative to the text
146 * @param dir the paragraph direction of this line
147 * @param directions the directions information of this line
Roozbeh Pournader112d9c72015-08-07 12:44:41 -0700148 * @param hasTabs true if the line might contain tabs
Doug Feltc982f602010-05-25 11:51:40 -0700149 * @param tabStops the tabStops. Can be null.
Doug Felte8e45f22010-03-29 14:58:40 -0700150 */
Roozbeh Pournader3630d082017-10-23 12:16:32 -0700151 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
152 public void set(TextPaint paint, CharSequence text, int start, int limit, int dir,
Doug Feltc982f602010-05-25 11:51:40 -0700153 Directions directions, boolean hasTabs, TabStops tabStops) {
Doug Felte8e45f22010-03-29 14:58:40 -0700154 mPaint = paint;
155 mText = text;
156 mStart = start;
157 mLen = limit - start;
158 mDir = dir;
159 mDirections = directions;
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -0700160 if (mDirections == null) {
161 throw new IllegalArgumentException("Directions cannot be null");
162 }
Doug Felte8e45f22010-03-29 14:58:40 -0700163 mHasTabs = hasTabs;
164 mSpanned = null;
Doug Felte8e45f22010-03-29 14:58:40 -0700165
166 boolean hasReplacement = false;
167 if (text instanceof Spanned) {
168 mSpanned = (Spanned) text;
Gilles Debunnec1f44832011-12-08 16:03:00 -0800169 mReplacementSpanSpanSet.init(mSpanned, start, limit);
170 hasReplacement = mReplacementSpanSpanSet.numberOfSpans > 0;
Doug Felte8e45f22010-03-29 14:58:40 -0700171 }
172
Seigo Nonaka783f9612018-01-20 12:11:13 -0800173 mMeasured = null;
174 if (text instanceof MeasuredText) {
175 MeasuredText mt = (MeasuredText) text;
176 if (mt.canUseMeasuredResult(paint)) {
177 mMeasured = mt;
178 }
179 }
180
Gilles Debunne1e3ac182011-03-08 14:22:34 -0800181 mCharsValid = hasReplacement || hasTabs || directions != Layout.DIRS_ALL_LEFT_TO_RIGHT;
Doug Felte8e45f22010-03-29 14:58:40 -0700182
183 if (mCharsValid) {
184 if (mChars == null || mChars.length < mLen) {
Adam Lesinski776abc22014-03-07 11:30:59 -0500185 mChars = ArrayUtils.newUnpaddedCharArray(mLen);
Doug Felte8e45f22010-03-29 14:58:40 -0700186 }
187 TextUtils.getChars(text, start, limit, mChars, 0);
Doug Felt0c702b82010-05-14 10:55:42 -0700188 if (hasReplacement) {
189 // Handle these all at once so we don't have to do it as we go.
190 // Replace the first character of each replacement run with the
191 // object-replacement character and the remainder with zero width
192 // non-break space aka BOM. Cursor movement code skips these
193 // zero-width characters.
194 char[] chars = mChars;
195 for (int i = start, inext; i < limit; i = inext) {
Gilles Debunnec1f44832011-12-08 16:03:00 -0800196 inext = mReplacementSpanSpanSet.getNextTransition(i, limit);
197 if (mReplacementSpanSpanSet.hasSpansIntersecting(i, inext)) {
Gilles Debunne1e3ac182011-03-08 14:22:34 -0800198 // transition into a span
Doug Felt0c702b82010-05-14 10:55:42 -0700199 chars[i - start] = '\ufffc';
200 for (int j = i - start + 1, e = inext - start; j < e; ++j) {
201 chars[j] = '\ufeff'; // used as ZWNBS, marks positions to skip
202 }
203 }
204 }
205 }
Doug Felte8e45f22010-03-29 14:58:40 -0700206 }
Doug Feltc982f602010-05-25 11:51:40 -0700207 mTabs = tabStops;
Seigo Nonaka09da71a2016-11-28 16:24:14 +0900208 mAddedWidth = 0;
209 }
210
211 /**
212 * Justify the line to the given width.
213 */
Roozbeh Pournader3630d082017-10-23 12:16:32 -0700214 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
215 public void justify(float justifyWidth) {
Seigo Nonaka09da71a2016-11-28 16:24:14 +0900216 int end = mLen;
217 while (end > 0 && isLineEndSpace(mText.charAt(mStart + end - 1))) {
218 end--;
219 }
220 final int spaces = countStretchableSpaces(0, end);
221 if (spaces == 0) {
222 // There are no stretchable spaces, so we can't help the justification by adding any
223 // width.
224 return;
225 }
226 final float width = Math.abs(measure(end, false, null));
227 mAddedWidth = (justifyWidth - width) / spaces;
Doug Felte8e45f22010-03-29 14:58:40 -0700228 }
229
230 /**
231 * Renders the TextLine.
232 *
233 * @param c the canvas to render on
234 * @param x the leading margin position
235 * @param top the top of the line
236 * @param y the baseline
237 * @param bottom the bottom of the line
238 */
239 void draw(Canvas c, float x, int top, int y, int bottom) {
240 if (!mHasTabs) {
241 if (mDirections == Layout.DIRS_ALL_LEFT_TO_RIGHT) {
Romain Guybc7cdb62011-05-26 18:48:49 -0700242 drawRun(c, 0, mLen, false, x, top, y, bottom, false);
Doug Felte8e45f22010-03-29 14:58:40 -0700243 return;
244 }
245 if (mDirections == Layout.DIRS_ALL_RIGHT_TO_LEFT) {
Romain Guybc7cdb62011-05-26 18:48:49 -0700246 drawRun(c, 0, mLen, true, x, top, y, bottom, false);
Doug Felte8e45f22010-03-29 14:58:40 -0700247 return;
248 }
249 }
250
251 float h = 0;
252 int[] runs = mDirections.mDirections;
Doug Felte8e45f22010-03-29 14:58:40 -0700253
254 int lastRunIndex = runs.length - 2;
255 for (int i = 0; i < runs.length; i += 2) {
256 int runStart = runs[i];
257 int runLimit = runStart + (runs[i+1] & Layout.RUN_LENGTH_MASK);
258 if (runLimit > mLen) {
259 runLimit = mLen;
260 }
261 boolean runIsRtl = (runs[i+1] & Layout.RUN_RTL_FLAG) != 0;
262
263 int segstart = runStart;
Doug Felte8e45f22010-03-29 14:58:40 -0700264 for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
265 int codept = 0;
Doug Felte8e45f22010-03-29 14:58:40 -0700266 if (mHasTabs && j < runLimit) {
267 codept = mChars[j];
Roozbeh Pournader112d9c72015-08-07 12:44:41 -0700268 if (codept >= 0xD800 && codept < 0xDC00 && j + 1 < runLimit) {
Doug Felte8e45f22010-03-29 14:58:40 -0700269 codept = Character.codePointAt(mChars, j);
Roozbeh Pournader112d9c72015-08-07 12:44:41 -0700270 if (codept > 0xFFFF) {
Doug Felte8e45f22010-03-29 14:58:40 -0700271 ++j;
272 continue;
273 }
274 }
275 }
276
Roozbeh Pournader112d9c72015-08-07 12:44:41 -0700277 if (j == runLimit || codept == '\t') {
Romain Guybc7cdb62011-05-26 18:48:49 -0700278 h += drawRun(c, segstart, j, runIsRtl, x+h, top, y, bottom,
Doug Felte8e45f22010-03-29 14:58:40 -0700279 i != lastRunIndex || j != mLen);
280
281 if (codept == '\t') {
282 h = mDir * nextTab(h * mDir);
Doug Felte8e45f22010-03-29 14:58:40 -0700283 }
284 segstart = j + 1;
285 }
286 }
287 }
288 }
289
290 /**
291 * Returns metrics information for the entire line.
292 *
293 * @param fmi receives font metrics information, can be null
294 * @return the signed width of the line
295 */
Roozbeh Pournader3630d082017-10-23 12:16:32 -0700296 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
297 public float metrics(FontMetricsInt fmi) {
Doug Felte8e45f22010-03-29 14:58:40 -0700298 return measure(mLen, false, fmi);
299 }
300
301 /**
302 * Returns information about a position on the line.
303 *
304 * @param offset the line-relative character offset, between 0 and the
305 * line length, inclusive
306 * @param trailing true to measure the trailing edge of the character
307 * before offset, false to measure the leading edge of the character
308 * at offset.
309 * @param fmi receives metrics information about the requested
310 * character, can be null.
311 * @return the signed offset from the leading margin to the requested
312 * character edge.
313 */
314 float measure(int offset, boolean trailing, FontMetricsInt fmi) {
315 int target = trailing ? offset - 1 : offset;
316 if (target < 0) {
317 return 0;
318 }
319
320 float h = 0;
321
322 if (!mHasTabs) {
323 if (mDirections == Layout.DIRS_ALL_LEFT_TO_RIGHT) {
Romain Guybc7cdb62011-05-26 18:48:49 -0700324 return measureRun(0, offset, mLen, false, fmi);
Doug Felte8e45f22010-03-29 14:58:40 -0700325 }
326 if (mDirections == Layout.DIRS_ALL_RIGHT_TO_LEFT) {
Romain Guybc7cdb62011-05-26 18:48:49 -0700327 return measureRun(0, offset, mLen, true, fmi);
Doug Felte8e45f22010-03-29 14:58:40 -0700328 }
329 }
330
331 char[] chars = mChars;
332 int[] runs = mDirections.mDirections;
333 for (int i = 0; i < runs.length; i += 2) {
334 int runStart = runs[i];
335 int runLimit = runStart + (runs[i+1] & Layout.RUN_LENGTH_MASK);
336 if (runLimit > mLen) {
337 runLimit = mLen;
338 }
339 boolean runIsRtl = (runs[i+1] & Layout.RUN_RTL_FLAG) != 0;
340
341 int segstart = runStart;
342 for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
343 int codept = 0;
Doug Felte8e45f22010-03-29 14:58:40 -0700344 if (mHasTabs && j < runLimit) {
345 codept = chars[j];
Roozbeh Pournader112d9c72015-08-07 12:44:41 -0700346 if (codept >= 0xD800 && codept < 0xDC00 && j + 1 < runLimit) {
Doug Felte8e45f22010-03-29 14:58:40 -0700347 codept = Character.codePointAt(chars, j);
Roozbeh Pournader112d9c72015-08-07 12:44:41 -0700348 if (codept > 0xFFFF) {
Doug Felte8e45f22010-03-29 14:58:40 -0700349 ++j;
350 continue;
351 }
352 }
353 }
354
Roozbeh Pournader112d9c72015-08-07 12:44:41 -0700355 if (j == runLimit || codept == '\t') {
Doug Felte8e45f22010-03-29 14:58:40 -0700356 boolean inSegment = target >= segstart && target < j;
357
358 boolean advance = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl;
359 if (inSegment && advance) {
Siyamed Sinira273a702017-10-05 11:22:12 -0700360 return h + measureRun(segstart, offset, j, runIsRtl, fmi);
Doug Felte8e45f22010-03-29 14:58:40 -0700361 }
362
Romain Guybc7cdb62011-05-26 18:48:49 -0700363 float w = measureRun(segstart, j, j, runIsRtl, fmi);
Doug Felte8e45f22010-03-29 14:58:40 -0700364 h += advance ? w : -w;
365
366 if (inSegment) {
Siyamed Sinira273a702017-10-05 11:22:12 -0700367 return h + measureRun(segstart, offset, j, runIsRtl, null);
Doug Felte8e45f22010-03-29 14:58:40 -0700368 }
369
370 if (codept == '\t') {
371 if (offset == j) {
372 return h;
373 }
374 h = mDir * nextTab(h * mDir);
375 if (target == j) {
376 return h;
377 }
378 }
379
Doug Felte8e45f22010-03-29 14:58:40 -0700380 segstart = j + 1;
381 }
382 }
383 }
384
385 return h;
386 }
387
388 /**
389 * Draws a unidirectional (but possibly multi-styled) run of text.
390 *
Romain Guybc7cdb62011-05-26 18:48:49 -0700391 *
Doug Felte8e45f22010-03-29 14:58:40 -0700392 * @param c the canvas to draw on
Doug Felte8e45f22010-03-29 14:58:40 -0700393 * @param start the line-relative start
394 * @param limit the line-relative limit
395 * @param runIsRtl true if the run is right-to-left
396 * @param x the position of the run that is closest to the leading margin
397 * @param top the top of the line
398 * @param y the baseline
399 * @param bottom the bottom of the line
400 * @param needWidth true if the width value is required.
401 * @return the signed width of the run, based on the paragraph direction.
402 * Only valid if needWidth is true.
403 */
Romain Guybc7cdb62011-05-26 18:48:49 -0700404 private float drawRun(Canvas c, int start,
Doug Felte8e45f22010-03-29 14:58:40 -0700405 int limit, boolean runIsRtl, float x, int top, int y, int bottom,
406 boolean needWidth) {
407
408 if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) {
Romain Guybc7cdb62011-05-26 18:48:49 -0700409 float w = -measureRun(start, limit, limit, runIsRtl, null);
410 handleRun(start, limit, limit, runIsRtl, c, x + w, top,
Doug Felt0c702b82010-05-14 10:55:42 -0700411 y, bottom, null, false);
Doug Felte8e45f22010-03-29 14:58:40 -0700412 return w;
413 }
414
Romain Guybc7cdb62011-05-26 18:48:49 -0700415 return handleRun(start, limit, limit, runIsRtl, c, x, top,
Doug Felt0c702b82010-05-14 10:55:42 -0700416 y, bottom, null, needWidth);
Doug Felte8e45f22010-03-29 14:58:40 -0700417 }
418
419 /**
420 * Measures a unidirectional (but possibly multi-styled) run of text.
421 *
Romain Guybc7cdb62011-05-26 18:48:49 -0700422 *
Doug Felte8e45f22010-03-29 14:58:40 -0700423 * @param start the line-relative start of the run
424 * @param offset the offset to measure to, between start and limit inclusive
425 * @param limit the line-relative limit of the run
426 * @param runIsRtl true if the run is right-to-left
427 * @param fmi receives metrics information about the requested
428 * run, can be null.
429 * @return the signed width from the start of the run to the leading edge
430 * of the character at offset, based on the run (not paragraph) direction
431 */
Romain Guybc7cdb62011-05-26 18:48:49 -0700432 private float measureRun(int start, int offset, int limit, boolean runIsRtl,
433 FontMetricsInt fmi) {
434 return handleRun(start, offset, limit, runIsRtl, null, 0, 0, 0, 0, fmi, true);
Doug Felte8e45f22010-03-29 14:58:40 -0700435 }
436
437 /**
438 * Walk the cursor through this line, skipping conjuncts and
439 * zero-width characters.
440 *
441 * <p>This function cannot properly walk the cursor off the ends of the line
442 * since it does not know about any shaping on the previous/following line
443 * that might affect the cursor position. Callers must either avoid these
444 * situations or handle the result specially.
445 *
Doug Felte8e45f22010-03-29 14:58:40 -0700446 * @param cursor the starting position of the cursor, between 0 and the
447 * length of the line, inclusive
448 * @param toLeft true if the caret is moving to the left.
449 * @return the new offset. If it is less than 0 or greater than the length
450 * of the line, the previous/following line should be examined to get the
451 * actual offset.
452 */
453 int getOffsetToLeftRightOf(int cursor, boolean toLeft) {
454 // 1) The caret marks the leading edge of a character. The character
455 // logically before it might be on a different level, and the active caret
456 // position is on the character at the lower level. If that character
457 // was the previous character, the caret is on its trailing edge.
458 // 2) Take this character/edge and move it in the indicated direction.
459 // This gives you a new character and a new edge.
460 // 3) This position is between two visually adjacent characters. One of
461 // these might be at a lower level. The active position is on the
462 // character at the lower level.
463 // 4) If the active position is on the trailing edge of the character,
464 // the new caret position is the following logical character, else it
465 // is the character.
466
467 int lineStart = 0;
468 int lineEnd = mLen;
469 boolean paraIsRtl = mDir == -1;
470 int[] runs = mDirections.mDirections;
471
472 int runIndex, runLevel = 0, runStart = lineStart, runLimit = lineEnd, newCaret = -1;
473 boolean trailing = false;
474
475 if (cursor == lineStart) {
476 runIndex = -2;
477 } else if (cursor == lineEnd) {
478 runIndex = runs.length;
479 } else {
480 // First, get information about the run containing the character with
481 // the active caret.
482 for (runIndex = 0; runIndex < runs.length; runIndex += 2) {
483 runStart = lineStart + runs[runIndex];
484 if (cursor >= runStart) {
485 runLimit = runStart + (runs[runIndex+1] & Layout.RUN_LENGTH_MASK);
486 if (runLimit > lineEnd) {
487 runLimit = lineEnd;
488 }
489 if (cursor < runLimit) {
490 runLevel = (runs[runIndex+1] >>> Layout.RUN_LEVEL_SHIFT) &
491 Layout.RUN_LEVEL_MASK;
492 if (cursor == runStart) {
493 // The caret is on a run boundary, see if we should
494 // use the position on the trailing edge of the previous
495 // logical character instead.
496 int prevRunIndex, prevRunLevel, prevRunStart, prevRunLimit;
497 int pos = cursor - 1;
498 for (prevRunIndex = 0; prevRunIndex < runs.length; prevRunIndex += 2) {
499 prevRunStart = lineStart + runs[prevRunIndex];
500 if (pos >= prevRunStart) {
501 prevRunLimit = prevRunStart +
502 (runs[prevRunIndex+1] & Layout.RUN_LENGTH_MASK);
503 if (prevRunLimit > lineEnd) {
504 prevRunLimit = lineEnd;
505 }
506 if (pos < prevRunLimit) {
507 prevRunLevel = (runs[prevRunIndex+1] >>> Layout.RUN_LEVEL_SHIFT)
508 & Layout.RUN_LEVEL_MASK;
509 if (prevRunLevel < runLevel) {
510 // Start from logically previous character.
511 runIndex = prevRunIndex;
512 runLevel = prevRunLevel;
513 runStart = prevRunStart;
514 runLimit = prevRunLimit;
515 trailing = true;
516 break;
517 }
518 }
519 }
520 }
521 }
522 break;
523 }
524 }
525 }
526
527 // caret might be == lineEnd. This is generally a space or paragraph
528 // separator and has an associated run, but might be the end of
529 // text, in which case it doesn't. If that happens, we ran off the
530 // end of the run list, and runIndex == runs.length. In this case,
531 // we are at a run boundary so we skip the below test.
532 if (runIndex != runs.length) {
533 boolean runIsRtl = (runLevel & 0x1) != 0;
534 boolean advance = toLeft == runIsRtl;
535 if (cursor != (advance ? runLimit : runStart) || advance != trailing) {
536 // Moving within or into the run, so we can move logically.
Doug Felt0c702b82010-05-14 10:55:42 -0700537 newCaret = getOffsetBeforeAfter(runIndex, runStart, runLimit,
538 runIsRtl, cursor, advance);
Doug Felte8e45f22010-03-29 14:58:40 -0700539 // If the new position is internal to the run, we're at the strong
540 // position already so we're finished.
541 if (newCaret != (advance ? runLimit : runStart)) {
542 return newCaret;
543 }
544 }
545 }
546 }
547
548 // If newCaret is -1, we're starting at a run boundary and crossing
549 // into another run. Otherwise we've arrived at a run boundary, and
550 // need to figure out which character to attach to. Note we might
551 // need to run this twice, if we cross a run boundary and end up at
552 // another run boundary.
553 while (true) {
554 boolean advance = toLeft == paraIsRtl;
555 int otherRunIndex = runIndex + (advance ? 2 : -2);
556 if (otherRunIndex >= 0 && otherRunIndex < runs.length) {
557 int otherRunStart = lineStart + runs[otherRunIndex];
558 int otherRunLimit = otherRunStart +
559 (runs[otherRunIndex+1] & Layout.RUN_LENGTH_MASK);
560 if (otherRunLimit > lineEnd) {
561 otherRunLimit = lineEnd;
562 }
563 int otherRunLevel = (runs[otherRunIndex+1] >>> Layout.RUN_LEVEL_SHIFT) &
564 Layout.RUN_LEVEL_MASK;
565 boolean otherRunIsRtl = (otherRunLevel & 1) != 0;
566
567 advance = toLeft == otherRunIsRtl;
568 if (newCaret == -1) {
Doug Felt0c702b82010-05-14 10:55:42 -0700569 newCaret = getOffsetBeforeAfter(otherRunIndex, otherRunStart,
570 otherRunLimit, otherRunIsRtl,
Doug Felte8e45f22010-03-29 14:58:40 -0700571 advance ? otherRunStart : otherRunLimit, advance);
572 if (newCaret == (advance ? otherRunLimit : otherRunStart)) {
573 // Crossed and ended up at a new boundary,
574 // repeat a second and final time.
575 runIndex = otherRunIndex;
576 runLevel = otherRunLevel;
577 continue;
578 }
579 break;
580 }
581
582 // The new caret is at a boundary.
583 if (otherRunLevel < runLevel) {
584 // The strong character is in the other run.
585 newCaret = advance ? otherRunStart : otherRunLimit;
586 }
587 break;
588 }
589
590 if (newCaret == -1) {
591 // We're walking off the end of the line. The paragraph
592 // level is always equal to or lower than any internal level, so
593 // the boundaries get the strong caret.
Doug Felt0c702b82010-05-14 10:55:42 -0700594 newCaret = advance ? mLen + 1 : -1;
Doug Felte8e45f22010-03-29 14:58:40 -0700595 break;
596 }
597
598 // Else we've arrived at the end of the line. That's a strong position.
599 // We might have arrived here by crossing over a run with no internal
600 // breaks and dropping out of the above loop before advancing one final
601 // time, so reset the caret.
602 // Note, we use '<=' below to handle a situation where the only run
603 // on the line is a counter-directional run. If we're not advancing,
604 // we can end up at the 'lineEnd' position but the caret we want is at
605 // the lineStart.
606 if (newCaret <= lineEnd) {
607 newCaret = advance ? lineEnd : lineStart;
608 }
609 break;
610 }
611
612 return newCaret;
613 }
614
615 /**
616 * Returns the next valid offset within this directional run, skipping
617 * conjuncts and zero-width characters. This should not be called to walk
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -0700618 * off the end of the line, since the returned values might not be valid
Doug Felt0c702b82010-05-14 10:55:42 -0700619 * on neighboring lines. If the returned offset is less than zero or
620 * greater than the line length, the offset should be recomputed on the
621 * preceding or following line, respectively.
Doug Felte8e45f22010-03-29 14:58:40 -0700622 *
623 * @param runIndex the run index
Doug Felt0c702b82010-05-14 10:55:42 -0700624 * @param runStart the start of the run
625 * @param runLimit the limit of the run
626 * @param runIsRtl true if the run is right-to-left
Doug Felte8e45f22010-03-29 14:58:40 -0700627 * @param offset the offset
628 * @param after true if the new offset should logically follow the provided
629 * offset
630 * @return the new offset
631 */
Doug Felt0c702b82010-05-14 10:55:42 -0700632 private int getOffsetBeforeAfter(int runIndex, int runStart, int runLimit,
633 boolean runIsRtl, int offset, boolean after) {
Doug Felte8e45f22010-03-29 14:58:40 -0700634
Doug Felt0c702b82010-05-14 10:55:42 -0700635 if (runIndex < 0 || offset == (after ? mLen : 0)) {
636 // Walking off end of line. Since we don't know
637 // what cursor positions are available on other lines, we can't
638 // return accurate values. These are a guess.
Doug Felte8e45f22010-03-29 14:58:40 -0700639 if (after) {
Doug Felt0c702b82010-05-14 10:55:42 -0700640 return TextUtils.getOffsetAfter(mText, offset + mStart) - mStart;
641 }
642 return TextUtils.getOffsetBefore(mText, offset + mStart) - mStart;
643 }
644
645 TextPaint wp = mWorkPaint;
646 wp.set(mPaint);
Seigo Nonaka09da71a2016-11-28 16:24:14 +0900647 wp.setWordSpacing(mAddedWidth);
Doug Felt0c702b82010-05-14 10:55:42 -0700648
649 int spanStart = runStart;
650 int spanLimit;
651 if (mSpanned == null) {
652 spanLimit = runLimit;
653 } else {
654 int target = after ? offset + 1 : offset;
655 int limit = mStart + runLimit;
656 while (true) {
657 spanLimit = mSpanned.nextSpanTransition(mStart + spanStart, limit,
658 MetricAffectingSpan.class) - mStart;
659 if (spanLimit >= target) {
660 break;
Doug Felte8e45f22010-03-29 14:58:40 -0700661 }
Doug Felt0c702b82010-05-14 10:55:42 -0700662 spanStart = spanLimit;
663 }
664
665 MetricAffectingSpan[] spans = mSpanned.getSpans(mStart + spanStart,
666 mStart + spanLimit, MetricAffectingSpan.class);
Gilles Debunne1e3ac182011-03-08 14:22:34 -0800667 spans = TextUtils.removeEmptySpans(spans, mSpanned, MetricAffectingSpan.class);
Doug Felt0c702b82010-05-14 10:55:42 -0700668
669 if (spans.length > 0) {
670 ReplacementSpan replacement = null;
671 for (int j = 0; j < spans.length; j++) {
672 MetricAffectingSpan span = spans[j];
673 if (span instanceof ReplacementSpan) {
674 replacement = (ReplacementSpan)span;
675 } else {
676 span.updateMeasureState(wp);
677 }
678 }
679
680 if (replacement != null) {
681 // If we have a replacement span, we're moving either to
682 // the start or end of this span.
683 return after ? spanLimit : spanStart;
Doug Felte8e45f22010-03-29 14:58:40 -0700684 }
685 }
Doug Felte8e45f22010-03-29 14:58:40 -0700686 }
687
Raph Levien051910b2014-06-15 18:25:29 -0700688 int dir = runIsRtl ? Paint.DIRECTION_RTL : Paint.DIRECTION_LTR;
Doug Felt0c702b82010-05-14 10:55:42 -0700689 int cursorOpt = after ? Paint.CURSOR_AFTER : Paint.CURSOR_BEFORE;
690 if (mCharsValid) {
691 return wp.getTextRunCursor(mChars, spanStart, spanLimit - spanStart,
Raph Levien051910b2014-06-15 18:25:29 -0700692 dir, offset, cursorOpt);
Doug Felt0c702b82010-05-14 10:55:42 -0700693 } else {
694 return wp.getTextRunCursor(mText, mStart + spanStart,
Raph Levien051910b2014-06-15 18:25:29 -0700695 mStart + spanLimit, dir, mStart + offset, cursorOpt) - mStart;
Doug Felte8e45f22010-03-29 14:58:40 -0700696 }
Doug Felte8e45f22010-03-29 14:58:40 -0700697 }
698
699 /**
Gilles Debunne0bb00092010-12-02 15:50:26 -0800700 * @param wp
701 */
702 private static void expandMetricsFromPaint(FontMetricsInt fmi, TextPaint wp) {
703 final int previousTop = fmi.top;
704 final int previousAscent = fmi.ascent;
705 final int previousDescent = fmi.descent;
706 final int previousBottom = fmi.bottom;
707 final int previousLeading = fmi.leading;
708
709 wp.getFontMetricsInt(fmi);
710
Fabrice Di Meglio8a5137a2011-09-22 18:49:43 -0700711 updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom,
712 previousLeading);
713 }
714
715 static void updateMetrics(FontMetricsInt fmi, int previousTop, int previousAscent,
716 int previousDescent, int previousBottom, int previousLeading) {
Gilles Debunne0bb00092010-12-02 15:50:26 -0800717 fmi.top = Math.min(fmi.top, previousTop);
718 fmi.ascent = Math.min(fmi.ascent, previousAscent);
719 fmi.descent = Math.max(fmi.descent, previousDescent);
720 fmi.bottom = Math.max(fmi.bottom, previousBottom);
721 fmi.leading = Math.max(fmi.leading, previousLeading);
722 }
723
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700724 private static void drawStroke(TextPaint wp, Canvas c, int color, float position,
725 float thickness, float xleft, float xright, float baseline) {
726 final float strokeTop = baseline + wp.baselineShift + position;
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700727
728 final int previousColor = wp.getColor();
729 final Paint.Style previousStyle = wp.getStyle();
730 final boolean previousAntiAlias = wp.isAntiAlias();
731
732 wp.setStyle(Paint.Style.FILL);
733 wp.setAntiAlias(true);
734
735 wp.setColor(color);
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700736 c.drawRect(xleft, strokeTop, xright, strokeTop + thickness, wp);
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700737
738 wp.setStyle(previousStyle);
739 wp.setColor(previousColor);
740 wp.setAntiAlias(previousAntiAlias);
741 }
742
743 private float getRunAdvance(TextPaint wp, int start, int end, int contextStart, int contextEnd,
744 boolean runIsRtl, int offset) {
745 if (mCharsValid) {
746 return wp.getRunAdvance(mChars, start, end, contextStart, contextEnd, runIsRtl, offset);
747 } else {
748 final int delta = mStart;
Seigo Nonaka783f9612018-01-20 12:11:13 -0800749 if (mMeasured == null) {
750 // TODO: Enable measured getRunAdvance for ReplacementSpan and RTL text.
751 return wp.getRunAdvance(mText, delta + start, delta + end,
752 delta + contextStart, delta + contextEnd, runIsRtl, delta + offset);
753 } else {
754 return mMeasured.getWidth(start + delta, end + delta);
755 }
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700756 }
757 }
758
Gilles Debunne0bb00092010-12-02 15:50:26 -0800759 /**
Doug Felte8e45f22010-03-29 14:58:40 -0700760 * Utility function for measuring and rendering text. The text must
Roozbeh Pournader112d9c72015-08-07 12:44:41 -0700761 * not include a tab.
Doug Felte8e45f22010-03-29 14:58:40 -0700762 *
763 * @param wp the working paint
764 * @param start the start of the text
Doug Felt0c702b82010-05-14 10:55:42 -0700765 * @param end the end of the text
Doug Felte8e45f22010-03-29 14:58:40 -0700766 * @param runIsRtl true if the run is right-to-left
767 * @param c the canvas, can be null if rendering is not needed
768 * @param x the edge of the run closest to the leading margin
769 * @param top the top of the line
770 * @param y the baseline
771 * @param bottom the bottom of the line
772 * @param fmi receives metrics information, can be null
773 * @param needWidth true if the width of the run is needed
Raph Levien909c7bc2015-11-30 21:05:46 -0800774 * @param offset the offset for the purpose of measuring
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700775 * @param decorations the list of locations and paremeters for drawing decorations
Doug Felte8e45f22010-03-29 14:58:40 -0700776 * @return the signed width of the run based on the run direction; only
777 * valid if needWidth is true
778 */
Doug Felt0c702b82010-05-14 10:55:42 -0700779 private float handleText(TextPaint wp, int start, int end,
780 int contextStart, int contextEnd, boolean runIsRtl,
781 Canvas c, float x, int top, int y, int bottom,
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700782 FontMetricsInt fmi, boolean needWidth, int offset,
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700783 @Nullable ArrayList<DecorationInfo> decorations) {
Doug Felte8e45f22010-03-29 14:58:40 -0700784
Seigo Nonaka09da71a2016-11-28 16:24:14 +0900785 wp.setWordSpacing(mAddedWidth);
Fabrice Di Meglio850dffa2011-08-08 09:45:09 -0700786 // Get metrics first (even for empty strings or "0" width runs)
787 if (fmi != null) {
788 expandMetricsFromPaint(fmi, wp);
789 }
Doug Felte8e45f22010-03-29 14:58:40 -0700790
Fabrice Di Meglio850dffa2011-08-08 09:45:09 -0700791 // No need to do anything if the run width is "0"
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700792 if (end == start) {
Fabrice Di Meglio850dffa2011-08-08 09:45:09 -0700793 return 0f;
794 }
795
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700796 float totalWidth = 0;
Fabrice Di Meglio850dffa2011-08-08 09:45:09 -0700797
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700798 final int numDecorations = decorations == null ? 0 : decorations.size();
799 if (needWidth || (c != null && (wp.bgColor != 0 || numDecorations != 0 || runIsRtl))) {
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700800 totalWidth = getRunAdvance(wp, start, end, contextStart, contextEnd, runIsRtl, offset);
Doug Felte8e45f22010-03-29 14:58:40 -0700801 }
802
Doug Felte8e45f22010-03-29 14:58:40 -0700803 if (c != null) {
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700804 final float leftX, rightX;
Doug Felte8e45f22010-03-29 14:58:40 -0700805 if (runIsRtl) {
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700806 leftX = x - totalWidth;
807 rightX = x;
808 } else {
809 leftX = x;
810 rightX = x + totalWidth;
Doug Felte8e45f22010-03-29 14:58:40 -0700811 }
812
813 if (wp.bgColor != 0) {
Gilles Debunnedd8f5ed2011-08-10 18:25:49 -0700814 int previousColor = wp.getColor();
815 Paint.Style previousStyle = wp.getStyle();
816
Doug Felte8e45f22010-03-29 14:58:40 -0700817 wp.setColor(wp.bgColor);
818 wp.setStyle(Paint.Style.FILL);
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700819 c.drawRect(leftX, top, rightX, bottom, wp);
Doug Felte8e45f22010-03-29 14:58:40 -0700820
Gilles Debunnedd8f5ed2011-08-10 18:25:49 -0700821 wp.setStyle(previousStyle);
822 wp.setColor(previousColor);
823 }
824
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700825 if (numDecorations != 0) {
826 for (int i = 0; i < numDecorations; i++) {
827 final DecorationInfo info = decorations.get(i);
Gilles Debunnedd8f5ed2011-08-10 18:25:49 -0700828
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700829 final int decorationStart = Math.max(info.start, start);
830 final int decorationEnd = Math.min(info.end, offset);
831 float decorationStartAdvance = getRunAdvance(
832 wp, start, end, contextStart, contextEnd, runIsRtl, decorationStart);
833 float decorationEndAdvance = getRunAdvance(
834 wp, start, end, contextStart, contextEnd, runIsRtl, decorationEnd);
835 final float decorationXLeft, decorationXRight;
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700836 if (runIsRtl) {
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700837 decorationXLeft = rightX - decorationEndAdvance;
838 decorationXRight = rightX - decorationStartAdvance;
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700839 } else {
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700840 decorationXLeft = leftX + decorationStartAdvance;
841 decorationXRight = leftX + decorationEndAdvance;
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700842 }
Siyamed Sinir702c9f92017-06-01 18:25:40 +0000843
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700844 // Theoretically, there could be cases where both Paint's and TextPaint's
845 // setUnderLineText() are called. For backward compatibility, we need to draw
846 // both underlines, the one with custom color first.
847 if (info.underlineColor != 0) {
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700848 drawStroke(wp, c, info.underlineColor, wp.getUnderlinePosition(),
849 info.underlineThickness, decorationXLeft, decorationXRight, y);
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700850 }
851 if (info.isUnderlineText) {
Roozbeh Pournaderca8a04a2017-06-06 18:30:29 -0700852 final float thickness =
Siyamed Sinira273a702017-10-05 11:22:12 -0700853 Math.max(wp.getUnderlineThickness(), 1.0f);
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700854 drawStroke(wp, c, wp.getColor(), wp.getUnderlinePosition(), thickness,
855 decorationXLeft, decorationXRight, y);
856 }
857
858 if (info.isStrikeThruText) {
859 final float thickness =
Siyamed Sinira273a702017-10-05 11:22:12 -0700860 Math.max(wp.getStrikeThruThickness(), 1.0f);
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700861 drawStroke(wp, c, wp.getColor(), wp.getStrikeThruPosition(), thickness,
862 decorationXLeft, decorationXRight, y);
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700863 }
864 }
Doug Felte8e45f22010-03-29 14:58:40 -0700865 }
866
Fabrice Di Meglioda12f382013-03-15 11:26:56 -0700867 drawTextRun(c, wp, start, end, contextStart, contextEnd, runIsRtl,
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700868 leftX, y + wp.baselineShift);
Doug Felte8e45f22010-03-29 14:58:40 -0700869 }
870
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700871 return runIsRtl ? -totalWidth : totalWidth;
Doug Felte8e45f22010-03-29 14:58:40 -0700872 }
873
874 /**
875 * Utility function for measuring and rendering a replacement.
876 *
Romain Guybc7cdb62011-05-26 18:48:49 -0700877 *
Doug Felte8e45f22010-03-29 14:58:40 -0700878 * @param replacement the replacement
879 * @param wp the work paint
Doug Felte8e45f22010-03-29 14:58:40 -0700880 * @param start the start of the run
881 * @param limit the limit of the run
882 * @param runIsRtl true if the run is right-to-left
883 * @param c the canvas, can be null if not rendering
884 * @param x the edge of the replacement closest to the leading margin
885 * @param top the top of the line
886 * @param y the baseline
887 * @param bottom the bottom of the line
888 * @param fmi receives metrics information, can be null
889 * @param needWidth true if the width of the replacement is needed
Doug Felte8e45f22010-03-29 14:58:40 -0700890 * @return the signed width of the run based on the run direction; only
891 * valid if needWidth is true
892 */
893 private float handleReplacement(ReplacementSpan replacement, TextPaint wp,
Romain Guybc7cdb62011-05-26 18:48:49 -0700894 int start, int limit, boolean runIsRtl, Canvas c,
Doug Felte8e45f22010-03-29 14:58:40 -0700895 float x, int top, int y, int bottom, FontMetricsInt fmi,
Doug Felt0c702b82010-05-14 10:55:42 -0700896 boolean needWidth) {
Doug Felte8e45f22010-03-29 14:58:40 -0700897
898 float ret = 0;
899
Doug Felt0c702b82010-05-14 10:55:42 -0700900 int textStart = mStart + start;
901 int textLimit = mStart + limit;
902
903 if (needWidth || (c != null && runIsRtl)) {
Fabrice Di Meglio8a5137a2011-09-22 18:49:43 -0700904 int previousTop = 0;
905 int previousAscent = 0;
906 int previousDescent = 0;
907 int previousBottom = 0;
908 int previousLeading = 0;
909
910 boolean needUpdateMetrics = (fmi != null);
911
912 if (needUpdateMetrics) {
913 previousTop = fmi.top;
914 previousAscent = fmi.ascent;
915 previousDescent = fmi.descent;
916 previousBottom = fmi.bottom;
917 previousLeading = fmi.leading;
918 }
919
Doug Felt0c702b82010-05-14 10:55:42 -0700920 ret = replacement.getSize(wp, mText, textStart, textLimit, fmi);
Fabrice Di Meglio8a5137a2011-09-22 18:49:43 -0700921
922 if (needUpdateMetrics) {
923 updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom,
924 previousLeading);
925 }
Doug Felte8e45f22010-03-29 14:58:40 -0700926 }
927
Doug Felt0c702b82010-05-14 10:55:42 -0700928 if (c != null) {
929 if (runIsRtl) {
930 x -= ret;
Doug Felte8e45f22010-03-29 14:58:40 -0700931 }
Doug Felt0c702b82010-05-14 10:55:42 -0700932 replacement.draw(c, mText, textStart, textLimit,
933 x, top, y, bottom, wp);
Doug Felte8e45f22010-03-29 14:58:40 -0700934 }
935
936 return runIsRtl ? -ret : ret;
937 }
938
Roozbeh Pournader46c6f4c2017-02-21 12:18:31 -0800939 private int adjustHyphenEdit(int start, int limit, int hyphenEdit) {
940 int result = hyphenEdit;
941 // Only draw hyphens on first or last run in line. Disable them otherwise.
942 if (start > 0) { // not the first run
943 result &= ~Paint.HYPHENEDIT_MASK_START_OF_LINE;
944 }
945 if (limit < mLen) { // not the last run
946 result &= ~Paint.HYPHENEDIT_MASK_END_OF_LINE;
947 }
948 return result;
949 }
950
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700951 private static final class DecorationInfo {
952 public boolean isStrikeThruText;
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700953 public boolean isUnderlineText;
954 public int underlineColor;
955 public float underlineThickness;
956 public int start = -1;
957 public int end = -1;
958
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700959 public boolean hasDecoration() {
960 return isStrikeThruText || isUnderlineText || underlineColor != 0;
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700961 }
962
963 // Copies the info, but not the start and end range.
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700964 public DecorationInfo copyInfo() {
965 final DecorationInfo copy = new DecorationInfo();
966 copy.isStrikeThruText = isStrikeThruText;
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700967 copy.isUnderlineText = isUnderlineText;
968 copy.underlineColor = underlineColor;
969 copy.underlineThickness = underlineThickness;
970 return copy;
971 }
972 }
973
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700974 private void extractDecorationInfo(@NonNull TextPaint paint, @NonNull DecorationInfo info) {
975 info.isStrikeThruText = paint.isStrikeThruText();
976 if (info.isStrikeThruText) {
977 paint.setStrikeThruText(false);
978 }
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700979 info.isUnderlineText = paint.isUnderlineText();
980 if (info.isUnderlineText) {
981 paint.setUnderlineText(false);
982 }
983 info.underlineColor = paint.underlineColor;
984 info.underlineThickness = paint.underlineThickness;
985 paint.setUnderlineText(0, 0.0f);
986 }
987
Doug Felte8e45f22010-03-29 14:58:40 -0700988 /**
989 * Utility function for handling a unidirectional run. The run must not
Roozbeh Pournader112d9c72015-08-07 12:44:41 -0700990 * contain tabs but can contain styles.
Doug Felte8e45f22010-03-29 14:58:40 -0700991 *
Romain Guybc7cdb62011-05-26 18:48:49 -0700992 *
Doug Felte8e45f22010-03-29 14:58:40 -0700993 * @param start the line-relative start of the run
Doug Felt0c702b82010-05-14 10:55:42 -0700994 * @param measureLimit the offset to measure to, between start and limit inclusive
Doug Felte8e45f22010-03-29 14:58:40 -0700995 * @param limit the limit of the run
996 * @param runIsRtl true if the run is right-to-left
997 * @param c the canvas, can be null
998 * @param x the end of the run closest to the leading margin
999 * @param top the top of the line
1000 * @param y the baseline
1001 * @param bottom the bottom of the line
1002 * @param fmi receives metrics information, can be null
1003 * @param needWidth true if the width is required
Doug Felte8e45f22010-03-29 14:58:40 -07001004 * @return the signed width of the run based on the run direction; only
1005 * valid if needWidth is true
1006 */
Romain Guybc7cdb62011-05-26 18:48:49 -07001007 private float handleRun(int start, int measureLimit,
Doug Felte8e45f22010-03-29 14:58:40 -07001008 int limit, boolean runIsRtl, Canvas c, float x, int top, int y,
Doug Felt0c702b82010-05-14 10:55:42 -07001009 int bottom, FontMetricsInt fmi, boolean needWidth) {
Doug Felte8e45f22010-03-29 14:58:40 -07001010
Siyamed Sinir9f3958c2016-08-26 15:23:47 -07001011 if (measureLimit < start || measureLimit > limit) {
1012 throw new IndexOutOfBoundsException("measureLimit (" + measureLimit + ") is out of "
1013 + "start (" + start + ") and limit (" + limit + ") bounds");
1014 }
1015
Gilles Debunnef483e512011-04-28 15:08:54 -07001016 // Case of an empty line, make sure we update fmi according to mPaint
1017 if (start == measureLimit) {
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001018 final TextPaint wp = mWorkPaint;
Gilles Debunnef483e512011-04-28 15:08:54 -07001019 wp.set(mPaint);
Fabrice Di Meglio15c097a2011-08-08 14:42:41 -07001020 if (fmi != null) {
1021 expandMetricsFromPaint(fmi, wp);
1022 }
1023 return 0f;
Gilles Debunnef483e512011-04-28 15:08:54 -07001024 }
1025
Roozbeh Pournader08836d42017-06-06 16:23:09 -07001026 final boolean needsSpanMeasurement;
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001027 if (mSpanned == null) {
Roozbeh Pournader08836d42017-06-06 16:23:09 -07001028 needsSpanMeasurement = false;
1029 } else {
1030 mMetricAffectingSpanSpanSet.init(mSpanned, mStart + start, mStart + limit);
1031 mCharacterStyleSpanSet.init(mSpanned, mStart + start, mStart + limit);
1032 needsSpanMeasurement = mMetricAffectingSpanSpanSet.numberOfSpans != 0
1033 || mCharacterStyleSpanSet.numberOfSpans != 0;
1034 }
1035
1036 if (!needsSpanMeasurement) {
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001037 final TextPaint wp = mWorkPaint;
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001038 wp.set(mPaint);
Roozbeh Pournader46c6f4c2017-02-21 12:18:31 -08001039 wp.setHyphenEdit(adjustHyphenEdit(start, limit, wp.getHyphenEdit()));
Raph Levien909c7bc2015-11-30 21:05:46 -08001040 return handleText(wp, start, limit, start, limit, runIsRtl, c, x, top,
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001041 y, bottom, fmi, needWidth, measureLimit, null);
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001042 }
1043
Doug Felte8e45f22010-03-29 14:58:40 -07001044 // Shaping needs to take into account context up to metric boundaries,
1045 // but rendering needs to take into account character style boundaries.
Doug Felt0c702b82010-05-14 10:55:42 -07001046 // So we iterate through metric runs to get metric bounds,
1047 // then within each metric run iterate through character style runs
1048 // for the run bounds.
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001049 final float originalX = x;
Doug Felt0c702b82010-05-14 10:55:42 -07001050 for (int i = start, inext; i < measureLimit; i = inext) {
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001051 final TextPaint wp = mWorkPaint;
Doug Felte8e45f22010-03-29 14:58:40 -07001052 wp.set(mPaint);
1053
Gilles Debunnec1f44832011-12-08 16:03:00 -08001054 inext = mMetricAffectingSpanSpanSet.getNextTransition(mStart + i, mStart + limit) -
1055 mStart;
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001056 int mlimit = Math.min(inext, measureLimit);
Doug Felte8e45f22010-03-29 14:58:40 -07001057
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001058 ReplacementSpan replacement = null;
Doug Felte8e45f22010-03-29 14:58:40 -07001059
Gilles Debunnec1f44832011-12-08 16:03:00 -08001060 for (int j = 0; j < mMetricAffectingSpanSpanSet.numberOfSpans; j++) {
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001061 // Both intervals [spanStarts..spanEnds] and [mStart + i..mStart + mlimit] are NOT
1062 // empty by construction. This special case in getSpans() explains the >= & <= tests
Gilles Debunnec1f44832011-12-08 16:03:00 -08001063 if ((mMetricAffectingSpanSpanSet.spanStarts[j] >= mStart + mlimit) ||
1064 (mMetricAffectingSpanSpanSet.spanEnds[j] <= mStart + i)) continue;
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001065 final MetricAffectingSpan span = mMetricAffectingSpanSpanSet.spans[j];
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001066 if (span instanceof ReplacementSpan) {
1067 replacement = (ReplacementSpan)span;
1068 } else {
1069 // We might have a replacement that uses the draw
1070 // state, otherwise measure state would suffice.
1071 span.updateDrawState(wp);
Doug Felte8e45f22010-03-29 14:58:40 -07001072 }
1073 }
1074
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001075 if (replacement != null) {
1076 x += handleReplacement(replacement, wp, i, mlimit, runIsRtl, c, x, top, y,
1077 bottom, fmi, needWidth || mlimit < measureLimit);
1078 continue;
1079 }
1080
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001081 final TextPaint activePaint = mActivePaint;
1082 activePaint.set(mPaint);
1083 int activeStart = i;
1084 int activeEnd = mlimit;
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001085 final DecorationInfo decorationInfo = mDecorationInfo;
1086 mDecorations.clear();
Raph Levien42ef515d2012-10-22 15:01:17 -07001087 for (int j = i, jnext; j < mlimit; j = jnext) {
Siyamed Sinir702c9f92017-06-01 18:25:40 +00001088 jnext = mCharacterStyleSpanSet.getNextTransition(mStart + j, mStart + inext) -
1089 mStart;
Doug Felte8e45f22010-03-29 14:58:40 -07001090
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001091 final int offset = Math.min(jnext, mlimit);
Raph Levien42ef515d2012-10-22 15:01:17 -07001092 wp.set(mPaint);
1093 for (int k = 0; k < mCharacterStyleSpanSet.numberOfSpans; k++) {
1094 // Intentionally using >= and <= as explained above
Siyamed Sinir702c9f92017-06-01 18:25:40 +00001095 if ((mCharacterStyleSpanSet.spanStarts[k] >= mStart + offset) ||
1096 (mCharacterStyleSpanSet.spanEnds[k] <= mStart + j)) continue;
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001097
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001098 final CharacterStyle span = mCharacterStyleSpanSet.spans[k];
Siyamed Sinir702c9f92017-06-01 18:25:40 +00001099 span.updateDrawState(wp);
Doug Felte8e45f22010-03-29 14:58:40 -07001100 }
Raph Levien42ef515d2012-10-22 15:01:17 -07001101
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001102 extractDecorationInfo(wp, decorationInfo);
Roozbeh Pournader46c6f4c2017-02-21 12:18:31 -08001103
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001104 if (j == i) {
1105 // First chunk of text. We can't handle it yet, since we may need to merge it
1106 // with the next chunk. So we just save the TextPaint for future comparisons
1107 // and use.
1108 activePaint.set(wp);
1109 } else if (!wp.hasEqualAttributes(activePaint)) {
1110 // The style of the present chunk of text is substantially different from the
1111 // style of the previous chunk. We need to handle the active piece of text
1112 // and restart with the present chunk.
1113 activePaint.setHyphenEdit(adjustHyphenEdit(
1114 activeStart, activeEnd, mPaint.getHyphenEdit()));
1115 x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, x,
1116 top, y, bottom, fmi, needWidth || activeEnd < measureLimit,
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001117 Math.min(activeEnd, mlimit), mDecorations);
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001118
1119 activeStart = j;
1120 activePaint.set(wp);
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001121 mDecorations.clear();
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001122 } else {
1123 // The present TextPaint is substantially equal to the last TextPaint except
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001124 // perhaps for decorations. We just need to expand the active piece of text to
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001125 // include the present chunk, which we always do anyway. We don't need to save
1126 // wp to activePaint, since they are already equal.
1127 }
1128
1129 activeEnd = jnext;
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001130 if (decorationInfo.hasDecoration()) {
1131 final DecorationInfo copy = decorationInfo.copyInfo();
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001132 copy.start = j;
1133 copy.end = jnext;
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001134 mDecorations.add(copy);
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001135 }
Doug Felte8e45f22010-03-29 14:58:40 -07001136 }
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001137 // Handle the final piece of text.
1138 activePaint.setHyphenEdit(adjustHyphenEdit(
1139 activeStart, activeEnd, mPaint.getHyphenEdit()));
1140 x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, x,
1141 top, y, bottom, fmi, needWidth || activeEnd < measureLimit,
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001142 Math.min(activeEnd, mlimit), mDecorations);
Doug Felte8e45f22010-03-29 14:58:40 -07001143 }
1144
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001145 return x - originalX;
Doug Felte8e45f22010-03-29 14:58:40 -07001146 }
1147
Doug Felte8e45f22010-03-29 14:58:40 -07001148 /**
1149 * Render a text run with the set-up paint.
1150 *
1151 * @param c the canvas
1152 * @param wp the paint used to render the text
Doug Felt0c702b82010-05-14 10:55:42 -07001153 * @param start the start of the run
1154 * @param end the end of the run
1155 * @param contextStart the start of context for the run
1156 * @param contextEnd the end of the context for the run
Fabrice Di Meglioda12f382013-03-15 11:26:56 -07001157 * @param runIsRtl true if the run is right-to-left
Doug Felte8e45f22010-03-29 14:58:40 -07001158 * @param x the x position of the left edge of the run
1159 * @param y the baseline of the run
1160 */
Doug Felt0c702b82010-05-14 10:55:42 -07001161 private void drawTextRun(Canvas c, TextPaint wp, int start, int end,
Fabrice Di Meglioda12f382013-03-15 11:26:56 -07001162 int contextStart, int contextEnd, boolean runIsRtl, float x, int y) {
Doug Felte8e45f22010-03-29 14:58:40 -07001163
Doug Felte8e45f22010-03-29 14:58:40 -07001164 if (mCharsValid) {
Doug Felt0c702b82010-05-14 10:55:42 -07001165 int count = end - start;
1166 int contextCount = contextEnd - contextStart;
1167 c.drawTextRun(mChars, start, count, contextStart, contextCount,
Raph Levien051910b2014-06-15 18:25:29 -07001168 x, y, runIsRtl, wp);
Doug Felte8e45f22010-03-29 14:58:40 -07001169 } else {
Doug Felt0c702b82010-05-14 10:55:42 -07001170 int delta = mStart;
1171 c.drawTextRun(mText, delta + start, delta + end,
Raph Levien051910b2014-06-15 18:25:29 -07001172 delta + contextStart, delta + contextEnd, x, y, runIsRtl, wp);
Doug Felte8e45f22010-03-29 14:58:40 -07001173 }
1174 }
1175
1176 /**
Doug Felte8e45f22010-03-29 14:58:40 -07001177 * Returns the next tab position.
1178 *
1179 * @param h the (unsigned) offset from the leading margin
1180 * @return the (unsigned) tab position after this offset
1181 */
1182 float nextTab(float h) {
Doug Feltc982f602010-05-25 11:51:40 -07001183 if (mTabs != null) {
1184 return mTabs.nextTab(h);
Doug Felte8e45f22010-03-29 14:58:40 -07001185 }
Doug Feltc982f602010-05-25 11:51:40 -07001186 return TabStops.nextDefaultStop(h, TAB_INCREMENT);
Doug Felte8e45f22010-03-29 14:58:40 -07001187 }
1188
Seigo Nonaka09da71a2016-11-28 16:24:14 +09001189 private boolean isStretchableWhitespace(int ch) {
Roozbeh Pournader3630d082017-10-23 12:16:32 -07001190 // TODO: Support NBSP and other stretchable whitespace (b/34013491 and b/68204709).
1191 return ch == 0x0020;
Seigo Nonaka09da71a2016-11-28 16:24:14 +09001192 }
1193
1194 /* Return the number of spaces in the text line, for the purpose of justification */
1195 private int countStretchableSpaces(int start, int end) {
1196 int count = 0;
Roozbeh Pournader3630d082017-10-23 12:16:32 -07001197 for (int i = start; i < end; i++) {
1198 final char c = mCharsValid ? mChars[i] : mText.charAt(i + mStart);
1199 if (isStretchableWhitespace(c)) {
1200 count++;
1201 }
Seigo Nonaka09da71a2016-11-28 16:24:14 +09001202 }
1203 return count;
1204 }
1205
1206 // Note: keep this in sync with Minikin LineBreaker::isLineEndSpace()
1207 public static boolean isLineEndSpace(char ch) {
1208 return ch == ' ' || ch == '\t' || ch == 0x1680
1209 || (0x2000 <= ch && ch <= 0x200A && ch != 0x2007)
1210 || ch == 0x205F || ch == 0x3000;
1211 }
1212
Doug Felte8e45f22010-03-29 14:58:40 -07001213 private static final int TAB_INCREMENT = 20;
1214}