blob: 949328fd3a99067291653264e1313a672725e5e4 [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
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +000019import android.annotation.IntRange;
Roozbeh Pournader538de5b2017-05-24 09:07:52 -070020import android.annotation.NonNull;
21import android.annotation.Nullable;
Mathew Inwoodefeab842018-08-14 15:21:30 +010022import android.annotation.UnsupportedAppUsage;
Doug Felte8e45f22010-03-29 14:58:40 -070023import android.graphics.Canvas;
24import android.graphics.Paint;
Doug Felte8e45f22010-03-29 14:58:40 -070025import android.graphics.Paint.FontMetricsInt;
Mathew Inwood8c854f82018-09-14 12:35:36 +010026import android.os.Build;
Doug Felte8e45f22010-03-29 14:58:40 -070027import android.text.Layout.Directions;
Doug Feltc982f602010-05-25 11:51:40 -070028import android.text.Layout.TabStops;
Doug Felte8e45f22010-03-29 14:58:40 -070029import android.text.style.CharacterStyle;
30import android.text.style.MetricAffectingSpan;
31import android.text.style.ReplacementSpan;
Doug Felte8e45f22010-03-29 14:58:40 -070032import android.util.Log;
33
Roozbeh Pournader3630d082017-10-23 12:16:32 -070034import com.android.internal.annotations.VisibleForTesting;
Gilles Debunnedd8f5ed2011-08-10 18:25:49 -070035import com.android.internal.util.ArrayUtils;
36
Roozbeh Pournader538de5b2017-05-24 09:07:52 -070037import java.util.ArrayList;
38
Doug Felte8e45f22010-03-29 14:58:40 -070039/**
40 * Represents a line of styled text, for measuring in visual order and
41 * for rendering.
42 *
43 * <p>Get a new instance using obtain(), and when finished with it, return it
44 * to the pool using recycle().
45 *
46 * <p>Call set to prepare the instance for use, then either draw, measure,
47 * metrics, or caretToLeftRightOf.
48 *
49 * @hide
50 */
Roozbeh Pournader3630d082017-10-23 12:16:32 -070051@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
52public class TextLine {
Romain Guybc7cdb62011-05-26 18:48:49 -070053 private static final boolean DEBUG = false;
54
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +000055 private static final char TAB_CHAR = '\t';
56
Doug Felte8e45f22010-03-29 14:58:40 -070057 private TextPaint mPaint;
Mathew Inwoodefeab842018-08-14 15:21:30 +010058 @UnsupportedAppUsage
Doug Felte8e45f22010-03-29 14:58:40 -070059 private CharSequence mText;
60 private int mStart;
61 private int mLen;
62 private int mDir;
63 private Directions mDirections;
64 private boolean mHasTabs;
Doug Feltc982f602010-05-25 11:51:40 -070065 private TabStops mTabs;
Doug Felte8e45f22010-03-29 14:58:40 -070066 private char[] mChars;
67 private boolean mCharsValid;
Mathew Inwood8c854f82018-09-14 12:35:36 +010068 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
Doug Felte8e45f22010-03-29 14:58:40 -070069 private Spanned mSpanned;
Seigo Nonakabeafa1f2018-02-01 21:39:24 -080070 private PrecomputedText mComputed;
Seigo Nonaka09da71a2016-11-28 16:24:14 +090071
Mihai Popace642dc2018-05-24 14:25:11 +010072 // The start and end of a potentially existing ellipsis on this text line.
73 // We use them to filter out replacement and metric affecting spans on ellipsized away chars.
74 private int mEllipsisStart;
75 private int mEllipsisEnd;
76
Seigo Nonaka09da71a2016-11-28 16:24:14 +090077 // Additional width of whitespace for justification. This value is per whitespace, thus
Haoyu Zhangeccdc6b2019-01-07 15:11:46 -080078 // the line width will increase by mAddedWidthForJustify x (number of stretchable whitespaces).
79 private float mAddedWidthForJustify;
80 private boolean mIsJustifying;
Roozbeh Pournader538de5b2017-05-24 09:07:52 -070081
Gilles Debunne345cb032010-06-16 17:13:23 -070082 private final TextPaint mWorkPaint = new TextPaint();
Roozbeh Pournader538de5b2017-05-24 09:07:52 -070083 private final TextPaint mActivePaint = new TextPaint();
Mathew Inwoodefeab842018-08-14 15:21:30 +010084 @UnsupportedAppUsage
Gilles Debunnec1f44832011-12-08 16:03:00 -080085 private final SpanSet<MetricAffectingSpan> mMetricAffectingSpanSpanSet =
86 new SpanSet<MetricAffectingSpan>(MetricAffectingSpan.class);
Mathew Inwoodefeab842018-08-14 15:21:30 +010087 @UnsupportedAppUsage
Gilles Debunnec1f44832011-12-08 16:03:00 -080088 private final SpanSet<CharacterStyle> mCharacterStyleSpanSet =
89 new SpanSet<CharacterStyle>(CharacterStyle.class);
Mathew Inwoodefeab842018-08-14 15:21:30 +010090 @UnsupportedAppUsage
Gilles Debunnec1f44832011-12-08 16:03:00 -080091 private final SpanSet<ReplacementSpan> mReplacementSpanSpanSet =
92 new SpanSet<ReplacementSpan>(ReplacementSpan.class);
Doug Felte8e45f22010-03-29 14:58:40 -070093
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -070094 private final DecorationInfo mDecorationInfo = new DecorationInfo();
Siyamed Sinira273a702017-10-05 11:22:12 -070095 private final ArrayList<DecorationInfo> mDecorations = new ArrayList<>();
Roozbeh Pournader538de5b2017-05-24 09:07:52 -070096
Mathew Inwoodefeab842018-08-14 15:21:30 +010097 @UnsupportedAppUsage
Romain Guybc7cdb62011-05-26 18:48:49 -070098 private static final TextLine[] sCached = new TextLine[3];
Doug Felte8e45f22010-03-29 14:58:40 -070099
100 /**
101 * Returns a new TextLine from the shared pool.
102 *
103 * @return an uninitialized TextLine
104 */
Roozbeh Pournader3630d082017-10-23 12:16:32 -0700105 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
Mathew Inwoodefeab842018-08-14 15:21:30 +0100106 @UnsupportedAppUsage
Roozbeh Pournader3630d082017-10-23 12:16:32 -0700107 public static TextLine obtain() {
Doug Felte8e45f22010-03-29 14:58:40 -0700108 TextLine tl;
Romain Guybc7cdb62011-05-26 18:48:49 -0700109 synchronized (sCached) {
110 for (int i = sCached.length; --i >= 0;) {
111 if (sCached[i] != null) {
112 tl = sCached[i];
113 sCached[i] = null;
Doug Felte8e45f22010-03-29 14:58:40 -0700114 return tl;
115 }
116 }
117 }
118 tl = new TextLine();
Romain Guybc7cdb62011-05-26 18:48:49 -0700119 if (DEBUG) {
120 Log.v("TLINE", "new: " + tl);
121 }
Doug Felte8e45f22010-03-29 14:58:40 -0700122 return tl;
123 }
124
125 /**
126 * Puts a TextLine back into the shared pool. Do not use this TextLine once
127 * it has been returned.
128 * @param tl the textLine
129 * @return null, as a convenience from clearing references to the provided
130 * TextLine
131 */
Roozbeh Pournader3630d082017-10-23 12:16:32 -0700132 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
133 public static TextLine recycle(TextLine tl) {
Doug Felte8e45f22010-03-29 14:58:40 -0700134 tl.mText = null;
135 tl.mPaint = null;
136 tl.mDirections = null;
Svet Ganov893d6fe2015-01-16 10:10:15 -0800137 tl.mSpanned = null;
138 tl.mTabs = null;
139 tl.mChars = null;
Seigo Nonakabeafa1f2018-02-01 21:39:24 -0800140 tl.mComputed = null;
Gilles Debunnec3fb7a12011-12-12 14:03:40 -0800141
142 tl.mMetricAffectingSpanSpanSet.recycle();
143 tl.mCharacterStyleSpanSet.recycle();
144 tl.mReplacementSpanSpanSet.recycle();
145
Romain Guybc7cdb62011-05-26 18:48:49 -0700146 synchronized(sCached) {
147 for (int i = 0; i < sCached.length; ++i) {
148 if (sCached[i] == null) {
149 sCached[i] = tl;
Gilles Debunnef902d7b2011-01-25 09:09:46 -0800150 break;
Doug Felte8e45f22010-03-29 14:58:40 -0700151 }
152 }
153 }
154 return null;
155 }
156
157 /**
158 * Initializes a TextLine and prepares it for use.
159 *
160 * @param paint the base paint for the line
161 * @param text the text, can be Styled
162 * @param start the start of the line relative to the text
163 * @param limit the limit of the line relative to the text
164 * @param dir the paragraph direction of this line
165 * @param directions the directions information of this line
Roozbeh Pournader112d9c72015-08-07 12:44:41 -0700166 * @param hasTabs true if the line might contain tabs
Mihai Popace642dc2018-05-24 14:25:11 +0100167 * @param tabStops the tabStops. Can be null
168 * @param ellipsisStart the start of the ellipsis relative to the line
169 * @param ellipsisEnd the end of the ellipsis relative to the line. When there
170 * is no ellipsis, this should be equal to ellipsisStart.
Doug Felte8e45f22010-03-29 14:58:40 -0700171 */
Roozbeh Pournader3630d082017-10-23 12:16:32 -0700172 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
Seigo Nonaka4e90fa22018-02-13 21:40:01 +0000173 public void set(TextPaint paint, CharSequence text, int start, int limit, int dir,
Mihai Popace642dc2018-05-24 14:25:11 +0100174 Directions directions, boolean hasTabs, TabStops tabStops,
175 int ellipsisStart, int ellipsisEnd) {
Doug Felte8e45f22010-03-29 14:58:40 -0700176 mPaint = paint;
177 mText = text;
178 mStart = start;
179 mLen = limit - start;
180 mDir = dir;
181 mDirections = directions;
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -0700182 if (mDirections == null) {
183 throw new IllegalArgumentException("Directions cannot be null");
184 }
Doug Felte8e45f22010-03-29 14:58:40 -0700185 mHasTabs = hasTabs;
186 mSpanned = null;
Doug Felte8e45f22010-03-29 14:58:40 -0700187
188 boolean hasReplacement = false;
189 if (text instanceof Spanned) {
190 mSpanned = (Spanned) text;
Gilles Debunnec1f44832011-12-08 16:03:00 -0800191 mReplacementSpanSpanSet.init(mSpanned, start, limit);
192 hasReplacement = mReplacementSpanSpanSet.numberOfSpans > 0;
Doug Felte8e45f22010-03-29 14:58:40 -0700193 }
194
Seigo Nonakabeafa1f2018-02-01 21:39:24 -0800195 mComputed = null;
196 if (text instanceof PrecomputedText) {
Seigo Nonaka53145632018-03-23 17:22:54 -0700197 // Here, no need to check line break strategy or hyphenation frequency since there is no
198 // line break concept here.
Seigo Nonakabeafa1f2018-02-01 21:39:24 -0800199 mComputed = (PrecomputedText) text;
Seigo Nonaka53145632018-03-23 17:22:54 -0700200 if (!mComputed.getParams().getTextPaint().equalsForTextMeasurement(paint)) {
201 mComputed = null;
202 }
Seigo Nonaka4e90fa22018-02-13 21:40:01 +0000203 }
204
Siyamed Sinir95621332018-11-16 23:38:23 +0000205 mCharsValid = hasReplacement;
Doug Felte8e45f22010-03-29 14:58:40 -0700206
207 if (mCharsValid) {
208 if (mChars == null || mChars.length < mLen) {
Adam Lesinski776abc22014-03-07 11:30:59 -0500209 mChars = ArrayUtils.newUnpaddedCharArray(mLen);
Doug Felte8e45f22010-03-29 14:58:40 -0700210 }
211 TextUtils.getChars(text, start, limit, mChars, 0);
Doug Felt0c702b82010-05-14 10:55:42 -0700212 if (hasReplacement) {
213 // Handle these all at once so we don't have to do it as we go.
214 // Replace the first character of each replacement run with the
215 // object-replacement character and the remainder with zero width
216 // non-break space aka BOM. Cursor movement code skips these
217 // zero-width characters.
218 char[] chars = mChars;
219 for (int i = start, inext; i < limit; i = inext) {
Gilles Debunnec1f44832011-12-08 16:03:00 -0800220 inext = mReplacementSpanSpanSet.getNextTransition(i, limit);
Mihai Popace642dc2018-05-24 14:25:11 +0100221 if (mReplacementSpanSpanSet.hasSpansIntersecting(i, inext)
222 && (i - start >= ellipsisEnd || inext - start <= ellipsisStart)) {
Gilles Debunne1e3ac182011-03-08 14:22:34 -0800223 // transition into a span
Doug Felt0c702b82010-05-14 10:55:42 -0700224 chars[i - start] = '\ufffc';
225 for (int j = i - start + 1, e = inext - start; j < e; ++j) {
226 chars[j] = '\ufeff'; // used as ZWNBS, marks positions to skip
227 }
228 }
229 }
230 }
Doug Felte8e45f22010-03-29 14:58:40 -0700231 }
Doug Feltc982f602010-05-25 11:51:40 -0700232 mTabs = tabStops;
Haoyu Zhangeccdc6b2019-01-07 15:11:46 -0800233 mAddedWidthForJustify = 0;
234 mIsJustifying = false;
Mihai Popace642dc2018-05-24 14:25:11 +0100235
236 mEllipsisStart = ellipsisStart != ellipsisEnd ? ellipsisStart : 0;
237 mEllipsisEnd = ellipsisStart != ellipsisEnd ? ellipsisEnd : 0;
Seigo Nonaka09da71a2016-11-28 16:24:14 +0900238 }
239
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000240 private char charAt(int i) {
241 return mCharsValid ? mChars[i] : mText.charAt(i + mStart);
242 }
243
Seigo Nonaka09da71a2016-11-28 16:24:14 +0900244 /**
245 * Justify the line to the given width.
246 */
Roozbeh Pournader3630d082017-10-23 12:16:32 -0700247 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
248 public void justify(float justifyWidth) {
Seigo Nonaka09da71a2016-11-28 16:24:14 +0900249 int end = mLen;
250 while (end > 0 && isLineEndSpace(mText.charAt(mStart + end - 1))) {
251 end--;
252 }
253 final int spaces = countStretchableSpaces(0, end);
254 if (spaces == 0) {
255 // There are no stretchable spaces, so we can't help the justification by adding any
256 // width.
257 return;
258 }
259 final float width = Math.abs(measure(end, false, null));
Haoyu Zhangeccdc6b2019-01-07 15:11:46 -0800260 mAddedWidthForJustify = (justifyWidth - width) / spaces;
261 mIsJustifying = true;
Doug Felte8e45f22010-03-29 14:58:40 -0700262 }
263
264 /**
265 * Renders the TextLine.
266 *
267 * @param c the canvas to render on
268 * @param x the leading margin position
269 * @param top the top of the line
270 * @param y the baseline
271 * @param bottom the bottom of the line
272 */
273 void draw(Canvas c, float x, int top, int y, int bottom) {
Doug Felte8e45f22010-03-29 14:58:40 -0700274 float h = 0;
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000275 final int runCount = mDirections.getRunCount();
276 for (int runIndex = 0; runIndex < runCount; runIndex++) {
277 final int runStart = mDirections.getRunStart(runIndex);
278 final int runLimit = Math.min(runStart + mDirections.getRunLength(runIndex), mLen);
279 final boolean runIsRtl = mDirections.isRunRtl(runIndex);
Doug Felte8e45f22010-03-29 14:58:40 -0700280
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000281 int segStart = runStart;
Doug Felte8e45f22010-03-29 14:58:40 -0700282 for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000283 if (j == runLimit || charAt(j) == TAB_CHAR) {
284 h += drawRun(c, segStart, j, runIsRtl, x + h, top, y, bottom,
285 runIndex != (runCount - 1) || j != mLen);
Doug Felte8e45f22010-03-29 14:58:40 -0700286
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000287 if (j != runLimit) { // charAt(j) == TAB_CHAR
Doug Felte8e45f22010-03-29 14:58:40 -0700288 h = mDir * nextTab(h * mDir);
Doug Felte8e45f22010-03-29 14:58:40 -0700289 }
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000290 segStart = j + 1;
Doug Felte8e45f22010-03-29 14:58:40 -0700291 }
292 }
293 }
294 }
295
296 /**
297 * Returns metrics information for the entire line.
298 *
299 * @param fmi receives font metrics information, can be null
300 * @return the signed width of the line
301 */
Roozbeh Pournader3630d082017-10-23 12:16:32 -0700302 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
303 public float metrics(FontMetricsInt fmi) {
Doug Felte8e45f22010-03-29 14:58:40 -0700304 return measure(mLen, false, fmi);
305 }
306
307 /**
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000308 * Returns the signed graphical offset from the leading margin.
Doug Felte8e45f22010-03-29 14:58:40 -0700309 *
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000310 * Following examples are all for measuring offset=3. LX(e.g. L0, L1, ...) denotes a
311 * character which has LTR BiDi property. On the other hand, RX(e.g. R0, R1, ...) denotes a
312 * character which has RTL BiDi property. Assuming all character has 1em width.
313 *
314 * Example 1: All LTR chars within LTR context
315 * Input Text (logical) : L0 L1 L2 L3 L4 L5 L6 L7 L8
316 * Input Text (visual) : L0 L1 L2 L3 L4 L5 L6 L7 L8
317 * Output(trailing=true) : |--------| (Returns 3em)
318 * Output(trailing=false): |--------| (Returns 3em)
319 *
320 * Example 2: All RTL chars within RTL context.
321 * Input Text (logical) : R0 R1 R2 R3 R4 R5 R6 R7 R8
322 * Input Text (visual) : R8 R7 R6 R5 R4 R3 R2 R1 R0
323 * Output(trailing=true) : |--------| (Returns -3em)
324 * Output(trailing=false): |--------| (Returns -3em)
325 *
326 * Example 3: BiDi chars within LTR context.
327 * Input Text (logical) : L0 L1 L2 R3 R4 R5 L6 L7 L8
328 * Input Text (visual) : L0 L1 L2 R5 R4 R3 L6 L7 L8
329 * Output(trailing=true) : |-----------------| (Returns 6em)
330 * Output(trailing=false): |--------| (Returns 3em)
331 *
332 * Example 4: BiDi chars within RTL context.
333 * Input Text (logical) : L0 L1 L2 R3 R4 R5 L6 L7 L8
334 * Input Text (visual) : L6 L7 L8 R5 R4 R3 L0 L1 L2
335 * Output(trailing=true) : |-----------------| (Returns -6em)
336 * Output(trailing=false): |--------| (Returns -3em)
337 *
338 * @param offset the line-relative character offset, between 0 and the line length, inclusive
339 * @param trailing no effect if the offset is not on the BiDi transition offset. If the offset
340 * is on the BiDi transition offset and true is passed, the offset is regarded
341 * as the edge of the trailing run's edge. If false, the offset is regarded as
342 * the edge of the preceding run's edge. See example above.
343 * @param fmi receives metrics information about the requested character, can be null
344 * @return the signed graphical offset from the leading margin to the requested character edge.
345 * The positive value means the offset is right from the leading edge. The negative
346 * value means the offset is left from the leading edge.
Doug Felte8e45f22010-03-29 14:58:40 -0700347 */
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000348 public float measure(@IntRange(from = 0) int offset, boolean trailing,
349 @NonNull FontMetricsInt fmi) {
350 if (offset > mLen) {
351 throw new IndexOutOfBoundsException(
352 "offset(" + offset + ") should be less than line limit(" + mLen + ")");
353 }
354 final int target = trailing ? offset - 1 : offset;
Doug Felte8e45f22010-03-29 14:58:40 -0700355 if (target < 0) {
356 return 0;
357 }
358
359 float h = 0;
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000360 for (int runIndex = 0; runIndex < mDirections.getRunCount(); runIndex++) {
361 final int runStart = mDirections.getRunStart(runIndex);
362 final int runLimit = Math.min(runStart + mDirections.getRunLength(runIndex), mLen);
363 final boolean runIsRtl = mDirections.isRunRtl(runIndex);
Doug Felte8e45f22010-03-29 14:58:40 -0700364
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000365 int segStart = runStart;
Doug Felte8e45f22010-03-29 14:58:40 -0700366 for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000367 if (j == runLimit || charAt(j) == TAB_CHAR) {
368 final boolean targetIsInThisSegment = target >= segStart && target < j;
369 final boolean sameDirection = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl;
Doug Felte8e45f22010-03-29 14:58:40 -0700370
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000371 if (targetIsInThisSegment && sameDirection) {
372 return h + measureRun(segStart, offset, j, runIsRtl, fmi);
Doug Felte8e45f22010-03-29 14:58:40 -0700373 }
374
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000375 final float segmentWidth = measureRun(segStart, j, j, runIsRtl, fmi);
376 h += sameDirection ? segmentWidth : -segmentWidth;
Doug Felte8e45f22010-03-29 14:58:40 -0700377
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000378 if (targetIsInThisSegment) {
379 return h + measureRun(segStart, offset, j, runIsRtl, null);
Doug Felte8e45f22010-03-29 14:58:40 -0700380 }
381
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000382 if (j != runLimit) { // charAt(j) == TAB_CHAR
Doug Felte8e45f22010-03-29 14:58:40 -0700383 if (offset == j) {
384 return h;
385 }
386 h = mDir * nextTab(h * mDir);
387 if (target == j) {
388 return h;
389 }
390 }
391
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000392 segStart = j + 1;
Doug Felte8e45f22010-03-29 14:58:40 -0700393 }
394 }
395 }
396
397 return h;
398 }
399
400 /**
Mihai Popa7626c862018-05-09 17:31:48 +0100401 * @see #measure(int, boolean, FontMetricsInt)
402 * @return The measure results for all possible offsets
403 */
404 @VisibleForTesting
405 public float[] measureAllOffsets(boolean[] trailing, FontMetricsInt fmi) {
406 float[] measurement = new float[mLen + 1];
407
408 int[] target = new int[mLen + 1];
409 for (int offset = 0; offset < target.length; ++offset) {
410 target[offset] = trailing[offset] ? offset - 1 : offset;
411 }
412 if (target[0] < 0) {
413 measurement[0] = 0;
414 }
415
416 float h = 0;
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000417 for (int runIndex = 0; runIndex < mDirections.getRunCount(); runIndex++) {
418 final int runStart = mDirections.getRunStart(runIndex);
419 final int runLimit = Math.min(runStart + mDirections.getRunLength(runIndex), mLen);
420 final boolean runIsRtl = mDirections.isRunRtl(runIndex);
Mihai Popa7626c862018-05-09 17:31:48 +0100421
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000422 int segStart = runStart;
Mihai Popa7626c862018-05-09 17:31:48 +0100423 for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; ++j) {
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000424 if (j == runLimit || charAt(j) == TAB_CHAR) {
425 final float oldh = h;
426 final boolean advance = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl;
427 final float w = measureRun(segStart, j, j, runIsRtl, fmi);
Mihai Popa7626c862018-05-09 17:31:48 +0100428 h += advance ? w : -w;
429
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000430 final float baseh = advance ? oldh : h;
Mihai Popa7626c862018-05-09 17:31:48 +0100431 FontMetricsInt crtfmi = advance ? fmi : null;
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000432 for (int offset = segStart; offset <= j && offset <= mLen; ++offset) {
433 if (target[offset] >= segStart && target[offset] < j) {
Mihai Popa7626c862018-05-09 17:31:48 +0100434 measurement[offset] =
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000435 baseh + measureRun(segStart, offset, j, runIsRtl, crtfmi);
Mihai Popa7626c862018-05-09 17:31:48 +0100436 }
437 }
438
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000439 if (j != runLimit) { // charAt(j) == TAB_CHAR
Mihai Popa7626c862018-05-09 17:31:48 +0100440 if (target[j] == j) {
441 measurement[j] = h;
442 }
443 h = mDir * nextTab(h * mDir);
444 if (target[j + 1] == j) {
445 measurement[j + 1] = h;
446 }
447 }
448
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000449 segStart = j + 1;
Mihai Popa7626c862018-05-09 17:31:48 +0100450 }
451 }
452 }
453 if (target[mLen] == mLen) {
454 measurement[mLen] = h;
455 }
456
457 return measurement;
458 }
459
460 /**
Doug Felte8e45f22010-03-29 14:58:40 -0700461 * Draws a unidirectional (but possibly multi-styled) run of text.
462 *
Romain Guybc7cdb62011-05-26 18:48:49 -0700463 *
Doug Felte8e45f22010-03-29 14:58:40 -0700464 * @param c the canvas to draw on
Doug Felte8e45f22010-03-29 14:58:40 -0700465 * @param start the line-relative start
466 * @param limit the line-relative limit
467 * @param runIsRtl true if the run is right-to-left
468 * @param x the position of the run that is closest to the leading margin
469 * @param top the top of the line
470 * @param y the baseline
471 * @param bottom the bottom of the line
472 * @param needWidth true if the width value is required.
473 * @return the signed width of the run, based on the paragraph direction.
474 * Only valid if needWidth is true.
475 */
Romain Guybc7cdb62011-05-26 18:48:49 -0700476 private float drawRun(Canvas c, int start,
Doug Felte8e45f22010-03-29 14:58:40 -0700477 int limit, boolean runIsRtl, float x, int top, int y, int bottom,
478 boolean needWidth) {
479
480 if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) {
Romain Guybc7cdb62011-05-26 18:48:49 -0700481 float w = -measureRun(start, limit, limit, runIsRtl, null);
482 handleRun(start, limit, limit, runIsRtl, c, x + w, top,
Doug Felt0c702b82010-05-14 10:55:42 -0700483 y, bottom, null, false);
Doug Felte8e45f22010-03-29 14:58:40 -0700484 return w;
485 }
486
Romain Guybc7cdb62011-05-26 18:48:49 -0700487 return handleRun(start, limit, limit, runIsRtl, c, x, top,
Doug Felt0c702b82010-05-14 10:55:42 -0700488 y, bottom, null, needWidth);
Doug Felte8e45f22010-03-29 14:58:40 -0700489 }
490
491 /**
492 * Measures a unidirectional (but possibly multi-styled) run of text.
493 *
Romain Guybc7cdb62011-05-26 18:48:49 -0700494 *
Doug Felte8e45f22010-03-29 14:58:40 -0700495 * @param start the line-relative start of the run
496 * @param offset the offset to measure to, between start and limit inclusive
497 * @param limit the line-relative limit of the run
498 * @param runIsRtl true if the run is right-to-left
499 * @param fmi receives metrics information about the requested
500 * run, can be null.
501 * @return the signed width from the start of the run to the leading edge
502 * of the character at offset, based on the run (not paragraph) direction
503 */
Romain Guybc7cdb62011-05-26 18:48:49 -0700504 private float measureRun(int start, int offset, int limit, boolean runIsRtl,
505 FontMetricsInt fmi) {
506 return handleRun(start, offset, limit, runIsRtl, null, 0, 0, 0, 0, fmi, true);
Doug Felte8e45f22010-03-29 14:58:40 -0700507 }
508
509 /**
510 * Walk the cursor through this line, skipping conjuncts and
511 * zero-width characters.
512 *
513 * <p>This function cannot properly walk the cursor off the ends of the line
514 * since it does not know about any shaping on the previous/following line
515 * that might affect the cursor position. Callers must either avoid these
516 * situations or handle the result specially.
517 *
Doug Felte8e45f22010-03-29 14:58:40 -0700518 * @param cursor the starting position of the cursor, between 0 and the
519 * length of the line, inclusive
520 * @param toLeft true if the caret is moving to the left.
521 * @return the new offset. If it is less than 0 or greater than the length
522 * of the line, the previous/following line should be examined to get the
523 * actual offset.
524 */
525 int getOffsetToLeftRightOf(int cursor, boolean toLeft) {
526 // 1) The caret marks the leading edge of a character. The character
527 // logically before it might be on a different level, and the active caret
528 // position is on the character at the lower level. If that character
529 // was the previous character, the caret is on its trailing edge.
530 // 2) Take this character/edge and move it in the indicated direction.
531 // This gives you a new character and a new edge.
532 // 3) This position is between two visually adjacent characters. One of
533 // these might be at a lower level. The active position is on the
534 // character at the lower level.
535 // 4) If the active position is on the trailing edge of the character,
536 // the new caret position is the following logical character, else it
537 // is the character.
538
539 int lineStart = 0;
540 int lineEnd = mLen;
541 boolean paraIsRtl = mDir == -1;
542 int[] runs = mDirections.mDirections;
543
544 int runIndex, runLevel = 0, runStart = lineStart, runLimit = lineEnd, newCaret = -1;
545 boolean trailing = false;
546
547 if (cursor == lineStart) {
548 runIndex = -2;
549 } else if (cursor == lineEnd) {
550 runIndex = runs.length;
551 } else {
552 // First, get information about the run containing the character with
553 // the active caret.
554 for (runIndex = 0; runIndex < runs.length; runIndex += 2) {
555 runStart = lineStart + runs[runIndex];
556 if (cursor >= runStart) {
557 runLimit = runStart + (runs[runIndex+1] & Layout.RUN_LENGTH_MASK);
558 if (runLimit > lineEnd) {
559 runLimit = lineEnd;
560 }
561 if (cursor < runLimit) {
562 runLevel = (runs[runIndex+1] >>> Layout.RUN_LEVEL_SHIFT) &
563 Layout.RUN_LEVEL_MASK;
564 if (cursor == runStart) {
565 // The caret is on a run boundary, see if we should
566 // use the position on the trailing edge of the previous
567 // logical character instead.
568 int prevRunIndex, prevRunLevel, prevRunStart, prevRunLimit;
569 int pos = cursor - 1;
570 for (prevRunIndex = 0; prevRunIndex < runs.length; prevRunIndex += 2) {
571 prevRunStart = lineStart + runs[prevRunIndex];
572 if (pos >= prevRunStart) {
573 prevRunLimit = prevRunStart +
574 (runs[prevRunIndex+1] & Layout.RUN_LENGTH_MASK);
575 if (prevRunLimit > lineEnd) {
576 prevRunLimit = lineEnd;
577 }
578 if (pos < prevRunLimit) {
579 prevRunLevel = (runs[prevRunIndex+1] >>> Layout.RUN_LEVEL_SHIFT)
580 & Layout.RUN_LEVEL_MASK;
581 if (prevRunLevel < runLevel) {
582 // Start from logically previous character.
583 runIndex = prevRunIndex;
584 runLevel = prevRunLevel;
585 runStart = prevRunStart;
586 runLimit = prevRunLimit;
587 trailing = true;
588 break;
589 }
590 }
591 }
592 }
593 }
594 break;
595 }
596 }
597 }
598
599 // caret might be == lineEnd. This is generally a space or paragraph
600 // separator and has an associated run, but might be the end of
601 // text, in which case it doesn't. If that happens, we ran off the
602 // end of the run list, and runIndex == runs.length. In this case,
603 // we are at a run boundary so we skip the below test.
604 if (runIndex != runs.length) {
605 boolean runIsRtl = (runLevel & 0x1) != 0;
606 boolean advance = toLeft == runIsRtl;
607 if (cursor != (advance ? runLimit : runStart) || advance != trailing) {
608 // Moving within or into the run, so we can move logically.
Doug Felt0c702b82010-05-14 10:55:42 -0700609 newCaret = getOffsetBeforeAfter(runIndex, runStart, runLimit,
610 runIsRtl, cursor, advance);
Doug Felte8e45f22010-03-29 14:58:40 -0700611 // If the new position is internal to the run, we're at the strong
612 // position already so we're finished.
613 if (newCaret != (advance ? runLimit : runStart)) {
614 return newCaret;
615 }
616 }
617 }
618 }
619
620 // If newCaret is -1, we're starting at a run boundary and crossing
621 // into another run. Otherwise we've arrived at a run boundary, and
622 // need to figure out which character to attach to. Note we might
623 // need to run this twice, if we cross a run boundary and end up at
624 // another run boundary.
625 while (true) {
626 boolean advance = toLeft == paraIsRtl;
627 int otherRunIndex = runIndex + (advance ? 2 : -2);
628 if (otherRunIndex >= 0 && otherRunIndex < runs.length) {
629 int otherRunStart = lineStart + runs[otherRunIndex];
630 int otherRunLimit = otherRunStart +
631 (runs[otherRunIndex+1] & Layout.RUN_LENGTH_MASK);
632 if (otherRunLimit > lineEnd) {
633 otherRunLimit = lineEnd;
634 }
635 int otherRunLevel = (runs[otherRunIndex+1] >>> Layout.RUN_LEVEL_SHIFT) &
636 Layout.RUN_LEVEL_MASK;
637 boolean otherRunIsRtl = (otherRunLevel & 1) != 0;
638
639 advance = toLeft == otherRunIsRtl;
640 if (newCaret == -1) {
Doug Felt0c702b82010-05-14 10:55:42 -0700641 newCaret = getOffsetBeforeAfter(otherRunIndex, otherRunStart,
642 otherRunLimit, otherRunIsRtl,
Doug Felte8e45f22010-03-29 14:58:40 -0700643 advance ? otherRunStart : otherRunLimit, advance);
644 if (newCaret == (advance ? otherRunLimit : otherRunStart)) {
645 // Crossed and ended up at a new boundary,
646 // repeat a second and final time.
647 runIndex = otherRunIndex;
648 runLevel = otherRunLevel;
649 continue;
650 }
651 break;
652 }
653
654 // The new caret is at a boundary.
655 if (otherRunLevel < runLevel) {
656 // The strong character is in the other run.
657 newCaret = advance ? otherRunStart : otherRunLimit;
658 }
659 break;
660 }
661
662 if (newCaret == -1) {
663 // We're walking off the end of the line. The paragraph
664 // level is always equal to or lower than any internal level, so
665 // the boundaries get the strong caret.
Doug Felt0c702b82010-05-14 10:55:42 -0700666 newCaret = advance ? mLen + 1 : -1;
Doug Felte8e45f22010-03-29 14:58:40 -0700667 break;
668 }
669
670 // Else we've arrived at the end of the line. That's a strong position.
671 // We might have arrived here by crossing over a run with no internal
672 // breaks and dropping out of the above loop before advancing one final
673 // time, so reset the caret.
674 // Note, we use '<=' below to handle a situation where the only run
675 // on the line is a counter-directional run. If we're not advancing,
676 // we can end up at the 'lineEnd' position but the caret we want is at
677 // the lineStart.
678 if (newCaret <= lineEnd) {
679 newCaret = advance ? lineEnd : lineStart;
680 }
681 break;
682 }
683
684 return newCaret;
685 }
686
687 /**
688 * Returns the next valid offset within this directional run, skipping
689 * conjuncts and zero-width characters. This should not be called to walk
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -0700690 * off the end of the line, since the returned values might not be valid
Doug Felt0c702b82010-05-14 10:55:42 -0700691 * on neighboring lines. If the returned offset is less than zero or
692 * greater than the line length, the offset should be recomputed on the
693 * preceding or following line, respectively.
Doug Felte8e45f22010-03-29 14:58:40 -0700694 *
695 * @param runIndex the run index
Doug Felt0c702b82010-05-14 10:55:42 -0700696 * @param runStart the start of the run
697 * @param runLimit the limit of the run
698 * @param runIsRtl true if the run is right-to-left
Doug Felte8e45f22010-03-29 14:58:40 -0700699 * @param offset the offset
700 * @param after true if the new offset should logically follow the provided
701 * offset
702 * @return the new offset
703 */
Doug Felt0c702b82010-05-14 10:55:42 -0700704 private int getOffsetBeforeAfter(int runIndex, int runStart, int runLimit,
705 boolean runIsRtl, int offset, boolean after) {
Doug Felte8e45f22010-03-29 14:58:40 -0700706
Doug Felt0c702b82010-05-14 10:55:42 -0700707 if (runIndex < 0 || offset == (after ? mLen : 0)) {
708 // Walking off end of line. Since we don't know
709 // what cursor positions are available on other lines, we can't
710 // return accurate values. These are a guess.
Doug Felte8e45f22010-03-29 14:58:40 -0700711 if (after) {
Doug Felt0c702b82010-05-14 10:55:42 -0700712 return TextUtils.getOffsetAfter(mText, offset + mStart) - mStart;
713 }
714 return TextUtils.getOffsetBefore(mText, offset + mStart) - mStart;
715 }
716
717 TextPaint wp = mWorkPaint;
718 wp.set(mPaint);
Haoyu Zhangeccdc6b2019-01-07 15:11:46 -0800719 if (mIsJustifying) {
720 wp.setWordSpacing(mAddedWidthForJustify);
721 }
Doug Felt0c702b82010-05-14 10:55:42 -0700722
723 int spanStart = runStart;
724 int spanLimit;
725 if (mSpanned == null) {
726 spanLimit = runLimit;
727 } else {
728 int target = after ? offset + 1 : offset;
729 int limit = mStart + runLimit;
730 while (true) {
731 spanLimit = mSpanned.nextSpanTransition(mStart + spanStart, limit,
732 MetricAffectingSpan.class) - mStart;
733 if (spanLimit >= target) {
734 break;
Doug Felte8e45f22010-03-29 14:58:40 -0700735 }
Doug Felt0c702b82010-05-14 10:55:42 -0700736 spanStart = spanLimit;
737 }
738
739 MetricAffectingSpan[] spans = mSpanned.getSpans(mStart + spanStart,
740 mStart + spanLimit, MetricAffectingSpan.class);
Gilles Debunne1e3ac182011-03-08 14:22:34 -0800741 spans = TextUtils.removeEmptySpans(spans, mSpanned, MetricAffectingSpan.class);
Doug Felt0c702b82010-05-14 10:55:42 -0700742
743 if (spans.length > 0) {
744 ReplacementSpan replacement = null;
745 for (int j = 0; j < spans.length; j++) {
746 MetricAffectingSpan span = spans[j];
747 if (span instanceof ReplacementSpan) {
748 replacement = (ReplacementSpan)span;
749 } else {
750 span.updateMeasureState(wp);
751 }
752 }
753
754 if (replacement != null) {
755 // If we have a replacement span, we're moving either to
756 // the start or end of this span.
757 return after ? spanLimit : spanStart;
Doug Felte8e45f22010-03-29 14:58:40 -0700758 }
759 }
Doug Felte8e45f22010-03-29 14:58:40 -0700760 }
761
Doug Felt0c702b82010-05-14 10:55:42 -0700762 int cursorOpt = after ? Paint.CURSOR_AFTER : Paint.CURSOR_BEFORE;
763 if (mCharsValid) {
764 return wp.getTextRunCursor(mChars, spanStart, spanLimit - spanStart,
Seigo Nonakafa95b832018-09-17 14:09:25 -0700765 runIsRtl, offset, cursorOpt);
Doug Felt0c702b82010-05-14 10:55:42 -0700766 } else {
767 return wp.getTextRunCursor(mText, mStart + spanStart,
Seigo Nonakafa95b832018-09-17 14:09:25 -0700768 mStart + spanLimit, runIsRtl, mStart + offset, cursorOpt) - mStart;
Doug Felte8e45f22010-03-29 14:58:40 -0700769 }
Doug Felte8e45f22010-03-29 14:58:40 -0700770 }
771
772 /**
Gilles Debunne0bb00092010-12-02 15:50:26 -0800773 * @param wp
774 */
775 private static void expandMetricsFromPaint(FontMetricsInt fmi, TextPaint wp) {
776 final int previousTop = fmi.top;
777 final int previousAscent = fmi.ascent;
778 final int previousDescent = fmi.descent;
779 final int previousBottom = fmi.bottom;
780 final int previousLeading = fmi.leading;
781
782 wp.getFontMetricsInt(fmi);
783
Fabrice Di Meglio8a5137a2011-09-22 18:49:43 -0700784 updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom,
785 previousLeading);
786 }
787
788 static void updateMetrics(FontMetricsInt fmi, int previousTop, int previousAscent,
789 int previousDescent, int previousBottom, int previousLeading) {
Gilles Debunne0bb00092010-12-02 15:50:26 -0800790 fmi.top = Math.min(fmi.top, previousTop);
791 fmi.ascent = Math.min(fmi.ascent, previousAscent);
792 fmi.descent = Math.max(fmi.descent, previousDescent);
793 fmi.bottom = Math.max(fmi.bottom, previousBottom);
794 fmi.leading = Math.max(fmi.leading, previousLeading);
795 }
796
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700797 private static void drawStroke(TextPaint wp, Canvas c, int color, float position,
798 float thickness, float xleft, float xright, float baseline) {
799 final float strokeTop = baseline + wp.baselineShift + position;
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700800
801 final int previousColor = wp.getColor();
802 final Paint.Style previousStyle = wp.getStyle();
803 final boolean previousAntiAlias = wp.isAntiAlias();
804
805 wp.setStyle(Paint.Style.FILL);
806 wp.setAntiAlias(true);
807
808 wp.setColor(color);
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700809 c.drawRect(xleft, strokeTop, xright, strokeTop + thickness, wp);
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700810
811 wp.setStyle(previousStyle);
812 wp.setColor(previousColor);
813 wp.setAntiAlias(previousAntiAlias);
814 }
815
816 private float getRunAdvance(TextPaint wp, int start, int end, int contextStart, int contextEnd,
817 boolean runIsRtl, int offset) {
818 if (mCharsValid) {
819 return wp.getRunAdvance(mChars, start, end, contextStart, contextEnd, runIsRtl, offset);
820 } else {
821 final int delta = mStart;
Seigo Nonakabeafa1f2018-02-01 21:39:24 -0800822 if (mComputed == null) {
Seigo Nonaka783f9612018-01-20 12:11:13 -0800823 return wp.getRunAdvance(mText, delta + start, delta + end,
824 delta + contextStart, delta + contextEnd, runIsRtl, delta + offset);
825 } else {
Seigo Nonakabeafa1f2018-02-01 21:39:24 -0800826 return mComputed.getWidth(start + delta, end + delta);
Seigo Nonaka783f9612018-01-20 12:11:13 -0800827 }
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700828 }
829 }
830
Gilles Debunne0bb00092010-12-02 15:50:26 -0800831 /**
Doug Felte8e45f22010-03-29 14:58:40 -0700832 * Utility function for measuring and rendering text. The text must
Roozbeh Pournader112d9c72015-08-07 12:44:41 -0700833 * not include a tab.
Doug Felte8e45f22010-03-29 14:58:40 -0700834 *
835 * @param wp the working paint
836 * @param start the start of the text
Doug Felt0c702b82010-05-14 10:55:42 -0700837 * @param end the end of the text
Doug Felte8e45f22010-03-29 14:58:40 -0700838 * @param runIsRtl true if the run is right-to-left
839 * @param c the canvas, can be null if rendering is not needed
840 * @param x the edge of the run closest to the leading margin
841 * @param top the top of the line
842 * @param y the baseline
843 * @param bottom the bottom of the line
844 * @param fmi receives metrics information, can be null
845 * @param needWidth true if the width of the run is needed
Raph Levien909c7bc2015-11-30 21:05:46 -0800846 * @param offset the offset for the purpose of measuring
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700847 * @param decorations the list of locations and paremeters for drawing decorations
Doug Felte8e45f22010-03-29 14:58:40 -0700848 * @return the signed width of the run based on the run direction; only
849 * valid if needWidth is true
850 */
Doug Felt0c702b82010-05-14 10:55:42 -0700851 private float handleText(TextPaint wp, int start, int end,
852 int contextStart, int contextEnd, boolean runIsRtl,
853 Canvas c, float x, int top, int y, int bottom,
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700854 FontMetricsInt fmi, boolean needWidth, int offset,
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700855 @Nullable ArrayList<DecorationInfo> decorations) {
Doug Felte8e45f22010-03-29 14:58:40 -0700856
Haoyu Zhangeccdc6b2019-01-07 15:11:46 -0800857 if (mIsJustifying) {
858 wp.setWordSpacing(mAddedWidthForJustify);
859 }
Fabrice Di Meglio850dffa2011-08-08 09:45:09 -0700860 // Get metrics first (even for empty strings or "0" width runs)
861 if (fmi != null) {
862 expandMetricsFromPaint(fmi, wp);
863 }
Doug Felte8e45f22010-03-29 14:58:40 -0700864
Fabrice Di Meglio850dffa2011-08-08 09:45:09 -0700865 // No need to do anything if the run width is "0"
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700866 if (end == start) {
Fabrice Di Meglio850dffa2011-08-08 09:45:09 -0700867 return 0f;
868 }
869
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700870 float totalWidth = 0;
Fabrice Di Meglio850dffa2011-08-08 09:45:09 -0700871
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700872 final int numDecorations = decorations == null ? 0 : decorations.size();
873 if (needWidth || (c != null && (wp.bgColor != 0 || numDecorations != 0 || runIsRtl))) {
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700874 totalWidth = getRunAdvance(wp, start, end, contextStart, contextEnd, runIsRtl, offset);
Doug Felte8e45f22010-03-29 14:58:40 -0700875 }
876
Doug Felte8e45f22010-03-29 14:58:40 -0700877 if (c != null) {
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700878 final float leftX, rightX;
Doug Felte8e45f22010-03-29 14:58:40 -0700879 if (runIsRtl) {
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700880 leftX = x - totalWidth;
881 rightX = x;
882 } else {
883 leftX = x;
884 rightX = x + totalWidth;
Doug Felte8e45f22010-03-29 14:58:40 -0700885 }
886
887 if (wp.bgColor != 0) {
Gilles Debunnedd8f5ed2011-08-10 18:25:49 -0700888 int previousColor = wp.getColor();
889 Paint.Style previousStyle = wp.getStyle();
890
Doug Felte8e45f22010-03-29 14:58:40 -0700891 wp.setColor(wp.bgColor);
892 wp.setStyle(Paint.Style.FILL);
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700893 c.drawRect(leftX, top, rightX, bottom, wp);
Doug Felte8e45f22010-03-29 14:58:40 -0700894
Gilles Debunnedd8f5ed2011-08-10 18:25:49 -0700895 wp.setStyle(previousStyle);
896 wp.setColor(previousColor);
897 }
898
Seigo Nonaka50fe7882018-05-21 14:56:40 -0700899 drawTextRun(c, wp, start, end, contextStart, contextEnd, runIsRtl,
900 leftX, y + wp.baselineShift);
901
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700902 if (numDecorations != 0) {
903 for (int i = 0; i < numDecorations; i++) {
904 final DecorationInfo info = decorations.get(i);
Gilles Debunnedd8f5ed2011-08-10 18:25:49 -0700905
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700906 final int decorationStart = Math.max(info.start, start);
907 final int decorationEnd = Math.min(info.end, offset);
908 float decorationStartAdvance = getRunAdvance(
909 wp, start, end, contextStart, contextEnd, runIsRtl, decorationStart);
910 float decorationEndAdvance = getRunAdvance(
911 wp, start, end, contextStart, contextEnd, runIsRtl, decorationEnd);
912 final float decorationXLeft, decorationXRight;
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700913 if (runIsRtl) {
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700914 decorationXLeft = rightX - decorationEndAdvance;
915 decorationXRight = rightX - decorationStartAdvance;
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700916 } else {
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700917 decorationXLeft = leftX + decorationStartAdvance;
918 decorationXRight = leftX + decorationEndAdvance;
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700919 }
Siyamed Sinir702c9f92017-06-01 18:25:40 +0000920
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700921 // Theoretically, there could be cases where both Paint's and TextPaint's
922 // setUnderLineText() are called. For backward compatibility, we need to draw
923 // both underlines, the one with custom color first.
924 if (info.underlineColor != 0) {
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700925 drawStroke(wp, c, info.underlineColor, wp.getUnderlinePosition(),
926 info.underlineThickness, decorationXLeft, decorationXRight, y);
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700927 }
928 if (info.isUnderlineText) {
Roozbeh Pournaderca8a04a2017-06-06 18:30:29 -0700929 final float thickness =
Siyamed Sinira273a702017-10-05 11:22:12 -0700930 Math.max(wp.getUnderlineThickness(), 1.0f);
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700931 drawStroke(wp, c, wp.getColor(), wp.getUnderlinePosition(), thickness,
932 decorationXLeft, decorationXRight, y);
933 }
934
935 if (info.isStrikeThruText) {
936 final float thickness =
Siyamed Sinira273a702017-10-05 11:22:12 -0700937 Math.max(wp.getStrikeThruThickness(), 1.0f);
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700938 drawStroke(wp, c, wp.getColor(), wp.getStrikeThruPosition(), thickness,
939 decorationXLeft, decorationXRight, y);
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700940 }
941 }
Doug Felte8e45f22010-03-29 14:58:40 -0700942 }
943
Doug Felte8e45f22010-03-29 14:58:40 -0700944 }
945
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700946 return runIsRtl ? -totalWidth : totalWidth;
Doug Felte8e45f22010-03-29 14:58:40 -0700947 }
948
949 /**
950 * Utility function for measuring and rendering a replacement.
951 *
Romain Guybc7cdb62011-05-26 18:48:49 -0700952 *
Doug Felte8e45f22010-03-29 14:58:40 -0700953 * @param replacement the replacement
954 * @param wp the work paint
Doug Felte8e45f22010-03-29 14:58:40 -0700955 * @param start the start of the run
956 * @param limit the limit of the run
957 * @param runIsRtl true if the run is right-to-left
958 * @param c the canvas, can be null if not rendering
959 * @param x the edge of the replacement closest to the leading margin
960 * @param top the top of the line
961 * @param y the baseline
962 * @param bottom the bottom of the line
963 * @param fmi receives metrics information, can be null
964 * @param needWidth true if the width of the replacement is needed
Doug Felte8e45f22010-03-29 14:58:40 -0700965 * @return the signed width of the run based on the run direction; only
966 * valid if needWidth is true
967 */
968 private float handleReplacement(ReplacementSpan replacement, TextPaint wp,
Romain Guybc7cdb62011-05-26 18:48:49 -0700969 int start, int limit, boolean runIsRtl, Canvas c,
Doug Felte8e45f22010-03-29 14:58:40 -0700970 float x, int top, int y, int bottom, FontMetricsInt fmi,
Doug Felt0c702b82010-05-14 10:55:42 -0700971 boolean needWidth) {
Doug Felte8e45f22010-03-29 14:58:40 -0700972
973 float ret = 0;
974
Doug Felt0c702b82010-05-14 10:55:42 -0700975 int textStart = mStart + start;
976 int textLimit = mStart + limit;
977
978 if (needWidth || (c != null && runIsRtl)) {
Fabrice Di Meglio8a5137a2011-09-22 18:49:43 -0700979 int previousTop = 0;
980 int previousAscent = 0;
981 int previousDescent = 0;
982 int previousBottom = 0;
983 int previousLeading = 0;
984
985 boolean needUpdateMetrics = (fmi != null);
986
987 if (needUpdateMetrics) {
988 previousTop = fmi.top;
989 previousAscent = fmi.ascent;
990 previousDescent = fmi.descent;
991 previousBottom = fmi.bottom;
992 previousLeading = fmi.leading;
993 }
994
Doug Felt0c702b82010-05-14 10:55:42 -0700995 ret = replacement.getSize(wp, mText, textStart, textLimit, fmi);
Fabrice Di Meglio8a5137a2011-09-22 18:49:43 -0700996
997 if (needUpdateMetrics) {
998 updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom,
999 previousLeading);
1000 }
Doug Felte8e45f22010-03-29 14:58:40 -07001001 }
1002
Doug Felt0c702b82010-05-14 10:55:42 -07001003 if (c != null) {
1004 if (runIsRtl) {
1005 x -= ret;
Doug Felte8e45f22010-03-29 14:58:40 -07001006 }
Doug Felt0c702b82010-05-14 10:55:42 -07001007 replacement.draw(c, mText, textStart, textLimit,
1008 x, top, y, bottom, wp);
Doug Felte8e45f22010-03-29 14:58:40 -07001009 }
1010
1011 return runIsRtl ? -ret : ret;
1012 }
1013
Seigo Nonaka9033e0c2018-09-06 18:42:10 -07001014 private int adjustHyphenEdit(int start, int limit, int packedHyphenEdit) {
1015 int result = packedHyphenEdit;
Roozbeh Pournader46c6f4c2017-02-21 12:18:31 -08001016 // Only draw hyphens on first or last run in line. Disable them otherwise.
1017 if (start > 0) { // not the first run
Seigo Nonaka9033e0c2018-09-06 18:42:10 -07001018 result = Hyphenator.packHyphenEdit(Hyphenator.START_HYPHEN_EDIT_NO_EDIT,
1019 Hyphenator.unpackEndHyphenEdit(packedHyphenEdit));
Roozbeh Pournader46c6f4c2017-02-21 12:18:31 -08001020 }
1021 if (limit < mLen) { // not the last run
Seigo Nonaka9033e0c2018-09-06 18:42:10 -07001022 result = Hyphenator.packHyphenEdit(Hyphenator.unpackStartHyphenEdit(packedHyphenEdit),
1023 Hyphenator.END_HYPHEN_EDIT_NO_EDIT);
Roozbeh Pournader46c6f4c2017-02-21 12:18:31 -08001024 result &= ~Paint.HYPHENEDIT_MASK_END_OF_LINE;
1025 }
1026 return result;
1027 }
1028
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001029 private static final class DecorationInfo {
1030 public boolean isStrikeThruText;
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001031 public boolean isUnderlineText;
1032 public int underlineColor;
1033 public float underlineThickness;
1034 public int start = -1;
1035 public int end = -1;
1036
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001037 public boolean hasDecoration() {
1038 return isStrikeThruText || isUnderlineText || underlineColor != 0;
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001039 }
1040
1041 // Copies the info, but not the start and end range.
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001042 public DecorationInfo copyInfo() {
1043 final DecorationInfo copy = new DecorationInfo();
1044 copy.isStrikeThruText = isStrikeThruText;
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001045 copy.isUnderlineText = isUnderlineText;
1046 copy.underlineColor = underlineColor;
1047 copy.underlineThickness = underlineThickness;
1048 return copy;
1049 }
1050 }
1051
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001052 private void extractDecorationInfo(@NonNull TextPaint paint, @NonNull DecorationInfo info) {
1053 info.isStrikeThruText = paint.isStrikeThruText();
1054 if (info.isStrikeThruText) {
1055 paint.setStrikeThruText(false);
1056 }
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001057 info.isUnderlineText = paint.isUnderlineText();
1058 if (info.isUnderlineText) {
1059 paint.setUnderlineText(false);
1060 }
1061 info.underlineColor = paint.underlineColor;
1062 info.underlineThickness = paint.underlineThickness;
1063 paint.setUnderlineText(0, 0.0f);
1064 }
1065
Doug Felte8e45f22010-03-29 14:58:40 -07001066 /**
1067 * Utility function for handling a unidirectional run. The run must not
Roozbeh Pournader112d9c72015-08-07 12:44:41 -07001068 * contain tabs but can contain styles.
Doug Felte8e45f22010-03-29 14:58:40 -07001069 *
Romain Guybc7cdb62011-05-26 18:48:49 -07001070 *
Doug Felte8e45f22010-03-29 14:58:40 -07001071 * @param start the line-relative start of the run
Doug Felt0c702b82010-05-14 10:55:42 -07001072 * @param measureLimit the offset to measure to, between start and limit inclusive
Doug Felte8e45f22010-03-29 14:58:40 -07001073 * @param limit the limit of the run
1074 * @param runIsRtl true if the run is right-to-left
1075 * @param c the canvas, can be null
1076 * @param x the end of the run closest to the leading margin
1077 * @param top the top of the line
1078 * @param y the baseline
1079 * @param bottom the bottom of the line
1080 * @param fmi receives metrics information, can be null
1081 * @param needWidth true if the width is required
Doug Felte8e45f22010-03-29 14:58:40 -07001082 * @return the signed width of the run based on the run direction; only
1083 * valid if needWidth is true
1084 */
Romain Guybc7cdb62011-05-26 18:48:49 -07001085 private float handleRun(int start, int measureLimit,
Doug Felte8e45f22010-03-29 14:58:40 -07001086 int limit, boolean runIsRtl, Canvas c, float x, int top, int y,
Doug Felt0c702b82010-05-14 10:55:42 -07001087 int bottom, FontMetricsInt fmi, boolean needWidth) {
Doug Felte8e45f22010-03-29 14:58:40 -07001088
Siyamed Sinir9f3958c2016-08-26 15:23:47 -07001089 if (measureLimit < start || measureLimit > limit) {
1090 throw new IndexOutOfBoundsException("measureLimit (" + measureLimit + ") is out of "
1091 + "start (" + start + ") and limit (" + limit + ") bounds");
1092 }
1093
Gilles Debunnef483e512011-04-28 15:08:54 -07001094 // Case of an empty line, make sure we update fmi according to mPaint
1095 if (start == measureLimit) {
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001096 final TextPaint wp = mWorkPaint;
Gilles Debunnef483e512011-04-28 15:08:54 -07001097 wp.set(mPaint);
Fabrice Di Meglio15c097a2011-08-08 14:42:41 -07001098 if (fmi != null) {
1099 expandMetricsFromPaint(fmi, wp);
1100 }
1101 return 0f;
Gilles Debunnef483e512011-04-28 15:08:54 -07001102 }
1103
Roozbeh Pournader08836d42017-06-06 16:23:09 -07001104 final boolean needsSpanMeasurement;
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001105 if (mSpanned == null) {
Roozbeh Pournader08836d42017-06-06 16:23:09 -07001106 needsSpanMeasurement = false;
1107 } else {
1108 mMetricAffectingSpanSpanSet.init(mSpanned, mStart + start, mStart + limit);
1109 mCharacterStyleSpanSet.init(mSpanned, mStart + start, mStart + limit);
1110 needsSpanMeasurement = mMetricAffectingSpanSpanSet.numberOfSpans != 0
1111 || mCharacterStyleSpanSet.numberOfSpans != 0;
1112 }
1113
1114 if (!needsSpanMeasurement) {
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001115 final TextPaint wp = mWorkPaint;
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001116 wp.set(mPaint);
Roozbeh Pournader46c6f4c2017-02-21 12:18:31 -08001117 wp.setHyphenEdit(adjustHyphenEdit(start, limit, wp.getHyphenEdit()));
Raph Levien909c7bc2015-11-30 21:05:46 -08001118 return handleText(wp, start, limit, start, limit, runIsRtl, c, x, top,
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001119 y, bottom, fmi, needWidth, measureLimit, null);
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001120 }
1121
Doug Felte8e45f22010-03-29 14:58:40 -07001122 // Shaping needs to take into account context up to metric boundaries,
1123 // but rendering needs to take into account character style boundaries.
Doug Felt0c702b82010-05-14 10:55:42 -07001124 // So we iterate through metric runs to get metric bounds,
1125 // then within each metric run iterate through character style runs
1126 // for the run bounds.
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001127 final float originalX = x;
Doug Felt0c702b82010-05-14 10:55:42 -07001128 for (int i = start, inext; i < measureLimit; i = inext) {
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001129 final TextPaint wp = mWorkPaint;
Doug Felte8e45f22010-03-29 14:58:40 -07001130 wp.set(mPaint);
1131
Gilles Debunnec1f44832011-12-08 16:03:00 -08001132 inext = mMetricAffectingSpanSpanSet.getNextTransition(mStart + i, mStart + limit) -
1133 mStart;
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001134 int mlimit = Math.min(inext, measureLimit);
Doug Felte8e45f22010-03-29 14:58:40 -07001135
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001136 ReplacementSpan replacement = null;
Doug Felte8e45f22010-03-29 14:58:40 -07001137
Gilles Debunnec1f44832011-12-08 16:03:00 -08001138 for (int j = 0; j < mMetricAffectingSpanSpanSet.numberOfSpans; j++) {
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001139 // Both intervals [spanStarts..spanEnds] and [mStart + i..mStart + mlimit] are NOT
1140 // empty by construction. This special case in getSpans() explains the >= & <= tests
Mihai Popace642dc2018-05-24 14:25:11 +01001141 if ((mMetricAffectingSpanSpanSet.spanStarts[j] >= mStart + mlimit)
1142 || (mMetricAffectingSpanSpanSet.spanEnds[j] <= mStart + i)) continue;
1143
1144 boolean insideEllipsis =
1145 mStart + mEllipsisStart <= mMetricAffectingSpanSpanSet.spanStarts[j]
1146 && mMetricAffectingSpanSpanSet.spanEnds[j] <= mStart + mEllipsisEnd;
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001147 final MetricAffectingSpan span = mMetricAffectingSpanSpanSet.spans[j];
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001148 if (span instanceof ReplacementSpan) {
Mihai Popace642dc2018-05-24 14:25:11 +01001149 replacement = !insideEllipsis ? (ReplacementSpan) span : null;
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001150 } else {
1151 // We might have a replacement that uses the draw
1152 // state, otherwise measure state would suffice.
1153 span.updateDrawState(wp);
Doug Felte8e45f22010-03-29 14:58:40 -07001154 }
1155 }
1156
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001157 if (replacement != null) {
1158 x += handleReplacement(replacement, wp, i, mlimit, runIsRtl, c, x, top, y,
1159 bottom, fmi, needWidth || mlimit < measureLimit);
1160 continue;
1161 }
1162
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001163 final TextPaint activePaint = mActivePaint;
1164 activePaint.set(mPaint);
1165 int activeStart = i;
1166 int activeEnd = mlimit;
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001167 final DecorationInfo decorationInfo = mDecorationInfo;
1168 mDecorations.clear();
Raph Levien42ef515d2012-10-22 15:01:17 -07001169 for (int j = i, jnext; j < mlimit; j = jnext) {
Siyamed Sinir702c9f92017-06-01 18:25:40 +00001170 jnext = mCharacterStyleSpanSet.getNextTransition(mStart + j, mStart + inext) -
1171 mStart;
Doug Felte8e45f22010-03-29 14:58:40 -07001172
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001173 final int offset = Math.min(jnext, mlimit);
Raph Levien42ef515d2012-10-22 15:01:17 -07001174 wp.set(mPaint);
1175 for (int k = 0; k < mCharacterStyleSpanSet.numberOfSpans; k++) {
1176 // Intentionally using >= and <= as explained above
Siyamed Sinir702c9f92017-06-01 18:25:40 +00001177 if ((mCharacterStyleSpanSet.spanStarts[k] >= mStart + offset) ||
1178 (mCharacterStyleSpanSet.spanEnds[k] <= mStart + j)) continue;
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001179
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001180 final CharacterStyle span = mCharacterStyleSpanSet.spans[k];
Siyamed Sinir702c9f92017-06-01 18:25:40 +00001181 span.updateDrawState(wp);
Doug Felte8e45f22010-03-29 14:58:40 -07001182 }
Raph Levien42ef515d2012-10-22 15:01:17 -07001183
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001184 extractDecorationInfo(wp, decorationInfo);
Roozbeh Pournader46c6f4c2017-02-21 12:18:31 -08001185
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001186 if (j == i) {
1187 // First chunk of text. We can't handle it yet, since we may need to merge it
1188 // with the next chunk. So we just save the TextPaint for future comparisons
1189 // and use.
1190 activePaint.set(wp);
Seigo Nonaka32b87e02018-09-18 13:26:24 -07001191 } else if (!equalAttributes(wp, activePaint)) {
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001192 // The style of the present chunk of text is substantially different from the
1193 // style of the previous chunk. We need to handle the active piece of text
1194 // and restart with the present chunk.
1195 activePaint.setHyphenEdit(adjustHyphenEdit(
1196 activeStart, activeEnd, mPaint.getHyphenEdit()));
1197 x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, x,
1198 top, y, bottom, fmi, needWidth || activeEnd < measureLimit,
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001199 Math.min(activeEnd, mlimit), mDecorations);
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001200
1201 activeStart = j;
1202 activePaint.set(wp);
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001203 mDecorations.clear();
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001204 } else {
1205 // The present TextPaint is substantially equal to the last TextPaint except
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001206 // perhaps for decorations. We just need to expand the active piece of text to
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001207 // include the present chunk, which we always do anyway. We don't need to save
1208 // wp to activePaint, since they are already equal.
1209 }
1210
1211 activeEnd = jnext;
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001212 if (decorationInfo.hasDecoration()) {
1213 final DecorationInfo copy = decorationInfo.copyInfo();
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001214 copy.start = j;
1215 copy.end = jnext;
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001216 mDecorations.add(copy);
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001217 }
Doug Felte8e45f22010-03-29 14:58:40 -07001218 }
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001219 // Handle the final piece of text.
1220 activePaint.setHyphenEdit(adjustHyphenEdit(
1221 activeStart, activeEnd, mPaint.getHyphenEdit()));
1222 x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, x,
1223 top, y, bottom, fmi, needWidth || activeEnd < measureLimit,
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001224 Math.min(activeEnd, mlimit), mDecorations);
Doug Felte8e45f22010-03-29 14:58:40 -07001225 }
1226
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001227 return x - originalX;
Doug Felte8e45f22010-03-29 14:58:40 -07001228 }
1229
Doug Felte8e45f22010-03-29 14:58:40 -07001230 /**
1231 * Render a text run with the set-up paint.
1232 *
1233 * @param c the canvas
1234 * @param wp the paint used to render the text
Doug Felt0c702b82010-05-14 10:55:42 -07001235 * @param start the start of the run
1236 * @param end the end of the run
1237 * @param contextStart the start of context for the run
1238 * @param contextEnd the end of the context for the run
Fabrice Di Meglioda12f382013-03-15 11:26:56 -07001239 * @param runIsRtl true if the run is right-to-left
Doug Felte8e45f22010-03-29 14:58:40 -07001240 * @param x the x position of the left edge of the run
1241 * @param y the baseline of the run
1242 */
Doug Felt0c702b82010-05-14 10:55:42 -07001243 private void drawTextRun(Canvas c, TextPaint wp, int start, int end,
Fabrice Di Meglioda12f382013-03-15 11:26:56 -07001244 int contextStart, int contextEnd, boolean runIsRtl, float x, int y) {
Doug Felte8e45f22010-03-29 14:58:40 -07001245
Doug Felte8e45f22010-03-29 14:58:40 -07001246 if (mCharsValid) {
Doug Felt0c702b82010-05-14 10:55:42 -07001247 int count = end - start;
1248 int contextCount = contextEnd - contextStart;
1249 c.drawTextRun(mChars, start, count, contextStart, contextCount,
Raph Levien051910b2014-06-15 18:25:29 -07001250 x, y, runIsRtl, wp);
Doug Felte8e45f22010-03-29 14:58:40 -07001251 } else {
Doug Felt0c702b82010-05-14 10:55:42 -07001252 int delta = mStart;
1253 c.drawTextRun(mText, delta + start, delta + end,
Raph Levien051910b2014-06-15 18:25:29 -07001254 delta + contextStart, delta + contextEnd, x, y, runIsRtl, wp);
Doug Felte8e45f22010-03-29 14:58:40 -07001255 }
1256 }
1257
1258 /**
Doug Felte8e45f22010-03-29 14:58:40 -07001259 * Returns the next tab position.
1260 *
1261 * @param h the (unsigned) offset from the leading margin
1262 * @return the (unsigned) tab position after this offset
1263 */
1264 float nextTab(float h) {
Doug Feltc982f602010-05-25 11:51:40 -07001265 if (mTabs != null) {
1266 return mTabs.nextTab(h);
Doug Felte8e45f22010-03-29 14:58:40 -07001267 }
Doug Feltc982f602010-05-25 11:51:40 -07001268 return TabStops.nextDefaultStop(h, TAB_INCREMENT);
Doug Felte8e45f22010-03-29 14:58:40 -07001269 }
1270
Seigo Nonaka09da71a2016-11-28 16:24:14 +09001271 private boolean isStretchableWhitespace(int ch) {
Roozbeh Pournader3630d082017-10-23 12:16:32 -07001272 // TODO: Support NBSP and other stretchable whitespace (b/34013491 and b/68204709).
1273 return ch == 0x0020;
Seigo Nonaka09da71a2016-11-28 16:24:14 +09001274 }
1275
1276 /* Return the number of spaces in the text line, for the purpose of justification */
1277 private int countStretchableSpaces(int start, int end) {
1278 int count = 0;
Roozbeh Pournader3630d082017-10-23 12:16:32 -07001279 for (int i = start; i < end; i++) {
1280 final char c = mCharsValid ? mChars[i] : mText.charAt(i + mStart);
1281 if (isStretchableWhitespace(c)) {
1282 count++;
1283 }
Seigo Nonaka09da71a2016-11-28 16:24:14 +09001284 }
1285 return count;
1286 }
1287
1288 // Note: keep this in sync with Minikin LineBreaker::isLineEndSpace()
1289 public static boolean isLineEndSpace(char ch) {
1290 return ch == ' ' || ch == '\t' || ch == 0x1680
1291 || (0x2000 <= ch && ch <= 0x200A && ch != 0x2007)
1292 || ch == 0x205F || ch == 0x3000;
1293 }
1294
Doug Felte8e45f22010-03-29 14:58:40 -07001295 private static final int TAB_INCREMENT = 20;
Seigo Nonaka32b87e02018-09-18 13:26:24 -07001296
1297 private static boolean equalAttributes(@NonNull TextPaint lp, @NonNull TextPaint rp) {
1298 return lp.getColorFilter() == rp.getColorFilter()
1299 && lp.getMaskFilter() == rp.getMaskFilter()
1300 && lp.getShader() == rp.getShader()
1301 && lp.getTypeface() == rp.getTypeface()
1302 && lp.getXfermode() == rp.getXfermode()
1303 && lp.getTextLocales().equals(rp.getTextLocales())
1304 && TextUtils.equals(lp.getFontFeatureSettings(), rp.getFontFeatureSettings())
1305 && TextUtils.equals(lp.getFontVariationSettings(), rp.getFontVariationSettings())
1306 && lp.getShadowLayerRadius() == rp.getShadowLayerRadius()
1307 && lp.getShadowLayerDx() == rp.getShadowLayerDx()
1308 && lp.getShadowLayerDy() == rp.getShadowLayerDy()
1309 && lp.getShadowLayerColor() == rp.getShadowLayerColor()
1310 && lp.getFlags() == rp.getFlags()
1311 && lp.getHinting() == rp.getHinting()
1312 && lp.getStyle() == rp.getStyle()
1313 && lp.getColor() == rp.getColor()
1314 && lp.getStrokeWidth() == rp.getStrokeWidth()
1315 && lp.getStrokeMiter() == rp.getStrokeMiter()
1316 && lp.getStrokeCap() == rp.getStrokeCap()
1317 && lp.getStrokeJoin() == rp.getStrokeJoin()
1318 && lp.getTextAlign() == rp.getTextAlign()
1319 && lp.isElegantTextHeight() == rp.isElegantTextHeight()
1320 && lp.getTextSize() == rp.getTextSize()
1321 && lp.getTextScaleX() == rp.getTextScaleX()
1322 && lp.getTextSkewX() == rp.getTextSkewX()
1323 && lp.getLetterSpacing() == rp.getLetterSpacing()
1324 && lp.getWordSpacing() == rp.getWordSpacing()
1325 && lp.getHyphenEdit() == rp.getHyphenEdit()
1326 && lp.bgColor == rp.bgColor
1327 && lp.baselineShift == rp.baselineShift
1328 && lp.linkColor == rp.linkColor
1329 && lp.drawableState == rp.drawableState
1330 && lp.density == rp.density
1331 && lp.underlineColor == rp.underlineColor
1332 && lp.underlineThickness == rp.underlineThickness;
1333 }
Doug Felte8e45f22010-03-29 14:58:40 -07001334}