blob: 86651060a394686d3e1aa9d408adf8fc237f6e79 [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
Haoyu Zhang97b1d502019-02-06 13:51:58 -080097 /** Not allowed to access. If it's for memory leak workaround, it was already fixed M. */
98 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
Romain Guybc7cdb62011-05-26 18:48:49 -070099 private static final TextLine[] sCached = new TextLine[3];
Doug Felte8e45f22010-03-29 14:58:40 -0700100
101 /**
102 * Returns a new TextLine from the shared pool.
103 *
104 * @return an uninitialized TextLine
105 */
Roozbeh Pournader3630d082017-10-23 12:16:32 -0700106 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
Mathew Inwoodefeab842018-08-14 15:21:30 +0100107 @UnsupportedAppUsage
Roozbeh Pournader3630d082017-10-23 12:16:32 -0700108 public static TextLine obtain() {
Doug Felte8e45f22010-03-29 14:58:40 -0700109 TextLine tl;
Romain Guybc7cdb62011-05-26 18:48:49 -0700110 synchronized (sCached) {
111 for (int i = sCached.length; --i >= 0;) {
112 if (sCached[i] != null) {
113 tl = sCached[i];
114 sCached[i] = null;
Doug Felte8e45f22010-03-29 14:58:40 -0700115 return tl;
116 }
117 }
118 }
119 tl = new TextLine();
Romain Guybc7cdb62011-05-26 18:48:49 -0700120 if (DEBUG) {
121 Log.v("TLINE", "new: " + tl);
122 }
Doug Felte8e45f22010-03-29 14:58:40 -0700123 return tl;
124 }
125
126 /**
127 * Puts a TextLine back into the shared pool. Do not use this TextLine once
128 * it has been returned.
129 * @param tl the textLine
130 * @return null, as a convenience from clearing references to the provided
131 * TextLine
132 */
Roozbeh Pournader3630d082017-10-23 12:16:32 -0700133 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
134 public static TextLine recycle(TextLine tl) {
Doug Felte8e45f22010-03-29 14:58:40 -0700135 tl.mText = null;
136 tl.mPaint = null;
137 tl.mDirections = null;
Svet Ganov893d6fe2015-01-16 10:10:15 -0800138 tl.mSpanned = null;
139 tl.mTabs = null;
140 tl.mChars = null;
Seigo Nonakabeafa1f2018-02-01 21:39:24 -0800141 tl.mComputed = null;
Gilles Debunnec3fb7a12011-12-12 14:03:40 -0800142
143 tl.mMetricAffectingSpanSpanSet.recycle();
144 tl.mCharacterStyleSpanSet.recycle();
145 tl.mReplacementSpanSpanSet.recycle();
146
Romain Guybc7cdb62011-05-26 18:48:49 -0700147 synchronized(sCached) {
148 for (int i = 0; i < sCached.length; ++i) {
149 if (sCached[i] == null) {
150 sCached[i] = tl;
Gilles Debunnef902d7b2011-01-25 09:09:46 -0800151 break;
Doug Felte8e45f22010-03-29 14:58:40 -0700152 }
153 }
154 }
155 return null;
156 }
157
158 /**
159 * Initializes a TextLine and prepares it for use.
160 *
161 * @param paint the base paint for the line
162 * @param text the text, can be Styled
163 * @param start the start of the line relative to the text
164 * @param limit the limit of the line relative to the text
165 * @param dir the paragraph direction of this line
166 * @param directions the directions information of this line
Roozbeh Pournader112d9c72015-08-07 12:44:41 -0700167 * @param hasTabs true if the line might contain tabs
Mihai Popace642dc2018-05-24 14:25:11 +0100168 * @param tabStops the tabStops. Can be null
169 * @param ellipsisStart the start of the ellipsis relative to the line
170 * @param ellipsisEnd the end of the ellipsis relative to the line. When there
171 * is no ellipsis, this should be equal to ellipsisStart.
Doug Felte8e45f22010-03-29 14:58:40 -0700172 */
Roozbeh Pournader3630d082017-10-23 12:16:32 -0700173 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
Seigo Nonaka4e90fa22018-02-13 21:40:01 +0000174 public void set(TextPaint paint, CharSequence text, int start, int limit, int dir,
Mihai Popace642dc2018-05-24 14:25:11 +0100175 Directions directions, boolean hasTabs, TabStops tabStops,
176 int ellipsisStart, int ellipsisEnd) {
Doug Felte8e45f22010-03-29 14:58:40 -0700177 mPaint = paint;
178 mText = text;
179 mStart = start;
180 mLen = limit - start;
181 mDir = dir;
182 mDirections = directions;
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -0700183 if (mDirections == null) {
184 throw new IllegalArgumentException("Directions cannot be null");
185 }
Doug Felte8e45f22010-03-29 14:58:40 -0700186 mHasTabs = hasTabs;
187 mSpanned = null;
Doug Felte8e45f22010-03-29 14:58:40 -0700188
189 boolean hasReplacement = false;
190 if (text instanceof Spanned) {
191 mSpanned = (Spanned) text;
Gilles Debunnec1f44832011-12-08 16:03:00 -0800192 mReplacementSpanSpanSet.init(mSpanned, start, limit);
193 hasReplacement = mReplacementSpanSpanSet.numberOfSpans > 0;
Doug Felte8e45f22010-03-29 14:58:40 -0700194 }
195
Seigo Nonakabeafa1f2018-02-01 21:39:24 -0800196 mComputed = null;
197 if (text instanceof PrecomputedText) {
Seigo Nonaka53145632018-03-23 17:22:54 -0700198 // Here, no need to check line break strategy or hyphenation frequency since there is no
199 // line break concept here.
Seigo Nonakabeafa1f2018-02-01 21:39:24 -0800200 mComputed = (PrecomputedText) text;
Seigo Nonaka53145632018-03-23 17:22:54 -0700201 if (!mComputed.getParams().getTextPaint().equalsForTextMeasurement(paint)) {
202 mComputed = null;
203 }
Seigo Nonaka4e90fa22018-02-13 21:40:01 +0000204 }
205
Siyamed Sinir95621332018-11-16 23:38:23 +0000206 mCharsValid = hasReplacement;
Doug Felte8e45f22010-03-29 14:58:40 -0700207
208 if (mCharsValid) {
209 if (mChars == null || mChars.length < mLen) {
Adam Lesinski776abc22014-03-07 11:30:59 -0500210 mChars = ArrayUtils.newUnpaddedCharArray(mLen);
Doug Felte8e45f22010-03-29 14:58:40 -0700211 }
212 TextUtils.getChars(text, start, limit, mChars, 0);
Doug Felt0c702b82010-05-14 10:55:42 -0700213 if (hasReplacement) {
214 // Handle these all at once so we don't have to do it as we go.
215 // Replace the first character of each replacement run with the
216 // object-replacement character and the remainder with zero width
217 // non-break space aka BOM. Cursor movement code skips these
218 // zero-width characters.
219 char[] chars = mChars;
220 for (int i = start, inext; i < limit; i = inext) {
Gilles Debunnec1f44832011-12-08 16:03:00 -0800221 inext = mReplacementSpanSpanSet.getNextTransition(i, limit);
Mihai Popace642dc2018-05-24 14:25:11 +0100222 if (mReplacementSpanSpanSet.hasSpansIntersecting(i, inext)
223 && (i - start >= ellipsisEnd || inext - start <= ellipsisStart)) {
Gilles Debunne1e3ac182011-03-08 14:22:34 -0800224 // transition into a span
Doug Felt0c702b82010-05-14 10:55:42 -0700225 chars[i - start] = '\ufffc';
226 for (int j = i - start + 1, e = inext - start; j < e; ++j) {
227 chars[j] = '\ufeff'; // used as ZWNBS, marks positions to skip
228 }
229 }
230 }
231 }
Doug Felte8e45f22010-03-29 14:58:40 -0700232 }
Doug Feltc982f602010-05-25 11:51:40 -0700233 mTabs = tabStops;
Haoyu Zhangeccdc6b2019-01-07 15:11:46 -0800234 mAddedWidthForJustify = 0;
235 mIsJustifying = false;
Mihai Popace642dc2018-05-24 14:25:11 +0100236
237 mEllipsisStart = ellipsisStart != ellipsisEnd ? ellipsisStart : 0;
238 mEllipsisEnd = ellipsisStart != ellipsisEnd ? ellipsisEnd : 0;
Seigo Nonaka09da71a2016-11-28 16:24:14 +0900239 }
240
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000241 private char charAt(int i) {
242 return mCharsValid ? mChars[i] : mText.charAt(i + mStart);
243 }
244
Seigo Nonaka09da71a2016-11-28 16:24:14 +0900245 /**
246 * Justify the line to the given width.
247 */
Roozbeh Pournader3630d082017-10-23 12:16:32 -0700248 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
249 public void justify(float justifyWidth) {
Seigo Nonaka09da71a2016-11-28 16:24:14 +0900250 int end = mLen;
251 while (end > 0 && isLineEndSpace(mText.charAt(mStart + end - 1))) {
252 end--;
253 }
254 final int spaces = countStretchableSpaces(0, end);
255 if (spaces == 0) {
256 // There are no stretchable spaces, so we can't help the justification by adding any
257 // width.
258 return;
259 }
260 final float width = Math.abs(measure(end, false, null));
Haoyu Zhangeccdc6b2019-01-07 15:11:46 -0800261 mAddedWidthForJustify = (justifyWidth - width) / spaces;
262 mIsJustifying = true;
Doug Felte8e45f22010-03-29 14:58:40 -0700263 }
264
265 /**
266 * Renders the TextLine.
267 *
268 * @param c the canvas to render on
269 * @param x the leading margin position
270 * @param top the top of the line
271 * @param y the baseline
272 * @param bottom the bottom of the line
273 */
274 void draw(Canvas c, float x, int top, int y, int bottom) {
Doug Felte8e45f22010-03-29 14:58:40 -0700275 float h = 0;
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000276 final int runCount = mDirections.getRunCount();
277 for (int runIndex = 0; runIndex < runCount; runIndex++) {
278 final int runStart = mDirections.getRunStart(runIndex);
279 final int runLimit = Math.min(runStart + mDirections.getRunLength(runIndex), mLen);
280 final boolean runIsRtl = mDirections.isRunRtl(runIndex);
Doug Felte8e45f22010-03-29 14:58:40 -0700281
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000282 int segStart = runStart;
Doug Felte8e45f22010-03-29 14:58:40 -0700283 for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000284 if (j == runLimit || charAt(j) == TAB_CHAR) {
285 h += drawRun(c, segStart, j, runIsRtl, x + h, top, y, bottom,
286 runIndex != (runCount - 1) || j != mLen);
Doug Felte8e45f22010-03-29 14:58:40 -0700287
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000288 if (j != runLimit) { // charAt(j) == TAB_CHAR
Doug Felte8e45f22010-03-29 14:58:40 -0700289 h = mDir * nextTab(h * mDir);
Doug Felte8e45f22010-03-29 14:58:40 -0700290 }
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000291 segStart = j + 1;
Doug Felte8e45f22010-03-29 14:58:40 -0700292 }
293 }
294 }
295 }
296
297 /**
298 * Returns metrics information for the entire line.
299 *
300 * @param fmi receives font metrics information, can be null
301 * @return the signed width of the line
302 */
Roozbeh Pournader3630d082017-10-23 12:16:32 -0700303 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
304 public float metrics(FontMetricsInt fmi) {
Doug Felte8e45f22010-03-29 14:58:40 -0700305 return measure(mLen, false, fmi);
306 }
307
308 /**
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000309 * Returns the signed graphical offset from the leading margin.
Doug Felte8e45f22010-03-29 14:58:40 -0700310 *
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000311 * Following examples are all for measuring offset=3. LX(e.g. L0, L1, ...) denotes a
312 * character which has LTR BiDi property. On the other hand, RX(e.g. R0, R1, ...) denotes a
313 * character which has RTL BiDi property. Assuming all character has 1em width.
314 *
315 * Example 1: All LTR chars within LTR context
316 * Input Text (logical) : L0 L1 L2 L3 L4 L5 L6 L7 L8
317 * Input Text (visual) : L0 L1 L2 L3 L4 L5 L6 L7 L8
318 * Output(trailing=true) : |--------| (Returns 3em)
319 * Output(trailing=false): |--------| (Returns 3em)
320 *
321 * Example 2: All RTL chars within RTL context.
322 * Input Text (logical) : R0 R1 R2 R3 R4 R5 R6 R7 R8
323 * Input Text (visual) : R8 R7 R6 R5 R4 R3 R2 R1 R0
324 * Output(trailing=true) : |--------| (Returns -3em)
325 * Output(trailing=false): |--------| (Returns -3em)
326 *
327 * Example 3: BiDi chars within LTR context.
328 * Input Text (logical) : L0 L1 L2 R3 R4 R5 L6 L7 L8
329 * Input Text (visual) : L0 L1 L2 R5 R4 R3 L6 L7 L8
330 * Output(trailing=true) : |-----------------| (Returns 6em)
331 * Output(trailing=false): |--------| (Returns 3em)
332 *
333 * Example 4: BiDi chars within RTL context.
334 * Input Text (logical) : L0 L1 L2 R3 R4 R5 L6 L7 L8
335 * Input Text (visual) : L6 L7 L8 R5 R4 R3 L0 L1 L2
336 * Output(trailing=true) : |-----------------| (Returns -6em)
337 * Output(trailing=false): |--------| (Returns -3em)
338 *
339 * @param offset the line-relative character offset, between 0 and the line length, inclusive
340 * @param trailing no effect if the offset is not on the BiDi transition offset. If the offset
341 * is on the BiDi transition offset and true is passed, the offset is regarded
342 * as the edge of the trailing run's edge. If false, the offset is regarded as
343 * the edge of the preceding run's edge. See example above.
344 * @param fmi receives metrics information about the requested character, can be null
345 * @return the signed graphical offset from the leading margin to the requested character edge.
346 * The positive value means the offset is right from the leading edge. The negative
347 * value means the offset is left from the leading edge.
Doug Felte8e45f22010-03-29 14:58:40 -0700348 */
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000349 public float measure(@IntRange(from = 0) int offset, boolean trailing,
350 @NonNull FontMetricsInt fmi) {
351 if (offset > mLen) {
352 throw new IndexOutOfBoundsException(
353 "offset(" + offset + ") should be less than line limit(" + mLen + ")");
354 }
355 final int target = trailing ? offset - 1 : offset;
Doug Felte8e45f22010-03-29 14:58:40 -0700356 if (target < 0) {
357 return 0;
358 }
359
360 float h = 0;
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000361 for (int runIndex = 0; runIndex < mDirections.getRunCount(); runIndex++) {
362 final int runStart = mDirections.getRunStart(runIndex);
363 final int runLimit = Math.min(runStart + mDirections.getRunLength(runIndex), mLen);
364 final boolean runIsRtl = mDirections.isRunRtl(runIndex);
Doug Felte8e45f22010-03-29 14:58:40 -0700365
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000366 int segStart = runStart;
Doug Felte8e45f22010-03-29 14:58:40 -0700367 for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000368 if (j == runLimit || charAt(j) == TAB_CHAR) {
369 final boolean targetIsInThisSegment = target >= segStart && target < j;
370 final boolean sameDirection = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl;
Doug Felte8e45f22010-03-29 14:58:40 -0700371
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000372 if (targetIsInThisSegment && sameDirection) {
373 return h + measureRun(segStart, offset, j, runIsRtl, fmi);
Doug Felte8e45f22010-03-29 14:58:40 -0700374 }
375
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000376 final float segmentWidth = measureRun(segStart, j, j, runIsRtl, fmi);
377 h += sameDirection ? segmentWidth : -segmentWidth;
Doug Felte8e45f22010-03-29 14:58:40 -0700378
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000379 if (targetIsInThisSegment) {
380 return h + measureRun(segStart, offset, j, runIsRtl, null);
Doug Felte8e45f22010-03-29 14:58:40 -0700381 }
382
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000383 if (j != runLimit) { // charAt(j) == TAB_CHAR
Doug Felte8e45f22010-03-29 14:58:40 -0700384 if (offset == j) {
385 return h;
386 }
387 h = mDir * nextTab(h * mDir);
388 if (target == j) {
389 return h;
390 }
391 }
392
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000393 segStart = j + 1;
Doug Felte8e45f22010-03-29 14:58:40 -0700394 }
395 }
396 }
397
398 return h;
399 }
400
401 /**
Mihai Popa7626c862018-05-09 17:31:48 +0100402 * @see #measure(int, boolean, FontMetricsInt)
403 * @return The measure results for all possible offsets
404 */
405 @VisibleForTesting
406 public float[] measureAllOffsets(boolean[] trailing, FontMetricsInt fmi) {
407 float[] measurement = new float[mLen + 1];
408
409 int[] target = new int[mLen + 1];
410 for (int offset = 0; offset < target.length; ++offset) {
411 target[offset] = trailing[offset] ? offset - 1 : offset;
412 }
413 if (target[0] < 0) {
414 measurement[0] = 0;
415 }
416
417 float h = 0;
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000418 for (int runIndex = 0; runIndex < mDirections.getRunCount(); runIndex++) {
419 final int runStart = mDirections.getRunStart(runIndex);
420 final int runLimit = Math.min(runStart + mDirections.getRunLength(runIndex), mLen);
421 final boolean runIsRtl = mDirections.isRunRtl(runIndex);
Mihai Popa7626c862018-05-09 17:31:48 +0100422
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000423 int segStart = runStart;
Mihai Popa7626c862018-05-09 17:31:48 +0100424 for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; ++j) {
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000425 if (j == runLimit || charAt(j) == TAB_CHAR) {
426 final float oldh = h;
427 final boolean advance = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl;
428 final float w = measureRun(segStart, j, j, runIsRtl, fmi);
Mihai Popa7626c862018-05-09 17:31:48 +0100429 h += advance ? w : -w;
430
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000431 final float baseh = advance ? oldh : h;
Mihai Popa7626c862018-05-09 17:31:48 +0100432 FontMetricsInt crtfmi = advance ? fmi : null;
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000433 for (int offset = segStart; offset <= j && offset <= mLen; ++offset) {
434 if (target[offset] >= segStart && target[offset] < j) {
Mihai Popa7626c862018-05-09 17:31:48 +0100435 measurement[offset] =
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000436 baseh + measureRun(segStart, offset, j, runIsRtl, crtfmi);
Mihai Popa7626c862018-05-09 17:31:48 +0100437 }
438 }
439
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000440 if (j != runLimit) { // charAt(j) == TAB_CHAR
Mihai Popa7626c862018-05-09 17:31:48 +0100441 if (target[j] == j) {
442 measurement[j] = h;
443 }
444 h = mDir * nextTab(h * mDir);
445 if (target[j + 1] == j) {
446 measurement[j + 1] = h;
447 }
448 }
449
Siyamed Sinirbf92c2f2018-11-16 23:38:12 +0000450 segStart = j + 1;
Mihai Popa7626c862018-05-09 17:31:48 +0100451 }
452 }
453 }
454 if (target[mLen] == mLen) {
455 measurement[mLen] = h;
456 }
457
458 return measurement;
459 }
460
461 /**
Doug Felte8e45f22010-03-29 14:58:40 -0700462 * Draws a unidirectional (but possibly multi-styled) run of text.
463 *
Romain Guybc7cdb62011-05-26 18:48:49 -0700464 *
Doug Felte8e45f22010-03-29 14:58:40 -0700465 * @param c the canvas to draw on
Doug Felte8e45f22010-03-29 14:58:40 -0700466 * @param start the line-relative start
467 * @param limit the line-relative limit
468 * @param runIsRtl true if the run is right-to-left
469 * @param x the position of the run that is closest to the leading margin
470 * @param top the top of the line
471 * @param y the baseline
472 * @param bottom the bottom of the line
473 * @param needWidth true if the width value is required.
474 * @return the signed width of the run, based on the paragraph direction.
475 * Only valid if needWidth is true.
476 */
Romain Guybc7cdb62011-05-26 18:48:49 -0700477 private float drawRun(Canvas c, int start,
Doug Felte8e45f22010-03-29 14:58:40 -0700478 int limit, boolean runIsRtl, float x, int top, int y, int bottom,
479 boolean needWidth) {
480
481 if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) {
Romain Guybc7cdb62011-05-26 18:48:49 -0700482 float w = -measureRun(start, limit, limit, runIsRtl, null);
483 handleRun(start, limit, limit, runIsRtl, c, x + w, top,
Doug Felt0c702b82010-05-14 10:55:42 -0700484 y, bottom, null, false);
Doug Felte8e45f22010-03-29 14:58:40 -0700485 return w;
486 }
487
Romain Guybc7cdb62011-05-26 18:48:49 -0700488 return handleRun(start, limit, limit, runIsRtl, c, x, top,
Doug Felt0c702b82010-05-14 10:55:42 -0700489 y, bottom, null, needWidth);
Doug Felte8e45f22010-03-29 14:58:40 -0700490 }
491
492 /**
493 * Measures a unidirectional (but possibly multi-styled) run of text.
494 *
Romain Guybc7cdb62011-05-26 18:48:49 -0700495 *
Doug Felte8e45f22010-03-29 14:58:40 -0700496 * @param start the line-relative start of the run
497 * @param offset the offset to measure to, between start and limit inclusive
498 * @param limit the line-relative limit of the run
499 * @param runIsRtl true if the run is right-to-left
500 * @param fmi receives metrics information about the requested
501 * run, can be null.
502 * @return the signed width from the start of the run to the leading edge
503 * of the character at offset, based on the run (not paragraph) direction
504 */
Romain Guybc7cdb62011-05-26 18:48:49 -0700505 private float measureRun(int start, int offset, int limit, boolean runIsRtl,
506 FontMetricsInt fmi) {
507 return handleRun(start, offset, limit, runIsRtl, null, 0, 0, 0, 0, fmi, true);
Doug Felte8e45f22010-03-29 14:58:40 -0700508 }
509
510 /**
511 * Walk the cursor through this line, skipping conjuncts and
512 * zero-width characters.
513 *
514 * <p>This function cannot properly walk the cursor off the ends of the line
515 * since it does not know about any shaping on the previous/following line
516 * that might affect the cursor position. Callers must either avoid these
517 * situations or handle the result specially.
518 *
Doug Felte8e45f22010-03-29 14:58:40 -0700519 * @param cursor the starting position of the cursor, between 0 and the
520 * length of the line, inclusive
521 * @param toLeft true if the caret is moving to the left.
522 * @return the new offset. If it is less than 0 or greater than the length
523 * of the line, the previous/following line should be examined to get the
524 * actual offset.
525 */
526 int getOffsetToLeftRightOf(int cursor, boolean toLeft) {
527 // 1) The caret marks the leading edge of a character. The character
528 // logically before it might be on a different level, and the active caret
529 // position is on the character at the lower level. If that character
530 // was the previous character, the caret is on its trailing edge.
531 // 2) Take this character/edge and move it in the indicated direction.
532 // This gives you a new character and a new edge.
533 // 3) This position is between two visually adjacent characters. One of
534 // these might be at a lower level. The active position is on the
535 // character at the lower level.
536 // 4) If the active position is on the trailing edge of the character,
537 // the new caret position is the following logical character, else it
538 // is the character.
539
540 int lineStart = 0;
541 int lineEnd = mLen;
542 boolean paraIsRtl = mDir == -1;
543 int[] runs = mDirections.mDirections;
544
545 int runIndex, runLevel = 0, runStart = lineStart, runLimit = lineEnd, newCaret = -1;
546 boolean trailing = false;
547
548 if (cursor == lineStart) {
549 runIndex = -2;
550 } else if (cursor == lineEnd) {
551 runIndex = runs.length;
552 } else {
553 // First, get information about the run containing the character with
554 // the active caret.
555 for (runIndex = 0; runIndex < runs.length; runIndex += 2) {
556 runStart = lineStart + runs[runIndex];
557 if (cursor >= runStart) {
558 runLimit = runStart + (runs[runIndex+1] & Layout.RUN_LENGTH_MASK);
559 if (runLimit > lineEnd) {
560 runLimit = lineEnd;
561 }
562 if (cursor < runLimit) {
563 runLevel = (runs[runIndex+1] >>> Layout.RUN_LEVEL_SHIFT) &
564 Layout.RUN_LEVEL_MASK;
565 if (cursor == runStart) {
566 // The caret is on a run boundary, see if we should
567 // use the position on the trailing edge of the previous
568 // logical character instead.
569 int prevRunIndex, prevRunLevel, prevRunStart, prevRunLimit;
570 int pos = cursor - 1;
571 for (prevRunIndex = 0; prevRunIndex < runs.length; prevRunIndex += 2) {
572 prevRunStart = lineStart + runs[prevRunIndex];
573 if (pos >= prevRunStart) {
574 prevRunLimit = prevRunStart +
575 (runs[prevRunIndex+1] & Layout.RUN_LENGTH_MASK);
576 if (prevRunLimit > lineEnd) {
577 prevRunLimit = lineEnd;
578 }
579 if (pos < prevRunLimit) {
580 prevRunLevel = (runs[prevRunIndex+1] >>> Layout.RUN_LEVEL_SHIFT)
581 & Layout.RUN_LEVEL_MASK;
582 if (prevRunLevel < runLevel) {
583 // Start from logically previous character.
584 runIndex = prevRunIndex;
585 runLevel = prevRunLevel;
586 runStart = prevRunStart;
587 runLimit = prevRunLimit;
588 trailing = true;
589 break;
590 }
591 }
592 }
593 }
594 }
595 break;
596 }
597 }
598 }
599
600 // caret might be == lineEnd. This is generally a space or paragraph
601 // separator and has an associated run, but might be the end of
602 // text, in which case it doesn't. If that happens, we ran off the
603 // end of the run list, and runIndex == runs.length. In this case,
604 // we are at a run boundary so we skip the below test.
605 if (runIndex != runs.length) {
606 boolean runIsRtl = (runLevel & 0x1) != 0;
607 boolean advance = toLeft == runIsRtl;
608 if (cursor != (advance ? runLimit : runStart) || advance != trailing) {
609 // Moving within or into the run, so we can move logically.
Doug Felt0c702b82010-05-14 10:55:42 -0700610 newCaret = getOffsetBeforeAfter(runIndex, runStart, runLimit,
611 runIsRtl, cursor, advance);
Doug Felte8e45f22010-03-29 14:58:40 -0700612 // If the new position is internal to the run, we're at the strong
613 // position already so we're finished.
614 if (newCaret != (advance ? runLimit : runStart)) {
615 return newCaret;
616 }
617 }
618 }
619 }
620
621 // If newCaret is -1, we're starting at a run boundary and crossing
622 // into another run. Otherwise we've arrived at a run boundary, and
623 // need to figure out which character to attach to. Note we might
624 // need to run this twice, if we cross a run boundary and end up at
625 // another run boundary.
626 while (true) {
627 boolean advance = toLeft == paraIsRtl;
628 int otherRunIndex = runIndex + (advance ? 2 : -2);
629 if (otherRunIndex >= 0 && otherRunIndex < runs.length) {
630 int otherRunStart = lineStart + runs[otherRunIndex];
631 int otherRunLimit = otherRunStart +
632 (runs[otherRunIndex+1] & Layout.RUN_LENGTH_MASK);
633 if (otherRunLimit > lineEnd) {
634 otherRunLimit = lineEnd;
635 }
636 int otherRunLevel = (runs[otherRunIndex+1] >>> Layout.RUN_LEVEL_SHIFT) &
637 Layout.RUN_LEVEL_MASK;
638 boolean otherRunIsRtl = (otherRunLevel & 1) != 0;
639
640 advance = toLeft == otherRunIsRtl;
641 if (newCaret == -1) {
Doug Felt0c702b82010-05-14 10:55:42 -0700642 newCaret = getOffsetBeforeAfter(otherRunIndex, otherRunStart,
643 otherRunLimit, otherRunIsRtl,
Doug Felte8e45f22010-03-29 14:58:40 -0700644 advance ? otherRunStart : otherRunLimit, advance);
645 if (newCaret == (advance ? otherRunLimit : otherRunStart)) {
646 // Crossed and ended up at a new boundary,
647 // repeat a second and final time.
648 runIndex = otherRunIndex;
649 runLevel = otherRunLevel;
650 continue;
651 }
652 break;
653 }
654
655 // The new caret is at a boundary.
656 if (otherRunLevel < runLevel) {
657 // The strong character is in the other run.
658 newCaret = advance ? otherRunStart : otherRunLimit;
659 }
660 break;
661 }
662
663 if (newCaret == -1) {
664 // We're walking off the end of the line. The paragraph
665 // level is always equal to or lower than any internal level, so
666 // the boundaries get the strong caret.
Doug Felt0c702b82010-05-14 10:55:42 -0700667 newCaret = advance ? mLen + 1 : -1;
Doug Felte8e45f22010-03-29 14:58:40 -0700668 break;
669 }
670
671 // Else we've arrived at the end of the line. That's a strong position.
672 // We might have arrived here by crossing over a run with no internal
673 // breaks and dropping out of the above loop before advancing one final
674 // time, so reset the caret.
675 // Note, we use '<=' below to handle a situation where the only run
676 // on the line is a counter-directional run. If we're not advancing,
677 // we can end up at the 'lineEnd' position but the caret we want is at
678 // the lineStart.
679 if (newCaret <= lineEnd) {
680 newCaret = advance ? lineEnd : lineStart;
681 }
682 break;
683 }
684
685 return newCaret;
686 }
687
688 /**
689 * Returns the next valid offset within this directional run, skipping
690 * conjuncts and zero-width characters. This should not be called to walk
Gilles Debunnecc3ec6c2010-06-23 10:30:27 -0700691 * off the end of the line, since the returned values might not be valid
Doug Felt0c702b82010-05-14 10:55:42 -0700692 * on neighboring lines. If the returned offset is less than zero or
693 * greater than the line length, the offset should be recomputed on the
694 * preceding or following line, respectively.
Doug Felte8e45f22010-03-29 14:58:40 -0700695 *
696 * @param runIndex the run index
Doug Felt0c702b82010-05-14 10:55:42 -0700697 * @param runStart the start of the run
698 * @param runLimit the limit of the run
699 * @param runIsRtl true if the run is right-to-left
Doug Felte8e45f22010-03-29 14:58:40 -0700700 * @param offset the offset
701 * @param after true if the new offset should logically follow the provided
702 * offset
703 * @return the new offset
704 */
Doug Felt0c702b82010-05-14 10:55:42 -0700705 private int getOffsetBeforeAfter(int runIndex, int runStart, int runLimit,
706 boolean runIsRtl, int offset, boolean after) {
Doug Felte8e45f22010-03-29 14:58:40 -0700707
Doug Felt0c702b82010-05-14 10:55:42 -0700708 if (runIndex < 0 || offset == (after ? mLen : 0)) {
709 // Walking off end of line. Since we don't know
710 // what cursor positions are available on other lines, we can't
711 // return accurate values. These are a guess.
Doug Felte8e45f22010-03-29 14:58:40 -0700712 if (after) {
Doug Felt0c702b82010-05-14 10:55:42 -0700713 return TextUtils.getOffsetAfter(mText, offset + mStart) - mStart;
714 }
715 return TextUtils.getOffsetBefore(mText, offset + mStart) - mStart;
716 }
717
718 TextPaint wp = mWorkPaint;
719 wp.set(mPaint);
Haoyu Zhangeccdc6b2019-01-07 15:11:46 -0800720 if (mIsJustifying) {
721 wp.setWordSpacing(mAddedWidthForJustify);
722 }
Doug Felt0c702b82010-05-14 10:55:42 -0700723
724 int spanStart = runStart;
725 int spanLimit;
726 if (mSpanned == null) {
727 spanLimit = runLimit;
728 } else {
729 int target = after ? offset + 1 : offset;
730 int limit = mStart + runLimit;
731 while (true) {
732 spanLimit = mSpanned.nextSpanTransition(mStart + spanStart, limit,
733 MetricAffectingSpan.class) - mStart;
734 if (spanLimit >= target) {
735 break;
Doug Felte8e45f22010-03-29 14:58:40 -0700736 }
Doug Felt0c702b82010-05-14 10:55:42 -0700737 spanStart = spanLimit;
738 }
739
740 MetricAffectingSpan[] spans = mSpanned.getSpans(mStart + spanStart,
741 mStart + spanLimit, MetricAffectingSpan.class);
Gilles Debunne1e3ac182011-03-08 14:22:34 -0800742 spans = TextUtils.removeEmptySpans(spans, mSpanned, MetricAffectingSpan.class);
Doug Felt0c702b82010-05-14 10:55:42 -0700743
744 if (spans.length > 0) {
745 ReplacementSpan replacement = null;
746 for (int j = 0; j < spans.length; j++) {
747 MetricAffectingSpan span = spans[j];
748 if (span instanceof ReplacementSpan) {
749 replacement = (ReplacementSpan)span;
750 } else {
751 span.updateMeasureState(wp);
752 }
753 }
754
755 if (replacement != null) {
756 // If we have a replacement span, we're moving either to
757 // the start or end of this span.
758 return after ? spanLimit : spanStart;
Doug Felte8e45f22010-03-29 14:58:40 -0700759 }
760 }
Doug Felte8e45f22010-03-29 14:58:40 -0700761 }
762
Doug Felt0c702b82010-05-14 10:55:42 -0700763 int cursorOpt = after ? Paint.CURSOR_AFTER : Paint.CURSOR_BEFORE;
764 if (mCharsValid) {
765 return wp.getTextRunCursor(mChars, spanStart, spanLimit - spanStart,
Seigo Nonakafa95b832018-09-17 14:09:25 -0700766 runIsRtl, offset, cursorOpt);
Doug Felt0c702b82010-05-14 10:55:42 -0700767 } else {
768 return wp.getTextRunCursor(mText, mStart + spanStart,
Seigo Nonakafa95b832018-09-17 14:09:25 -0700769 mStart + spanLimit, runIsRtl, mStart + offset, cursorOpt) - mStart;
Doug Felte8e45f22010-03-29 14:58:40 -0700770 }
Doug Felte8e45f22010-03-29 14:58:40 -0700771 }
772
773 /**
Gilles Debunne0bb00092010-12-02 15:50:26 -0800774 * @param wp
775 */
776 private static void expandMetricsFromPaint(FontMetricsInt fmi, TextPaint wp) {
777 final int previousTop = fmi.top;
778 final int previousAscent = fmi.ascent;
779 final int previousDescent = fmi.descent;
780 final int previousBottom = fmi.bottom;
781 final int previousLeading = fmi.leading;
782
783 wp.getFontMetricsInt(fmi);
784
Fabrice Di Meglio8a5137a2011-09-22 18:49:43 -0700785 updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom,
786 previousLeading);
787 }
788
789 static void updateMetrics(FontMetricsInt fmi, int previousTop, int previousAscent,
790 int previousDescent, int previousBottom, int previousLeading) {
Gilles Debunne0bb00092010-12-02 15:50:26 -0800791 fmi.top = Math.min(fmi.top, previousTop);
792 fmi.ascent = Math.min(fmi.ascent, previousAscent);
793 fmi.descent = Math.max(fmi.descent, previousDescent);
794 fmi.bottom = Math.max(fmi.bottom, previousBottom);
795 fmi.leading = Math.max(fmi.leading, previousLeading);
796 }
797
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700798 private static void drawStroke(TextPaint wp, Canvas c, int color, float position,
799 float thickness, float xleft, float xright, float baseline) {
800 final float strokeTop = baseline + wp.baselineShift + position;
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700801
802 final int previousColor = wp.getColor();
803 final Paint.Style previousStyle = wp.getStyle();
804 final boolean previousAntiAlias = wp.isAntiAlias();
805
806 wp.setStyle(Paint.Style.FILL);
807 wp.setAntiAlias(true);
808
809 wp.setColor(color);
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700810 c.drawRect(xleft, strokeTop, xright, strokeTop + thickness, wp);
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700811
812 wp.setStyle(previousStyle);
813 wp.setColor(previousColor);
814 wp.setAntiAlias(previousAntiAlias);
815 }
816
817 private float getRunAdvance(TextPaint wp, int start, int end, int contextStart, int contextEnd,
818 boolean runIsRtl, int offset) {
819 if (mCharsValid) {
820 return wp.getRunAdvance(mChars, start, end, contextStart, contextEnd, runIsRtl, offset);
821 } else {
822 final int delta = mStart;
Seigo Nonakabeafa1f2018-02-01 21:39:24 -0800823 if (mComputed == null) {
Seigo Nonaka783f9612018-01-20 12:11:13 -0800824 return wp.getRunAdvance(mText, delta + start, delta + end,
825 delta + contextStart, delta + contextEnd, runIsRtl, delta + offset);
826 } else {
Seigo Nonakabeafa1f2018-02-01 21:39:24 -0800827 return mComputed.getWidth(start + delta, end + delta);
Seigo Nonaka783f9612018-01-20 12:11:13 -0800828 }
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700829 }
830 }
831
Gilles Debunne0bb00092010-12-02 15:50:26 -0800832 /**
Doug Felte8e45f22010-03-29 14:58:40 -0700833 * Utility function for measuring and rendering text. The text must
Roozbeh Pournader112d9c72015-08-07 12:44:41 -0700834 * not include a tab.
Doug Felte8e45f22010-03-29 14:58:40 -0700835 *
836 * @param wp the working paint
837 * @param start the start of the text
Doug Felt0c702b82010-05-14 10:55:42 -0700838 * @param end the end of the text
Doug Felte8e45f22010-03-29 14:58:40 -0700839 * @param runIsRtl true if the run is right-to-left
840 * @param c the canvas, can be null if rendering is not needed
841 * @param x the edge of the run closest to the leading margin
842 * @param top the top of the line
843 * @param y the baseline
844 * @param bottom the bottom of the line
845 * @param fmi receives metrics information, can be null
846 * @param needWidth true if the width of the run is needed
Raph Levien909c7bc2015-11-30 21:05:46 -0800847 * @param offset the offset for the purpose of measuring
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700848 * @param decorations the list of locations and paremeters for drawing decorations
Doug Felte8e45f22010-03-29 14:58:40 -0700849 * @return the signed width of the run based on the run direction; only
850 * valid if needWidth is true
851 */
Doug Felt0c702b82010-05-14 10:55:42 -0700852 private float handleText(TextPaint wp, int start, int end,
853 int contextStart, int contextEnd, boolean runIsRtl,
854 Canvas c, float x, int top, int y, int bottom,
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700855 FontMetricsInt fmi, boolean needWidth, int offset,
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700856 @Nullable ArrayList<DecorationInfo> decorations) {
Doug Felte8e45f22010-03-29 14:58:40 -0700857
Haoyu Zhangeccdc6b2019-01-07 15:11:46 -0800858 if (mIsJustifying) {
859 wp.setWordSpacing(mAddedWidthForJustify);
860 }
Fabrice Di Meglio850dffa2011-08-08 09:45:09 -0700861 // Get metrics first (even for empty strings or "0" width runs)
862 if (fmi != null) {
863 expandMetricsFromPaint(fmi, wp);
864 }
Doug Felte8e45f22010-03-29 14:58:40 -0700865
Fabrice Di Meglio850dffa2011-08-08 09:45:09 -0700866 // No need to do anything if the run width is "0"
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700867 if (end == start) {
Fabrice Di Meglio850dffa2011-08-08 09:45:09 -0700868 return 0f;
869 }
870
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700871 float totalWidth = 0;
Fabrice Di Meglio850dffa2011-08-08 09:45:09 -0700872
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700873 final int numDecorations = decorations == null ? 0 : decorations.size();
874 if (needWidth || (c != null && (wp.bgColor != 0 || numDecorations != 0 || runIsRtl))) {
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700875 totalWidth = getRunAdvance(wp, start, end, contextStart, contextEnd, runIsRtl, offset);
Doug Felte8e45f22010-03-29 14:58:40 -0700876 }
877
Doug Felte8e45f22010-03-29 14:58:40 -0700878 if (c != null) {
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700879 final float leftX, rightX;
Doug Felte8e45f22010-03-29 14:58:40 -0700880 if (runIsRtl) {
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700881 leftX = x - totalWidth;
882 rightX = x;
883 } else {
884 leftX = x;
885 rightX = x + totalWidth;
Doug Felte8e45f22010-03-29 14:58:40 -0700886 }
887
888 if (wp.bgColor != 0) {
Gilles Debunnedd8f5ed2011-08-10 18:25:49 -0700889 int previousColor = wp.getColor();
890 Paint.Style previousStyle = wp.getStyle();
891
Doug Felte8e45f22010-03-29 14:58:40 -0700892 wp.setColor(wp.bgColor);
893 wp.setStyle(Paint.Style.FILL);
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700894 c.drawRect(leftX, top, rightX, bottom, wp);
Doug Felte8e45f22010-03-29 14:58:40 -0700895
Gilles Debunnedd8f5ed2011-08-10 18:25:49 -0700896 wp.setStyle(previousStyle);
897 wp.setColor(previousColor);
898 }
899
Seigo Nonaka50fe7882018-05-21 14:56:40 -0700900 drawTextRun(c, wp, start, end, contextStart, contextEnd, runIsRtl,
901 leftX, y + wp.baselineShift);
902
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700903 if (numDecorations != 0) {
904 for (int i = 0; i < numDecorations; i++) {
905 final DecorationInfo info = decorations.get(i);
Gilles Debunnedd8f5ed2011-08-10 18:25:49 -0700906
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700907 final int decorationStart = Math.max(info.start, start);
908 final int decorationEnd = Math.min(info.end, offset);
909 float decorationStartAdvance = getRunAdvance(
910 wp, start, end, contextStart, contextEnd, runIsRtl, decorationStart);
911 float decorationEndAdvance = getRunAdvance(
912 wp, start, end, contextStart, contextEnd, runIsRtl, decorationEnd);
913 final float decorationXLeft, decorationXRight;
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700914 if (runIsRtl) {
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700915 decorationXLeft = rightX - decorationEndAdvance;
916 decorationXRight = rightX - decorationStartAdvance;
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700917 } else {
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700918 decorationXLeft = leftX + decorationStartAdvance;
919 decorationXRight = leftX + decorationEndAdvance;
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700920 }
Siyamed Sinir702c9f92017-06-01 18:25:40 +0000921
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700922 // Theoretically, there could be cases where both Paint's and TextPaint's
923 // setUnderLineText() are called. For backward compatibility, we need to draw
924 // both underlines, the one with custom color first.
925 if (info.underlineColor != 0) {
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700926 drawStroke(wp, c, info.underlineColor, wp.getUnderlinePosition(),
927 info.underlineThickness, decorationXLeft, decorationXRight, y);
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700928 }
929 if (info.isUnderlineText) {
Roozbeh Pournaderca8a04a2017-06-06 18:30:29 -0700930 final float thickness =
Siyamed Sinira273a702017-10-05 11:22:12 -0700931 Math.max(wp.getUnderlineThickness(), 1.0f);
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700932 drawStroke(wp, c, wp.getColor(), wp.getUnderlinePosition(), thickness,
933 decorationXLeft, decorationXRight, y);
934 }
935
936 if (info.isStrikeThruText) {
937 final float thickness =
Siyamed Sinira273a702017-10-05 11:22:12 -0700938 Math.max(wp.getStrikeThruThickness(), 1.0f);
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -0700939 drawStroke(wp, c, wp.getColor(), wp.getStrikeThruPosition(), thickness,
940 decorationXLeft, decorationXRight, y);
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700941 }
942 }
Doug Felte8e45f22010-03-29 14:58:40 -0700943 }
944
Doug Felte8e45f22010-03-29 14:58:40 -0700945 }
946
Roozbeh Pournader538de5b2017-05-24 09:07:52 -0700947 return runIsRtl ? -totalWidth : totalWidth;
Doug Felte8e45f22010-03-29 14:58:40 -0700948 }
949
950 /**
951 * Utility function for measuring and rendering a replacement.
952 *
Romain Guybc7cdb62011-05-26 18:48:49 -0700953 *
Doug Felte8e45f22010-03-29 14:58:40 -0700954 * @param replacement the replacement
955 * @param wp the work paint
Doug Felte8e45f22010-03-29 14:58:40 -0700956 * @param start the start of the run
957 * @param limit the limit of the run
958 * @param runIsRtl true if the run is right-to-left
959 * @param c the canvas, can be null if not rendering
960 * @param x the edge of the replacement closest to the leading margin
961 * @param top the top of the line
962 * @param y the baseline
963 * @param bottom the bottom of the line
964 * @param fmi receives metrics information, can be null
965 * @param needWidth true if the width of the replacement is needed
Doug Felte8e45f22010-03-29 14:58:40 -0700966 * @return the signed width of the run based on the run direction; only
967 * valid if needWidth is true
968 */
969 private float handleReplacement(ReplacementSpan replacement, TextPaint wp,
Romain Guybc7cdb62011-05-26 18:48:49 -0700970 int start, int limit, boolean runIsRtl, Canvas c,
Doug Felte8e45f22010-03-29 14:58:40 -0700971 float x, int top, int y, int bottom, FontMetricsInt fmi,
Doug Felt0c702b82010-05-14 10:55:42 -0700972 boolean needWidth) {
Doug Felte8e45f22010-03-29 14:58:40 -0700973
974 float ret = 0;
975
Doug Felt0c702b82010-05-14 10:55:42 -0700976 int textStart = mStart + start;
977 int textLimit = mStart + limit;
978
979 if (needWidth || (c != null && runIsRtl)) {
Fabrice Di Meglio8a5137a2011-09-22 18:49:43 -0700980 int previousTop = 0;
981 int previousAscent = 0;
982 int previousDescent = 0;
983 int previousBottom = 0;
984 int previousLeading = 0;
985
986 boolean needUpdateMetrics = (fmi != null);
987
988 if (needUpdateMetrics) {
989 previousTop = fmi.top;
990 previousAscent = fmi.ascent;
991 previousDescent = fmi.descent;
992 previousBottom = fmi.bottom;
993 previousLeading = fmi.leading;
994 }
995
Doug Felt0c702b82010-05-14 10:55:42 -0700996 ret = replacement.getSize(wp, mText, textStart, textLimit, fmi);
Fabrice Di Meglio8a5137a2011-09-22 18:49:43 -0700997
998 if (needUpdateMetrics) {
999 updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom,
1000 previousLeading);
1001 }
Doug Felte8e45f22010-03-29 14:58:40 -07001002 }
1003
Doug Felt0c702b82010-05-14 10:55:42 -07001004 if (c != null) {
1005 if (runIsRtl) {
1006 x -= ret;
Doug Felte8e45f22010-03-29 14:58:40 -07001007 }
Doug Felt0c702b82010-05-14 10:55:42 -07001008 replacement.draw(c, mText, textStart, textLimit,
1009 x, top, y, bottom, wp);
Doug Felte8e45f22010-03-29 14:58:40 -07001010 }
1011
1012 return runIsRtl ? -ret : ret;
1013 }
1014
Seigo Nonakafb1b4792019-03-08 14:05:08 -08001015 private int adjustStartHyphenEdit(int start, @Paint.StartHyphenEdit int startHyphenEdit) {
1016 // Only draw hyphens on first in line. Disable them otherwise.
1017 return start > 0 ? Paint.START_HYPHEN_EDIT_NO_EDIT : startHyphenEdit;
1018 }
1019
1020 private int adjustEndHyphenEdit(int limit, @Paint.EndHyphenEdit int endHyphenEdit) {
1021 // Only draw hyphens on last run in line. Disable them otherwise.
1022 return limit < mLen ? Paint.END_HYPHEN_EDIT_NO_EDIT : endHyphenEdit;
Roozbeh Pournader46c6f4c2017-02-21 12:18:31 -08001023 }
1024
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001025 private static final class DecorationInfo {
1026 public boolean isStrikeThruText;
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001027 public boolean isUnderlineText;
1028 public int underlineColor;
1029 public float underlineThickness;
1030 public int start = -1;
1031 public int end = -1;
1032
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001033 public boolean hasDecoration() {
1034 return isStrikeThruText || isUnderlineText || underlineColor != 0;
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001035 }
1036
1037 // Copies the info, but not the start and end range.
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001038 public DecorationInfo copyInfo() {
1039 final DecorationInfo copy = new DecorationInfo();
1040 copy.isStrikeThruText = isStrikeThruText;
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001041 copy.isUnderlineText = isUnderlineText;
1042 copy.underlineColor = underlineColor;
1043 copy.underlineThickness = underlineThickness;
1044 return copy;
1045 }
1046 }
1047
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001048 private void extractDecorationInfo(@NonNull TextPaint paint, @NonNull DecorationInfo info) {
1049 info.isStrikeThruText = paint.isStrikeThruText();
1050 if (info.isStrikeThruText) {
1051 paint.setStrikeThruText(false);
1052 }
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001053 info.isUnderlineText = paint.isUnderlineText();
1054 if (info.isUnderlineText) {
1055 paint.setUnderlineText(false);
1056 }
1057 info.underlineColor = paint.underlineColor;
1058 info.underlineThickness = paint.underlineThickness;
1059 paint.setUnderlineText(0, 0.0f);
1060 }
1061
Doug Felte8e45f22010-03-29 14:58:40 -07001062 /**
1063 * Utility function for handling a unidirectional run. The run must not
Roozbeh Pournader112d9c72015-08-07 12:44:41 -07001064 * contain tabs but can contain styles.
Doug Felte8e45f22010-03-29 14:58:40 -07001065 *
Romain Guybc7cdb62011-05-26 18:48:49 -07001066 *
Doug Felte8e45f22010-03-29 14:58:40 -07001067 * @param start the line-relative start of the run
Doug Felt0c702b82010-05-14 10:55:42 -07001068 * @param measureLimit the offset to measure to, between start and limit inclusive
Doug Felte8e45f22010-03-29 14:58:40 -07001069 * @param limit the limit of the run
1070 * @param runIsRtl true if the run is right-to-left
1071 * @param c the canvas, can be null
1072 * @param x the end of the run closest to the leading margin
1073 * @param top the top of the line
1074 * @param y the baseline
1075 * @param bottom the bottom of the line
1076 * @param fmi receives metrics information, can be null
1077 * @param needWidth true if the width is required
Doug Felte8e45f22010-03-29 14:58:40 -07001078 * @return the signed width of the run based on the run direction; only
1079 * valid if needWidth is true
1080 */
Romain Guybc7cdb62011-05-26 18:48:49 -07001081 private float handleRun(int start, int measureLimit,
Doug Felte8e45f22010-03-29 14:58:40 -07001082 int limit, boolean runIsRtl, Canvas c, float x, int top, int y,
Doug Felt0c702b82010-05-14 10:55:42 -07001083 int bottom, FontMetricsInt fmi, boolean needWidth) {
Doug Felte8e45f22010-03-29 14:58:40 -07001084
Siyamed Sinir9f3958c2016-08-26 15:23:47 -07001085 if (measureLimit < start || measureLimit > limit) {
1086 throw new IndexOutOfBoundsException("measureLimit (" + measureLimit + ") is out of "
1087 + "start (" + start + ") and limit (" + limit + ") bounds");
1088 }
1089
Gilles Debunnef483e512011-04-28 15:08:54 -07001090 // Case of an empty line, make sure we update fmi according to mPaint
1091 if (start == measureLimit) {
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001092 final TextPaint wp = mWorkPaint;
Gilles Debunnef483e512011-04-28 15:08:54 -07001093 wp.set(mPaint);
Fabrice Di Meglio15c097a2011-08-08 14:42:41 -07001094 if (fmi != null) {
1095 expandMetricsFromPaint(fmi, wp);
1096 }
1097 return 0f;
Gilles Debunnef483e512011-04-28 15:08:54 -07001098 }
1099
Roozbeh Pournader08836d42017-06-06 16:23:09 -07001100 final boolean needsSpanMeasurement;
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001101 if (mSpanned == null) {
Roozbeh Pournader08836d42017-06-06 16:23:09 -07001102 needsSpanMeasurement = false;
1103 } else {
1104 mMetricAffectingSpanSpanSet.init(mSpanned, mStart + start, mStart + limit);
1105 mCharacterStyleSpanSet.init(mSpanned, mStart + start, mStart + limit);
1106 needsSpanMeasurement = mMetricAffectingSpanSpanSet.numberOfSpans != 0
1107 || mCharacterStyleSpanSet.numberOfSpans != 0;
1108 }
1109
1110 if (!needsSpanMeasurement) {
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001111 final TextPaint wp = mWorkPaint;
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001112 wp.set(mPaint);
Seigo Nonakafb1b4792019-03-08 14:05:08 -08001113 wp.setStartHyphenEdit(adjustStartHyphenEdit(start, wp.getStartHyphenEdit()));
1114 wp.setEndHyphenEdit(adjustEndHyphenEdit(limit, wp.getEndHyphenEdit()));
Raph Levien909c7bc2015-11-30 21:05:46 -08001115 return handleText(wp, start, limit, start, limit, runIsRtl, c, x, top,
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001116 y, bottom, fmi, needWidth, measureLimit, null);
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001117 }
1118
Doug Felte8e45f22010-03-29 14:58:40 -07001119 // Shaping needs to take into account context up to metric boundaries,
1120 // but rendering needs to take into account character style boundaries.
Doug Felt0c702b82010-05-14 10:55:42 -07001121 // So we iterate through metric runs to get metric bounds,
1122 // then within each metric run iterate through character style runs
1123 // for the run bounds.
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001124 final float originalX = x;
Doug Felt0c702b82010-05-14 10:55:42 -07001125 for (int i = start, inext; i < measureLimit; i = inext) {
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001126 final TextPaint wp = mWorkPaint;
Doug Felte8e45f22010-03-29 14:58:40 -07001127 wp.set(mPaint);
1128
Gilles Debunnec1f44832011-12-08 16:03:00 -08001129 inext = mMetricAffectingSpanSpanSet.getNextTransition(mStart + i, mStart + limit) -
1130 mStart;
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001131 int mlimit = Math.min(inext, measureLimit);
Doug Felte8e45f22010-03-29 14:58:40 -07001132
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001133 ReplacementSpan replacement = null;
Doug Felte8e45f22010-03-29 14:58:40 -07001134
Gilles Debunnec1f44832011-12-08 16:03:00 -08001135 for (int j = 0; j < mMetricAffectingSpanSpanSet.numberOfSpans; j++) {
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001136 // Both intervals [spanStarts..spanEnds] and [mStart + i..mStart + mlimit] are NOT
1137 // empty by construction. This special case in getSpans() explains the >= & <= tests
Mihai Popace642dc2018-05-24 14:25:11 +01001138 if ((mMetricAffectingSpanSpanSet.spanStarts[j] >= mStart + mlimit)
1139 || (mMetricAffectingSpanSpanSet.spanEnds[j] <= mStart + i)) continue;
1140
1141 boolean insideEllipsis =
1142 mStart + mEllipsisStart <= mMetricAffectingSpanSpanSet.spanStarts[j]
1143 && mMetricAffectingSpanSpanSet.spanEnds[j] <= mStart + mEllipsisEnd;
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001144 final MetricAffectingSpan span = mMetricAffectingSpanSpanSet.spans[j];
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001145 if (span instanceof ReplacementSpan) {
Mihai Popace642dc2018-05-24 14:25:11 +01001146 replacement = !insideEllipsis ? (ReplacementSpan) span : null;
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001147 } else {
1148 // We might have a replacement that uses the draw
1149 // state, otherwise measure state would suffice.
1150 span.updateDrawState(wp);
Doug Felte8e45f22010-03-29 14:58:40 -07001151 }
1152 }
1153
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001154 if (replacement != null) {
1155 x += handleReplacement(replacement, wp, i, mlimit, runIsRtl, c, x, top, y,
1156 bottom, fmi, needWidth || mlimit < measureLimit);
1157 continue;
1158 }
1159
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001160 final TextPaint activePaint = mActivePaint;
1161 activePaint.set(mPaint);
1162 int activeStart = i;
1163 int activeEnd = mlimit;
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001164 final DecorationInfo decorationInfo = mDecorationInfo;
1165 mDecorations.clear();
Raph Levien42ef515d2012-10-22 15:01:17 -07001166 for (int j = i, jnext; j < mlimit; j = jnext) {
Siyamed Sinir702c9f92017-06-01 18:25:40 +00001167 jnext = mCharacterStyleSpanSet.getNextTransition(mStart + j, mStart + inext) -
1168 mStart;
Doug Felte8e45f22010-03-29 14:58:40 -07001169
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001170 final int offset = Math.min(jnext, mlimit);
Raph Levien42ef515d2012-10-22 15:01:17 -07001171 wp.set(mPaint);
1172 for (int k = 0; k < mCharacterStyleSpanSet.numberOfSpans; k++) {
1173 // Intentionally using >= and <= as explained above
Siyamed Sinir702c9f92017-06-01 18:25:40 +00001174 if ((mCharacterStyleSpanSet.spanStarts[k] >= mStart + offset) ||
1175 (mCharacterStyleSpanSet.spanEnds[k] <= mStart + j)) continue;
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001176
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001177 final CharacterStyle span = mCharacterStyleSpanSet.spans[k];
Siyamed Sinir702c9f92017-06-01 18:25:40 +00001178 span.updateDrawState(wp);
Doug Felte8e45f22010-03-29 14:58:40 -07001179 }
Raph Levien42ef515d2012-10-22 15:01:17 -07001180
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001181 extractDecorationInfo(wp, decorationInfo);
Roozbeh Pournader46c6f4c2017-02-21 12:18:31 -08001182
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001183 if (j == i) {
1184 // First chunk of text. We can't handle it yet, since we may need to merge it
1185 // with the next chunk. So we just save the TextPaint for future comparisons
1186 // and use.
1187 activePaint.set(wp);
Seigo Nonaka32b87e02018-09-18 13:26:24 -07001188 } else if (!equalAttributes(wp, activePaint)) {
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001189 // The style of the present chunk of text is substantially different from the
1190 // style of the previous chunk. We need to handle the active piece of text
1191 // and restart with the present chunk.
Seigo Nonakafb1b4792019-03-08 14:05:08 -08001192 activePaint.setStartHyphenEdit(
1193 adjustStartHyphenEdit(activeStart, mPaint.getStartHyphenEdit()));
1194 activePaint.setEndHyphenEdit(
1195 adjustEndHyphenEdit(activeEnd, mPaint.getEndHyphenEdit()));
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001196 x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, x,
1197 top, y, bottom, fmi, needWidth || activeEnd < measureLimit,
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001198 Math.min(activeEnd, mlimit), mDecorations);
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001199
1200 activeStart = j;
1201 activePaint.set(wp);
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001202 mDecorations.clear();
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001203 } else {
1204 // The present TextPaint is substantially equal to the last TextPaint except
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001205 // perhaps for decorations. We just need to expand the active piece of text to
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001206 // include the present chunk, which we always do anyway. We don't need to save
1207 // wp to activePaint, since they are already equal.
1208 }
1209
1210 activeEnd = jnext;
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001211 if (decorationInfo.hasDecoration()) {
1212 final DecorationInfo copy = decorationInfo.copyInfo();
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001213 copy.start = j;
1214 copy.end = jnext;
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001215 mDecorations.add(copy);
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001216 }
Doug Felte8e45f22010-03-29 14:58:40 -07001217 }
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001218 // Handle the final piece of text.
Seigo Nonakafb1b4792019-03-08 14:05:08 -08001219 activePaint.setStartHyphenEdit(
1220 adjustStartHyphenEdit(activeStart, mPaint.getStartHyphenEdit()));
1221 activePaint.setEndHyphenEdit(
1222 adjustEndHyphenEdit(activeEnd, mPaint.getEndHyphenEdit()));
Roozbeh Pournader538de5b2017-05-24 09:07:52 -07001223 x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, x,
1224 top, y, bottom, fmi, needWidth || activeEnd < measureLimit,
Roozbeh Pournader1378a9d2017-07-13 12:45:20 -07001225 Math.min(activeEnd, mlimit), mDecorations);
Doug Felte8e45f22010-03-29 14:58:40 -07001226 }
1227
Gilles Debunne945ee9b2011-09-19 19:18:18 -07001228 return x - originalX;
Doug Felte8e45f22010-03-29 14:58:40 -07001229 }
1230
Doug Felte8e45f22010-03-29 14:58:40 -07001231 /**
1232 * Render a text run with the set-up paint.
1233 *
1234 * @param c the canvas
1235 * @param wp the paint used to render the text
Doug Felt0c702b82010-05-14 10:55:42 -07001236 * @param start the start of the run
1237 * @param end the end of the run
1238 * @param contextStart the start of context for the run
1239 * @param contextEnd the end of the context for the run
Fabrice Di Meglioda12f382013-03-15 11:26:56 -07001240 * @param runIsRtl true if the run is right-to-left
Doug Felte8e45f22010-03-29 14:58:40 -07001241 * @param x the x position of the left edge of the run
1242 * @param y the baseline of the run
1243 */
Doug Felt0c702b82010-05-14 10:55:42 -07001244 private void drawTextRun(Canvas c, TextPaint wp, int start, int end,
Fabrice Di Meglioda12f382013-03-15 11:26:56 -07001245 int contextStart, int contextEnd, boolean runIsRtl, float x, int y) {
Doug Felte8e45f22010-03-29 14:58:40 -07001246
Doug Felte8e45f22010-03-29 14:58:40 -07001247 if (mCharsValid) {
Doug Felt0c702b82010-05-14 10:55:42 -07001248 int count = end - start;
1249 int contextCount = contextEnd - contextStart;
1250 c.drawTextRun(mChars, start, count, contextStart, contextCount,
Raph Levien051910b2014-06-15 18:25:29 -07001251 x, y, runIsRtl, wp);
Doug Felte8e45f22010-03-29 14:58:40 -07001252 } else {
Doug Felt0c702b82010-05-14 10:55:42 -07001253 int delta = mStart;
1254 c.drawTextRun(mText, delta + start, delta + end,
Raph Levien051910b2014-06-15 18:25:29 -07001255 delta + contextStart, delta + contextEnd, x, y, runIsRtl, wp);
Doug Felte8e45f22010-03-29 14:58:40 -07001256 }
1257 }
1258
1259 /**
Doug Felte8e45f22010-03-29 14:58:40 -07001260 * Returns the next tab position.
1261 *
1262 * @param h the (unsigned) offset from the leading margin
1263 * @return the (unsigned) tab position after this offset
1264 */
1265 float nextTab(float h) {
Doug Feltc982f602010-05-25 11:51:40 -07001266 if (mTabs != null) {
1267 return mTabs.nextTab(h);
Doug Felte8e45f22010-03-29 14:58:40 -07001268 }
Doug Feltc982f602010-05-25 11:51:40 -07001269 return TabStops.nextDefaultStop(h, TAB_INCREMENT);
Doug Felte8e45f22010-03-29 14:58:40 -07001270 }
1271
Seigo Nonaka09da71a2016-11-28 16:24:14 +09001272 private boolean isStretchableWhitespace(int ch) {
Roozbeh Pournader3630d082017-10-23 12:16:32 -07001273 // TODO: Support NBSP and other stretchable whitespace (b/34013491 and b/68204709).
1274 return ch == 0x0020;
Seigo Nonaka09da71a2016-11-28 16:24:14 +09001275 }
1276
1277 /* Return the number of spaces in the text line, for the purpose of justification */
1278 private int countStretchableSpaces(int start, int end) {
1279 int count = 0;
Roozbeh Pournader3630d082017-10-23 12:16:32 -07001280 for (int i = start; i < end; i++) {
1281 final char c = mCharsValid ? mChars[i] : mText.charAt(i + mStart);
1282 if (isStretchableWhitespace(c)) {
1283 count++;
1284 }
Seigo Nonaka09da71a2016-11-28 16:24:14 +09001285 }
1286 return count;
1287 }
1288
1289 // Note: keep this in sync with Minikin LineBreaker::isLineEndSpace()
1290 public static boolean isLineEndSpace(char ch) {
1291 return ch == ' ' || ch == '\t' || ch == 0x1680
1292 || (0x2000 <= ch && ch <= 0x200A && ch != 0x2007)
1293 || ch == 0x205F || ch == 0x3000;
1294 }
1295
Doug Felte8e45f22010-03-29 14:58:40 -07001296 private static final int TAB_INCREMENT = 20;
Seigo Nonaka32b87e02018-09-18 13:26:24 -07001297
1298 private static boolean equalAttributes(@NonNull TextPaint lp, @NonNull TextPaint rp) {
1299 return lp.getColorFilter() == rp.getColorFilter()
1300 && lp.getMaskFilter() == rp.getMaskFilter()
1301 && lp.getShader() == rp.getShader()
1302 && lp.getTypeface() == rp.getTypeface()
1303 && lp.getXfermode() == rp.getXfermode()
1304 && lp.getTextLocales().equals(rp.getTextLocales())
1305 && TextUtils.equals(lp.getFontFeatureSettings(), rp.getFontFeatureSettings())
1306 && TextUtils.equals(lp.getFontVariationSettings(), rp.getFontVariationSettings())
1307 && lp.getShadowLayerRadius() == rp.getShadowLayerRadius()
1308 && lp.getShadowLayerDx() == rp.getShadowLayerDx()
1309 && lp.getShadowLayerDy() == rp.getShadowLayerDy()
1310 && lp.getShadowLayerColor() == rp.getShadowLayerColor()
1311 && lp.getFlags() == rp.getFlags()
1312 && lp.getHinting() == rp.getHinting()
1313 && lp.getStyle() == rp.getStyle()
1314 && lp.getColor() == rp.getColor()
1315 && lp.getStrokeWidth() == rp.getStrokeWidth()
1316 && lp.getStrokeMiter() == rp.getStrokeMiter()
1317 && lp.getStrokeCap() == rp.getStrokeCap()
1318 && lp.getStrokeJoin() == rp.getStrokeJoin()
1319 && lp.getTextAlign() == rp.getTextAlign()
1320 && lp.isElegantTextHeight() == rp.isElegantTextHeight()
1321 && lp.getTextSize() == rp.getTextSize()
1322 && lp.getTextScaleX() == rp.getTextScaleX()
1323 && lp.getTextSkewX() == rp.getTextSkewX()
1324 && lp.getLetterSpacing() == rp.getLetterSpacing()
1325 && lp.getWordSpacing() == rp.getWordSpacing()
Seigo Nonakafb1b4792019-03-08 14:05:08 -08001326 && lp.getStartHyphenEdit() == rp.getStartHyphenEdit()
1327 && lp.getEndHyphenEdit() == rp.getEndHyphenEdit()
Seigo Nonaka32b87e02018-09-18 13:26:24 -07001328 && lp.bgColor == rp.bgColor
1329 && lp.baselineShift == rp.baselineShift
1330 && lp.linkColor == rp.linkColor
1331 && lp.drawableState == rp.drawableState
1332 && lp.density == rp.density
1333 && lp.underlineColor == rp.underlineColor
1334 && lp.underlineThickness == rp.underlineThickness;
1335 }
Doug Felte8e45f22010-03-29 14:58:40 -07001336}