1 /*
<lambda>null2 * Copyright 2020 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 androidx.compose.ui.text.platform
18
19 import android.graphics.Typeface
20 import android.os.Build
21 import android.text.SpannableString
22 import android.text.Spanned
23 import android.text.style.ScaleXSpan
24 import android.text.style.StrikethroughSpan
25 import android.text.style.StyleSpan
26 import android.text.style.TypefaceSpan
27 import android.text.style.UnderlineSpan
28 import androidx.annotation.RequiresApi
29 import androidx.annotation.RestrictTo
30 import androidx.compose.ui.text.AnnotatedString
31 import androidx.compose.ui.text.ExperimentalTextApi
32 import androidx.compose.ui.text.InternalTextApi
33 import androidx.compose.ui.text.LinkAnnotation
34 import androidx.compose.ui.text.SpanStyle
35 import androidx.compose.ui.text.font.FontFamily
36 import androidx.compose.ui.text.font.FontStyle
37 import androidx.compose.ui.text.font.FontSynthesis
38 import androidx.compose.ui.text.font.FontWeight
39 import androidx.compose.ui.text.font.GenericFontFamily
40 import androidx.compose.ui.text.font.getAndroidTypefaceStyle
41 import androidx.compose.ui.text.platform.extensions.setBackground
42 import androidx.compose.ui.text.platform.extensions.setColor
43 import androidx.compose.ui.text.platform.extensions.setFontSize
44 import androidx.compose.ui.text.platform.extensions.setLocaleList
45 import androidx.compose.ui.text.platform.extensions.toSpan
46 import androidx.compose.ui.text.style.TextDecoration
47 import androidx.compose.ui.unit.Density
48 import androidx.compose.ui.util.fastForEach
49
50 @OptIn(ExperimentalTextApi::class)
51 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
52 @InternalTextApi // used in ui:ui
53 fun AnnotatedString.toAccessibilitySpannableString(
54 density: Density,
55 fontFamilyResolver: FontFamily.Resolver,
56 urlSpanCache: URLSpanCache,
57 ): SpannableString {
58 val spannableString = SpannableString(text)
59 spanStylesOrNull?.fastForEach { (style, start, end) ->
60 // b/232238615 looking up fonts inside of accessibility does not honor overwritten
61 // FontFamilyResolver. This is not safe until Font.ResourceLoader is fully removed.
62 val noFontStyle = style.copy(fontFamily = null)
63 spannableString.setSpanStyle(noFontStyle, start, end, density, fontFamilyResolver)
64 }
65
66 getTtsAnnotations(0, length).fastForEach { (ttsAnnotation, start, end) ->
67 spannableString.setSpan(
68 ttsAnnotation.toSpan(),
69 start,
70 end,
71 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
72 )
73 }
74
75 @Suppress("Deprecation")
76 getUrlAnnotations(0, length).fastForEach { (urlAnnotation, start, end) ->
77 spannableString.setSpan(
78 urlSpanCache.toURLSpan(urlAnnotation),
79 start,
80 end,
81 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
82 )
83 }
84
85 getLinkAnnotations(0, length).fastForEach { linkRange ->
86 if (linkRange.start != linkRange.end) {
87 val link = linkRange.item
88 if (link is LinkAnnotation.Url && link.linkInteractionListener == null) {
89 spannableString.setSpan(
90 urlSpanCache.toURLSpan(linkRange.toUrlLink()),
91 linkRange.start,
92 linkRange.end,
93 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
94 )
95 } else {
96 spannableString.setSpan(
97 urlSpanCache.toClickableSpan(linkRange),
98 linkRange.start,
99 linkRange.end,
100 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
101 )
102 }
103 }
104 }
105 return spannableString
106 }
107
108 /** Apply the serializable styles to SpannableString. */
SpannableStringnull109 private fun SpannableString.setSpanStyle(
110 spanStyle: SpanStyle,
111 start: Int,
112 end: Int,
113 density: Density,
114 fontFamilyResolver: FontFamily.Resolver
115 ) {
116 setColor(spanStyle.color, start, end)
117
118 setFontSize(spanStyle.fontSize, density, start, end)
119
120 if (spanStyle.fontWeight != null || spanStyle.fontStyle != null) {
121 // If current typeface is bold, StyleSpan won't change it to normal. The same applies to
122 // font style, so use normal as default works here.
123 // This is also a bug in framework span. But we can't find a good solution so far.
124 val fontWeight = spanStyle.fontWeight ?: FontWeight.Normal
125 val fontStyle = spanStyle.fontStyle ?: FontStyle.Normal
126 setSpan(
127 StyleSpan(getAndroidTypefaceStyle(fontWeight, fontStyle)),
128 start,
129 end,
130 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
131 )
132 }
133
134 // TypefaceSpan accepts Typeface as parameter only after P. And only font family string can be
135 // pass to other thread.
136 // Here we try to create TypefaceSpan with font family string if possible.
137 if (spanStyle.fontFamily != null) {
138 if (spanStyle.fontFamily is GenericFontFamily) {
139 setSpan(
140 TypefaceSpan(spanStyle.fontFamily.name),
141 start,
142 end,
143 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
144 )
145 } else {
146 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
147 // TODO(b/214587005): Check for async here and uncache
148 val typeface =
149 fontFamilyResolver
150 .resolve(
151 fontFamily = spanStyle.fontFamily,
152 fontSynthesis = spanStyle.fontSynthesis ?: FontSynthesis.All
153 )
154 .value as Typeface
155 setSpan(
156 Api28Impl.createTypefaceSpan(typeface),
157 start,
158 end,
159 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
160 )
161 }
162 }
163 }
164
165 if (spanStyle.textDecoration != null) {
166 // This doesn't match how we rendering the styles. When TextDecoration.None is set, it
167 // should remove the underline and lineThrough effect on the given range. Here we didn't
168 // remove any previously applied spans yet.
169 if (TextDecoration.Underline in spanStyle.textDecoration) {
170 setSpan(UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
171 }
172 if (TextDecoration.LineThrough in spanStyle.textDecoration) {
173 setSpan(StrikethroughSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
174 }
175 }
176
177 if (spanStyle.textGeometricTransform != null) {
178 setSpan(
179 ScaleXSpan(spanStyle.textGeometricTransform.scaleX),
180 start,
181 end,
182 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
183 )
184 }
185
186 setLocaleList(spanStyle.localeList, start, end)
187
188 setBackground(spanStyle.background, start, end)
189 }
190
191 @RequiresApi(28)
192 private object Api28Impl {
createTypefaceSpannull193 fun createTypefaceSpan(typeface: Typeface): TypefaceSpan = TypefaceSpan(typeface)
194 }
195
196 private fun AnnotatedString.Range<LinkAnnotation>.toUrlLink() =
197 AnnotatedString.Range(this.item as LinkAnnotation.Url, this.start, this.end)
198