blob: 8dddd63d45748da44ffe84e63775d9ebe281587e [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
2 * Copyright (C) 2006 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.text;
18
Roozbeh Pournader22a167c2017-08-21 12:53:44 -070019import android.annotation.FloatRange;
20import android.annotation.IntRange;
21import android.annotation.NonNull;
Raph Levien531c30c2015-04-30 16:29:59 -070022import android.annotation.Nullable;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080023import android.graphics.Paint;
Roozbeh Pournaderef7cfa12017-06-15 12:39:04 -070024import android.os.LocaleList;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080025import android.text.style.LeadingMarginSpan;
Gilles Debunne66111472010-11-19 11:04:37 -080026import android.text.style.LeadingMarginSpan.LeadingMarginSpan2;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080027import android.text.style.LineHeightSpan;
28import android.text.style.MetricAffectingSpan;
Doug Feltc982f602010-05-25 11:51:40 -070029import android.text.style.TabStopSpan;
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -070030import android.util.Log;
Raph Levien39b4db72015-03-25 13:18:20 -070031import android.util.Pools.SynchronizedPool;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080032
Doug Feltcb3791202011-07-07 11:57:48 -070033import com.android.internal.util.ArrayUtils;
Adam Lesinski776abc22014-03-07 11:30:59 -050034import com.android.internal.util.GrowingArrayUtils;
Doug Feltcb3791202011-07-07 11:57:48 -070035
Raph Levien091dba22015-08-31 16:21:20 -070036import java.nio.ByteBuffer;
Anish Athalyec8f9e622014-07-21 15:26:34 -070037import java.util.Arrays;
Raph Levien4c1f12e2015-03-02 16:29:23 -080038import java.util.Locale;
Anish Athalyec8f9e622014-07-21 15:26:34 -070039
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080040/**
41 * StaticLayout is a Layout for text that will not be edited after it
42 * is laid out. Use {@link DynamicLayout} for text that may change.
43 * <p>This is used by widgets to control text layout. You should not need
44 * to use this class directly unless you are implementing your own widget
45 * or custom display object, or would be tempted to call
Doug Felt4e0c5e52010-03-15 16:56:02 -070046 * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int,
47 * float, float, android.graphics.Paint)
48 * Canvas.drawText()} directly.</p>
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080049 */
Fabrice Di Meglio121c82c2011-02-15 15:44:49 -080050public class StaticLayout extends Layout {
51
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -070052 static final String TAG = "StaticLayout";
53
Raph Leviend3ab6922015-03-02 14:30:53 -080054 /**
Roozbeh Pournader22a167c2017-08-21 12:53:44 -070055 * Builder for static layouts. The builder is the preferred pattern for constructing
56 * StaticLayout objects and should be preferred over the constructors, particularly to access
57 * newer features. To build a static layout, first call {@link #obtain} with the required
58 * arguments (text, paint, and width), then call setters for optional parameters, and finally
59 * {@link #build} to build the StaticLayout object. Parameters not explicitly set will get
Raph Levien531c30c2015-04-30 16:29:59 -070060 * default values.
Raph Leviend3ab6922015-03-02 14:30:53 -080061 */
62 public final static class Builder {
Raph Levien4c1f12e2015-03-02 16:29:23 -080063 private Builder() {
64 mNativePtr = nNewBuilder();
65 }
66
Raph Levien531c30c2015-04-30 16:29:59 -070067 /**
Roozbeh Pournader22a167c2017-08-21 12:53:44 -070068 * Obtain a builder for constructing StaticLayout objects.
Raph Levien531c30c2015-04-30 16:29:59 -070069 *
70 * @param source The text to be laid out, optionally with spans
71 * @param start The index of the start of the text
72 * @param end The index + 1 of the end of the text
73 * @param paint The base paint used for layout
74 * @param width The width in pixels
75 * @return a builder object used for constructing the StaticLayout
76 */
Roozbeh Pournader22a167c2017-08-21 12:53:44 -070077 @NonNull
78 public static Builder obtain(@NonNull CharSequence source, @IntRange(from = 0) int start,
79 @IntRange(from = 0) int end, @NonNull TextPaint paint,
80 @IntRange(from = 0) int width) {
Raph Levien39b4db72015-03-25 13:18:20 -070081 Builder b = sPool.acquire();
Raph Leviend3ab6922015-03-02 14:30:53 -080082 if (b == null) {
83 b = new Builder();
84 }
85
86 // set default initial values
Raph Levien39b4db72015-03-25 13:18:20 -070087 b.mText = source;
88 b.mStart = start;
89 b.mEnd = end;
Raph Levienebd66ca2015-04-30 15:27:57 -070090 b.mPaint = paint;
Raph Levien39b4db72015-03-25 13:18:20 -070091 b.mWidth = width;
92 b.mAlignment = Alignment.ALIGN_NORMAL;
Raph Leviend3ab6922015-03-02 14:30:53 -080093 b.mTextDir = TextDirectionHeuristics.FIRSTSTRONG_LTR;
Roozbeh Pournader22a167c2017-08-21 12:53:44 -070094 b.mSpacingMult = DEFAULT_LINESPACING_MULTIPLIER;
95 b.mSpacingAdd = DEFAULT_LINESPACING_ADDITION;
Raph Leviend3ab6922015-03-02 14:30:53 -080096 b.mIncludePad = true;
Roozbeh Pournader737dfea2017-08-10 11:32:24 -070097 b.mFallbackLineSpacing = false;
Raph Levien39b4db72015-03-25 13:18:20 -070098 b.mEllipsizedWidth = width;
Raph Leviend3ab6922015-03-02 14:30:53 -080099 b.mEllipsize = null;
100 b.mMaxLines = Integer.MAX_VALUE;
Raph Levien3bd60c72015-05-06 14:26:35 -0700101 b.mBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE;
Roozbeh Pournader95c7a132015-05-12 12:01:06 -0700102 b.mHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE;
Seigo Nonaka4b4730d2017-03-31 09:42:16 -0700103 b.mJustificationMode = Layout.JUSTIFICATION_MODE_NONE;
Raph Leviend3ab6922015-03-02 14:30:53 -0800104
105 b.mMeasuredText = MeasuredText.obtain();
106 return b;
107 }
108
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700109 /**
110 * This method should be called after the layout is finished getting constructed and the
111 * builder needs to be cleaned up and returned to the pool.
112 */
113 private static void recycle(@NonNull Builder b) {
Raph Leviend3ab6922015-03-02 14:30:53 -0800114 b.mPaint = null;
115 b.mText = null;
116 MeasuredText.recycle(b.mMeasuredText);
Raph Levien3bd60c72015-05-06 14:26:35 -0700117 b.mMeasuredText = null;
Raph Levien2ea52902015-07-01 14:39:31 -0700118 b.mLeftIndents = null;
119 b.mRightIndents = null;
Raph Levien3bd60c72015-05-06 14:26:35 -0700120 nFinishBuilder(b.mNativePtr);
Raph Levien39b4db72015-03-25 13:18:20 -0700121 sPool.release(b);
Raph Leviend3ab6922015-03-02 14:30:53 -0800122 }
123
124 // release any expensive state
125 /* package */ void finish() {
Raph Levien4c1f12e2015-03-02 16:29:23 -0800126 nFinishBuilder(mNativePtr);
Raph Levien22ba7862015-07-29 12:34:13 -0700127 mText = null;
128 mPaint = null;
129 mLeftIndents = null;
130 mRightIndents = null;
Raph Leviend3ab6922015-03-02 14:30:53 -0800131 mMeasuredText.finish();
132 }
133
134 public Builder setText(CharSequence source) {
135 return setText(source, 0, source.length());
136 }
137
Raph Levien531c30c2015-04-30 16:29:59 -0700138 /**
139 * Set the text. Only useful when re-using the builder, which is done for
140 * the internal implementation of {@link DynamicLayout} but not as part
141 * of normal {@link StaticLayout} usage.
142 *
143 * @param source The text to be laid out, optionally with spans
144 * @param start The index of the start of the text
145 * @param end The index + 1 of the end of the text
146 * @return this builder, useful for chaining
147 *
148 * @hide
149 */
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700150 @NonNull
151 public Builder setText(@NonNull CharSequence source, int start, int end) {
Raph Leviend3ab6922015-03-02 14:30:53 -0800152 mText = source;
153 mStart = start;
154 mEnd = end;
155 return this;
156 }
157
Raph Levien531c30c2015-04-30 16:29:59 -0700158 /**
159 * Set the paint. Internal for reuse cases only.
160 *
161 * @param paint The base paint used for layout
162 * @return this builder, useful for chaining
163 *
164 * @hide
165 */
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700166 @NonNull
167 public Builder setPaint(@NonNull TextPaint paint) {
Raph Leviend3ab6922015-03-02 14:30:53 -0800168 mPaint = paint;
169 return this;
170 }
171
Raph Levien531c30c2015-04-30 16:29:59 -0700172 /**
173 * Set the width. Internal for reuse cases only.
174 *
175 * @param width The width in pixels
176 * @return this builder, useful for chaining
177 *
178 * @hide
179 */
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700180 @NonNull
181 public Builder setWidth(@IntRange(from = 0) int width) {
Raph Leviend3ab6922015-03-02 14:30:53 -0800182 mWidth = width;
183 if (mEllipsize == null) {
184 mEllipsizedWidth = width;
185 }
186 return this;
187 }
188
Raph Levien531c30c2015-04-30 16:29:59 -0700189 /**
190 * Set the alignment. The default is {@link Layout.Alignment#ALIGN_NORMAL}.
191 *
192 * @param alignment Alignment for the resulting {@link StaticLayout}
193 * @return this builder, useful for chaining
194 */
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700195 @NonNull
196 public Builder setAlignment(@NonNull Alignment alignment) {
Raph Levien39b4db72015-03-25 13:18:20 -0700197 mAlignment = alignment;
198 return this;
199 }
200
Raph Levien531c30c2015-04-30 16:29:59 -0700201 /**
202 * Set the text direction heuristic. The text direction heuristic is used to
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700203 * resolve text direction per-paragraph based on the input text. The default is
Raph Levien531c30c2015-04-30 16:29:59 -0700204 * {@link TextDirectionHeuristics#FIRSTSTRONG_LTR}.
205 *
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700206 * @param textDir text direction heuristic for resolving bidi behavior.
Raph Levien531c30c2015-04-30 16:29:59 -0700207 * @return this builder, useful for chaining
208 */
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700209 @NonNull
210 public Builder setTextDirection(@NonNull TextDirectionHeuristic textDir) {
Raph Leviend3ab6922015-03-02 14:30:53 -0800211 mTextDir = textDir;
212 return this;
213 }
214
Raph Levien531c30c2015-04-30 16:29:59 -0700215 /**
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700216 * Set line spacing parameters. Each line will have its line spacing multiplied by
217 * {@code spacingMult} and then increased by {@code spacingAdd}. The default is 0.0 for
218 * {@code spacingAdd} and 1.0 for {@code spacingMult}.
Raph Levien531c30c2015-04-30 16:29:59 -0700219 *
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700220 * @param spacingAdd the amount of line spacing addition
221 * @param spacingMult the line spacing multiplier
Raph Levien531c30c2015-04-30 16:29:59 -0700222 * @return this builder, useful for chaining
223 * @see android.widget.TextView#setLineSpacing
224 */
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700225 @NonNull
226 public Builder setLineSpacing(float spacingAdd, @FloatRange(from = 0.0) float spacingMult) {
Raph Levien531c30c2015-04-30 16:29:59 -0700227 mSpacingAdd = spacingAdd;
Raph Leviend3ab6922015-03-02 14:30:53 -0800228 mSpacingMult = spacingMult;
229 return this;
230 }
231
Raph Levien531c30c2015-04-30 16:29:59 -0700232 /**
233 * Set whether to include extra space beyond font ascent and descent (which is
234 * needed to avoid clipping in some languages, such as Arabic and Kannada). The
235 * default is {@code true}.
236 *
237 * @param includePad whether to include padding
238 * @return this builder, useful for chaining
239 * @see android.widget.TextView#setIncludeFontPadding
240 */
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700241 @NonNull
Raph Leviend3ab6922015-03-02 14:30:53 -0800242 public Builder setIncludePad(boolean includePad) {
243 mIncludePad = includePad;
244 return this;
245 }
246
Raph Levien531c30c2015-04-30 16:29:59 -0700247 /**
Roozbeh Pournader737dfea2017-08-10 11:32:24 -0700248 * Set whether to respect the ascent and descent of the fallback fonts that are used in
249 * displaying the text (which is needed to avoid text from consecutive lines running into
250 * each other). If set, fallback fonts that end up getting used can increase the ascent
251 * and descent of the lines that they are used on.
252 *
253 * <p>For backward compatibility reasons, the default is {@code false}, but setting this to
254 * true is strongly recommended. It is required to be true if text could be in languages
255 * like Burmese or Tibetan where text is typically much taller or deeper than Latin text.
256 *
257 * @param useLineSpacingFromFallbacks whether to expand linespacing based on fallback fonts
258 * @return this builder, useful for chaining
259 */
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700260 @NonNull
Roozbeh Pournader737dfea2017-08-10 11:32:24 -0700261 public Builder setUseLineSpacingFromFallbacks(boolean useLineSpacingFromFallbacks) {
262 mFallbackLineSpacing = useLineSpacingFromFallbacks;
263 return this;
264 }
265
266 /**
Raph Levien531c30c2015-04-30 16:29:59 -0700267 * Set the width as used for ellipsizing purposes, if it differs from the
268 * normal layout width. The default is the {@code width}
269 * passed to {@link #obtain}.
270 *
271 * @param ellipsizedWidth width used for ellipsizing, in pixels
272 * @return this builder, useful for chaining
273 * @see android.widget.TextView#setEllipsize
274 */
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700275 @NonNull
276 public Builder setEllipsizedWidth(@IntRange(from = 0) int ellipsizedWidth) {
Raph Leviend3ab6922015-03-02 14:30:53 -0800277 mEllipsizedWidth = ellipsizedWidth;
278 return this;
279 }
280
Raph Levien531c30c2015-04-30 16:29:59 -0700281 /**
282 * Set ellipsizing on the layout. Causes words that are longer than the view
283 * is wide, or exceeding the number of lines (see #setMaxLines) in the case
284 * of {@link android.text.TextUtils.TruncateAt#END} or
285 * {@link android.text.TextUtils.TruncateAt#MARQUEE}, to be ellipsized instead
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700286 * of broken. The default is {@code null}, indicating no ellipsis is to be applied.
Raph Levien531c30c2015-04-30 16:29:59 -0700287 *
288 * @param ellipsize type of ellipsis behavior
289 * @return this builder, useful for chaining
290 * @see android.widget.TextView#setEllipsize
291 */
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700292 @NonNull
Raph Levien531c30c2015-04-30 16:29:59 -0700293 public Builder setEllipsize(@Nullable TextUtils.TruncateAt ellipsize) {
Raph Leviend3ab6922015-03-02 14:30:53 -0800294 mEllipsize = ellipsize;
295 return this;
296 }
297
Raph Levien531c30c2015-04-30 16:29:59 -0700298 /**
299 * Set maximum number of lines. This is particularly useful in the case of
300 * ellipsizing, where it changes the layout of the last line. The default is
301 * unlimited.
302 *
303 * @param maxLines maximum number of lines in the layout
304 * @return this builder, useful for chaining
305 * @see android.widget.TextView#setMaxLines
306 */
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700307 @NonNull
308 public Builder setMaxLines(@IntRange(from = 0) int maxLines) {
Raph Leviend3ab6922015-03-02 14:30:53 -0800309 mMaxLines = maxLines;
310 return this;
311 }
312
Raph Levien531c30c2015-04-30 16:29:59 -0700313 /**
314 * Set break strategy, useful for selecting high quality or balanced paragraph
315 * layout options. The default is {@link Layout#BREAK_STRATEGY_SIMPLE}.
316 *
317 * @param breakStrategy break strategy for paragraph layout
318 * @return this builder, useful for chaining
319 * @see android.widget.TextView#setBreakStrategy
320 */
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700321 @NonNull
Raph Levien39b4db72015-03-25 13:18:20 -0700322 public Builder setBreakStrategy(@BreakStrategy int breakStrategy) {
323 mBreakStrategy = breakStrategy;
324 return this;
325 }
326
Raph Levien531c30c2015-04-30 16:29:59 -0700327 /**
Roozbeh Pournader95c7a132015-05-12 12:01:06 -0700328 * Set hyphenation frequency, to control the amount of automatic hyphenation used. The
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700329 * possible values are defined in {@link Layout}, by constants named with the pattern
330 * {@code HYPHENATION_FREQUENCY_*}. The default is
331 * {@link Layout#HYPHENATION_FREQUENCY_NONE}.
Roozbeh Pournader95c7a132015-05-12 12:01:06 -0700332 *
333 * @param hyphenationFrequency hyphenation frequency for the paragraph
334 * @return this builder, useful for chaining
335 * @see android.widget.TextView#setHyphenationFrequency
336 */
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700337 @NonNull
Roozbeh Pournader95c7a132015-05-12 12:01:06 -0700338 public Builder setHyphenationFrequency(@HyphenationFrequency int hyphenationFrequency) {
339 mHyphenationFrequency = hyphenationFrequency;
340 return this;
341 }
342
343 /**
Raph Levien531c30c2015-04-30 16:29:59 -0700344 * Set indents. Arguments are arrays holding an indent amount, one per line, measured in
345 * pixels. For lines past the last element in the array, the last element repeats.
346 *
347 * @param leftIndents array of indent values for left margin, in pixels
348 * @param rightIndents array of indent values for right margin, in pixels
349 * @return this builder, useful for chaining
Raph Levien531c30c2015-04-30 16:29:59 -0700350 */
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700351 @NonNull
352 public Builder setIndents(@Nullable int[] leftIndents, @Nullable int[] rightIndents) {
Raph Levien2ea52902015-07-01 14:39:31 -0700353 mLeftIndents = leftIndents;
354 mRightIndents = rightIndents;
Raph Leviene319d5a2015-04-14 23:51:07 -0700355 int leftLen = leftIndents == null ? 0 : leftIndents.length;
356 int rightLen = rightIndents == null ? 0 : rightIndents.length;
357 int[] indents = new int[Math.max(leftLen, rightLen)];
358 for (int i = 0; i < indents.length; i++) {
359 int leftMargin = i < leftLen ? leftIndents[i] : 0;
360 int rightMargin = i < rightLen ? rightIndents[i] : 0;
361 indents[i] = leftMargin + rightMargin;
362 }
363 nSetIndents(mNativePtr, indents);
364 return this;
365 }
366
Raph Levien70616ec2015-03-04 10:41:30 -0800367 /**
Seigo Nonaka4b4730d2017-03-31 09:42:16 -0700368 * Set paragraph justification mode. The default value is
369 * {@link Layout#JUSTIFICATION_MODE_NONE}. If the last line is too short for justification,
370 * the last line will be displayed with the alignment set by {@link #setAlignment}.
Seigo Nonaka09da71a2016-11-28 16:24:14 +0900371 *
Seigo Nonaka4b4730d2017-03-31 09:42:16 -0700372 * @param justificationMode justification mode for the paragraph.
Seigo Nonaka09da71a2016-11-28 16:24:14 +0900373 * @return this builder, useful for chaining.
374 */
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700375 @NonNull
Seigo Nonaka4b4730d2017-03-31 09:42:16 -0700376 public Builder setJustificationMode(@JustificationMode int justificationMode) {
377 mJustificationMode = justificationMode;
Seigo Nonaka09da71a2016-11-28 16:24:14 +0900378 return this;
379 }
380
Siyamed Sinir442c1512017-07-24 12:18:27 -0700381 /**
382 * Sets whether the line spacing should be applied for the last line. Default value is
383 * {@code false}.
384 *
385 * @hide
386 */
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700387 @NonNull
Siyamed Sinir442c1512017-07-24 12:18:27 -0700388 /* package */ Builder setAddLastLineLineSpacing(boolean value) {
389 mAddLastLineLineSpacing = value;
390 return this;
391 }
392
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700393 @NonNull
394 private long[] getHyphenators(@NonNull LocaleList locales) {
Roozbeh Pournaderef7cfa12017-06-15 12:39:04 -0700395 final int length = locales.size();
396 final long[] result = new long[length];
397 for (int i = 0; i < length; i++) {
398 final Locale locale = locales.get(i);
399 result[i] = Hyphenator.get(locale).getNativePtr();
400 }
401 return result;
402 }
403
Seigo Nonaka09da71a2016-11-28 16:24:14 +0900404 /**
Raph Levien70616ec2015-03-04 10:41:30 -0800405 * Measurement and break iteration is done in native code. The protocol for using
406 * the native code is as follows.
407 *
Raph Levien26d443a2015-03-30 14:18:32 -0700408 * For each paragraph, do a nSetupParagraph, which sets paragraph text, line width, tab
Roozbeh Pournader95c7a132015-05-12 12:01:06 -0700409 * stops, break strategy, and hyphenation frequency (and possibly other parameters in the
410 * future).
Raph Levienc94f7422015-03-06 19:19:48 -0800411 *
412 * Then, for each run within the paragraph:
Roozbeh Pournaderef7cfa12017-06-15 12:39:04 -0700413 * - setLocales (this must be done at least for the first run, optional afterwards)
Raph Levien70616ec2015-03-04 10:41:30 -0800414 * - one of the following, depending on the type of run:
415 * + addStyleRun (a text run, to be measured in native code)
416 * + addMeasuredRun (a run already measured in Java, passed into native code)
417 * + addReplacementRun (a replacement run, width is given)
418 *
419 * After measurement, nGetWidths() is valid if the widths are needed (eg for ellipsis).
420 * Run nComputeLineBreaks() to obtain line breaks for the paragraph.
421 *
422 * After all paragraphs, call finish() to release expensive buffers.
423 */
424
Roozbeh Pournaderef7cfa12017-06-15 12:39:04 -0700425 private void setLocales(LocaleList locales) {
426 if (!locales.equals(mLocales)) {
427 nSetLocales(mNativePtr, locales.toLanguageTags(), getHyphenators(locales));
428 mLocales = locales;
Raph Levien4c1f12e2015-03-02 16:29:23 -0800429 }
430 }
431
Raph Levien70616ec2015-03-04 10:41:30 -0800432 /* package */ float addStyleRun(TextPaint paint, int start, int end, boolean isRtl) {
Roozbeh Pournaderef7cfa12017-06-15 12:39:04 -0700433 setLocales(paint.getTextLocales());
Seigo Nonaka318ca042017-08-01 16:36:18 -0700434 return nAddStyleRun(mNativePtr, paint.getNativeInstance(), start, end, isRtl);
Raph Levien70616ec2015-03-04 10:41:30 -0800435 }
436
437 /* package */ void addMeasuredRun(int start, int end, float[] widths) {
438 nAddMeasuredRun(mNativePtr, start, end, widths);
439 }
440
441 /* package */ void addReplacementRun(int start, int end, float width) {
442 nAddReplacementRun(mNativePtr, start, end, width);
443 }
444
Raph Levien531c30c2015-04-30 16:29:59 -0700445 /**
446 * Build the {@link StaticLayout} after options have been set.
447 *
448 * <p>Note: the builder object must not be reused in any way after calling this
449 * method. Setting parameters after calling this method, or calling it a second
450 * time on the same builder object, will likely lead to unexpected results.
451 *
452 * @return the newly constructed {@link StaticLayout} object
453 */
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700454 @NonNull
Raph Leviend3ab6922015-03-02 14:30:53 -0800455 public StaticLayout build() {
Raph Levien39b4db72015-03-25 13:18:20 -0700456 StaticLayout result = new StaticLayout(this);
457 Builder.recycle(this);
Raph Leviend3ab6922015-03-02 14:30:53 -0800458 return result;
459 }
460
Raph Levien4c1f12e2015-03-02 16:29:23 -0800461 @Override
462 protected void finalize() throws Throwable {
463 try {
464 nFreeBuilder(mNativePtr);
465 } finally {
466 super.finalize();
467 }
468 }
469
470 /* package */ long mNativePtr;
471
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700472 private CharSequence mText;
473 private int mStart;
474 private int mEnd;
475 private TextPaint mPaint;
476 private int mWidth;
477 private Alignment mAlignment;
478 private TextDirectionHeuristic mTextDir;
479 private float mSpacingMult;
480 private float mSpacingAdd;
481 private boolean mIncludePad;
482 private boolean mFallbackLineSpacing;
483 private int mEllipsizedWidth;
484 private TextUtils.TruncateAt mEllipsize;
485 private int mMaxLines;
486 private int mBreakStrategy;
487 private int mHyphenationFrequency;
488 private int[] mLeftIndents;
489 private int[] mRightIndents;
490 private int mJustificationMode;
491 private boolean mAddLastLineLineSpacing;
Raph Leviend3ab6922015-03-02 14:30:53 -0800492
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700493 private final Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt();
Raph Leviend3ab6922015-03-02 14:30:53 -0800494
495 // This will go away and be subsumed by native builder code
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700496 private MeasuredText mMeasuredText;
Raph Leviend3ab6922015-03-02 14:30:53 -0800497
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700498 private LocaleList mLocales;
Raph Levien4c1f12e2015-03-02 16:29:23 -0800499
Raph Levien39b4db72015-03-25 13:18:20 -0700500 private static final SynchronizedPool<Builder> sPool = new SynchronizedPool<Builder>(3);
Raph Leviend3ab6922015-03-02 14:30:53 -0800501 }
502
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800503 public StaticLayout(CharSequence source, TextPaint paint,
504 int width,
505 Alignment align, float spacingmult, float spacingadd,
506 boolean includepad) {
507 this(source, 0, source.length(), paint, width, align,
508 spacingmult, spacingadd, includepad);
509 }
510
Doug Feltcb3791202011-07-07 11:57:48 -0700511 /**
512 * @hide
513 */
514 public StaticLayout(CharSequence source, TextPaint paint,
515 int width, Alignment align, TextDirectionHeuristic textDir,
516 float spacingmult, float spacingadd,
517 boolean includepad) {
518 this(source, 0, source.length(), paint, width, align, textDir,
519 spacingmult, spacingadd, includepad);
520 }
521
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800522 public StaticLayout(CharSequence source, int bufstart, int bufend,
523 TextPaint paint, int outerwidth,
524 Alignment align,
525 float spacingmult, float spacingadd,
526 boolean includepad) {
527 this(source, bufstart, bufend, paint, outerwidth, align,
528 spacingmult, spacingadd, includepad, null, 0);
529 }
530
Doug Feltcb3791202011-07-07 11:57:48 -0700531 /**
532 * @hide
533 */
534 public StaticLayout(CharSequence source, int bufstart, int bufend,
535 TextPaint paint, int outerwidth,
536 Alignment align, TextDirectionHeuristic textDir,
537 float spacingmult, float spacingadd,
538 boolean includepad) {
539 this(source, bufstart, bufend, paint, outerwidth, align, textDir,
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -0700540 spacingmult, spacingadd, includepad, null, 0, Integer.MAX_VALUE);
Doug Feltcb3791202011-07-07 11:57:48 -0700541}
542
543 public StaticLayout(CharSequence source, int bufstart, int bufend,
544 TextPaint paint, int outerwidth,
545 Alignment align,
546 float spacingmult, float spacingadd,
547 boolean includepad,
548 TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
549 this(source, bufstart, bufend, paint, outerwidth, align,
550 TextDirectionHeuristics.FIRSTSTRONG_LTR,
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -0700551 spacingmult, spacingadd, includepad, ellipsize, ellipsizedWidth, Integer.MAX_VALUE);
Doug Feltcb3791202011-07-07 11:57:48 -0700552 }
553
554 /**
555 * @hide
556 */
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800557 public StaticLayout(CharSequence source, int bufstart, int bufend,
558 TextPaint paint, int outerwidth,
Doug Feltcb3791202011-07-07 11:57:48 -0700559 Alignment align, TextDirectionHeuristic textDir,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800560 float spacingmult, float spacingadd,
561 boolean includepad,
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -0700562 TextUtils.TruncateAt ellipsize, int ellipsizedWidth, int maxLines) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800563 super((ellipsize == null)
Doug Felt4e0c5e52010-03-15 16:56:02 -0700564 ? source
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800565 : (source instanceof Spanned)
566 ? new SpannedEllipsizer(source)
567 : new Ellipsizer(source),
Doug Feltcb3791202011-07-07 11:57:48 -0700568 paint, outerwidth, align, textDir, spacingmult, spacingadd);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800569
Raph Levienebd66ca2015-04-30 15:27:57 -0700570 Builder b = Builder.obtain(source, bufstart, bufend, paint, outerwidth)
Raph Levien39b4db72015-03-25 13:18:20 -0700571 .setAlignment(align)
Raph Leviena6a08282015-06-03 13:20:45 -0700572 .setTextDirection(textDir)
Raph Levien531c30c2015-04-30 16:29:59 -0700573 .setLineSpacing(spacingadd, spacingmult)
Raph Leviend3ab6922015-03-02 14:30:53 -0800574 .setIncludePad(includepad)
575 .setEllipsizedWidth(ellipsizedWidth)
576 .setEllipsize(ellipsize)
577 .setMaxLines(maxLines);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800578 /*
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700579 * This is annoying, but we can't refer to the layout until superclass construction is
580 * finished, and the superclass constructor wants the reference to the display text.
Doug Felt4e0c5e52010-03-15 16:56:02 -0700581 *
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700582 * In other words, the two Ellipsizer classes in Layout.java need a (Dynamic|Static)Layout
583 * as a parameter to do their calculations, but the Ellipsizers also need to be the input
584 * to the superclass's constructor (Layout). In order to go around the circular
585 * dependency, we construct the Ellipsizer with only one of the parameters, the text. And
586 * we fill in the rest of the needed information (layout, width, and method) later, here.
587 *
588 * This will break if the superclass constructor ever actually cares about the content
589 * instead of just holding the reference.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800590 */
591 if (ellipsize != null) {
592 Ellipsizer e = (Ellipsizer) getText();
593
594 e.mLayout = this;
595 e.mWidth = ellipsizedWidth;
596 e.mMethod = ellipsize;
597 mEllipsizedWidth = ellipsizedWidth;
598
599 mColumns = COLUMNS_ELLIPSIZE;
600 } else {
601 mColumns = COLUMNS_NORMAL;
602 mEllipsizedWidth = outerwidth;
603 }
604
Siyamed Siniraf398512017-07-25 19:08:42 -0700605 mLineDirections = ArrayUtils.newUnpaddedArray(Directions.class, 2);
606 mLines = ArrayUtils.newUnpaddedIntArray(2 * mColumns);
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -0700607 mMaximumVisibleLineCount = maxLines;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800608
Raph Levien70616ec2015-03-04 10:41:30 -0800609 generate(b, b.mIncludePad, b.mIncludePad);
Doug Felte8e45f22010-03-29 14:58:40 -0700610
Raph Leviend3ab6922015-03-02 14:30:53 -0800611 Builder.recycle(b);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800612 }
613
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -0700614 /* package */ StaticLayout(@Nullable CharSequence text) {
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -0700615 super(text, null, 0, null, 0, 0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800616
617 mColumns = COLUMNS_ELLIPSIZE;
Siyamed Siniraf398512017-07-25 19:08:42 -0700618 mLineDirections = ArrayUtils.newUnpaddedArray(Directions.class, 2);
619 mLines = ArrayUtils.newUnpaddedIntArray(2 * mColumns);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800620 }
621
Raph Levien39b4db72015-03-25 13:18:20 -0700622 private StaticLayout(Builder b) {
623 super((b.mEllipsize == null)
624 ? b.mText
625 : (b.mText instanceof Spanned)
626 ? new SpannedEllipsizer(b.mText)
627 : new Ellipsizer(b.mText),
628 b.mPaint, b.mWidth, b.mAlignment, b.mSpacingMult, b.mSpacingAdd);
629
630 if (b.mEllipsize != null) {
631 Ellipsizer e = (Ellipsizer) getText();
632
633 e.mLayout = this;
634 e.mWidth = b.mEllipsizedWidth;
635 e.mMethod = b.mEllipsize;
636 mEllipsizedWidth = b.mEllipsizedWidth;
637
638 mColumns = COLUMNS_ELLIPSIZE;
639 } else {
640 mColumns = COLUMNS_NORMAL;
641 mEllipsizedWidth = b.mWidth;
642 }
643
Siyamed Siniraf398512017-07-25 19:08:42 -0700644 mLineDirections = ArrayUtils.newUnpaddedArray(Directions.class, 2);
645 mLines = ArrayUtils.newUnpaddedIntArray(2 * mColumns);
Raph Levien39b4db72015-03-25 13:18:20 -0700646 mMaximumVisibleLineCount = b.mMaxLines;
647
Raph Levien2ea52902015-07-01 14:39:31 -0700648 mLeftIndents = b.mLeftIndents;
649 mRightIndents = b.mRightIndents;
Seigo Nonaka4b4730d2017-03-31 09:42:16 -0700650 setJustificationMode(b.mJustificationMode);
Raph Levien2ea52902015-07-01 14:39:31 -0700651
Raph Levien39b4db72015-03-25 13:18:20 -0700652 generate(b, b.mIncludePad, b.mIncludePad);
653 }
654
Raph Leviend3ab6922015-03-02 14:30:53 -0800655 /* package */ void generate(Builder b, boolean includepad, boolean trackpad) {
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -0700656 final CharSequence source = b.mText;
Raph Leviend3ab6922015-03-02 14:30:53 -0800657 int bufStart = b.mStart;
658 int bufEnd = b.mEnd;
659 TextPaint paint = b.mPaint;
660 int outerWidth = b.mWidth;
661 TextDirectionHeuristic textDir = b.mTextDir;
Roozbeh Pournader737dfea2017-08-10 11:32:24 -0700662 final boolean fallbackLineSpacing = b.mFallbackLineSpacing;
Raph Leviend3ab6922015-03-02 14:30:53 -0800663 float spacingmult = b.mSpacingMult;
664 float spacingadd = b.mSpacingAdd;
665 float ellipsizedWidth = b.mEllipsizedWidth;
666 TextUtils.TruncateAt ellipsize = b.mEllipsize;
Siyamed Sinir442c1512017-07-24 12:18:27 -0700667 final boolean addLastLineSpacing = b.mAddLastLineLineSpacing;
Raph Levien4c1f12e2015-03-02 16:29:23 -0800668 LineBreaks lineBreaks = new LineBreaks(); // TODO: move to builder to avoid allocation costs
Anish Athalyec8f9e622014-07-21 15:26:34 -0700669 // store span end locations
670 int[] spanEndCache = new int[4];
671 // store fontMetrics per span range
672 // must be a multiple of 4 (and > 0) (store top, bottom, ascent, and descent per range)
673 int[] fmCache = new int[4 * 4];
Roozbeh Pournaderef7cfa12017-06-15 12:39:04 -0700674 b.setLocales(paint.getTextLocales());
Anish Athalye88b5b0b2014-06-24 14:39:43 -0700675
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800676 mLineCount = 0;
Siyamed Sinira19cd512017-08-03 22:01:56 -0700677 mEllipsized = false;
678 mMaxLineHeight = DEFAULT_MAX_LINE_HEIGHT;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800679
680 int v = 0;
681 boolean needMultiply = (spacingmult != 1 || spacingadd != 0);
682
Raph Leviend3ab6922015-03-02 14:30:53 -0800683 Paint.FontMetricsInt fm = b.mFontMetricsInt;
Fabrice Di Meglio121c82c2011-02-15 15:44:49 -0800684 int[] chooseHtv = null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800685
Raph Leviend3ab6922015-03-02 14:30:53 -0800686 MeasuredText measured = b.mMeasuredText;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800687
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800688 Spanned spanned = null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800689 if (source instanceof Spanned)
690 spanned = (Spanned) source;
691
Doug Felte8e45f22010-03-29 14:58:40 -0700692 int paraEnd;
Fabrice Di Meglio121c82c2011-02-15 15:44:49 -0800693 for (int paraStart = bufStart; paraStart <= bufEnd; paraStart = paraEnd) {
694 paraEnd = TextUtils.indexOf(source, CHAR_NEW_LINE, paraStart, bufEnd);
Doug Felte8e45f22010-03-29 14:58:40 -0700695 if (paraEnd < 0)
Fabrice Di Meglio121c82c2011-02-15 15:44:49 -0800696 paraEnd = bufEnd;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800697 else
Doug Felte8e45f22010-03-29 14:58:40 -0700698 paraEnd++;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800699
Anish Athalyec8f9e622014-07-21 15:26:34 -0700700 int firstWidthLineCount = 1;
Fabrice Di Meglio121c82c2011-02-15 15:44:49 -0800701 int firstWidth = outerWidth;
702 int restWidth = outerWidth;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800703
Fabrice Di Meglio121c82c2011-02-15 15:44:49 -0800704 LineHeightSpan[] chooseHt = null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800705
706 if (spanned != null) {
Eric Fischer74d31ef2010-08-05 15:29:36 -0700707 LeadingMarginSpan[] sp = getParagraphSpans(spanned, paraStart, paraEnd,
Doug Felte8e45f22010-03-29 14:58:40 -0700708 LeadingMarginSpan.class);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800709 for (int i = 0; i < sp.length; i++) {
Mark Wagner7b5676e2009-10-16 11:44:23 -0700710 LeadingMarginSpan lms = sp[i];
Fabrice Di Meglio121c82c2011-02-15 15:44:49 -0800711 firstWidth -= sp[i].getLeadingMargin(true);
712 restWidth -= sp[i].getLeadingMargin(false);
Doug Feltcb3791202011-07-07 11:57:48 -0700713
Doug Feltc982f602010-05-25 11:51:40 -0700714 // LeadingMarginSpan2 is odd. The count affects all
Anish Athalyeab08c6d2014-08-08 12:09:58 -0700715 // leading margin spans, not just this particular one
Doug Feltc982f602010-05-25 11:51:40 -0700716 if (lms instanceof LeadingMarginSpan2) {
717 LeadingMarginSpan2 lms2 = (LeadingMarginSpan2) lms;
Anish Athalyec8f9e622014-07-21 15:26:34 -0700718 firstWidthLineCount = Math.max(firstWidthLineCount,
719 lms2.getLeadingMarginLineCount());
Mark Wagner7b5676e2009-10-16 11:44:23 -0700720 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800721 }
722
Fabrice Di Meglio121c82c2011-02-15 15:44:49 -0800723 chooseHt = getParagraphSpans(spanned, paraStart, paraEnd, LineHeightSpan.class);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800724
Roozbeh Pournader431e5062015-10-16 02:27:03 -0700725 if (chooseHt.length == 0) {
726 chooseHt = null; // So that out() would not assume it has any contents
727 } else {
Fabrice Di Meglio121c82c2011-02-15 15:44:49 -0800728 if (chooseHtv == null ||
729 chooseHtv.length < chooseHt.length) {
Adam Lesinski776abc22014-03-07 11:30:59 -0500730 chooseHtv = ArrayUtils.newUnpaddedIntArray(chooseHt.length);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800731 }
732
Fabrice Di Meglio121c82c2011-02-15 15:44:49 -0800733 for (int i = 0; i < chooseHt.length; i++) {
734 int o = spanned.getSpanStart(chooseHt[i]);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800735
Doug Felte8e45f22010-03-29 14:58:40 -0700736 if (o < paraStart) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800737 // starts in this layout, before the
738 // current paragraph
739
Fabrice Di Meglio121c82c2011-02-15 15:44:49 -0800740 chooseHtv[i] = getLineTop(getLineForOffset(o));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800741 } else {
742 // starts in this paragraph
743
Fabrice Di Meglio121c82c2011-02-15 15:44:49 -0800744 chooseHtv[i] = v;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800745 }
746 }
747 }
748 }
749
Raph Levien70616ec2015-03-04 10:41:30 -0800750 measured.setPara(source, paraStart, paraEnd, textDir, b);
Doug Felte8e45f22010-03-29 14:58:40 -0700751 char[] chs = measured.mChars;
752 float[] widths = measured.mWidths;
753 byte[] chdirs = measured.mLevels;
754 int dir = measured.mDir;
755 boolean easy = measured.mEasy;
Raph Levienc94f7422015-03-06 19:19:48 -0800756
757 // tab stop locations
758 int[] variableTabStops = null;
759 if (spanned != null) {
760 TabStopSpan[] spans = getParagraphSpans(spanned, paraStart,
761 paraEnd, TabStopSpan.class);
762 if (spans.length > 0) {
763 int[] stops = new int[spans.length];
764 for (int i = 0; i < spans.length; i++) {
765 stops[i] = spans[i].getTabStop();
766 }
767 Arrays.sort(stops, 0, stops.length);
768 variableTabStops = stops;
769 }
770 }
771
Raph Levienc94f7422015-03-06 19:19:48 -0800772 nSetupParagraph(b.mNativePtr, chs, paraEnd - paraStart,
773 firstWidth, firstWidthLineCount, restWidth,
Seigo Nonaka09da71a2016-11-28 16:24:14 +0900774 variableTabStops, TAB_INCREMENT, b.mBreakStrategy, b.mHyphenationFrequency,
Seigo Nonaka4b4730d2017-03-31 09:42:16 -0700775 // TODO: Support more justification mode, e.g. letter spacing, stretching.
776 b.mJustificationMode != Layout.JUSTIFICATION_MODE_NONE);
Raph Levien2ea52902015-07-01 14:39:31 -0700777 if (mLeftIndents != null || mRightIndents != null) {
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -0700778 // TODO(performance): it would be better to do this once per layout rather
Raph Levien2ea52902015-07-01 14:39:31 -0700779 // than once per paragraph, but that would require a change to the native
780 // interface.
781 int leftLen = mLeftIndents == null ? 0 : mLeftIndents.length;
782 int rightLen = mRightIndents == null ? 0 : mRightIndents.length;
Siyamed Sinirf9a08862016-04-12 19:30:44 -0700783 int indentsLen = Math.max(1, Math.max(leftLen, rightLen) - mLineCount);
Raph Levien2ea52902015-07-01 14:39:31 -0700784 int[] indents = new int[indentsLen];
785 for (int i = 0; i < indentsLen; i++) {
786 int leftMargin = mLeftIndents == null ? 0 :
787 mLeftIndents[Math.min(i + mLineCount, leftLen - 1)];
788 int rightMargin = mRightIndents == null ? 0 :
789 mRightIndents[Math.min(i + mLineCount, rightLen - 1)];
790 indents[i] = leftMargin + rightMargin;
791 }
792 nSetIndents(b.mNativePtr, indents);
793 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800794
Anish Athalyec8f9e622014-07-21 15:26:34 -0700795 // measurement has to be done before performing line breaking
796 // but we don't want to recompute fontmetrics or span ranges the
797 // second time, so we cache those and then use those stored values
798 int fmCacheCount = 0;
799 int spanEndCacheCount = 0;
Gilles Debunnecd943a72012-06-07 17:54:47 -0700800 for (int spanStart = paraStart, spanEnd; spanStart < paraEnd; spanStart = spanEnd) {
Anish Athalyec8f9e622014-07-21 15:26:34 -0700801 if (fmCacheCount * 4 >= fmCache.length) {
802 int[] grow = new int[fmCacheCount * 4 * 2];
803 System.arraycopy(fmCache, 0, grow, 0, fmCacheCount * 4);
804 fmCache = grow;
805 }
806
807 if (spanEndCacheCount >= spanEndCache.length) {
808 int[] grow = new int[spanEndCacheCount * 2];
809 System.arraycopy(spanEndCache, 0, grow, 0, spanEndCacheCount);
810 spanEndCache = grow;
811 }
Doug Felte8e45f22010-03-29 14:58:40 -0700812
Gilles Debunnecd943a72012-06-07 17:54:47 -0700813 if (spanned == null) {
814 spanEnd = paraEnd;
Doug Felt23241882010-06-02 14:41:06 -0700815 int spanLen = spanEnd - spanStart;
Gilles Debunnecd943a72012-06-07 17:54:47 -0700816 measured.addStyleRun(paint, spanLen, fm);
817 } else {
818 spanEnd = spanned.nextSpanTransition(spanStart, paraEnd,
819 MetricAffectingSpan.class);
820 int spanLen = spanEnd - spanStart;
821 MetricAffectingSpan[] spans =
Doug Felt23241882010-06-02 14:41:06 -0700822 spanned.getSpans(spanStart, spanEnd, MetricAffectingSpan.class);
Gilles Debunnecd943a72012-06-07 17:54:47 -0700823 spans = TextUtils.removeEmptySpans(spans, spanned, MetricAffectingSpan.class);
824 measured.addStyleRun(paint, spans, spanLen, fm);
Doug Felt23241882010-06-02 14:41:06 -0700825 }
826
Anish Athalyec8f9e622014-07-21 15:26:34 -0700827 // the order of storage here (top, bottom, ascent, descent) has to match the code below
828 // where these values are retrieved
829 fmCache[fmCacheCount * 4 + 0] = fm.top;
830 fmCache[fmCacheCount * 4 + 1] = fm.bottom;
831 fmCache[fmCacheCount * 4 + 2] = fm.ascent;
832 fmCache[fmCacheCount * 4 + 3] = fm.descent;
833 fmCacheCount++;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800834
Anish Athalyec8f9e622014-07-21 15:26:34 -0700835 spanEndCache[spanEndCacheCount] = spanEnd;
836 spanEndCacheCount++;
837 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800838
Raph Levien70616ec2015-03-04 10:41:30 -0800839 nGetWidths(b.mNativePtr, widths);
Raph Levienc94f7422015-03-06 19:19:48 -0800840 int breakCount = nComputeLineBreaks(b.mNativePtr, lineBreaks, lineBreaks.breaks,
Roozbeh Pournader737dfea2017-08-10 11:32:24 -0700841 lineBreaks.widths, lineBreaks.ascents, lineBreaks.descents, lineBreaks.flags,
842 lineBreaks.breaks.length);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800843
Roozbeh Pournader737dfea2017-08-10 11:32:24 -0700844 final int[] breaks = lineBreaks.breaks;
845 final float[] lineWidths = lineBreaks.widths;
846 final float[] ascents = lineBreaks.ascents;
847 final float[] descents = lineBreaks.descents;
848 final int[] flags = lineBreaks.flags;
Anish Athalyec8f9e622014-07-21 15:26:34 -0700849
Keisuke Kuroyanagif4a3f3a2015-06-10 09:37:32 +0900850 final int remainingLineCount = mMaximumVisibleLineCount - mLineCount;
851 final boolean ellipsisMayBeApplied = ellipsize != null
852 && (ellipsize == TextUtils.TruncateAt.END
853 || (mMaximumVisibleLineCount == 1
854 && ellipsize != TextUtils.TruncateAt.MARQUEE));
Roozbeh Pournader9ea756f2017-07-25 11:20:29 -0700855 if (0 < remainingLineCount && remainingLineCount < breakCount
856 && ellipsisMayBeApplied) {
Keisuke Kuroyanagif4a3f3a2015-06-10 09:37:32 +0900857 // Calculate width and flag.
858 float width = 0;
Roozbeh Pournader737dfea2017-08-10 11:32:24 -0700859 int flag = 0; // XXX May need to also have starting hyphen edit
Keisuke Kuroyanagif4a3f3a2015-06-10 09:37:32 +0900860 for (int i = remainingLineCount - 1; i < breakCount; i++) {
Keisuke Kuroyanagi78f0d832016-05-10 12:21:33 -0700861 if (i == breakCount - 1) {
862 width += lineWidths[i];
863 } else {
864 for (int j = (i == 0 ? 0 : breaks[i - 1]); j < breaks[i]; j++) {
865 width += widths[j];
866 }
867 }
Roozbeh Pournader737dfea2017-08-10 11:32:24 -0700868 flag |= flags[i] & TAB_MASK;
Keisuke Kuroyanagif4a3f3a2015-06-10 09:37:32 +0900869 }
Keisuke Kuroyanagi78f0d832016-05-10 12:21:33 -0700870 // Treat the last line and overflowed lines as a single line.
871 breaks[remainingLineCount - 1] = breaks[breakCount - 1];
Keisuke Kuroyanagif4a3f3a2015-06-10 09:37:32 +0900872 lineWidths[remainingLineCount - 1] = width;
873 flags[remainingLineCount - 1] = flag;
874
875 breakCount = remainingLineCount;
876 }
877
Anish Athalyec8f9e622014-07-21 15:26:34 -0700878 // here is the offset of the starting character of the line we are currently measuring
879 int here = paraStart;
880
881 int fmTop = 0, fmBottom = 0, fmAscent = 0, fmDescent = 0;
882 int fmCacheIndex = 0;
883 int spanEndCacheIndex = 0;
884 int breakIndex = 0;
885 for (int spanStart = paraStart, spanEnd; spanStart < paraEnd; spanStart = spanEnd) {
886 // retrieve end of span
887 spanEnd = spanEndCache[spanEndCacheIndex++];
888
889 // retrieve cached metrics, order matches above
890 fm.top = fmCache[fmCacheIndex * 4 + 0];
891 fm.bottom = fmCache[fmCacheIndex * 4 + 1];
892 fm.ascent = fmCache[fmCacheIndex * 4 + 2];
893 fm.descent = fmCache[fmCacheIndex * 4 + 3];
894 fmCacheIndex++;
895
896 if (fm.top < fmTop) {
897 fmTop = fm.top;
898 }
899 if (fm.ascent < fmAscent) {
900 fmAscent = fm.ascent;
901 }
902 if (fm.descent > fmDescent) {
903 fmDescent = fm.descent;
904 }
905 if (fm.bottom > fmBottom) {
906 fmBottom = fm.bottom;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800907 }
908
Anish Athalyec8f9e622014-07-21 15:26:34 -0700909 // skip breaks ending before current span range
910 while (breakIndex < breakCount && paraStart + breaks[breakIndex] < spanStart) {
911 breakIndex++;
912 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800913
Anish Athalyec8f9e622014-07-21 15:26:34 -0700914 while (breakIndex < breakCount && paraStart + breaks[breakIndex] <= spanEnd) {
915 int endPos = paraStart + breaks[breakIndex];
916
Raph Levience4155a2015-03-11 11:02:33 -0700917 boolean moreChars = (endPos < bufEnd);
Raph Levien4c02e832014-12-12 11:17:01 -0800918
Roozbeh Pournader737dfea2017-08-10 11:32:24 -0700919 final int ascent = fallbackLineSpacing
920 ? Math.min(fmAscent, (int) Math.round(ascents[breakIndex]))
921 : fmAscent;
922 final int descent = fallbackLineSpacing
923 ? Math.max(fmDescent, (int) Math.round(descents[breakIndex]))
924 : fmDescent;
Anish Athalyec8f9e622014-07-21 15:26:34 -0700925 v = out(source, here, endPos,
Roozbeh Pournader737dfea2017-08-10 11:32:24 -0700926 ascent, descent, fmTop, fmBottom,
Roozbeh Pournader431e5062015-10-16 02:27:03 -0700927 v, spacingmult, spacingadd, chooseHt, chooseHtv, fm, flags[breakIndex],
Anish Athalyec8f9e622014-07-21 15:26:34 -0700928 needMultiply, chdirs, dir, easy, bufEnd, includepad, trackpad,
Siyamed Sinir442c1512017-07-24 12:18:27 -0700929 addLastLineSpacing, chs, widths, paraStart, ellipsize,
930 ellipsizedWidth, lineWidths[breakIndex], paint, moreChars);
Anish Athalyec8f9e622014-07-21 15:26:34 -0700931
932 if (endPos < spanEnd) {
933 // preserve metrics for current span
934 fmTop = fm.top;
935 fmBottom = fm.bottom;
936 fmAscent = fm.ascent;
937 fmDescent = fm.descent;
938 } else {
939 fmTop = fmBottom = fmAscent = fmDescent = 0;
940 }
941
942 here = endPos;
943 breakIndex++;
944
Siyamed Sinir0745c722016-05-31 20:39:33 -0700945 if (mLineCount >= mMaximumVisibleLineCount && mEllipsized) {
Anish Athalyec8f9e622014-07-21 15:26:34 -0700946 return;
947 }
948 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800949 }
950
Fabrice Di Meglio121c82c2011-02-15 15:44:49 -0800951 if (paraEnd == bufEnd)
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800952 break;
953 }
954
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -0700955 if ((bufEnd == bufStart || source.charAt(bufEnd - 1) == CHAR_NEW_LINE) &&
Fabrice Di Meglioad0b0512011-10-04 17:21:26 -0700956 mLineCount < mMaximumVisibleLineCount) {
Raph Levien70616ec2015-03-04 10:41:30 -0800957 measured.setPara(source, bufEnd, bufEnd, textDir, b);
Fabrice Di Meglioe6318892013-06-18 20:03:41 -0700958
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800959 paint.getFontMetricsInt(fm);
960
961 v = out(source,
Fabrice Di Meglio121c82c2011-02-15 15:44:49 -0800962 bufEnd, bufEnd, fm.ascent, fm.descent,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800963 fm.top, fm.bottom,
964 v,
965 spacingmult, spacingadd, null,
Raph Levien26d443a2015-03-30 14:18:32 -0700966 null, fm, 0,
Fabrice Di Meglioe6318892013-06-18 20:03:41 -0700967 needMultiply, measured.mLevels, measured.mDir, measured.mEasy, bufEnd,
Siyamed Sinir442c1512017-07-24 12:18:27 -0700968 includepad, trackpad, addLastLineSpacing, null,
Gilles Debunned300e752011-10-17 13:37:36 -0700969 null, bufStart, ellipsize,
970 ellipsizedWidth, 0, paint, false);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800971 }
972 }
973
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -0700974 // The parameters that are not changed in the method are marked as final to make the code
975 // easier to understand.
976 private int out(final CharSequence text, final int start, final int end, int above, int below,
977 int top, int bottom, int v, final float spacingmult, final float spacingadd,
978 final LineHeightSpan[] chooseHt, final int[] chooseHtv, final Paint.FontMetricsInt fm,
979 final int flags, final boolean needMultiply, final byte[] chdirs, final int dir,
980 final boolean easy, final int bufEnd, final boolean includePad, final boolean trackPad,
981 final boolean addLastLineLineSpacing, final char[] chs, final float[] widths,
982 final int widthStart, final TextUtils.TruncateAt ellipsize, final float ellipsisWidth,
983 final float textWidth, final TextPaint paint, final boolean moreChars) {
Siyamed Siniraf398512017-07-25 19:08:42 -0700984 final int j = mLineCount;
985 final int off = j * mColumns;
986 final int want = off + mColumns + TOP;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800987 int[] lines = mLines;
988
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800989 if (want >= lines.length) {
Siyamed Siniraf398512017-07-25 19:08:42 -0700990 final int[] grow = ArrayUtils.newUnpaddedIntArray(GrowingArrayUtils.growSize(want));
Adam Lesinski776abc22014-03-07 11:30:59 -0500991 System.arraycopy(lines, 0, grow, 0, lines.length);
992 mLines = grow;
993 lines = grow;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800994 }
995
Siyamed Siniraf398512017-07-25 19:08:42 -0700996 if (j >= mLineDirections.length) {
997 final Directions[] grow = ArrayUtils.newUnpaddedArray(Directions.class,
998 GrowingArrayUtils.growSize(j));
999 System.arraycopy(mLineDirections, 0, grow, 0, mLineDirections.length);
1000 mLineDirections = grow;
1001 }
1002
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001003 lines[off + START] = start;
1004 lines[off + TOP] = v;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001005
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001006 // Information about hyphenation, tabs, and directions are needed for determining
1007 // ellipsization, so the values should be assigned before ellipsization.
Eric Fischera9f1dd02009-08-12 15:00:10 -07001008
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001009 // TODO: could move TAB to share same column as HYPHEN, simplifying this code and gaining
1010 // one bit for start field
1011 lines[off + TAB] |= flags & TAB_MASK;
1012 lines[off + HYPHEN] = flags;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001013
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001014 lines[off + DIR] |= dir << DIR_SHIFT;
1015 // easy means all chars < the first RTL, so no emoji, no nothing
1016 // XXX a run with no text or all spaces is easy but might be an empty
1017 // RTL paragraph. Make sure easy is false if this is the case.
1018 if (easy) {
1019 mLineDirections[j] = DIRS_ALL_LEFT_TO_RIGHT;
1020 } else {
1021 mLineDirections[j] = AndroidBidi.directions(dir, chdirs, start - widthStart, chs,
1022 start - widthStart, end - start);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001023 }
1024
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001025 final boolean firstLine = (j == 0);
1026 final boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount);
Siyamed Sinir0745c722016-05-31 20:39:33 -07001027
1028 if (ellipsize != null) {
1029 // If there is only one line, then do any type of ellipsis except when it is MARQUEE
1030 // if there are multiple lines, just allow END ellipsis on the last line
1031 boolean forceEllipsis = moreChars && (mLineCount + 1 == mMaximumVisibleLineCount);
1032
1033 boolean doEllipsis =
1034 (((mMaximumVisibleLineCount == 1 && moreChars) || (firstLine && !moreChars)) &&
1035 ellipsize != TextUtils.TruncateAt.MARQUEE) ||
1036 (!firstLine && (currentLineIsTheLastVisibleOne || !moreChars) &&
1037 ellipsize == TextUtils.TruncateAt.END);
1038 if (doEllipsis) {
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001039 calculateEllipsis(text, start, end, widths, widthStart,
1040 ellipsisWidth - getTotalInsets(j), ellipsize, j,
1041 textWidth, paint, forceEllipsis, dir);
Siyamed Sinir0745c722016-05-31 20:39:33 -07001042 }
1043 }
1044
Siyamed Sinirfb0b2dc2017-07-26 22:08:24 -07001045 final boolean lastLine;
1046 if (mEllipsized) {
1047 lastLine = true;
1048 } else {
1049 final boolean lastCharIsNewLine = widthStart != bufEnd && bufEnd > 0
1050 && text.charAt(bufEnd - 1) == CHAR_NEW_LINE;
1051 if (end == bufEnd && !lastCharIsNewLine) {
1052 lastLine = true;
1053 } else if (start == bufEnd && lastCharIsNewLine) {
1054 lastLine = true;
1055 } else {
1056 lastLine = false;
1057 }
1058 }
Raph Leviend97b0972014-04-24 12:51:35 -07001059
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001060 if (chooseHt != null) {
1061 fm.ascent = above;
1062 fm.descent = below;
1063 fm.top = top;
1064 fm.bottom = bottom;
1065
1066 for (int i = 0; i < chooseHt.length; i++) {
1067 if (chooseHt[i] instanceof LineHeightSpan.WithDensity) {
1068 ((LineHeightSpan.WithDensity) chooseHt[i])
1069 .chooseHeight(text, start, end, chooseHtv[i], v, fm, paint);
1070
1071 } else {
1072 chooseHt[i].chooseHeight(text, start, end, chooseHtv[i], v, fm);
1073 }
1074 }
1075
1076 above = fm.ascent;
1077 below = fm.descent;
1078 top = fm.top;
1079 bottom = fm.bottom;
1080 }
1081
Raph Leviend97b0972014-04-24 12:51:35 -07001082 if (firstLine) {
Fabrice Di Meglio121c82c2011-02-15 15:44:49 -08001083 if (trackPad) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001084 mTopPadding = top - above;
1085 }
1086
Fabrice Di Meglio121c82c2011-02-15 15:44:49 -08001087 if (includePad) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001088 above = top;
1089 }
1090 }
Raph Leviend97b0972014-04-24 12:51:35 -07001091
Raph Leviend97b0972014-04-24 12:51:35 -07001092 if (lastLine) {
Fabrice Di Meglio121c82c2011-02-15 15:44:49 -08001093 if (trackPad) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001094 mBottomPadding = bottom - below;
1095 }
1096
Fabrice Di Meglio121c82c2011-02-15 15:44:49 -08001097 if (includePad) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001098 below = bottom;
1099 }
1100 }
1101
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001102 final int extra;
Siyamed Sinir442c1512017-07-24 12:18:27 -07001103 if (needMultiply && (addLastLineLineSpacing || !lastLine)) {
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001104 final double ex = (below - above) * (spacingmult - 1) + spacingadd;
Doug Felt10657582010-02-22 11:19:01 -08001105 if (ex >= 0) {
Fabrice Di Meglio121c82c2011-02-15 15:44:49 -08001106 extra = (int)(ex + EXTRA_ROUNDING);
Doug Felt10657582010-02-22 11:19:01 -08001107 } else {
Fabrice Di Meglio121c82c2011-02-15 15:44:49 -08001108 extra = -(int)(-ex + EXTRA_ROUNDING);
Doug Felt10657582010-02-22 11:19:01 -08001109 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001110 } else {
1111 extra = 0;
1112 }
1113
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001114 lines[off + DESCENT] = below + extra;
Siyamed Sinir0fa89d62017-07-24 20:46:41 -07001115 lines[off + EXTRA] = extra;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001116
Siyamed Sinir0745c722016-05-31 20:39:33 -07001117 // special case for non-ellipsized last visible line when maxLines is set
1118 // store the height as if it was ellipsized
1119 if (!mEllipsized && currentLineIsTheLastVisibleOne) {
1120 // below calculation as if it was the last line
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001121 final int maxLineBelow = includePad ? bottom : below;
Siyamed Sinir0745c722016-05-31 20:39:33 -07001122 // similar to the calculation of v below, without the extra.
1123 mMaxLineHeight = v + (maxLineBelow - above);
1124 }
1125
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001126 v += (below - above) + extra;
1127 lines[off + mColumns + START] = end;
1128 lines[off + mColumns + TOP] = v;
1129
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001130 mLineCount++;
1131 return v;
1132 }
1133
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001134 private void calculateEllipsis(CharSequence text, int lineStart, int lineEnd, float[] widths,
1135 int widthStart, float avail, TextUtils.TruncateAt where, int line, float textWidth,
1136 TextPaint paint, boolean forceEllipsis, int dir) {
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07001137 if (textWidth <= avail && !forceEllipsis) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001138 // Everything fits!
1139 mLines[mColumns * line + ELLIPSIS_START] = 0;
1140 mLines[mColumns * line + ELLIPSIS_COUNT] = 0;
1141 return;
1142 }
1143
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001144 float tempAvail = avail;
1145 int numberOfTries = 0;
1146 boolean lineFits = false;
1147 mWorkPaint.set(paint);
1148 do {
1149 final float ellipsizedWidth = guessEllipsis(text, lineStart, lineEnd, widths,
1150 widthStart, tempAvail, where, line, textWidth, mWorkPaint, forceEllipsis, dir);
1151 if (ellipsizedWidth <= avail) {
1152 lineFits = true;
1153 } else {
1154 numberOfTries++;
1155 if (numberOfTries > 10) {
1156 // If the text still doesn't fit after ten tries, assume it will never fit and
1157 // ellipsize it all.
1158 mLines[mColumns * line + ELLIPSIS_START] = 0;
1159 mLines[mColumns * line + ELLIPSIS_COUNT] = lineEnd - lineStart;
1160 lineFits = true;
1161 } else {
1162 // Some side effect of ellipsization has caused the text to go over the
1163 // available width. Let's make the available width shorter by exactly that
1164 // amount and retry.
1165 tempAvail -= ellipsizedWidth - avail;
1166 }
1167 }
1168 } while (!lineFits);
1169 mEllipsized = true;
1170 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001171
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001172 // Returns the width of the ellipsized line which in some rare cases can actually be larger
1173 // than 'avail' (due to kerning or other context-based effect of replacement of text by
1174 // ellipsis). If all the line needs to ellipsized away, or it's an invalud hyphenation mode,
1175 // returns 0 so the caller can stop iterating.
1176 //
1177 // This method temporarily modifies the TextPaint passed to it, so the TextPaint passed to it
1178 // should not be accessed while the method is running.
1179 private float guessEllipsis(CharSequence text, int lineStart, int lineEnd, float[] widths,
1180 int widthStart, float avail, TextUtils.TruncateAt where, int line, float textWidth,
1181 TextPaint paint, boolean forceEllipsis, int dir) {
1182 final int savedHyphenEdit = paint.getHyphenEdit();
1183 paint.setHyphenEdit(0);
1184 final float ellipsisWidth = paint.measureText(TextUtils.getEllipsisString(where));
1185 final int ellipsisStart;
1186 final int ellipsisCount;
1187 final int len = lineEnd - lineStart;
1188 final int offset = lineStart - widthStart;
1189
1190 int hyphen = getHyphen(line);
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07001191 // We only support start ellipsis on a single line
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001192 if (where == TextUtils.TruncateAt.START) {
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07001193 if (mMaximumVisibleLineCount == 1) {
1194 float sum = 0;
1195 int i;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001196
Keisuke Kuroyanagied2eea12015-04-14 18:18:35 +09001197 for (i = len; i > 0; i--) {
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001198 final float w = widths[i - 1 + offset];
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07001199 if (w + sum + ellipsisWidth > avail) {
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001200 while (i < len && widths[i + offset] == 0.0f) {
Keisuke Kuroyanagi82d1c442016-09-14 14:30:14 +09001201 i++;
1202 }
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07001203 break;
1204 }
1205
1206 sum += w;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001207 }
1208
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07001209 ellipsisStart = 0;
1210 ellipsisCount = i;
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001211 // Strip the potential hyphenation at beginning of line.
1212 hyphen &= ~Paint.HYPHENEDIT_MASK_START_OF_LINE;
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07001213 } else {
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001214 ellipsisStart = 0;
1215 ellipsisCount = 0;
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07001216 if (Log.isLoggable(TAG, Log.WARN)) {
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001217 Log.w(TAG, "Start ellipsis only supported with one line");
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07001218 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001219 }
Fabrice Di Megliocb332642011-09-23 19:08:04 -07001220 } else if (where == TextUtils.TruncateAt.END || where == TextUtils.TruncateAt.MARQUEE ||
1221 where == TextUtils.TruncateAt.END_SMALL) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001222 float sum = 0;
1223 int i;
1224
1225 for (i = 0; i < len; i++) {
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001226 final float w = widths[i + offset];
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001227
Fabrice Di Meglio121c82c2011-02-15 15:44:49 -08001228 if (w + sum + ellipsisWidth > avail) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001229 break;
1230 }
1231
1232 sum += w;
1233 }
1234
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001235 if (forceEllipsis && i == len && len > 0) {
Fabrice Di Meglioaef455f2011-08-29 15:39:11 -07001236 ellipsisStart = len - 1;
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07001237 ellipsisCount = 1;
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001238 } else {
1239 ellipsisStart = i;
1240 ellipsisCount = len - i;
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07001241 }
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001242 // Strip the potential hyphenation at end of line.
1243 hyphen &= ~Paint.HYPHENEDIT_MASK_END_OF_LINE;
1244 } else { // where = TextUtils.TruncateAt.MIDDLE
1245 // We only support middle ellipsis on a single line.
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07001246 if (mMaximumVisibleLineCount == 1) {
1247 float lsum = 0, rsum = 0;
1248 int left = 0, right = len;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001249
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001250 final float ravail = (avail - ellipsisWidth) / 2;
Raph Levien0e3c5e82014-12-04 13:26:07 -08001251 for (right = len; right > 0; right--) {
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001252 final float w = widths[right - 1 + offset];
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001253
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07001254 if (w + rsum > ravail) {
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001255 while (right < len && widths[right + offset] == 0.0f) {
Keisuke Kuroyanagi82d1c442016-09-14 14:30:14 +09001256 right++;
1257 }
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07001258 break;
1259 }
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07001260 rsum += w;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001261 }
1262
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001263 final float lavail = avail - ellipsisWidth - rsum;
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07001264 for (left = 0; left < right; left++) {
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001265 final float w = widths[left + offset];
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001266
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07001267 if (w + lsum > lavail) {
1268 break;
1269 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001270
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07001271 lsum += w;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001272 }
1273
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07001274 ellipsisStart = left;
1275 ellipsisCount = right - left;
1276 } else {
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001277 ellipsisStart = 0;
1278 ellipsisCount = 0;
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07001279 if (Log.isLoggable(TAG, Log.WARN)) {
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001280 Log.w(TAG, "Middle ellipsis only supported with one line");
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07001281 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001282 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001283 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001284 mLines[mColumns * line + ELLIPSIS_START] = ellipsisStart;
1285 mLines[mColumns * line + ELLIPSIS_COUNT] = ellipsisCount;
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001286
1287 if (ellipsisStart == 0 && (ellipsisCount == 0 || ellipsisCount == len)) {
1288 // Unsupported ellipsization mode or all text is ellipsized away. Return 0.
1289 return 0.0f;
1290 }
1291
1292 final boolean isSpanned = text instanceof Spanned;
1293 final Ellipsizer ellipsizedText = isSpanned
1294 ? new SpannedEllipsizer(text)
1295 : new Ellipsizer(text);
1296 ellipsizedText.mLayout = this;
1297 ellipsizedText.mMethod = where;
1298
1299 final boolean hasTabs = getLineContainsTab(line);
1300 final TabStops tabStops;
1301 if (hasTabs && isSpanned) {
1302 final TabStopSpan[] tabs = getParagraphSpans((Spanned) ellipsizedText, lineStart,
1303 lineEnd, TabStopSpan.class);
1304 if (tabs.length == 0) {
1305 tabStops = null;
1306 } else {
1307 tabStops = new TabStops(TAB_INCREMENT, tabs);
1308 }
1309 } else {
1310 tabStops = null;
1311 }
1312 paint.setHyphenEdit(hyphen);
1313 final TextLine textline = TextLine.obtain();
1314 textline.set(paint, ellipsizedText, lineStart, lineEnd, dir, getLineDirections(line),
1315 hasTabs, tabStops);
1316 // Since TextLine.metric() returns negative values for RTL text, multiplication by dir
1317 // converts it to an actual width. Note that we don't want to use the absolute value,
1318 // since we may actually have glyphs with negative advances, which by definition always
1319 // fit.
1320 final float ellipsizedWidth = textline.metrics(null) * dir;
1321 TextLine.recycle(textline);
1322 paint.setHyphenEdit(savedHyphenEdit);
1323 return ellipsizedWidth;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001324 }
1325
Selim Cinek365ec092017-03-09 00:10:52 -08001326 private float getTotalInsets(int line) {
1327 int totalIndent = 0;
1328 if (mLeftIndents != null) {
1329 totalIndent = mLeftIndents[Math.min(line, mLeftIndents.length - 1)];
1330 }
1331 if (mRightIndents != null) {
1332 totalIndent += mRightIndents[Math.min(line, mRightIndents.length - 1)];
1333 }
1334 return totalIndent;
1335 }
1336
Doug Felte8e45f22010-03-29 14:58:40 -07001337 // Override the base class so we can directly access our members,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001338 // rather than relying on member functions.
1339 // The logic mirrors that of Layout.getLineForVertical
1340 // FIXME: It may be faster to do a linear search for layouts without many lines.
Gilles Debunne66111472010-11-19 11:04:37 -08001341 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001342 public int getLineForVertical(int vertical) {
1343 int high = mLineCount;
1344 int low = -1;
1345 int guess;
1346 int[] lines = mLines;
1347 while (high - low > 1) {
1348 guess = (high + low) >> 1;
1349 if (lines[mColumns * guess + TOP] > vertical){
1350 high = guess;
1351 } else {
1352 low = guess;
1353 }
1354 }
1355 if (low < 0) {
1356 return 0;
1357 } else {
1358 return low;
1359 }
1360 }
1361
Gilles Debunne66111472010-11-19 11:04:37 -08001362 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001363 public int getLineCount() {
1364 return mLineCount;
1365 }
1366
Gilles Debunne66111472010-11-19 11:04:37 -08001367 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001368 public int getLineTop(int line) {
Raph Levien07e6c232016-04-04 12:34:06 -07001369 return mLines[mColumns * line + TOP];
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001370 }
1371
Siyamed Sinir0fa89d62017-07-24 20:46:41 -07001372 /**
1373 * @hide
1374 */
1375 @Override
1376 public int getLineExtra(int line) {
1377 return mLines[mColumns * line + EXTRA];
1378 }
1379
Gilles Debunne66111472010-11-19 11:04:37 -08001380 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001381 public int getLineDescent(int line) {
Raph Levien07e6c232016-04-04 12:34:06 -07001382 return mLines[mColumns * line + DESCENT];
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001383 }
1384
Gilles Debunne66111472010-11-19 11:04:37 -08001385 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001386 public int getLineStart(int line) {
1387 return mLines[mColumns * line + START] & START_MASK;
1388 }
1389
Gilles Debunne66111472010-11-19 11:04:37 -08001390 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001391 public int getParagraphDirection(int line) {
1392 return mLines[mColumns * line + DIR] >> DIR_SHIFT;
1393 }
1394
Gilles Debunne66111472010-11-19 11:04:37 -08001395 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001396 public boolean getLineContainsTab(int line) {
1397 return (mLines[mColumns * line + TAB] & TAB_MASK) != 0;
1398 }
1399
Gilles Debunne66111472010-11-19 11:04:37 -08001400 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001401 public final Directions getLineDirections(int line) {
Siyamed Sinir0fa89d62017-07-24 20:46:41 -07001402 if (line > getLineCount()) {
1403 throw new ArrayIndexOutOfBoundsException();
1404 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001405 return mLineDirections[line];
1406 }
1407
Gilles Debunne66111472010-11-19 11:04:37 -08001408 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001409 public int getTopPadding() {
1410 return mTopPadding;
1411 }
1412
Gilles Debunne66111472010-11-19 11:04:37 -08001413 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001414 public int getBottomPadding() {
1415 return mBottomPadding;
1416 }
1417
Raph Levien26d443a2015-03-30 14:18:32 -07001418 /**
1419 * @hide
1420 */
1421 @Override
1422 public int getHyphen(int line) {
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +09001423 return mLines[mColumns * line + HYPHEN] & HYPHEN_MASK;
Raph Levien26d443a2015-03-30 14:18:32 -07001424 }
1425
Raph Levien2ea52902015-07-01 14:39:31 -07001426 /**
1427 * @hide
1428 */
1429 @Override
1430 public int getIndentAdjust(int line, Alignment align) {
1431 if (align == Alignment.ALIGN_LEFT) {
1432 if (mLeftIndents == null) {
1433 return 0;
1434 } else {
1435 return mLeftIndents[Math.min(line, mLeftIndents.length - 1)];
1436 }
1437 } else if (align == Alignment.ALIGN_RIGHT) {
1438 if (mRightIndents == null) {
1439 return 0;
1440 } else {
1441 return -mRightIndents[Math.min(line, mRightIndents.length - 1)];
1442 }
1443 } else if (align == Alignment.ALIGN_CENTER) {
1444 int left = 0;
1445 if (mLeftIndents != null) {
1446 left = mLeftIndents[Math.min(line, mLeftIndents.length - 1)];
1447 }
1448 int right = 0;
1449 if (mRightIndents != null) {
1450 right = mRightIndents[Math.min(line, mRightIndents.length - 1)];
1451 }
1452 return (left - right) >> 1;
1453 } else {
1454 throw new AssertionError("unhandled alignment " + align);
1455 }
1456 }
1457
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001458 @Override
1459 public int getEllipsisCount(int line) {
1460 if (mColumns < COLUMNS_ELLIPSIZE) {
1461 return 0;
1462 }
1463
1464 return mLines[mColumns * line + ELLIPSIS_COUNT];
1465 }
1466
1467 @Override
1468 public int getEllipsisStart(int line) {
1469 if (mColumns < COLUMNS_ELLIPSIZE) {
1470 return 0;
1471 }
1472
1473 return mLines[mColumns * line + ELLIPSIS_START];
1474 }
1475
1476 @Override
1477 public int getEllipsizedWidth() {
1478 return mEllipsizedWidth;
1479 }
1480
Siyamed Sinir0745c722016-05-31 20:39:33 -07001481 /**
1482 * Return the total height of this layout.
1483 *
1484 * @param cap if true and max lines is set, returns the height of the layout at the max lines.
1485 *
1486 * @hide
1487 */
1488 public int getHeight(boolean cap) {
1489 if (cap && mLineCount >= mMaximumVisibleLineCount && mMaxLineHeight == -1 &&
1490 Log.isLoggable(TAG, Log.WARN)) {
1491 Log.w(TAG, "maxLineHeight should not be -1. "
1492 + " maxLines:" + mMaximumVisibleLineCount
1493 + " lineCount:" + mLineCount);
1494 }
1495
1496 return cap && mLineCount >= mMaximumVisibleLineCount && mMaxLineHeight != -1 ?
1497 mMaxLineHeight : super.getHeight();
1498 }
1499
Raph Levien70616ec2015-03-04 10:41:30 -08001500 private static native long nNewBuilder();
1501 private static native void nFreeBuilder(long nativePtr);
1502 private static native void nFinishBuilder(long nativePtr);
Raph Levien26d443a2015-03-30 14:18:32 -07001503
Roozbeh Pournadera59c3fe2017-02-27 10:13:44 -08001504 /* package */ static native long nLoadHyphenator(ByteBuffer buf, int offset,
1505 int minPrefix, int minSuffix);
Raph Levien26d443a2015-03-30 14:18:32 -07001506
Roozbeh Pournaderef7cfa12017-06-15 12:39:04 -07001507 private static native void nSetLocales(long nativePtr, String locales,
1508 long[] nativeHyphenators);
Raph Levien70616ec2015-03-04 10:41:30 -08001509
Raph Leviene319d5a2015-04-14 23:51:07 -07001510 private static native void nSetIndents(long nativePtr, int[] indents);
1511
Raph Levienc94f7422015-03-06 19:19:48 -08001512 // Set up paragraph text and settings; done as one big method to minimize jni crossings
1513 private static native void nSetupParagraph(long nativePtr, char[] text, int length,
1514 float firstWidth, int firstWidthLineCount, float restWidth,
Seigo Nonaka09da71a2016-11-28 16:24:14 +09001515 int[] variableTabStops, int defaultTabStop, int breakStrategy, int hyphenationFrequency,
1516 boolean isJustified);
Raph Levien70616ec2015-03-04 10:41:30 -08001517
Seigo Nonaka318ca042017-08-01 16:36:18 -07001518 private static native float nAddStyleRun(long nativePtr, long nativePaint, int start, int end,
1519 boolean isRtl);
Raph Levien70616ec2015-03-04 10:41:30 -08001520
1521 private static native void nAddMeasuredRun(long nativePtr,
1522 int start, int end, float[] widths);
1523
1524 private static native void nAddReplacementRun(long nativePtr, int start, int end, float width);
1525
1526 private static native void nGetWidths(long nativePtr, float[] widths);
1527
Anish Athalyec8f9e622014-07-21 15:26:34 -07001528 // populates LineBreaks and returns the number of breaks found
1529 //
1530 // the arrays inside the LineBreaks objects are passed in as well
1531 // to reduce the number of JNI calls in the common case where the
1532 // arrays do not have to be resized
Raph Levienc94f7422015-03-06 19:19:48 -08001533 private static native int nComputeLineBreaks(long nativePtr, LineBreaks recycle,
Roozbeh Pournader737dfea2017-08-10 11:32:24 -07001534 int[] recycleBreaks, float[] recycleWidths, float[] recycleAscents,
1535 float[] recycleDescents, int[] recycleFlags, int recycleLength);
Anish Athalye88b5b0b2014-06-24 14:39:43 -07001536
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001537 private int mLineCount;
1538 private int mTopPadding, mBottomPadding;
1539 private int mColumns;
1540 private int mEllipsizedWidth;
1541
Siyamed Sinir0745c722016-05-31 20:39:33 -07001542 /**
1543 * Keeps track if ellipsize is applied to the text.
1544 */
1545 private boolean mEllipsized;
1546
1547 /**
1548 * If maxLines is set, ellipsize is not set, and the actual line count of text is greater than
1549 * or equal to maxLine, this variable holds the ideal visual height of the maxLine'th line
1550 * starting from the top of the layout. If maxLines is not set its value will be -1.
1551 *
1552 * The value is the same as getLineTop(maxLines) for ellipsized version where structurally no
1553 * more than maxLines is contained.
1554 */
Siyamed Sinira19cd512017-08-03 22:01:56 -07001555 private int mMaxLineHeight = DEFAULT_MAX_LINE_HEIGHT;
Siyamed Sinir0745c722016-05-31 20:39:33 -07001556
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001557 private TextPaint mWorkPaint = new TextPaint();
1558
Siyamed Sinir0fa89d62017-07-24 20:46:41 -07001559 private static final int COLUMNS_NORMAL = 5;
1560 private static final int COLUMNS_ELLIPSIZE = 7;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001561 private static final int START = 0;
1562 private static final int DIR = START;
1563 private static final int TAB = START;
1564 private static final int TOP = 1;
1565 private static final int DESCENT = 2;
Siyamed Sinir0fa89d62017-07-24 20:46:41 -07001566 private static final int EXTRA = 3;
1567 private static final int HYPHEN = 4;
1568 private static final int ELLIPSIS_START = 5;
1569 private static final int ELLIPSIS_COUNT = 6;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001570
1571 private int[] mLines;
1572 private Directions[] mLineDirections;
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07001573 private int mMaximumVisibleLineCount = Integer.MAX_VALUE;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001574
1575 private static final int START_MASK = 0x1FFFFFFF;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001576 private static final int DIR_SHIFT = 30;
1577 private static final int TAB_MASK = 0x20000000;
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +09001578 private static final int HYPHEN_MASK = 0xFF;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001579
Doug Feltc982f602010-05-25 11:51:40 -07001580 private static final int TAB_INCREMENT = 20; // same as Layout, but that's private
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001581
Fabrice Di Meglio121c82c2011-02-15 15:44:49 -08001582 private static final char CHAR_NEW_LINE = '\n';
Fabrice Di Meglio121c82c2011-02-15 15:44:49 -08001583
1584 private static final double EXTRA_ROUNDING = 0.5;
Fabrice Di Megliocb332642011-09-23 19:08:04 -07001585
Siyamed Sinira19cd512017-08-03 22:01:56 -07001586 private static final int DEFAULT_MAX_LINE_HEIGHT = -1;
1587
Anish Athalyec8f9e622014-07-21 15:26:34 -07001588 // This is used to return three arrays from a single JNI call when
1589 // performing line breaking
Deepanshu Gupta70539192014-10-15 15:57:40 -07001590 /*package*/ static class LineBreaks {
Anish Athalyec8f9e622014-07-21 15:26:34 -07001591 private static final int INITIAL_SIZE = 16;
1592 public int[] breaks = new int[INITIAL_SIZE];
1593 public float[] widths = new float[INITIAL_SIZE];
Roozbeh Pournader737dfea2017-08-10 11:32:24 -07001594 public float[] ascents = new float[INITIAL_SIZE];
1595 public float[] descents = new float[INITIAL_SIZE];
Roozbeh Pournader112d9c72015-08-07 12:44:41 -07001596 public int[] flags = new int[INITIAL_SIZE]; // hasTab
Anish Athalyec8f9e622014-07-21 15:26:34 -07001597 // breaks, widths, and flags should all have the same length
1598 }
1599
Raph Levien2ea52902015-07-01 14:39:31 -07001600 private int[] mLeftIndents;
1601 private int[] mRightIndents;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001602}