Skip to content

Commit d49fff3

Browse files
ymariandsn5ft
authored andcommitted
Add a StaticLayoutCompat class that supports Max Lines
PiperOrigin-RevId: 286223820
1 parent dc17035 commit d49fff3

File tree

2 files changed

+343
-0
lines changed

2 files changed

+343
-0
lines changed
Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
/*
2+
* Copyright (C) 2019 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+
17+
package com.google.android.material.internal;
18+
19+
import static androidx.core.util.Preconditions.checkNotNull;
20+
21+
import android.os.Build;
22+
import android.os.Build.VERSION_CODES;
23+
import androidx.annotation.IntRange;
24+
import androidx.annotation.NonNull;
25+
import androidx.annotation.Nullable;
26+
import androidx.annotation.RestrictTo;
27+
import androidx.annotation.RestrictTo.Scope;
28+
import android.text.Layout;
29+
import android.text.Layout.Alignment;
30+
import android.text.StaticLayout;
31+
import android.text.TextDirectionHeuristic;
32+
import android.text.TextDirectionHeuristics;
33+
import android.text.TextPaint;
34+
import android.text.TextUtils;
35+
import java.lang.reflect.Constructor;
36+
37+
/**
38+
* Class to create StaticLayout using StaticLayout.Builder on API23+ and a hidden StaticLayout
39+
* constructor before that.
40+
*
41+
* <p>Usage:
42+
*
43+
* <pre>{@code
44+
* StaticLayout staticLayout =
45+
* StaticLayoutBuilderCompat.obtain("Lorem Ipsum", new TextPaint(), 100)
46+
* .setAlignment(Alignment.ALIGN_NORMAL)
47+
* .build();
48+
* }</pre>
49+
*
50+
* @hide
51+
*/
52+
@RestrictTo(Scope.LIBRARY_GROUP)
53+
final class StaticLayoutBuilderCompat {
54+
55+
private static final String TEXT_DIR_CLASS = "android.text.TextDirectionHeuristic";
56+
private static final String TEXT_DIRS_CLASS = "android.text.TextDirectionHeuristics";
57+
private static final String TEXT_DIR_FIRSTSTRONG_LTR = "FIRSTSTRONG_LTR";
58+
59+
private static boolean initialized;
60+
61+
@Nullable private static Constructor<StaticLayout> constructor;
62+
@Nullable private static Object textDirection;
63+
64+
private final CharSequence source;
65+
private final TextPaint paint;
66+
private final int width;
67+
private int start;
68+
private int end;
69+
70+
private Alignment alignment;
71+
private int maxLines;
72+
private boolean includePad;
73+
@Nullable private TextUtils.TruncateAt ellipsize;
74+
75+
private StaticLayoutBuilderCompat(CharSequence source, TextPaint paint, int width) {
76+
this.source = source;
77+
this.paint = paint;
78+
this.width = width;
79+
this.start = 0;
80+
this.end = source.length();
81+
this.alignment = Alignment.ALIGN_NORMAL;
82+
this.maxLines = Integer.MAX_VALUE;
83+
this.includePad = true;
84+
this.ellipsize = null;
85+
}
86+
87+
/**
88+
* Obtain a builder for constructing StaticLayout objects.
89+
*
90+
* @param source The text to be laid out, optionally with spans
91+
* @param paint The base paint used for layout
92+
* @param width The width in pixels
93+
* @return a builder object used for constructing the StaticLayout
94+
*/
95+
@NonNull
96+
public static StaticLayoutBuilderCompat obtain(
97+
@NonNull CharSequence source, @NonNull TextPaint paint, @IntRange(from = 0) int width) {
98+
return new StaticLayoutBuilderCompat(source, paint, width);
99+
}
100+
101+
/**
102+
* Set the alignment. The default is {@link Layout.Alignment#ALIGN_NORMAL}.
103+
*
104+
* @param alignment Alignment for the resulting {@link StaticLayout}
105+
* @return this builder, useful for chaining
106+
*/
107+
@NonNull
108+
public StaticLayoutBuilderCompat setAlignment(@NonNull Alignment alignment) {
109+
this.alignment = alignment;
110+
return this;
111+
}
112+
113+
/**
114+
* Set whether to include extra space beyond font ascent and descent (which is needed to avoid
115+
* clipping in some languages, such as Arabic and Kannada). The default is {@code true}.
116+
*
117+
* @param includePad whether to include padding
118+
* @return this builder, useful for chaining
119+
* @see android.widget.TextView#setIncludeFontPadding
120+
*/
121+
@NonNull
122+
public StaticLayoutBuilderCompat setIncludePad(boolean includePad) {
123+
this.includePad = includePad;
124+
return this;
125+
}
126+
127+
/**
128+
* Set the index of the start of the text
129+
*
130+
* @return this builder, useful for chaining
131+
*/
132+
@NonNull
133+
public StaticLayoutBuilderCompat setStart(@IntRange(from = 0) int start) {
134+
this.start = start;
135+
return this;
136+
}
137+
138+
/**
139+
* Set the index + 1 of the end of the text
140+
*
141+
* @return this builder, useful for chaining
142+
* @see android.widget.TextView#setIncludeFontPadding
143+
*/
144+
@NonNull
145+
public StaticLayoutBuilderCompat setEnd(@IntRange(from = 0) int end) {
146+
this.end = end;
147+
return this;
148+
}
149+
150+
/**
151+
* Set maximum number of lines. This is particularly useful in the case of ellipsizing, where it
152+
* changes the layout of the last line. The default is unlimited.
153+
*
154+
* @param maxLines maximum number of lines in the layout
155+
* @return this builder, useful for chaining
156+
* @see android.widget.TextView#setMaxLines
157+
*/
158+
@NonNull
159+
public StaticLayoutBuilderCompat setMaxLines(@IntRange(from = 0) int maxLines) {
160+
this.maxLines = maxLines;
161+
return this;
162+
}
163+
164+
/**
165+
* Set ellipsizing on the layout. Causes words that are longer than the view is wide, or exceeding
166+
* the number of lines (see #setMaxLines).
167+
*
168+
* @param ellipsize type of ellipsis behavior
169+
* @return this builder, useful for chaining
170+
* @see android.widget.TextView#setEllipsize
171+
*/
172+
@NonNull
173+
public StaticLayoutBuilderCompat setEllipsize(@Nullable TextUtils.TruncateAt ellipsize) {
174+
this.ellipsize = ellipsize;
175+
return this;
176+
}
177+
178+
/** A method that allows to create a StaticLayout with maxLines on all supported API levels. */
179+
public StaticLayout build() throws StaticLayoutBuilderCompatException {
180+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
181+
// Marshmallow introduced StaticLayout.Builder which allows us not to use
182+
// the hidden constructor.
183+
StaticLayout.Builder builder = StaticLayout.Builder.obtain(source, start, end, paint, width);
184+
builder.setAlignment(alignment);
185+
builder.setIncludePad(includePad);
186+
if (ellipsize != null) {
187+
builder.setEllipsize(ellipsize);
188+
}
189+
builder.setMaxLines(maxLines);
190+
return builder.build();
191+
}
192+
193+
createConstructorWithReflection();
194+
195+
// Use the hidden constructor on older API levels.
196+
try {
197+
return checkNotNull(constructor)
198+
.newInstance(
199+
source,
200+
start,
201+
end,
202+
paint,
203+
width,
204+
alignment,
205+
checkNotNull(textDirection),
206+
1.0f,
207+
0.0f,
208+
includePad,
209+
ellipsize,
210+
width,
211+
maxLines);
212+
} catch (Exception cause) {
213+
throw new StaticLayoutBuilderCompatException(cause);
214+
}
215+
}
216+
217+
/**
218+
* set constructor to this hidden {@link StaticLayout constructor.}
219+
*
220+
* <pre>{@code
221+
* StaticLayout(
222+
* CharSequence source,
223+
* int bufstart,
224+
* int bufend,
225+
* TextPaint paint,
226+
* int outerwidth,
227+
* Alignment align,
228+
* TextDirectionHeuristic textDir,
229+
* float spacingmult,
230+
* float spacingadd,
231+
* boolean includepad,
232+
* TextUtils.TruncateAt ellipsize,
233+
* int ellipsizedWidth,
234+
* int maxLines)
235+
* }</pre>
236+
*/
237+
private static void createConstructorWithReflection() throws StaticLayoutBuilderCompatException {
238+
if (initialized) {
239+
return;
240+
}
241+
242+
try {
243+
final Class<?> textDirClass;
244+
if (Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR2) {
245+
textDirClass = TextDirectionHeuristic.class;
246+
textDirection = TextDirectionHeuristics.FIRSTSTRONG_LTR;
247+
} else {
248+
ClassLoader loader = StaticLayoutBuilderCompat.class.getClassLoader();
249+
textDirClass = loader.loadClass(TEXT_DIR_CLASS);
250+
Class<?> textDirsClass = loader.loadClass(TEXT_DIRS_CLASS);
251+
textDirection = textDirsClass.getField(TEXT_DIR_FIRSTSTRONG_LTR).get(textDirsClass);
252+
}
253+
254+
final Class<?>[] signature =
255+
new Class<?>[] {
256+
CharSequence.class,
257+
int.class,
258+
int.class,
259+
TextPaint.class,
260+
int.class,
261+
Alignment.class,
262+
textDirClass,
263+
float.class,
264+
float.class,
265+
boolean.class,
266+
TextUtils.TruncateAt.class,
267+
int.class,
268+
int.class
269+
};
270+
271+
constructor = StaticLayout.class.getDeclaredConstructor(signature);
272+
constructor.setAccessible(true);
273+
initialized = true;
274+
} catch (Exception cause) {
275+
throw new StaticLayoutBuilderCompatException(cause);
276+
}
277+
}
278+
279+
static class StaticLayoutBuilderCompatException extends Exception {
280+
281+
StaticLayoutBuilderCompatException(Throwable cause) {
282+
super("Error thrown initializing StaticLayout", cause);
283+
}
284+
}
285+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright (C) 2019 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+
17+
package com.google.android.material.internal;
18+
19+
import static android.os.Build.VERSION_CODES.LOLLIPOP;
20+
import static com.google.common.truth.Truth.assertThat;
21+
22+
import android.os.Build.VERSION_CODES;
23+
import android.text.Layout.Alignment;
24+
import android.text.StaticLayout;
25+
import android.text.TextPaint;
26+
import android.text.TextUtils.TruncateAt;
27+
import org.junit.Test;
28+
import org.junit.runner.RunWith;
29+
import org.robolectric.RobolectricTestRunner;
30+
import org.robolectric.annotation.Config;
31+
32+
/** Tests for {@link StaticLayoutBuilderCompat} */
33+
@RunWith(RobolectricTestRunner.class)
34+
public class StaticLayoutBuilderCompatTest {
35+
36+
private static final String LONG_STRING =
37+
"Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. "
38+
+ "Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur "
39+
+ "ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. "
40+
+ "Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, "
41+
+ "vulputate eget, arcu. In enim justo, rhoncus ut, imperdiet a";
42+
43+
@Config(minSdk = LOLLIPOP, maxSdk = VERSION_CODES.P)
44+
@Test
45+
public void createStaticLayout_withMaxLines_LongString() throws Exception {
46+
int maxLines = 3;
47+
48+
StaticLayout staticLayout =
49+
StaticLayoutBuilderCompat.obtain(LONG_STRING, new TextPaint(), 100)
50+
.setAlignment(Alignment.ALIGN_NORMAL)
51+
.setIncludePad(true)
52+
.setEllipsize(TruncateAt.END)
53+
.setMaxLines(maxLines)
54+
.build();
55+
56+
assertThat(staticLayout).isNotNull();
57+
}
58+
}

0 commit comments

Comments
 (0)