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