blob: 3303b54e462b03f62dc9927d48069ee56b444539 [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 return this;
356 }
357
Raph Levien70616ec2015-03-04 10:41:30 -0800358 /**
Seigo Nonaka4b4730d2017-03-31 09:42:16 -0700359 * Set paragraph justification mode. The default value is
360 * {@link Layout#JUSTIFICATION_MODE_NONE}. If the last line is too short for justification,
361 * the last line will be displayed with the alignment set by {@link #setAlignment}.
Seigo Nonaka09da71a2016-11-28 16:24:14 +0900362 *
Seigo Nonaka4b4730d2017-03-31 09:42:16 -0700363 * @param justificationMode justification mode for the paragraph.
Seigo Nonaka09da71a2016-11-28 16:24:14 +0900364 * @return this builder, useful for chaining.
365 */
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700366 @NonNull
Seigo Nonaka4b4730d2017-03-31 09:42:16 -0700367 public Builder setJustificationMode(@JustificationMode int justificationMode) {
368 mJustificationMode = justificationMode;
Seigo Nonaka09da71a2016-11-28 16:24:14 +0900369 return this;
370 }
371
Siyamed Sinir442c1512017-07-24 12:18:27 -0700372 /**
373 * Sets whether the line spacing should be applied for the last line. Default value is
374 * {@code false}.
375 *
376 * @hide
377 */
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700378 @NonNull
Siyamed Sinir442c1512017-07-24 12:18:27 -0700379 /* package */ Builder setAddLastLineLineSpacing(boolean value) {
380 mAddLastLineLineSpacing = value;
381 return this;
382 }
383
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700384 @NonNull
385 private long[] getHyphenators(@NonNull LocaleList locales) {
Roozbeh Pournaderef7cfa12017-06-15 12:39:04 -0700386 final int length = locales.size();
387 final long[] result = new long[length];
388 for (int i = 0; i < length; i++) {
389 final Locale locale = locales.get(i);
390 result[i] = Hyphenator.get(locale).getNativePtr();
391 }
392 return result;
393 }
394
Seigo Nonaka09da71a2016-11-28 16:24:14 +0900395 /**
Raph Levien70616ec2015-03-04 10:41:30 -0800396 * Measurement and break iteration is done in native code. The protocol for using
397 * the native code is as follows.
398 *
Raph Levien26d443a2015-03-30 14:18:32 -0700399 * For each paragraph, do a nSetupParagraph, which sets paragraph text, line width, tab
Roozbeh Pournader95c7a132015-05-12 12:01:06 -0700400 * stops, break strategy, and hyphenation frequency (and possibly other parameters in the
401 * future).
Raph Levienc94f7422015-03-06 19:19:48 -0800402 *
403 * Then, for each run within the paragraph:
Roozbeh Pournaderef7cfa12017-06-15 12:39:04 -0700404 * - setLocales (this must be done at least for the first run, optional afterwards)
Raph Levien70616ec2015-03-04 10:41:30 -0800405 * - one of the following, depending on the type of run:
406 * + addStyleRun (a text run, to be measured in native code)
407 * + addMeasuredRun (a run already measured in Java, passed into native code)
408 * + addReplacementRun (a replacement run, width is given)
409 *
410 * After measurement, nGetWidths() is valid if the widths are needed (eg for ellipsis).
411 * Run nComputeLineBreaks() to obtain line breaks for the paragraph.
412 *
413 * After all paragraphs, call finish() to release expensive buffers.
414 */
415
Roozbeh Pournaderef7cfa12017-06-15 12:39:04 -0700416 private void setLocales(LocaleList locales) {
417 if (!locales.equals(mLocales)) {
418 nSetLocales(mNativePtr, locales.toLanguageTags(), getHyphenators(locales));
419 mLocales = locales;
Raph Levien4c1f12e2015-03-02 16:29:23 -0800420 }
421 }
422
Raph Levien70616ec2015-03-04 10:41:30 -0800423 /* package */ float addStyleRun(TextPaint paint, int start, int end, boolean isRtl) {
Roozbeh Pournaderef7cfa12017-06-15 12:39:04 -0700424 setLocales(paint.getTextLocales());
Seigo Nonaka318ca042017-08-01 16:36:18 -0700425 return nAddStyleRun(mNativePtr, paint.getNativeInstance(), start, end, isRtl);
Raph Levien70616ec2015-03-04 10:41:30 -0800426 }
427
428 /* package */ void addMeasuredRun(int start, int end, float[] widths) {
429 nAddMeasuredRun(mNativePtr, start, end, widths);
430 }
431
432 /* package */ void addReplacementRun(int start, int end, float width) {
433 nAddReplacementRun(mNativePtr, start, end, width);
434 }
435
Raph Levien531c30c2015-04-30 16:29:59 -0700436 /**
437 * Build the {@link StaticLayout} after options have been set.
438 *
439 * <p>Note: the builder object must not be reused in any way after calling this
440 * method. Setting parameters after calling this method, or calling it a second
441 * time on the same builder object, will likely lead to unexpected results.
442 *
443 * @return the newly constructed {@link StaticLayout} object
444 */
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700445 @NonNull
Raph Leviend3ab6922015-03-02 14:30:53 -0800446 public StaticLayout build() {
Raph Levien39b4db72015-03-25 13:18:20 -0700447 StaticLayout result = new StaticLayout(this);
448 Builder.recycle(this);
Raph Leviend3ab6922015-03-02 14:30:53 -0800449 return result;
450 }
451
Raph Levien4c1f12e2015-03-02 16:29:23 -0800452 @Override
453 protected void finalize() throws Throwable {
454 try {
455 nFreeBuilder(mNativePtr);
456 } finally {
457 super.finalize();
458 }
459 }
460
461 /* package */ long mNativePtr;
462
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700463 private CharSequence mText;
464 private int mStart;
465 private int mEnd;
466 private TextPaint mPaint;
467 private int mWidth;
468 private Alignment mAlignment;
469 private TextDirectionHeuristic mTextDir;
470 private float mSpacingMult;
471 private float mSpacingAdd;
472 private boolean mIncludePad;
473 private boolean mFallbackLineSpacing;
474 private int mEllipsizedWidth;
475 private TextUtils.TruncateAt mEllipsize;
476 private int mMaxLines;
477 private int mBreakStrategy;
478 private int mHyphenationFrequency;
Seigo Nonakabafe1972017-08-24 15:30:29 -0700479 @Nullable private int[] mLeftIndents;
480 @Nullable private int[] mRightIndents;
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700481 private int mJustificationMode;
482 private boolean mAddLastLineLineSpacing;
Raph Leviend3ab6922015-03-02 14:30:53 -0800483
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700484 private final Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt();
Raph Leviend3ab6922015-03-02 14:30:53 -0800485
486 // This will go away and be subsumed by native builder code
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700487 private MeasuredText mMeasuredText;
Raph Leviend3ab6922015-03-02 14:30:53 -0800488
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700489 private LocaleList mLocales;
Raph Levien4c1f12e2015-03-02 16:29:23 -0800490
Raph Levien39b4db72015-03-25 13:18:20 -0700491 private static final SynchronizedPool<Builder> sPool = new SynchronizedPool<Builder>(3);
Raph Leviend3ab6922015-03-02 14:30:53 -0800492 }
493
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800494 public StaticLayout(CharSequence source, TextPaint paint,
495 int width,
496 Alignment align, float spacingmult, float spacingadd,
497 boolean includepad) {
498 this(source, 0, source.length(), paint, width, align,
499 spacingmult, spacingadd, includepad);
500 }
501
Doug Feltcb3791202011-07-07 11:57:48 -0700502 /**
503 * @hide
504 */
505 public StaticLayout(CharSequence source, TextPaint paint,
506 int width, Alignment align, TextDirectionHeuristic textDir,
507 float spacingmult, float spacingadd,
508 boolean includepad) {
509 this(source, 0, source.length(), paint, width, align, textDir,
510 spacingmult, spacingadd, includepad);
511 }
512
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800513 public StaticLayout(CharSequence source, int bufstart, int bufend,
514 TextPaint paint, int outerwidth,
515 Alignment align,
516 float spacingmult, float spacingadd,
517 boolean includepad) {
518 this(source, bufstart, bufend, paint, outerwidth, align,
519 spacingmult, spacingadd, includepad, null, 0);
520 }
521
Doug Feltcb3791202011-07-07 11:57:48 -0700522 /**
523 * @hide
524 */
525 public StaticLayout(CharSequence source, int bufstart, int bufend,
526 TextPaint paint, int outerwidth,
527 Alignment align, TextDirectionHeuristic textDir,
528 float spacingmult, float spacingadd,
529 boolean includepad) {
530 this(source, bufstart, bufend, paint, outerwidth, align, textDir,
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -0700531 spacingmult, spacingadd, includepad, null, 0, Integer.MAX_VALUE);
Doug Feltcb3791202011-07-07 11:57:48 -0700532}
533
534 public StaticLayout(CharSequence source, int bufstart, int bufend,
535 TextPaint paint, int outerwidth,
536 Alignment align,
537 float spacingmult, float spacingadd,
538 boolean includepad,
539 TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
540 this(source, bufstart, bufend, paint, outerwidth, align,
541 TextDirectionHeuristics.FIRSTSTRONG_LTR,
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -0700542 spacingmult, spacingadd, includepad, ellipsize, ellipsizedWidth, Integer.MAX_VALUE);
Doug Feltcb3791202011-07-07 11:57:48 -0700543 }
544
545 /**
546 * @hide
547 */
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800548 public StaticLayout(CharSequence source, int bufstart, int bufend,
549 TextPaint paint, int outerwidth,
Doug Feltcb3791202011-07-07 11:57:48 -0700550 Alignment align, TextDirectionHeuristic textDir,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800551 float spacingmult, float spacingadd,
552 boolean includepad,
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -0700553 TextUtils.TruncateAt ellipsize, int ellipsizedWidth, int maxLines) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800554 super((ellipsize == null)
Doug Felt4e0c5e52010-03-15 16:56:02 -0700555 ? source
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800556 : (source instanceof Spanned)
557 ? new SpannedEllipsizer(source)
558 : new Ellipsizer(source),
Doug Feltcb3791202011-07-07 11:57:48 -0700559 paint, outerwidth, align, textDir, spacingmult, spacingadd);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800560
Raph Levienebd66ca2015-04-30 15:27:57 -0700561 Builder b = Builder.obtain(source, bufstart, bufend, paint, outerwidth)
Raph Levien39b4db72015-03-25 13:18:20 -0700562 .setAlignment(align)
Raph Leviena6a08282015-06-03 13:20:45 -0700563 .setTextDirection(textDir)
Raph Levien531c30c2015-04-30 16:29:59 -0700564 .setLineSpacing(spacingadd, spacingmult)
Raph Leviend3ab6922015-03-02 14:30:53 -0800565 .setIncludePad(includepad)
566 .setEllipsizedWidth(ellipsizedWidth)
567 .setEllipsize(ellipsize)
568 .setMaxLines(maxLines);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800569 /*
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700570 * This is annoying, but we can't refer to the layout until superclass construction is
571 * finished, and the superclass constructor wants the reference to the display text.
Doug Felt4e0c5e52010-03-15 16:56:02 -0700572 *
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700573 * In other words, the two Ellipsizer classes in Layout.java need a (Dynamic|Static)Layout
574 * as a parameter to do their calculations, but the Ellipsizers also need to be the input
575 * to the superclass's constructor (Layout). In order to go around the circular
576 * dependency, we construct the Ellipsizer with only one of the parameters, the text. And
577 * we fill in the rest of the needed information (layout, width, and method) later, here.
578 *
579 * This will break if the superclass constructor ever actually cares about the content
580 * instead of just holding the reference.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800581 */
582 if (ellipsize != null) {
583 Ellipsizer e = (Ellipsizer) getText();
584
585 e.mLayout = this;
586 e.mWidth = ellipsizedWidth;
587 e.mMethod = ellipsize;
588 mEllipsizedWidth = ellipsizedWidth;
589
590 mColumns = COLUMNS_ELLIPSIZE;
591 } else {
592 mColumns = COLUMNS_NORMAL;
593 mEllipsizedWidth = outerwidth;
594 }
595
Siyamed Siniraf398512017-07-25 19:08:42 -0700596 mLineDirections = ArrayUtils.newUnpaddedArray(Directions.class, 2);
597 mLines = ArrayUtils.newUnpaddedIntArray(2 * mColumns);
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -0700598 mMaximumVisibleLineCount = maxLines;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800599
Raph Levien70616ec2015-03-04 10:41:30 -0800600 generate(b, b.mIncludePad, b.mIncludePad);
Doug Felte8e45f22010-03-29 14:58:40 -0700601
Raph Leviend3ab6922015-03-02 14:30:53 -0800602 Builder.recycle(b);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800603 }
604
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -0700605 /* package */ StaticLayout(@Nullable CharSequence text) {
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -0700606 super(text, null, 0, null, 0, 0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800607
608 mColumns = COLUMNS_ELLIPSIZE;
Siyamed Siniraf398512017-07-25 19:08:42 -0700609 mLineDirections = ArrayUtils.newUnpaddedArray(Directions.class, 2);
610 mLines = ArrayUtils.newUnpaddedIntArray(2 * mColumns);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800611 }
612
Raph Levien39b4db72015-03-25 13:18:20 -0700613 private StaticLayout(Builder b) {
614 super((b.mEllipsize == null)
615 ? b.mText
616 : (b.mText instanceof Spanned)
617 ? new SpannedEllipsizer(b.mText)
618 : new Ellipsizer(b.mText),
Roozbeh Pournader158dfaf2017-09-21 13:23:50 -0700619 b.mPaint, b.mWidth, b.mAlignment, b.mTextDir, b.mSpacingMult, b.mSpacingAdd);
Raph Levien39b4db72015-03-25 13:18:20 -0700620
621 if (b.mEllipsize != null) {
622 Ellipsizer e = (Ellipsizer) getText();
623
624 e.mLayout = this;
625 e.mWidth = b.mEllipsizedWidth;
626 e.mMethod = b.mEllipsize;
627 mEllipsizedWidth = b.mEllipsizedWidth;
628
629 mColumns = COLUMNS_ELLIPSIZE;
630 } else {
631 mColumns = COLUMNS_NORMAL;
632 mEllipsizedWidth = b.mWidth;
633 }
634
Siyamed Siniraf398512017-07-25 19:08:42 -0700635 mLineDirections = ArrayUtils.newUnpaddedArray(Directions.class, 2);
636 mLines = ArrayUtils.newUnpaddedIntArray(2 * mColumns);
Raph Levien39b4db72015-03-25 13:18:20 -0700637 mMaximumVisibleLineCount = b.mMaxLines;
638
Raph Levien2ea52902015-07-01 14:39:31 -0700639 mLeftIndents = b.mLeftIndents;
640 mRightIndents = b.mRightIndents;
Seigo Nonaka4b4730d2017-03-31 09:42:16 -0700641 setJustificationMode(b.mJustificationMode);
Raph Levien2ea52902015-07-01 14:39:31 -0700642
Raph Levien39b4db72015-03-25 13:18:20 -0700643 generate(b, b.mIncludePad, b.mIncludePad);
644 }
645
Raph Leviend3ab6922015-03-02 14:30:53 -0800646 /* package */ void generate(Builder b, boolean includepad, boolean trackpad) {
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -0700647 final CharSequence source = b.mText;
Raph Leviend3ab6922015-03-02 14:30:53 -0800648 int bufStart = b.mStart;
649 int bufEnd = b.mEnd;
650 TextPaint paint = b.mPaint;
651 int outerWidth = b.mWidth;
652 TextDirectionHeuristic textDir = b.mTextDir;
Roozbeh Pournader737dfea2017-08-10 11:32:24 -0700653 final boolean fallbackLineSpacing = b.mFallbackLineSpacing;
Raph Leviend3ab6922015-03-02 14:30:53 -0800654 float spacingmult = b.mSpacingMult;
655 float spacingadd = b.mSpacingAdd;
656 float ellipsizedWidth = b.mEllipsizedWidth;
657 TextUtils.TruncateAt ellipsize = b.mEllipsize;
Siyamed Sinir442c1512017-07-24 12:18:27 -0700658 final boolean addLastLineSpacing = b.mAddLastLineLineSpacing;
Raph Levien4c1f12e2015-03-02 16:29:23 -0800659 LineBreaks lineBreaks = new LineBreaks(); // TODO: move to builder to avoid allocation costs
Anish Athalyec8f9e622014-07-21 15:26:34 -0700660 // store span end locations
661 int[] spanEndCache = new int[4];
662 // store fontMetrics per span range
663 // must be a multiple of 4 (and > 0) (store top, bottom, ascent, and descent per range)
664 int[] fmCache = new int[4 * 4];
Roozbeh Pournaderef7cfa12017-06-15 12:39:04 -0700665 b.setLocales(paint.getTextLocales());
Anish Athalye88b5b0b2014-06-24 14:39:43 -0700666
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800667 mLineCount = 0;
Siyamed Sinira19cd512017-08-03 22:01:56 -0700668 mEllipsized = false;
Siyamed Sinira8d982d2017-08-21 13:27:47 -0700669 mMaxLineHeight = mMaximumVisibleLineCount < 1 ? 0 : DEFAULT_MAX_LINE_HEIGHT;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800670
671 int v = 0;
672 boolean needMultiply = (spacingmult != 1 || spacingadd != 0);
673
Raph Leviend3ab6922015-03-02 14:30:53 -0800674 Paint.FontMetricsInt fm = b.mFontMetricsInt;
Fabrice Di Meglio121c82c2011-02-15 15:44:49 -0800675 int[] chooseHtv = null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800676
Raph Leviend3ab6922015-03-02 14:30:53 -0800677 MeasuredText measured = b.mMeasuredText;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800678
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800679 Spanned spanned = null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800680 if (source instanceof Spanned)
681 spanned = (Spanned) source;
682
Seigo Nonakabafe1972017-08-24 15:30:29 -0700683 final int[] indents;
684 if (mLeftIndents != null || mRightIndents != null) {
685 final int leftLen = mLeftIndents == null ? 0 : mLeftIndents.length;
686 final int rightLen = mRightIndents == null ? 0 : mRightIndents.length;
687 final int indentsLen = Math.max(leftLen, rightLen);
688 indents = new int[indentsLen];
689 for (int i = 0; i < leftLen; i++) {
690 indents[i] = mLeftIndents[i];
691 }
692 for (int i = 0; i < rightLen; i++) {
693 indents[i] += mRightIndents[i];
694 }
695 } else {
696 indents = null;
697 }
698
Doug Felte8e45f22010-03-29 14:58:40 -0700699 int paraEnd;
Fabrice Di Meglio121c82c2011-02-15 15:44:49 -0800700 for (int paraStart = bufStart; paraStart <= bufEnd; paraStart = paraEnd) {
701 paraEnd = TextUtils.indexOf(source, CHAR_NEW_LINE, paraStart, bufEnd);
Doug Felte8e45f22010-03-29 14:58:40 -0700702 if (paraEnd < 0)
Fabrice Di Meglio121c82c2011-02-15 15:44:49 -0800703 paraEnd = bufEnd;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800704 else
Doug Felte8e45f22010-03-29 14:58:40 -0700705 paraEnd++;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800706
Anish Athalyec8f9e622014-07-21 15:26:34 -0700707 int firstWidthLineCount = 1;
Fabrice Di Meglio121c82c2011-02-15 15:44:49 -0800708 int firstWidth = outerWidth;
709 int restWidth = outerWidth;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800710
Fabrice Di Meglio121c82c2011-02-15 15:44:49 -0800711 LineHeightSpan[] chooseHt = null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800712
713 if (spanned != null) {
Eric Fischer74d31ef2010-08-05 15:29:36 -0700714 LeadingMarginSpan[] sp = getParagraphSpans(spanned, paraStart, paraEnd,
Doug Felte8e45f22010-03-29 14:58:40 -0700715 LeadingMarginSpan.class);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800716 for (int i = 0; i < sp.length; i++) {
Mark Wagner7b5676e2009-10-16 11:44:23 -0700717 LeadingMarginSpan lms = sp[i];
Fabrice Di Meglio121c82c2011-02-15 15:44:49 -0800718 firstWidth -= sp[i].getLeadingMargin(true);
719 restWidth -= sp[i].getLeadingMargin(false);
Doug Feltcb3791202011-07-07 11:57:48 -0700720
Doug Feltc982f602010-05-25 11:51:40 -0700721 // LeadingMarginSpan2 is odd. The count affects all
Anish Athalyeab08c6d2014-08-08 12:09:58 -0700722 // leading margin spans, not just this particular one
Doug Feltc982f602010-05-25 11:51:40 -0700723 if (lms instanceof LeadingMarginSpan2) {
724 LeadingMarginSpan2 lms2 = (LeadingMarginSpan2) lms;
Anish Athalyec8f9e622014-07-21 15:26:34 -0700725 firstWidthLineCount = Math.max(firstWidthLineCount,
726 lms2.getLeadingMarginLineCount());
Mark Wagner7b5676e2009-10-16 11:44:23 -0700727 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800728 }
729
Fabrice Di Meglio121c82c2011-02-15 15:44:49 -0800730 chooseHt = getParagraphSpans(spanned, paraStart, paraEnd, LineHeightSpan.class);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800731
Roozbeh Pournader431e5062015-10-16 02:27:03 -0700732 if (chooseHt.length == 0) {
733 chooseHt = null; // So that out() would not assume it has any contents
734 } else {
Fabrice Di Meglio121c82c2011-02-15 15:44:49 -0800735 if (chooseHtv == null ||
736 chooseHtv.length < chooseHt.length) {
Adam Lesinski776abc22014-03-07 11:30:59 -0500737 chooseHtv = ArrayUtils.newUnpaddedIntArray(chooseHt.length);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800738 }
739
Fabrice Di Meglio121c82c2011-02-15 15:44:49 -0800740 for (int i = 0; i < chooseHt.length; i++) {
741 int o = spanned.getSpanStart(chooseHt[i]);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800742
Doug Felte8e45f22010-03-29 14:58:40 -0700743 if (o < paraStart) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800744 // starts in this layout, before the
745 // current paragraph
746
Fabrice Di Meglio121c82c2011-02-15 15:44:49 -0800747 chooseHtv[i] = getLineTop(getLineForOffset(o));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800748 } else {
749 // starts in this paragraph
750
Fabrice Di Meglio121c82c2011-02-15 15:44:49 -0800751 chooseHtv[i] = v;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800752 }
753 }
754 }
755 }
756
Raph Levien70616ec2015-03-04 10:41:30 -0800757 measured.setPara(source, paraStart, paraEnd, textDir, b);
Doug Felte8e45f22010-03-29 14:58:40 -0700758 char[] chs = measured.mChars;
759 float[] widths = measured.mWidths;
760 byte[] chdirs = measured.mLevels;
761 int dir = measured.mDir;
762 boolean easy = measured.mEasy;
Raph Levienc94f7422015-03-06 19:19:48 -0800763
764 // tab stop locations
765 int[] variableTabStops = null;
766 if (spanned != null) {
767 TabStopSpan[] spans = getParagraphSpans(spanned, paraStart,
768 paraEnd, TabStopSpan.class);
769 if (spans.length > 0) {
770 int[] stops = new int[spans.length];
771 for (int i = 0; i < spans.length; i++) {
772 stops[i] = spans[i].getTabStop();
773 }
774 Arrays.sort(stops, 0, stops.length);
775 variableTabStops = stops;
776 }
777 }
778
Raph Levienc94f7422015-03-06 19:19:48 -0800779 nSetupParagraph(b.mNativePtr, chs, paraEnd - paraStart,
780 firstWidth, firstWidthLineCount, restWidth,
Seigo Nonaka09da71a2016-11-28 16:24:14 +0900781 variableTabStops, TAB_INCREMENT, b.mBreakStrategy, b.mHyphenationFrequency,
Seigo Nonaka4b4730d2017-03-31 09:42:16 -0700782 // TODO: Support more justification mode, e.g. letter spacing, stretching.
Seigo Nonaka749e57e2017-08-29 15:00:05 -0700783 b.mJustificationMode != Layout.JUSTIFICATION_MODE_NONE, indents, mLineCount);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800784
Anish Athalyec8f9e622014-07-21 15:26:34 -0700785 // measurement has to be done before performing line breaking
786 // but we don't want to recompute fontmetrics or span ranges the
787 // second time, so we cache those and then use those stored values
788 int fmCacheCount = 0;
789 int spanEndCacheCount = 0;
Gilles Debunnecd943a72012-06-07 17:54:47 -0700790 for (int spanStart = paraStart, spanEnd; spanStart < paraEnd; spanStart = spanEnd) {
Anish Athalyec8f9e622014-07-21 15:26:34 -0700791 if (fmCacheCount * 4 >= fmCache.length) {
792 int[] grow = new int[fmCacheCount * 4 * 2];
793 System.arraycopy(fmCache, 0, grow, 0, fmCacheCount * 4);
794 fmCache = grow;
795 }
796
797 if (spanEndCacheCount >= spanEndCache.length) {
798 int[] grow = new int[spanEndCacheCount * 2];
799 System.arraycopy(spanEndCache, 0, grow, 0, spanEndCacheCount);
800 spanEndCache = grow;
801 }
Doug Felte8e45f22010-03-29 14:58:40 -0700802
Gilles Debunnecd943a72012-06-07 17:54:47 -0700803 if (spanned == null) {
804 spanEnd = paraEnd;
Doug Felt23241882010-06-02 14:41:06 -0700805 int spanLen = spanEnd - spanStart;
Gilles Debunnecd943a72012-06-07 17:54:47 -0700806 measured.addStyleRun(paint, spanLen, fm);
807 } else {
808 spanEnd = spanned.nextSpanTransition(spanStart, paraEnd,
809 MetricAffectingSpan.class);
810 int spanLen = spanEnd - spanStart;
811 MetricAffectingSpan[] spans =
Doug Felt23241882010-06-02 14:41:06 -0700812 spanned.getSpans(spanStart, spanEnd, MetricAffectingSpan.class);
Gilles Debunnecd943a72012-06-07 17:54:47 -0700813 spans = TextUtils.removeEmptySpans(spans, spanned, MetricAffectingSpan.class);
814 measured.addStyleRun(paint, spans, spanLen, fm);
Doug Felt23241882010-06-02 14:41:06 -0700815 }
816
Anish Athalyec8f9e622014-07-21 15:26:34 -0700817 // the order of storage here (top, bottom, ascent, descent) has to match the code below
818 // where these values are retrieved
819 fmCache[fmCacheCount * 4 + 0] = fm.top;
820 fmCache[fmCacheCount * 4 + 1] = fm.bottom;
821 fmCache[fmCacheCount * 4 + 2] = fm.ascent;
822 fmCache[fmCacheCount * 4 + 3] = fm.descent;
823 fmCacheCount++;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800824
Anish Athalyec8f9e622014-07-21 15:26:34 -0700825 spanEndCache[spanEndCacheCount] = spanEnd;
826 spanEndCacheCount++;
827 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800828
Raph Levien70616ec2015-03-04 10:41:30 -0800829 nGetWidths(b.mNativePtr, widths);
Raph Levienc94f7422015-03-06 19:19:48 -0800830 int breakCount = nComputeLineBreaks(b.mNativePtr, lineBreaks, lineBreaks.breaks,
Roozbeh Pournader737dfea2017-08-10 11:32:24 -0700831 lineBreaks.widths, lineBreaks.ascents, lineBreaks.descents, lineBreaks.flags,
832 lineBreaks.breaks.length);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800833
Roozbeh Pournader737dfea2017-08-10 11:32:24 -0700834 final int[] breaks = lineBreaks.breaks;
835 final float[] lineWidths = lineBreaks.widths;
836 final float[] ascents = lineBreaks.ascents;
837 final float[] descents = lineBreaks.descents;
838 final int[] flags = lineBreaks.flags;
Anish Athalyec8f9e622014-07-21 15:26:34 -0700839
Keisuke Kuroyanagif4a3f3a2015-06-10 09:37:32 +0900840 final int remainingLineCount = mMaximumVisibleLineCount - mLineCount;
841 final boolean ellipsisMayBeApplied = ellipsize != null
842 && (ellipsize == TextUtils.TruncateAt.END
843 || (mMaximumVisibleLineCount == 1
844 && ellipsize != TextUtils.TruncateAt.MARQUEE));
Roozbeh Pournader9ea756f2017-07-25 11:20:29 -0700845 if (0 < remainingLineCount && remainingLineCount < breakCount
846 && ellipsisMayBeApplied) {
Keisuke Kuroyanagif4a3f3a2015-06-10 09:37:32 +0900847 // Calculate width and flag.
848 float width = 0;
Roozbeh Pournader737dfea2017-08-10 11:32:24 -0700849 int flag = 0; // XXX May need to also have starting hyphen edit
Keisuke Kuroyanagif4a3f3a2015-06-10 09:37:32 +0900850 for (int i = remainingLineCount - 1; i < breakCount; i++) {
Keisuke Kuroyanagi78f0d832016-05-10 12:21:33 -0700851 if (i == breakCount - 1) {
852 width += lineWidths[i];
853 } else {
854 for (int j = (i == 0 ? 0 : breaks[i - 1]); j < breaks[i]; j++) {
855 width += widths[j];
856 }
857 }
Roozbeh Pournader737dfea2017-08-10 11:32:24 -0700858 flag |= flags[i] & TAB_MASK;
Keisuke Kuroyanagif4a3f3a2015-06-10 09:37:32 +0900859 }
Keisuke Kuroyanagi78f0d832016-05-10 12:21:33 -0700860 // Treat the last line and overflowed lines as a single line.
861 breaks[remainingLineCount - 1] = breaks[breakCount - 1];
Keisuke Kuroyanagif4a3f3a2015-06-10 09:37:32 +0900862 lineWidths[remainingLineCount - 1] = width;
863 flags[remainingLineCount - 1] = flag;
864
865 breakCount = remainingLineCount;
866 }
867
Anish Athalyec8f9e622014-07-21 15:26:34 -0700868 // here is the offset of the starting character of the line we are currently measuring
869 int here = paraStart;
870
871 int fmTop = 0, fmBottom = 0, fmAscent = 0, fmDescent = 0;
872 int fmCacheIndex = 0;
873 int spanEndCacheIndex = 0;
874 int breakIndex = 0;
875 for (int spanStart = paraStart, spanEnd; spanStart < paraEnd; spanStart = spanEnd) {
876 // retrieve end of span
877 spanEnd = spanEndCache[spanEndCacheIndex++];
878
879 // retrieve cached metrics, order matches above
880 fm.top = fmCache[fmCacheIndex * 4 + 0];
881 fm.bottom = fmCache[fmCacheIndex * 4 + 1];
882 fm.ascent = fmCache[fmCacheIndex * 4 + 2];
883 fm.descent = fmCache[fmCacheIndex * 4 + 3];
884 fmCacheIndex++;
885
886 if (fm.top < fmTop) {
887 fmTop = fm.top;
888 }
889 if (fm.ascent < fmAscent) {
890 fmAscent = fm.ascent;
891 }
892 if (fm.descent > fmDescent) {
893 fmDescent = fm.descent;
894 }
895 if (fm.bottom > fmBottom) {
896 fmBottom = fm.bottom;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800897 }
898
Anish Athalyec8f9e622014-07-21 15:26:34 -0700899 // skip breaks ending before current span range
900 while (breakIndex < breakCount && paraStart + breaks[breakIndex] < spanStart) {
901 breakIndex++;
902 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800903
Anish Athalyec8f9e622014-07-21 15:26:34 -0700904 while (breakIndex < breakCount && paraStart + breaks[breakIndex] <= spanEnd) {
905 int endPos = paraStart + breaks[breakIndex];
906
Raph Levience4155a2015-03-11 11:02:33 -0700907 boolean moreChars = (endPos < bufEnd);
Raph Levien4c02e832014-12-12 11:17:01 -0800908
Roozbeh Pournader737dfea2017-08-10 11:32:24 -0700909 final int ascent = fallbackLineSpacing
910 ? Math.min(fmAscent, (int) Math.round(ascents[breakIndex]))
911 : fmAscent;
912 final int descent = fallbackLineSpacing
913 ? Math.max(fmDescent, (int) Math.round(descents[breakIndex]))
914 : fmDescent;
Anish Athalyec8f9e622014-07-21 15:26:34 -0700915 v = out(source, here, endPos,
Roozbeh Pournader737dfea2017-08-10 11:32:24 -0700916 ascent, descent, fmTop, fmBottom,
Roozbeh Pournader431e5062015-10-16 02:27:03 -0700917 v, spacingmult, spacingadd, chooseHt, chooseHtv, fm, flags[breakIndex],
Anish Athalyec8f9e622014-07-21 15:26:34 -0700918 needMultiply, chdirs, dir, easy, bufEnd, includepad, trackpad,
Siyamed Sinir442c1512017-07-24 12:18:27 -0700919 addLastLineSpacing, chs, widths, paraStart, ellipsize,
920 ellipsizedWidth, lineWidths[breakIndex], paint, moreChars);
Anish Athalyec8f9e622014-07-21 15:26:34 -0700921
922 if (endPos < spanEnd) {
923 // preserve metrics for current span
924 fmTop = fm.top;
925 fmBottom = fm.bottom;
926 fmAscent = fm.ascent;
927 fmDescent = fm.descent;
928 } else {
929 fmTop = fmBottom = fmAscent = fmDescent = 0;
930 }
931
932 here = endPos;
933 breakIndex++;
934
Siyamed Sinir0745c722016-05-31 20:39:33 -0700935 if (mLineCount >= mMaximumVisibleLineCount && mEllipsized) {
Anish Athalyec8f9e622014-07-21 15:26:34 -0700936 return;
937 }
938 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800939 }
940
Fabrice Di Meglio121c82c2011-02-15 15:44:49 -0800941 if (paraEnd == bufEnd)
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800942 break;
943 }
944
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -0700945 if ((bufEnd == bufStart || source.charAt(bufEnd - 1) == CHAR_NEW_LINE) &&
Fabrice Di Meglioad0b0512011-10-04 17:21:26 -0700946 mLineCount < mMaximumVisibleLineCount) {
Raph Levien70616ec2015-03-04 10:41:30 -0800947 measured.setPara(source, bufEnd, bufEnd, textDir, b);
Fabrice Di Meglioe6318892013-06-18 20:03:41 -0700948
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800949 paint.getFontMetricsInt(fm);
950
951 v = out(source,
Fabrice Di Meglio121c82c2011-02-15 15:44:49 -0800952 bufEnd, bufEnd, fm.ascent, fm.descent,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800953 fm.top, fm.bottom,
954 v,
955 spacingmult, spacingadd, null,
Raph Levien26d443a2015-03-30 14:18:32 -0700956 null, fm, 0,
Fabrice Di Meglioe6318892013-06-18 20:03:41 -0700957 needMultiply, measured.mLevels, measured.mDir, measured.mEasy, bufEnd,
Siyamed Sinir442c1512017-07-24 12:18:27 -0700958 includepad, trackpad, addLastLineSpacing, null,
Gilles Debunned300e752011-10-17 13:37:36 -0700959 null, bufStart, ellipsize,
960 ellipsizedWidth, 0, paint, false);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800961 }
962 }
963
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -0700964 // The parameters that are not changed in the method are marked as final to make the code
965 // easier to understand.
966 private int out(final CharSequence text, final int start, final int end, int above, int below,
967 int top, int bottom, int v, final float spacingmult, final float spacingadd,
968 final LineHeightSpan[] chooseHt, final int[] chooseHtv, final Paint.FontMetricsInt fm,
969 final int flags, final boolean needMultiply, final byte[] chdirs, final int dir,
970 final boolean easy, final int bufEnd, final boolean includePad, final boolean trackPad,
971 final boolean addLastLineLineSpacing, final char[] chs, final float[] widths,
972 final int widthStart, final TextUtils.TruncateAt ellipsize, final float ellipsisWidth,
973 final float textWidth, final TextPaint paint, final boolean moreChars) {
Siyamed Siniraf398512017-07-25 19:08:42 -0700974 final int j = mLineCount;
975 final int off = j * mColumns;
976 final int want = off + mColumns + TOP;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800977 int[] lines = mLines;
978
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800979 if (want >= lines.length) {
Siyamed Siniraf398512017-07-25 19:08:42 -0700980 final int[] grow = ArrayUtils.newUnpaddedIntArray(GrowingArrayUtils.growSize(want));
Adam Lesinski776abc22014-03-07 11:30:59 -0500981 System.arraycopy(lines, 0, grow, 0, lines.length);
982 mLines = grow;
983 lines = grow;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800984 }
985
Siyamed Siniraf398512017-07-25 19:08:42 -0700986 if (j >= mLineDirections.length) {
987 final Directions[] grow = ArrayUtils.newUnpaddedArray(Directions.class,
988 GrowingArrayUtils.growSize(j));
989 System.arraycopy(mLineDirections, 0, grow, 0, mLineDirections.length);
990 mLineDirections = grow;
991 }
992
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -0700993 lines[off + START] = start;
994 lines[off + TOP] = v;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800995
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -0700996 // Information about hyphenation, tabs, and directions are needed for determining
997 // ellipsization, so the values should be assigned before ellipsization.
Eric Fischera9f1dd02009-08-12 15:00:10 -0700998
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -0700999 // TODO: could move TAB to share same column as HYPHEN, simplifying this code and gaining
1000 // one bit for start field
1001 lines[off + TAB] |= flags & TAB_MASK;
1002 lines[off + HYPHEN] = flags;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001003
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001004 lines[off + DIR] |= dir << DIR_SHIFT;
1005 // easy means all chars < the first RTL, so no emoji, no nothing
1006 // XXX a run with no text or all spaces is easy but might be an empty
1007 // RTL paragraph. Make sure easy is false if this is the case.
1008 if (easy) {
1009 mLineDirections[j] = DIRS_ALL_LEFT_TO_RIGHT;
1010 } else {
1011 mLineDirections[j] = AndroidBidi.directions(dir, chdirs, start - widthStart, chs,
1012 start - widthStart, end - start);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001013 }
1014
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001015 final boolean firstLine = (j == 0);
1016 final boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount);
Siyamed Sinir0745c722016-05-31 20:39:33 -07001017
1018 if (ellipsize != null) {
1019 // If there is only one line, then do any type of ellipsis except when it is MARQUEE
1020 // if there are multiple lines, just allow END ellipsis on the last line
1021 boolean forceEllipsis = moreChars && (mLineCount + 1 == mMaximumVisibleLineCount);
1022
1023 boolean doEllipsis =
1024 (((mMaximumVisibleLineCount == 1 && moreChars) || (firstLine && !moreChars)) &&
1025 ellipsize != TextUtils.TruncateAt.MARQUEE) ||
1026 (!firstLine && (currentLineIsTheLastVisibleOne || !moreChars) &&
1027 ellipsize == TextUtils.TruncateAt.END);
1028 if (doEllipsis) {
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001029 calculateEllipsis(text, start, end, widths, widthStart,
1030 ellipsisWidth - getTotalInsets(j), ellipsize, j,
1031 textWidth, paint, forceEllipsis, dir);
Siyamed Sinir0745c722016-05-31 20:39:33 -07001032 }
1033 }
1034
Siyamed Sinirfb0b2dc2017-07-26 22:08:24 -07001035 final boolean lastLine;
1036 if (mEllipsized) {
1037 lastLine = true;
1038 } else {
1039 final boolean lastCharIsNewLine = widthStart != bufEnd && bufEnd > 0
1040 && text.charAt(bufEnd - 1) == CHAR_NEW_LINE;
1041 if (end == bufEnd && !lastCharIsNewLine) {
1042 lastLine = true;
1043 } else if (start == bufEnd && lastCharIsNewLine) {
1044 lastLine = true;
1045 } else {
1046 lastLine = false;
1047 }
1048 }
Raph Leviend97b0972014-04-24 12:51:35 -07001049
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001050 if (chooseHt != null) {
1051 fm.ascent = above;
1052 fm.descent = below;
1053 fm.top = top;
1054 fm.bottom = bottom;
1055
1056 for (int i = 0; i < chooseHt.length; i++) {
1057 if (chooseHt[i] instanceof LineHeightSpan.WithDensity) {
1058 ((LineHeightSpan.WithDensity) chooseHt[i])
1059 .chooseHeight(text, start, end, chooseHtv[i], v, fm, paint);
1060
1061 } else {
1062 chooseHt[i].chooseHeight(text, start, end, chooseHtv[i], v, fm);
1063 }
1064 }
1065
1066 above = fm.ascent;
1067 below = fm.descent;
1068 top = fm.top;
1069 bottom = fm.bottom;
1070 }
1071
Raph Leviend97b0972014-04-24 12:51:35 -07001072 if (firstLine) {
Fabrice Di Meglio121c82c2011-02-15 15:44:49 -08001073 if (trackPad) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001074 mTopPadding = top - above;
1075 }
1076
Fabrice Di Meglio121c82c2011-02-15 15:44:49 -08001077 if (includePad) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001078 above = top;
1079 }
1080 }
Raph Leviend97b0972014-04-24 12:51:35 -07001081
Raph Leviend97b0972014-04-24 12:51:35 -07001082 if (lastLine) {
Fabrice Di Meglio121c82c2011-02-15 15:44:49 -08001083 if (trackPad) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001084 mBottomPadding = bottom - below;
1085 }
1086
Fabrice Di Meglio121c82c2011-02-15 15:44:49 -08001087 if (includePad) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001088 below = bottom;
1089 }
1090 }
1091
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001092 final int extra;
Siyamed Sinir442c1512017-07-24 12:18:27 -07001093 if (needMultiply && (addLastLineLineSpacing || !lastLine)) {
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001094 final double ex = (below - above) * (spacingmult - 1) + spacingadd;
Doug Felt10657582010-02-22 11:19:01 -08001095 if (ex >= 0) {
Fabrice Di Meglio121c82c2011-02-15 15:44:49 -08001096 extra = (int)(ex + EXTRA_ROUNDING);
Doug Felt10657582010-02-22 11:19:01 -08001097 } else {
Fabrice Di Meglio121c82c2011-02-15 15:44:49 -08001098 extra = -(int)(-ex + EXTRA_ROUNDING);
Doug Felt10657582010-02-22 11:19:01 -08001099 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001100 } else {
1101 extra = 0;
1102 }
1103
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001104 lines[off + DESCENT] = below + extra;
Siyamed Sinir0fa89d62017-07-24 20:46:41 -07001105 lines[off + EXTRA] = extra;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001106
Siyamed Sinir0745c722016-05-31 20:39:33 -07001107 // special case for non-ellipsized last visible line when maxLines is set
1108 // store the height as if it was ellipsized
1109 if (!mEllipsized && currentLineIsTheLastVisibleOne) {
1110 // below calculation as if it was the last line
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001111 final int maxLineBelow = includePad ? bottom : below;
Siyamed Sinir0745c722016-05-31 20:39:33 -07001112 // similar to the calculation of v below, without the extra.
1113 mMaxLineHeight = v + (maxLineBelow - above);
1114 }
1115
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001116 v += (below - above) + extra;
1117 lines[off + mColumns + START] = end;
1118 lines[off + mColumns + TOP] = v;
1119
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001120 mLineCount++;
1121 return v;
1122 }
1123
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001124 private void calculateEllipsis(CharSequence text, int lineStart, int lineEnd, float[] widths,
1125 int widthStart, float avail, TextUtils.TruncateAt where, int line, float textWidth,
1126 TextPaint paint, boolean forceEllipsis, int dir) {
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07001127 if (textWidth <= avail && !forceEllipsis) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001128 // Everything fits!
1129 mLines[mColumns * line + ELLIPSIS_START] = 0;
1130 mLines[mColumns * line + ELLIPSIS_COUNT] = 0;
1131 return;
1132 }
1133
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001134 float tempAvail = avail;
1135 int numberOfTries = 0;
1136 boolean lineFits = false;
1137 mWorkPaint.set(paint);
1138 do {
1139 final float ellipsizedWidth = guessEllipsis(text, lineStart, lineEnd, widths,
1140 widthStart, tempAvail, where, line, textWidth, mWorkPaint, forceEllipsis, dir);
1141 if (ellipsizedWidth <= avail) {
1142 lineFits = true;
1143 } else {
1144 numberOfTries++;
1145 if (numberOfTries > 10) {
1146 // If the text still doesn't fit after ten tries, assume it will never fit and
1147 // ellipsize it all.
1148 mLines[mColumns * line + ELLIPSIS_START] = 0;
1149 mLines[mColumns * line + ELLIPSIS_COUNT] = lineEnd - lineStart;
1150 lineFits = true;
1151 } else {
1152 // Some side effect of ellipsization has caused the text to go over the
1153 // available width. Let's make the available width shorter by exactly that
1154 // amount and retry.
1155 tempAvail -= ellipsizedWidth - avail;
1156 }
1157 }
1158 } while (!lineFits);
1159 mEllipsized = true;
1160 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001161
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001162 // Returns the width of the ellipsized line which in some rare cases can actually be larger
1163 // than 'avail' (due to kerning or other context-based effect of replacement of text by
1164 // ellipsis). If all the line needs to ellipsized away, or it's an invalud hyphenation mode,
1165 // returns 0 so the caller can stop iterating.
1166 //
1167 // This method temporarily modifies the TextPaint passed to it, so the TextPaint passed to it
1168 // should not be accessed while the method is running.
1169 private float guessEllipsis(CharSequence text, int lineStart, int lineEnd, float[] widths,
1170 int widthStart, float avail, TextUtils.TruncateAt where, int line, float textWidth,
1171 TextPaint paint, boolean forceEllipsis, int dir) {
1172 final int savedHyphenEdit = paint.getHyphenEdit();
1173 paint.setHyphenEdit(0);
1174 final float ellipsisWidth = paint.measureText(TextUtils.getEllipsisString(where));
1175 final int ellipsisStart;
1176 final int ellipsisCount;
1177 final int len = lineEnd - lineStart;
1178 final int offset = lineStart - widthStart;
1179
1180 int hyphen = getHyphen(line);
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07001181 // We only support start ellipsis on a single line
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001182 if (where == TextUtils.TruncateAt.START) {
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07001183 if (mMaximumVisibleLineCount == 1) {
1184 float sum = 0;
1185 int i;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001186
Keisuke Kuroyanagied2eea12015-04-14 18:18:35 +09001187 for (i = len; i > 0; i--) {
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001188 final float w = widths[i - 1 + offset];
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07001189 if (w + sum + ellipsisWidth > avail) {
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001190 while (i < len && widths[i + offset] == 0.0f) {
Keisuke Kuroyanagi82d1c442016-09-14 14:30:14 +09001191 i++;
1192 }
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07001193 break;
1194 }
1195
1196 sum += w;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001197 }
1198
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07001199 ellipsisStart = 0;
1200 ellipsisCount = i;
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001201 // Strip the potential hyphenation at beginning of line.
1202 hyphen &= ~Paint.HYPHENEDIT_MASK_START_OF_LINE;
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07001203 } else {
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001204 ellipsisStart = 0;
1205 ellipsisCount = 0;
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07001206 if (Log.isLoggable(TAG, Log.WARN)) {
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001207 Log.w(TAG, "Start ellipsis only supported with one line");
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07001208 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001209 }
Fabrice Di Megliocb332642011-09-23 19:08:04 -07001210 } else if (where == TextUtils.TruncateAt.END || where == TextUtils.TruncateAt.MARQUEE ||
1211 where == TextUtils.TruncateAt.END_SMALL) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001212 float sum = 0;
1213 int i;
1214
1215 for (i = 0; i < len; i++) {
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001216 final float w = widths[i + offset];
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001217
Fabrice Di Meglio121c82c2011-02-15 15:44:49 -08001218 if (w + sum + ellipsisWidth > avail) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001219 break;
1220 }
1221
1222 sum += w;
1223 }
1224
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001225 if (forceEllipsis && i == len && len > 0) {
Fabrice Di Meglioaef455f2011-08-29 15:39:11 -07001226 ellipsisStart = len - 1;
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07001227 ellipsisCount = 1;
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001228 } else {
1229 ellipsisStart = i;
1230 ellipsisCount = len - i;
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07001231 }
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001232 // Strip the potential hyphenation at end of line.
1233 hyphen &= ~Paint.HYPHENEDIT_MASK_END_OF_LINE;
1234 } else { // where = TextUtils.TruncateAt.MIDDLE
1235 // We only support middle ellipsis on a single line.
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07001236 if (mMaximumVisibleLineCount == 1) {
1237 float lsum = 0, rsum = 0;
1238 int left = 0, right = len;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001239
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001240 final float ravail = (avail - ellipsisWidth) / 2;
Raph Levien0e3c5e82014-12-04 13:26:07 -08001241 for (right = len; right > 0; right--) {
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001242 final float w = widths[right - 1 + offset];
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001243
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07001244 if (w + rsum > ravail) {
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001245 while (right < len && widths[right + offset] == 0.0f) {
Keisuke Kuroyanagi82d1c442016-09-14 14:30:14 +09001246 right++;
1247 }
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07001248 break;
1249 }
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07001250 rsum += w;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001251 }
1252
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001253 final float lavail = avail - ellipsisWidth - rsum;
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07001254 for (left = 0; left < right; left++) {
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001255 final float w = widths[left + offset];
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001256
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07001257 if (w + lsum > lavail) {
1258 break;
1259 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001260
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07001261 lsum += w;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001262 }
1263
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07001264 ellipsisStart = left;
1265 ellipsisCount = right - left;
1266 } else {
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001267 ellipsisStart = 0;
1268 ellipsisCount = 0;
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07001269 if (Log.isLoggable(TAG, Log.WARN)) {
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001270 Log.w(TAG, "Middle ellipsis only supported with one line");
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07001271 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001272 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001273 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001274 mLines[mColumns * line + ELLIPSIS_START] = ellipsisStart;
1275 mLines[mColumns * line + ELLIPSIS_COUNT] = ellipsisCount;
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001276
1277 if (ellipsisStart == 0 && (ellipsisCount == 0 || ellipsisCount == len)) {
1278 // Unsupported ellipsization mode or all text is ellipsized away. Return 0.
1279 return 0.0f;
1280 }
1281
1282 final boolean isSpanned = text instanceof Spanned;
1283 final Ellipsizer ellipsizedText = isSpanned
1284 ? new SpannedEllipsizer(text)
1285 : new Ellipsizer(text);
1286 ellipsizedText.mLayout = this;
1287 ellipsizedText.mMethod = where;
1288
1289 final boolean hasTabs = getLineContainsTab(line);
1290 final TabStops tabStops;
1291 if (hasTabs && isSpanned) {
1292 final TabStopSpan[] tabs = getParagraphSpans((Spanned) ellipsizedText, lineStart,
1293 lineEnd, TabStopSpan.class);
1294 if (tabs.length == 0) {
1295 tabStops = null;
1296 } else {
1297 tabStops = new TabStops(TAB_INCREMENT, tabs);
1298 }
1299 } else {
1300 tabStops = null;
1301 }
1302 paint.setHyphenEdit(hyphen);
1303 final TextLine textline = TextLine.obtain();
1304 textline.set(paint, ellipsizedText, lineStart, lineEnd, dir, getLineDirections(line),
1305 hasTabs, tabStops);
1306 // Since TextLine.metric() returns negative values for RTL text, multiplication by dir
1307 // converts it to an actual width. Note that we don't want to use the absolute value,
1308 // since we may actually have glyphs with negative advances, which by definition always
1309 // fit.
1310 final float ellipsizedWidth = textline.metrics(null) * dir;
1311 TextLine.recycle(textline);
1312 paint.setHyphenEdit(savedHyphenEdit);
1313 return ellipsizedWidth;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001314 }
1315
Selim Cinek365ec092017-03-09 00:10:52 -08001316 private float getTotalInsets(int line) {
1317 int totalIndent = 0;
1318 if (mLeftIndents != null) {
1319 totalIndent = mLeftIndents[Math.min(line, mLeftIndents.length - 1)];
1320 }
1321 if (mRightIndents != null) {
1322 totalIndent += mRightIndents[Math.min(line, mRightIndents.length - 1)];
1323 }
1324 return totalIndent;
1325 }
1326
Doug Felte8e45f22010-03-29 14:58:40 -07001327 // Override the base class so we can directly access our members,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001328 // rather than relying on member functions.
1329 // The logic mirrors that of Layout.getLineForVertical
1330 // FIXME: It may be faster to do a linear search for layouts without many lines.
Gilles Debunne66111472010-11-19 11:04:37 -08001331 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001332 public int getLineForVertical(int vertical) {
1333 int high = mLineCount;
1334 int low = -1;
1335 int guess;
1336 int[] lines = mLines;
1337 while (high - low > 1) {
1338 guess = (high + low) >> 1;
1339 if (lines[mColumns * guess + TOP] > vertical){
1340 high = guess;
1341 } else {
1342 low = guess;
1343 }
1344 }
1345 if (low < 0) {
1346 return 0;
1347 } else {
1348 return low;
1349 }
1350 }
1351
Gilles Debunne66111472010-11-19 11:04:37 -08001352 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001353 public int getLineCount() {
1354 return mLineCount;
1355 }
1356
Gilles Debunne66111472010-11-19 11:04:37 -08001357 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001358 public int getLineTop(int line) {
Raph Levien07e6c232016-04-04 12:34:06 -07001359 return mLines[mColumns * line + TOP];
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001360 }
1361
Siyamed Sinir0fa89d62017-07-24 20:46:41 -07001362 /**
1363 * @hide
1364 */
1365 @Override
1366 public int getLineExtra(int line) {
1367 return mLines[mColumns * line + EXTRA];
1368 }
1369
Gilles Debunne66111472010-11-19 11:04:37 -08001370 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001371 public int getLineDescent(int line) {
Raph Levien07e6c232016-04-04 12:34:06 -07001372 return mLines[mColumns * line + DESCENT];
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001373 }
1374
Gilles Debunne66111472010-11-19 11:04:37 -08001375 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001376 public int getLineStart(int line) {
1377 return mLines[mColumns * line + START] & START_MASK;
1378 }
1379
Gilles Debunne66111472010-11-19 11:04:37 -08001380 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001381 public int getParagraphDirection(int line) {
1382 return mLines[mColumns * line + DIR] >> DIR_SHIFT;
1383 }
1384
Gilles Debunne66111472010-11-19 11:04:37 -08001385 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001386 public boolean getLineContainsTab(int line) {
1387 return (mLines[mColumns * line + TAB] & TAB_MASK) != 0;
1388 }
1389
Gilles Debunne66111472010-11-19 11:04:37 -08001390 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001391 public final Directions getLineDirections(int line) {
Siyamed Sinir0fa89d62017-07-24 20:46:41 -07001392 if (line > getLineCount()) {
1393 throw new ArrayIndexOutOfBoundsException();
1394 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001395 return mLineDirections[line];
1396 }
1397
Gilles Debunne66111472010-11-19 11:04:37 -08001398 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001399 public int getTopPadding() {
1400 return mTopPadding;
1401 }
1402
Gilles Debunne66111472010-11-19 11:04:37 -08001403 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001404 public int getBottomPadding() {
1405 return mBottomPadding;
1406 }
1407
Raph Levien26d443a2015-03-30 14:18:32 -07001408 /**
1409 * @hide
1410 */
1411 @Override
1412 public int getHyphen(int line) {
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +09001413 return mLines[mColumns * line + HYPHEN] & HYPHEN_MASK;
Raph Levien26d443a2015-03-30 14:18:32 -07001414 }
1415
Raph Levien2ea52902015-07-01 14:39:31 -07001416 /**
1417 * @hide
1418 */
1419 @Override
1420 public int getIndentAdjust(int line, Alignment align) {
1421 if (align == Alignment.ALIGN_LEFT) {
1422 if (mLeftIndents == null) {
1423 return 0;
1424 } else {
1425 return mLeftIndents[Math.min(line, mLeftIndents.length - 1)];
1426 }
1427 } else if (align == Alignment.ALIGN_RIGHT) {
1428 if (mRightIndents == null) {
1429 return 0;
1430 } else {
1431 return -mRightIndents[Math.min(line, mRightIndents.length - 1)];
1432 }
1433 } else if (align == Alignment.ALIGN_CENTER) {
1434 int left = 0;
1435 if (mLeftIndents != null) {
1436 left = mLeftIndents[Math.min(line, mLeftIndents.length - 1)];
1437 }
1438 int right = 0;
1439 if (mRightIndents != null) {
1440 right = mRightIndents[Math.min(line, mRightIndents.length - 1)];
1441 }
1442 return (left - right) >> 1;
1443 } else {
1444 throw new AssertionError("unhandled alignment " + align);
1445 }
1446 }
1447
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001448 @Override
1449 public int getEllipsisCount(int line) {
1450 if (mColumns < COLUMNS_ELLIPSIZE) {
1451 return 0;
1452 }
1453
1454 return mLines[mColumns * line + ELLIPSIS_COUNT];
1455 }
1456
1457 @Override
1458 public int getEllipsisStart(int line) {
1459 if (mColumns < COLUMNS_ELLIPSIZE) {
1460 return 0;
1461 }
1462
1463 return mLines[mColumns * line + ELLIPSIS_START];
1464 }
1465
1466 @Override
1467 public int getEllipsizedWidth() {
1468 return mEllipsizedWidth;
1469 }
1470
Siyamed Sinir0745c722016-05-31 20:39:33 -07001471 /**
1472 * Return the total height of this layout.
1473 *
1474 * @param cap if true and max lines is set, returns the height of the layout at the max lines.
1475 *
1476 * @hide
1477 */
1478 public int getHeight(boolean cap) {
1479 if (cap && mLineCount >= mMaximumVisibleLineCount && mMaxLineHeight == -1 &&
1480 Log.isLoggable(TAG, Log.WARN)) {
1481 Log.w(TAG, "maxLineHeight should not be -1. "
1482 + " maxLines:" + mMaximumVisibleLineCount
1483 + " lineCount:" + mLineCount);
1484 }
1485
1486 return cap && mLineCount >= mMaximumVisibleLineCount && mMaxLineHeight != -1 ?
1487 mMaxLineHeight : super.getHeight();
1488 }
1489
Raph Levien70616ec2015-03-04 10:41:30 -08001490 private static native long nNewBuilder();
1491 private static native void nFreeBuilder(long nativePtr);
1492 private static native void nFinishBuilder(long nativePtr);
Raph Levien26d443a2015-03-30 14:18:32 -07001493
Roozbeh Pournadera59c3fe2017-02-27 10:13:44 -08001494 /* package */ static native long nLoadHyphenator(ByteBuffer buf, int offset,
1495 int minPrefix, int minSuffix);
Raph Levien26d443a2015-03-30 14:18:32 -07001496
Roozbeh Pournaderef7cfa12017-06-15 12:39:04 -07001497 private static native void nSetLocales(long nativePtr, String locales,
1498 long[] nativeHyphenators);
Raph Levien70616ec2015-03-04 10:41:30 -08001499
Raph Levienc94f7422015-03-06 19:19:48 -08001500 // Set up paragraph text and settings; done as one big method to minimize jni crossings
Seigo Nonakabafe1972017-08-24 15:30:29 -07001501 private static native void nSetupParagraph(
1502 @NonNull long nativePtr, @NonNull char[] text, @IntRange(from = 0) int length,
1503 @FloatRange(from = 0.0f) float firstWidth, @IntRange(from = 0) int firstWidthLineCount,
1504 @FloatRange(from = 0.0f) float restWidth, @Nullable int[] variableTabStops,
1505 int defaultTabStop, @BreakStrategy int breakStrategy,
1506 @HyphenationFrequency int hyphenationFrequency, boolean isJustified,
Seigo Nonaka749e57e2017-08-29 15:00:05 -07001507 @Nullable int[] indents, @IntRange(from = 0) int indentsOffset);
Raph Levien70616ec2015-03-04 10:41:30 -08001508
Seigo Nonaka318ca042017-08-01 16:36:18 -07001509 private static native float nAddStyleRun(long nativePtr, long nativePaint, int start, int end,
1510 boolean isRtl);
Raph Levien70616ec2015-03-04 10:41:30 -08001511
1512 private static native void nAddMeasuredRun(long nativePtr,
1513 int start, int end, float[] widths);
1514
1515 private static native void nAddReplacementRun(long nativePtr, int start, int end, float width);
1516
1517 private static native void nGetWidths(long nativePtr, float[] widths);
1518
Anish Athalyec8f9e622014-07-21 15:26:34 -07001519 // populates LineBreaks and returns the number of breaks found
1520 //
1521 // the arrays inside the LineBreaks objects are passed in as well
1522 // to reduce the number of JNI calls in the common case where the
1523 // arrays do not have to be resized
Raph Levienc94f7422015-03-06 19:19:48 -08001524 private static native int nComputeLineBreaks(long nativePtr, LineBreaks recycle,
Roozbeh Pournader737dfea2017-08-10 11:32:24 -07001525 int[] recycleBreaks, float[] recycleWidths, float[] recycleAscents,
1526 float[] recycleDescents, int[] recycleFlags, int recycleLength);
Anish Athalye88b5b0b2014-06-24 14:39:43 -07001527
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001528 private int mLineCount;
1529 private int mTopPadding, mBottomPadding;
1530 private int mColumns;
1531 private int mEllipsizedWidth;
1532
Siyamed Sinir0745c722016-05-31 20:39:33 -07001533 /**
1534 * Keeps track if ellipsize is applied to the text.
1535 */
1536 private boolean mEllipsized;
1537
1538 /**
1539 * If maxLines is set, ellipsize is not set, and the actual line count of text is greater than
1540 * or equal to maxLine, this variable holds the ideal visual height of the maxLine'th line
1541 * starting from the top of the layout. If maxLines is not set its value will be -1.
1542 *
1543 * The value is the same as getLineTop(maxLines) for ellipsized version where structurally no
1544 * more than maxLines is contained.
1545 */
Siyamed Sinira19cd512017-08-03 22:01:56 -07001546 private int mMaxLineHeight = DEFAULT_MAX_LINE_HEIGHT;
Siyamed Sinir0745c722016-05-31 20:39:33 -07001547
Roozbeh Pournadere88b5df2017-07-25 13:52:57 -07001548 private TextPaint mWorkPaint = new TextPaint();
1549
Siyamed Sinir0fa89d62017-07-24 20:46:41 -07001550 private static final int COLUMNS_NORMAL = 5;
1551 private static final int COLUMNS_ELLIPSIZE = 7;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001552 private static final int START = 0;
1553 private static final int DIR = START;
1554 private static final int TAB = START;
1555 private static final int TOP = 1;
1556 private static final int DESCENT = 2;
Siyamed Sinir0fa89d62017-07-24 20:46:41 -07001557 private static final int EXTRA = 3;
1558 private static final int HYPHEN = 4;
1559 private static final int ELLIPSIS_START = 5;
1560 private static final int ELLIPSIS_COUNT = 6;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001561
1562 private int[] mLines;
1563 private Directions[] mLineDirections;
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07001564 private int mMaximumVisibleLineCount = Integer.MAX_VALUE;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001565
1566 private static final int START_MASK = 0x1FFFFFFF;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001567 private static final int DIR_SHIFT = 30;
1568 private static final int TAB_MASK = 0x20000000;
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +09001569 private static final int HYPHEN_MASK = 0xFF;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001570
Doug Feltc982f602010-05-25 11:51:40 -07001571 private static final int TAB_INCREMENT = 20; // same as Layout, but that's private
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001572
Fabrice Di Meglio121c82c2011-02-15 15:44:49 -08001573 private static final char CHAR_NEW_LINE = '\n';
Fabrice Di Meglio121c82c2011-02-15 15:44:49 -08001574
1575 private static final double EXTRA_ROUNDING = 0.5;
Fabrice Di Megliocb332642011-09-23 19:08:04 -07001576
Siyamed Sinira19cd512017-08-03 22:01:56 -07001577 private static final int DEFAULT_MAX_LINE_HEIGHT = -1;
1578
Anish Athalyec8f9e622014-07-21 15:26:34 -07001579 // This is used to return three arrays from a single JNI call when
1580 // performing line breaking
Deepanshu Gupta70539192014-10-15 15:57:40 -07001581 /*package*/ static class LineBreaks {
Anish Athalyec8f9e622014-07-21 15:26:34 -07001582 private static final int INITIAL_SIZE = 16;
1583 public int[] breaks = new int[INITIAL_SIZE];
1584 public float[] widths = new float[INITIAL_SIZE];
Roozbeh Pournader737dfea2017-08-10 11:32:24 -07001585 public float[] ascents = new float[INITIAL_SIZE];
1586 public float[] descents = new float[INITIAL_SIZE];
Roozbeh Pournader112d9c72015-08-07 12:44:41 -07001587 public int[] flags = new int[INITIAL_SIZE]; // hasTab
Anish Athalyec8f9e622014-07-21 15:26:34 -07001588 // breaks, widths, and flags should all have the same length
1589 }
1590
Seigo Nonakabafe1972017-08-24 15:30:29 -07001591 @Nullable private int[] mLeftIndents;
1592 @Nullable private int[] mRightIndents;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001593}