1 /*
2  * Copyright 2018 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 package androidx.compose.ui.text.android
17 
18 import android.graphics.text.LineBreakConfig
19 import android.os.Build
20 import android.text.Layout.Alignment
21 import android.text.StaticLayout
22 import android.text.StaticLayout.Builder
23 import android.text.TextDirectionHeuristic
24 import android.text.TextPaint
25 import android.text.TextUtils.TruncateAt
26 import android.util.Log
27 import androidx.annotation.FloatRange
28 import androidx.annotation.IntRange
29 import androidx.annotation.RequiresApi
30 import androidx.compose.ui.text.android.LayoutCompat.BreakStrategy
31 import androidx.compose.ui.text.android.LayoutCompat.HyphenationFrequency
32 import androidx.compose.ui.text.android.LayoutCompat.JustificationMode
33 import androidx.compose.ui.text.android.LayoutCompat.LineBreakStyle
34 import androidx.compose.ui.text.android.LayoutCompat.LineBreakWordStyle
35 import androidx.compose.ui.text.internal.requirePrecondition
36 import java.lang.reflect.Constructor
37 import java.lang.reflect.InvocationTargetException
38 
39 private const val TAG = "StaticLayoutFactory"
40 
41 @InternalPlatformTextApi
42 object StaticLayoutFactory {
43 
44     private val delegate: StaticLayoutFactoryImpl =
45         if (Build.VERSION.SDK_INT >= 23) {
46             StaticLayoutFactory23()
47         } else {
48             StaticLayoutFactoryDefault()
49         }
50 
51     /** Builder class for StaticLayout. */
createnull52     fun create(
53         text: CharSequence,
54         paint: TextPaint,
55         width: Int,
56         start: Int = 0,
57         end: Int = text.length,
58         textDir: TextDirectionHeuristic = LayoutCompat.DEFAULT_TEXT_DIRECTION_HEURISTIC,
59         alignment: Alignment = LayoutCompat.DEFAULT_LAYOUT_ALIGNMENT,
60         @IntRange(from = 0) maxLines: Int = LayoutCompat.DEFAULT_MAX_LINES,
61         ellipsize: TruncateAt? = null,
62         @IntRange(from = 0) ellipsizedWidth: Int = width,
63         @FloatRange(from = 0.0)
64         lineSpacingMultiplier: Float = LayoutCompat.DEFAULT_LINESPACING_MULTIPLIER,
65         lineSpacingExtra: Float = LayoutCompat.DEFAULT_LINESPACING_EXTRA,
66         @JustificationMode justificationMode: Int = LayoutCompat.DEFAULT_JUSTIFICATION_MODE,
67         includePadding: Boolean = LayoutCompat.DEFAULT_INCLUDE_PADDING,
68         useFallbackLineSpacing: Boolean = LayoutCompat.DEFAULT_FALLBACK_LINE_SPACING,
69         @BreakStrategy breakStrategy: Int = LayoutCompat.DEFAULT_BREAK_STRATEGY,
70         @LineBreakStyle lineBreakStyle: Int = LayoutCompat.DEFAULT_LINE_BREAK_STYLE,
71         @LineBreakWordStyle lineBreakWordStyle: Int = LayoutCompat.DEFAULT_LINE_BREAK_WORD_STYLE,
72         @HyphenationFrequency
73         hyphenationFrequency: Int = LayoutCompat.DEFAULT_HYPHENATION_FREQUENCY,
74         leftIndents: IntArray? = null,
75         rightIndents: IntArray? = null
76     ): StaticLayout {
77         return delegate.create(
78             StaticLayoutParams(
79                 text = text,
80                 start = start,
81                 end = end,
82                 paint = paint,
83                 width = width,
84                 textDir = textDir,
85                 alignment = alignment,
86                 maxLines = maxLines,
87                 ellipsize = ellipsize,
88                 ellipsizedWidth = ellipsizedWidth,
89                 lineSpacingMultiplier = lineSpacingMultiplier,
90                 lineSpacingExtra = lineSpacingExtra,
91                 justificationMode = justificationMode,
92                 includePadding = includePadding,
93                 useFallbackLineSpacing = useFallbackLineSpacing,
94                 breakStrategy = breakStrategy,
95                 lineBreakStyle = lineBreakStyle,
96                 lineBreakWordStyle = lineBreakWordStyle,
97                 hyphenationFrequency = hyphenationFrequency,
98                 leftIndents = leftIndents,
99                 rightIndents = rightIndents
100             )
101         )
102     }
103 
104     /**
105      * Returns whether fallbackLineSpacing is enabled for the given layout.
106      *
107      * @param layout StaticLayout instance
108      * @param useFallbackLineSpacing fallbackLineSpacing configuration passed while creating the
109      *   StaticLayout.
110      */
isFallbackLineSpacingEnablednull111     fun isFallbackLineSpacingEnabled(
112         layout: StaticLayout,
113         useFallbackLineSpacing: Boolean
114     ): Boolean {
115         return delegate.isFallbackLineSpacingEnabled(layout, useFallbackLineSpacing)
116     }
117 }
118 
119 private class StaticLayoutParams(
120     val text: CharSequence,
121     val start: Int = 0,
122     val end: Int,
123     val paint: TextPaint,
124     val width: Int,
125     val textDir: TextDirectionHeuristic,
126     val alignment: Alignment,
127     val maxLines: Int,
128     val ellipsize: TruncateAt?,
129     val ellipsizedWidth: Int,
130     val lineSpacingMultiplier: Float,
131     val lineSpacingExtra: Float,
132     val justificationMode: Int,
133     val includePadding: Boolean,
134     val useFallbackLineSpacing: Boolean,
135     val breakStrategy: Int,
136     val lineBreakStyle: Int,
137     val lineBreakWordStyle: Int,
138     val hyphenationFrequency: Int,
139     val leftIndents: IntArray?,
140     val rightIndents: IntArray?
141 ) {
142     init {
<lambda>null143         requirePrecondition(start in 0..end) { "invalid start value" }
<lambda>null144         requirePrecondition(end in 0..text.length) { "invalid end value" }
<lambda>null145         requirePrecondition(maxLines >= 0) { "invalid maxLines value" }
<lambda>null146         requirePrecondition(width >= 0) { "invalid width value" }
<lambda>null147         requirePrecondition(ellipsizedWidth >= 0) { "invalid ellipsizedWidth value" }
<lambda>null148         requirePrecondition(lineSpacingMultiplier >= 0f) { "invalid lineSpacingMultiplier value" }
149     }
150 }
151 
152 private interface StaticLayoutFactoryImpl {
153 
154     // API level specific, do not inline to prevent ART class verification breakages
createnull155     fun create(params: StaticLayoutParams): StaticLayout
156 
157     fun isFallbackLineSpacingEnabled(layout: StaticLayout, useFallbackLineSpacing: Boolean): Boolean
158 }
159 
160 @RequiresApi(23)
161 private class StaticLayoutFactory23 : StaticLayoutFactoryImpl {
162 
163     override fun create(params: StaticLayoutParams): StaticLayout {
164         return Builder.obtain(params.text, params.start, params.end, params.paint, params.width)
165             .apply {
166                 setTextDirection(params.textDir)
167                 setAlignment(params.alignment)
168                 setMaxLines(params.maxLines)
169                 setEllipsize(params.ellipsize)
170                 setEllipsizedWidth(params.ellipsizedWidth)
171                 setLineSpacing(params.lineSpacingExtra, params.lineSpacingMultiplier)
172                 setIncludePad(params.includePadding)
173                 setBreakStrategy(params.breakStrategy)
174                 setHyphenationFrequency(params.hyphenationFrequency)
175                 setIndents(params.leftIndents, params.rightIndents)
176                 if (Build.VERSION.SDK_INT >= 26) {
177                     StaticLayoutFactory26.setJustificationMode(this, params.justificationMode)
178                 }
179                 if (Build.VERSION.SDK_INT >= 28) {
180                     StaticLayoutFactory28.setUseLineSpacingFromFallbacks(
181                         this,
182                         params.useFallbackLineSpacing
183                     )
184                 }
185                 if (Build.VERSION.SDK_INT >= 33) {
186                     StaticLayoutFactory33.setLineBreakConfig(
187                         this,
188                         params.lineBreakStyle,
189                         params.lineBreakWordStyle
190                     )
191                 }
192                 if (Build.VERSION.SDK_INT >= 35) { // b/391378120
193                     // Due to a bug in API35, the useBoundsForWidth flag may become true if it was
194                     // true in the recycled object. To avoid unexpected line break behavior,
195                     // manually disable useBoundsForWidth every time we create a StaticLayout.
196                     StaticLayoutFactory35.disableUseBoundsForWidth(this)
197                 }
198             }
199             .build()
200     }
201 
202     override fun isFallbackLineSpacingEnabled(
203         layout: StaticLayout,
204         useFallbackLineSpacing: Boolean
205     ): Boolean {
206         return if (Build.VERSION.SDK_INT >= 33) {
207             StaticLayoutFactory33.isFallbackLineSpacingEnabled(layout)
208         } else if (Build.VERSION.SDK_INT >= 28) {
209             useFallbackLineSpacing
210         } else {
211             false
212         }
213     }
214 }
215 
216 @RequiresApi(26)
217 private object StaticLayoutFactory26 {
218     @JvmStatic
setJustificationModenull219     fun setJustificationMode(builder: Builder, justificationMode: Int) {
220         builder.setJustificationMode(justificationMode)
221     }
222 }
223 
224 @RequiresApi(28)
225 private object StaticLayoutFactory28 {
226     @JvmStatic
setUseLineSpacingFromFallbacksnull227     fun setUseLineSpacingFromFallbacks(builder: Builder, useFallbackLineSpacing: Boolean) {
228         builder.setUseLineSpacingFromFallbacks(useFallbackLineSpacing)
229     }
230 }
231 
232 @RequiresApi(33)
233 private object StaticLayoutFactory33 {
234     @JvmStatic
isFallbackLineSpacingEnablednull235     fun isFallbackLineSpacingEnabled(layout: StaticLayout): Boolean {
236         return layout.isFallbackLineSpacingEnabled
237     }
238 
239     @JvmStatic
setLineBreakConfignull240     fun setLineBreakConfig(builder: Builder, lineBreakStyle: Int, lineBreakWordStyle: Int) {
241         val lineBreakConfig =
242             LineBreakConfig.Builder()
243                 .setLineBreakStyle(lineBreakStyle)
244                 .setLineBreakWordStyle(lineBreakWordStyle)
245                 .build()
246         builder.setLineBreakConfig(lineBreakConfig)
247     }
248 }
249 
250 @RequiresApi(35)
251 private object StaticLayoutFactory35 {
252     @JvmStatic
disableUseBoundsForWidthnull253     fun disableUseBoundsForWidth(builder: Builder) {
254         builder.setUseBoundsForWidth(false)
255     }
256 }
257 
258 private class StaticLayoutFactoryDefault : StaticLayoutFactoryImpl {
259 
260     companion object {
261         private var isInitialized = false
262         private var staticLayoutConstructor: Constructor<StaticLayout>? = null
263 
getStaticLayoutConstructornull264         private fun getStaticLayoutConstructor(): Constructor<StaticLayout>? {
265             if (isInitialized) return staticLayoutConstructor
266             isInitialized = true
267             try {
268                 staticLayoutConstructor =
269                     StaticLayout::class
270                         .java
271                         .getConstructor(
272                             CharSequence::class.java,
273                             Int::class.javaPrimitiveType, /* start */
274                             Int::class.javaPrimitiveType, /* end */
275                             TextPaint::class.java,
276                             Int::class.javaPrimitiveType, /* width */
277                             Alignment::class.java,
278                             TextDirectionHeuristic::class.java,
279                             Float::class.javaPrimitiveType, /* lineSpacingMultiplier */
280                             Float::class.javaPrimitiveType, /* lineSpacingExtra */
281                             Boolean::class.javaPrimitiveType, /* includePadding */
282                             TruncateAt::class.java,
283                             Int::class.javaPrimitiveType, /* ellipsizeWidth */
284                             Int::class.javaPrimitiveType /* maxLines */
285                         )
286             } catch (e: NoSuchMethodException) {
287                 staticLayoutConstructor = null
288                 Log.e(TAG, "unable to collect necessary constructor.")
289             }
290 
291             return staticLayoutConstructor
292         }
293     }
294 
createnull295     override fun create(params: StaticLayoutParams): StaticLayout {
296         // On API 21 to 23, try to call the StaticLayoutConstructor which supports the
297         // textDir and maxLines.
298         val result =
299             getStaticLayoutConstructor()?.let {
300                 try {
301                     it.newInstance(
302                         params.text,
303                         params.start,
304                         params.end,
305                         params.paint,
306                         params.width,
307                         params.alignment,
308                         params.textDir,
309                         params.lineSpacingMultiplier,
310                         params.lineSpacingExtra,
311                         params.includePadding,
312                         params.ellipsize,
313                         params.ellipsizedWidth,
314                         params.maxLines
315                     )
316                 } catch (e: IllegalAccessException) {
317                     staticLayoutConstructor = null
318                     Log.e(TAG, "unable to call constructor")
319                     null
320                 } catch (e: InstantiationException) {
321                     staticLayoutConstructor = null
322                     Log.e(TAG, "unable to call constructor")
323                     null
324                 } catch (e: InvocationTargetException) {
325                     staticLayoutConstructor = null
326                     Log.e(TAG, "unable to call constructor")
327                     null
328                 }
329             }
330 
331         if (result != null) return result
332 
333         // On API 21 to 23 where it failed to find StaticLayout.Builder, create with
334         // deprecated constructor, textDir and maxLines won't work in this case.
335         @Suppress("DEPRECATION")
336         return StaticLayout(
337             params.text,
338             params.start,
339             params.end,
340             params.paint,
341             params.width,
342             params.alignment,
343             params.lineSpacingMultiplier,
344             params.lineSpacingExtra,
345             params.includePadding,
346             params.ellipsize,
347             params.ellipsizedWidth
348         )
349     }
350 
isFallbackLineSpacingEnablednull351     override fun isFallbackLineSpacingEnabled(
352         layout: StaticLayout,
353         useFallbackLineSpacing: Boolean
354     ): Boolean {
355         return false
356     }
357 }
358